mirror of
https://github.com/FoundationAgents/MetaGPT.git
synced 2026-06-17 15:35:21 +02:00
Merge branch 'minecraft' of github.com:geekan/MetaGPT into minecraft
This commit is contained in:
commit
eb9ea304a5
215 changed files with 10530 additions and 1257 deletions
|
|
@ -4,4 +4,4 @@ sudo npm install -g @mermaid-js/mermaid-cli
|
|||
|
||||
# Step 2: Ensure that Python 3.9+ is installed on your system. You can check this by using:
|
||||
python --version
|
||||
python setup.py install
|
||||
pip install -e.
|
||||
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
*.html linguist-detectable=false
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
|
|
@ -16,7 +16,8 @@ dist/
|
|||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib/*
|
||||
!/metagpt/mineflayer_env/mineflayer/lib/* # Mineflayer
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
|
|
@ -114,6 +115,7 @@ venv/
|
|||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
*/ckpt
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
|
|
@ -148,8 +150,7 @@ allure-results
|
|||
.DS_Store
|
||||
.vscode
|
||||
|
||||
|
||||
*.txt
|
||||
log.txt
|
||||
docs/scripts/set_env.sh
|
||||
key.yaml
|
||||
output.json
|
||||
|
|
@ -164,3 +165,5 @@ workspace/*
|
|||
tmp
|
||||
output.wav
|
||||
metagpt/roles/idea_agent.py
|
||||
.aider*
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ COPY . /app/metagpt
|
|||
WORKDIR /app/metagpt
|
||||
RUN mkdir workspace &&\
|
||||
pip install --no-cache-dir -r requirements.txt &&\
|
||||
python setup.py install
|
||||
pip install -e.
|
||||
|
||||
# Running with an infinite loop using the tail command
|
||||
CMD ["sh", "-c", "tail -f /dev/null"]
|
||||
|
|
|
|||
81
README.md
81
README.md
|
|
@ -33,6 +33,13 @@ # MetaGPT: The Multi-Agent Framework
|
|||
|
||||
<p align="center">Software Company Multi-Role Schematic (Gradually Implementing)</p>
|
||||
|
||||
## MetaGPT's Abilities
|
||||
|
||||
|
||||
https://github.com/geekan/MetaGPT/assets/34952977/34345016-5d13-489d-b9f9-b82ace413419
|
||||
|
||||
|
||||
|
||||
## Examples (fully generated by GPT-4)
|
||||
|
||||
For example, if you type `python startup.py "Design a RecSys like Toutiao"`, you would get many outputs, one of them is data & api design
|
||||
|
|
@ -41,6 +48,9 @@ ## Examples (fully generated by GPT-4)
|
|||
|
||||
It costs approximately **$0.2** (in GPT-4 API fees) to generate one example with analysis and design, and around **$2.0** for a full project.
|
||||
|
||||
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
### Installation Video Guide
|
||||
|
|
@ -50,7 +60,7 @@ ### Installation Video Guide
|
|||
### Traditional Installation
|
||||
|
||||
```bash
|
||||
# Step 1: Ensure that NPM is installed on your system. Then install mermaid-js.
|
||||
# Step 1: Ensure that NPM is installed on your system. Then install mermaid-js. (If you don't have npm in your computer, please go to the Node.js offical website to install Node.js https://nodejs.org/ and then you will have npm tool in your computer.)
|
||||
npm --version
|
||||
sudo npm install -g @mermaid-js/mermaid-cli
|
||||
|
||||
|
|
@ -60,7 +70,7 @@ # Step 2: Ensure that Python 3.9+ is installed on your system. You can check thi
|
|||
# Step 3: Clone the repository to your local machine, and install it.
|
||||
git clone https://github.com/geekan/metagpt
|
||||
cd metagpt
|
||||
python setup.py install
|
||||
pip install -e.
|
||||
```
|
||||
|
||||
**Note:**
|
||||
|
|
@ -81,7 +91,72 @@ # Step 3: Clone the repository to your local machine, and install it.
|
|||
MMDC: "./node_modules/.bin/mmdc"
|
||||
```
|
||||
|
||||
- if `python setup.py install` fails with error `[Errno 13] Permission denied: '/usr/local/lib/python3.11/dist-packages/test-easy-install-13129.write-test'`, try instead running `python setup.py install --user`
|
||||
- if `pip install -e.` fails with error `[Errno 13] Permission denied: '/usr/local/lib/python3.11/dist-packages/test-easy-install-13129.write-test'`, try instead running `pip install -e. --user`
|
||||
|
||||
- To convert Mermaid charts to SVG, PNG, and PDF formats. In addition to the Node.js version of Mermaid-CLI, you now have the option to use Python version Playwright, pyppeteer or mermaid.ink for this task.
|
||||
|
||||
- Playwright
|
||||
- **Install Playwright**
|
||||
|
||||
```bash
|
||||
pip install playwright
|
||||
```
|
||||
|
||||
- **Install the Required Browsers**
|
||||
|
||||
to support PDF conversion, please install Chrominum.
|
||||
|
||||
```bash
|
||||
playwright install --with-deps chromium
|
||||
```
|
||||
|
||||
- **modify `config.yaml`**
|
||||
|
||||
uncomment MERMAID_ENGINE from config.yaml and change it to `playwright`
|
||||
|
||||
```yaml
|
||||
MERMAID_ENGINE: playwright
|
||||
```
|
||||
|
||||
- pyppeteer
|
||||
- **Install pyppeteer**
|
||||
|
||||
```bash
|
||||
pip install pyppeteer
|
||||
```
|
||||
|
||||
- **Use your own Browsers**
|
||||
|
||||
pyppeteer alow you use installed browsers, please set the following envirment
|
||||
|
||||
```bash
|
||||
export PUPPETEER_EXECUTABLE_PATH = /path/to/your/chromium or edge or chrome
|
||||
```
|
||||
|
||||
please do not use this command to install browser, it is too old
|
||||
|
||||
```bash
|
||||
pyppeteer-install
|
||||
```
|
||||
|
||||
- **modify `config.yaml`**
|
||||
|
||||
uncomment MERMAID_ENGINE from config.yaml and change it to `pyppeteer`
|
||||
|
||||
```yaml
|
||||
MERMAID_ENGINE: pyppeteer
|
||||
```
|
||||
|
||||
- mermaid.ink
|
||||
- **modify `config.yaml`**
|
||||
|
||||
uncomment MERMAID_ENGINE from config.yaml and change it to `ink`
|
||||
|
||||
```yaml
|
||||
MERMAID_ENGINE: ink
|
||||
```
|
||||
|
||||
Note: this method does not support pdf export.
|
||||
|
||||
### Installation by Docker
|
||||
|
||||
|
|
|
|||
69
Temp.md
Normal file
69
Temp.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
## MG-MC记录文档
|
||||
|
||||
### 0926: 环境信息获取和更新 on_event()实际内容
|
||||
|
||||
1.Nodejs + Mineflayer配置
|
||||
|
||||
A.自行安装[Node.js (nodejs.org)](https://nodejs.org/en)
|
||||
|
||||
B.clone完之后,必须重新继续Mineflayer配置
|
||||
|
||||
```bash
|
||||
cd metagpt/mineflayer_env/mineflayer
|
||||
npm install -g npx
|
||||
npm install
|
||||
cd mineflayer-collectblock
|
||||
npm install
|
||||
npx tsc
|
||||
cd ..
|
||||
npm install
|
||||
```
|
||||
|
||||
2.在mg环境上额外执行
|
||||
|
||||
```python
|
||||
pip install -r mc_requirement.txt
|
||||
```
|
||||
|
||||
3.配置完游戏后,在 minecraft_run.py 下修改
|
||||
|
||||
```python
|
||||
mc_player.set_port(2465) # Modify this to your LAN port
|
||||
```
|
||||
|
||||
python minecraft_run.py
|
||||
|
||||
<img src="docs/resources/workspace/minecraft_tests/on_event.jpeg" style="zoom:67%;" />
|
||||
|
||||
|
||||
|
||||
### 0927:Action_developer 更新
|
||||
|
||||
对应需实现 GenerateActionCode ,完成对应的和 GameEnvironment 的交
|
||||
互和 Environment 的信息传递
|
||||
|
||||
测试结果
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
### 0930:Curriculum agent 更新
|
||||
|
||||
对应需实现 DesignTask和DesignCurriculum,以及与Environment 的信息传递。
|
||||
|
||||
|
||||
|
||||
**BUG FIX(0930):**
|
||||
|
||||
A.在前面的提交中,由于ignore了mineflayer下的lib,会造成如下报错
|
||||
|
||||
```bash
|
||||
metagpt.minecraft_team:on_event:143 - Failed to retrieve Minecraft events: HTTPConnectionPool(host='127.0.0.1', port=3000): Max retries exceeded with url: /start (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7fa7a0556130>: Failed to establish a new connection: [Errno 111] Connection refused'))
|
||||
```
|
||||
|
||||
解决方法:
|
||||
|
||||
1. 若本地已克隆项目不好更改可尝试:删除 metagpt/mineflayer_env/mineflayer + 重新copy voyager/env/mineflayer到目录下 + (npm install...0926.B命令)
|
||||
2. 重新拉取最新提交+重新配置
|
||||
|
||||
|
|
@ -5,9 +5,9 @@
|
|||
## The official OPENAI_API_BASE is https://api.openai.com/v1
|
||||
## If the official OPENAI_API_BASE is not available, we recommend using the [openai-forward](https://github.com/beidongjiedeguang/openai-forward).
|
||||
## Or, you can configure OPENAI_PROXY to access official OPENAI_API_BASE.
|
||||
OPENAI_API_BASE: "https://api.openai.com/v1"
|
||||
OPENAI_API_BASE: "https://openai-forward.metadl.com/v1"
|
||||
#OPENAI_PROXY: "http://127.0.0.1:8118"
|
||||
#OPENAI_API_KEY: "YOUR_API_KEY"
|
||||
OPENAI_API_KEY: "YOUR_API_KEY"
|
||||
OPENAI_API_MODEL: "gpt-4"
|
||||
MAX_TOKENS: 1500
|
||||
RPM: 10
|
||||
|
|
@ -76,3 +76,12 @@ SD_T2I_API: "/sdapi/v1/txt2img"
|
|||
### for Research
|
||||
MODEL_FOR_RESEARCHER_SUMMARY: gpt-3.5-turbo
|
||||
MODEL_FOR_RESEARCHER_REPORT: gpt-3.5-turbo-16k
|
||||
|
||||
### choose the engine for mermaid conversion,
|
||||
# default is nodejs, you can change it to playwright,pyppeteer or ink
|
||||
# MERMAID_ENGINE: nodejs
|
||||
|
||||
### browser path for pyppeteer engine, support Chrome, Chromium,MS Edge
|
||||
#PYPPETEER_EXECUTABLE_PATH: "/usr/bin/google-chrome-stable"
|
||||
|
||||
PROMPT_FORMAT: json #json or markdown
|
||||
|
|
@ -17,14 +17,15 @@
|
|||
1. EN
|
||||
|
||||
1. Demo Video: [MetaGPT: Multi-Agent AI Programming Framework](https://www.youtube.com/watch?v=8RNzxZBTW8M)
|
||||
1. Tutorial: [MetaGPT: Deploy POWERFUL Autonomous Ai Agents BETTER Than SUPERAGI!](https://www.youtube.com/watch?v=q16Gi9pTG_M&t=659s)
|
||||
2. Tutorial: [MetaGPT: Deploy POWERFUL Autonomous Ai Agents BETTER Than SUPERAGI!](https://www.youtube.com/watch?v=q16Gi9pTG_M&t=659s)
|
||||
3. Author's thoughts video(EN): [MetaGPT Matthew Berman](https://youtu.be/uT75J_KG_aY?si=EgbfQNAwD8F5Y1Ak)
|
||||
|
||||
1. CN
|
||||
|
||||
1. Demo Video: [MetaGPT:一行代码搭建你的虚拟公司_哔哩哔哩_bilibili](https://www.bilibili.com/video/BV1NP411C7GW/?spm_id_from=333.999.0.0&vd_source=735773c218b47da1b4bd1b98a33c5c77)
|
||||
1. Tutorial: [一个提示词写游戏 Flappy bird, 比AutoGPT强10倍的MetaGPT,最接近AGI的AI项目](https://youtu.be/Bp95b8yIH5c)
|
||||
1. Author's thoughts video(CN): [MetaGPT作者深度解析直播回放_哔哩哔哩_bilibili](https://www.bilibili.com/video/BV1Ru411V7XL/?spm_id_from=333.337.search-card.all.click)
|
||||
|
||||
2. Author's thoughts video(CN): [MetaGPT作者深度解析直播回放_哔哩哔哩_bilibili](https://www.bilibili.com/video/BV1Ru411V7XL/?spm_id_from=333.337.search-card.all.click)
|
||||
|
||||
<!---->
|
||||
|
||||
3. ### How to become a contributor?
|
||||
|
|
@ -153,6 +154,7 @@
|
|||
|
||||
1. Youtube(CN):[一个提示词写游戏 Flappy bird, 比AutoGPT强10倍的MetaGPT,最接近AGI的AI项目=一个软件公司产品经理+程序员](https://youtu.be/Bp95b8yIH5c)
|
||||
1. Youtube(EN)https://www.youtube.com/watch?v=q16Gi9pTG_M&t=659s
|
||||
2. video(EN): [MetaGPT Matthew Berman](https://youtu.be/uT75J_KG_aY?si=EgbfQNAwD8F5Y1Ak)
|
||||
|
||||
1. openai.error.RateLimitError: You exceeded your current quota, please check your plan and billing details
|
||||
|
||||
|
|
@ -161,7 +163,7 @@
|
|||
|
||||
1. What does "borg" mean in n_borg?
|
||||
|
||||
1. https://en.wikipedia.org/wiki/Borg
|
||||
1. [Wikipedia borg meaning ](https://en.wikipedia.org/wiki/Borg)
|
||||
1. The Borg civilization operates based on a hive or collective mentality, known as "the Collective." Every Borg individual is connected to the collective via a sophisticated subspace network, ensuring continuous oversight and guidance for every member. This collective consciousness allows them to not only "share the same thoughts" but also to adapt swiftly to new strategies. While individual members of the collective rarely communicate, the collective "voice" sometimes transmits aboard ships.
|
||||
|
||||
1. How to use the Claude API?
|
||||
|
|
|
|||
|
|
@ -33,6 +33,11 @@ # MetaGPT: 多智能体框架
|
|||
|
||||
<p align="center">软件公司多角色示意图(正在逐步实现)</p>
|
||||
|
||||
## MetaGPT 的能力
|
||||
|
||||
https://github.com/geekan/MetaGPT/assets/34952977/34345016-5d13-489d-b9f9-b82ace413419
|
||||
|
||||
|
||||
## 示例(均由 GPT-4 生成)
|
||||
|
||||
例如,键入`python startup.py "写个类似今日头条的推荐系统"`并回车,你会获得一系列输出,其一是数据结构与API设计
|
||||
|
|
@ -56,7 +61,7 @@ # 第 2 步:确保您的系统上安装了 Python 3.9+。您可以使用以下
|
|||
# 第 3 步:克隆仓库到您的本地机器,并进行安装。
|
||||
git clone https://github.com/geekan/metagpt
|
||||
cd metagpt
|
||||
python setup.py install
|
||||
pip install -e.
|
||||
```
|
||||
|
||||
**注意:**
|
||||
|
|
@ -76,7 +81,7 @@ # 第 3 步:克隆仓库到您的本地机器,并进行安装。
|
|||
MMDC: "./node_modules/.bin/mmdc"
|
||||
```
|
||||
|
||||
- 如果`python setup.py install`失败并显示错误`[Errno 13] Permission denied: '/usr/local/lib/python3.11/dist-packages/test-easy-install-13129.write-test'`,请尝试使用`python setup.py install --user`运行。
|
||||
- 如果`pip install -e.`失败并显示错误`[Errno 13] Permission denied: '/usr/local/lib/python3.11/dist-packages/test-easy-install-13129.write-test'`,请尝试使用`pip install -e. --user`运行。
|
||||
|
||||
### Docker安装
|
||||
|
||||
|
|
@ -131,10 +136,10 @@ # 复制配置文件并进行必要的修改
|
|||
cp config/config.yaml config/key.yaml
|
||||
```
|
||||
|
||||
| 变量名 | config/key.yaml | env |
|
||||
|--------------------------------------------|-------------------------------------------|--------------------------------|
|
||||
| OPENAI_API_KEY # 用您自己的密钥替换 | OPENAI_API_KEY: "sk-..." | export OPENAI_API_KEY="sk-..." |
|
||||
| OPENAI_API_BASE # 可选 | OPENAI_API_BASE: "https://<YOUR_SITE>/v1" | export OPENAI_API_BASE="https://<YOUR_SITE>/v1" |
|
||||
| 变量名 | config/key.yaml | env |
|
||||
| ----------------------------------- | ----------------------------------------- | ----------------------------------------------- |
|
||||
| OPENAI_API_KEY # 用您自己的密钥替换 | OPENAI_API_KEY: "sk-..." | export OPENAI_API_KEY="sk-..." |
|
||||
| OPENAI_API_BASE # 可选 | OPENAI_API_BASE: "https://<YOUR_SITE>/v1" | export OPENAI_API_BASE="https://<YOUR_SITE>/v1" |
|
||||
|
||||
## 示例:启动一个创业公司
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,11 @@ # MetaGPT: マルチエージェントフレームワーク
|
|||
|
||||
<p align="center">ソフトウェア会社のマルチロール図式(順次導入)</p>
|
||||
|
||||
## MetaGPTの能力
|
||||
|
||||
https://github.com/geekan/MetaGPT/assets/34952977/34345016-5d13-489d-b9f9-b82ace413419
|
||||
|
||||
|
||||
## 例(GPT-4 で完全生成)
|
||||
|
||||
例えば、`python startup.py "Toutiao のような RecSys をデザインする"`と入力すると、多くの出力が得られます
|
||||
|
|
@ -43,6 +48,10 @@ ## 例(GPT-4 で完全生成)
|
|||
|
||||
## インストール
|
||||
|
||||
### インストールビデオガイド
|
||||
|
||||
- [Matthew Berman: How To Install MetaGPT - Build A Startup With One Prompt!!](https://youtu.be/uT75J_KG_aY)
|
||||
|
||||
### 伝統的なインストール
|
||||
|
||||
```bash
|
||||
|
|
@ -56,7 +65,7 @@ # ステップ 2: Python 3.9+ がシステムにインストールされてい
|
|||
# ステップ 3: リポジトリをローカルマシンにクローンし、インストールする。
|
||||
git clone https://github.com/geekan/metagpt
|
||||
cd metagpt
|
||||
python setup.py install
|
||||
pip install -e.
|
||||
```
|
||||
|
||||
**注:**
|
||||
|
|
@ -66,18 +75,18 @@ # ステップ 3: リポジトリをローカルマシンにクローンし、
|
|||
|
||||
- このツールをグローバルにインストールする[問題を抱えている](https://github.com/mermaidjs/mermaid.cli/issues/15)人もいます。ローカルにインストールするのが代替の解決策です、
|
||||
|
||||
```bash
|
||||
npm install @mermaid-js/mermaid-cli
|
||||
```
|
||||
```bash
|
||||
npm install @mermaid-js/mermaid-cli
|
||||
```
|
||||
|
||||
- config.yml に mmdc のコンフィギュレーションを記述するのを忘れないこと
|
||||
|
||||
```yml
|
||||
PUPPETEER_CONFIG: "./config/puppeteer-config.json"
|
||||
MMDC: "./node_modules/.bin/mmdc"
|
||||
```
|
||||
```yml
|
||||
PUPPETEER_CONFIG: "./config/puppeteer-config.json"
|
||||
MMDC: "./node_modules/.bin/mmdc"
|
||||
```
|
||||
|
||||
- もし `python setup.py install` がエラー `[Errno 13] Permission denied: '/usr/local/lib/python3.11/dist-packages/test-easy-install-13129.write-test'` で失敗したら、代わりに `python setup.py install --user` を実行してみてください
|
||||
- もし `pip install -e.` がエラー `[Errno 13] Permission denied: '/usr/local/lib/python3.11/dist-packages/test-easy-install-13129.write-test'` で失敗したら、代わりに `pip install -e. --user` を実行してみてください
|
||||
|
||||
### Docker によるインストール
|
||||
|
||||
|
|
@ -132,26 +141,32 @@ # 設定ファイルをコピーし、必要な修正を加える。
|
|||
cp config/config.yaml config/key.yaml
|
||||
```
|
||||
|
||||
| 変数名 | config/key.yaml | env |
|
||||
| ------------------------------------------ | ----------------------------------------- | ----------------------------------------------- |
|
||||
| OPENAI_API_KEY # 自分のキーに置き換える | OPENAI_API_KEY: "sk-..." | export OPENAI_API_KEY="sk-..." |
|
||||
| OPENAI_API_BASE # オプション | OPENAI_API_BASE: "https://<YOUR_SITE>/v1" | export OPENAI_API_BASE="https://<YOUR_SITE>/v1" |
|
||||
| 変数名 | config/key.yaml | env |
|
||||
| --------------------------------------- | ----------------------------------------- | ----------------------------------------------- |
|
||||
| OPENAI_API_KEY # 自分のキーに置き換える | OPENAI_API_KEY: "sk-..." | export OPENAI_API_KEY="sk-..." |
|
||||
| OPENAI_API_BASE # オプション | OPENAI_API_BASE: "https://<YOUR_SITE>/v1" | export OPENAI_API_BASE="https://<YOUR_SITE>/v1" |
|
||||
|
||||
## チュートリアル: スタートアップの開始
|
||||
|
||||
```shell
|
||||
# スクリプトの実行
|
||||
python startup.py "Write a cli snake game"
|
||||
# コードレビューを利用すれば、コストはかかるが、より良いコード品質を選ぶことができます。
|
||||
# プロジェクトの実施にエンジニアを雇わないこと
|
||||
python startup.py "Write a cli snake game" --implement False
|
||||
# エンジニアを雇い、コードレビューを行う
|
||||
python startup.py "Write a cli snake game" --code_review True
|
||||
```
|
||||
|
||||
スクリプトを実行すると、`workspace/` ディレクトリに新しいプロジェクトが見つかります。
|
||||
|
||||
### プラットフォームまたはツールの設定
|
||||
|
||||
要件を述べるときに、どのプラットフォームまたはツールを使用するかを指定できます。
|
||||
|
||||
```shell
|
||||
python startup.py "pygame をベースとした cli ヘビゲームを書く"
|
||||
```
|
||||
|
||||
### 使用方法
|
||||
|
||||
```
|
||||
|
|
@ -200,16 +215,18 @@ ### コードウォークスルー
|
|||
`examples` でシングル・ロール(ナレッジ・ベース付き)と LLM のみの例を詳しく見ることができます。
|
||||
|
||||
## クイックスタート
|
||||
|
||||
ローカル環境のインストールや設定は、ユーザーによっては難しいものです。以下のチュートリアルで MetaGPT の魅力をすぐに体験できます。
|
||||
|
||||
- [MetaGPT クイックスタート](https://deepwisdom.feishu.cn/wiki/CyY9wdJc4iNqArku3Lncl4v8n2b)
|
||||
|
||||
試着する Huggingface Space
|
||||
Hugging Face Space で試す
|
||||
- https://huggingface.co/spaces/deepwisdom/MetaGPT
|
||||
|
||||
## 引用
|
||||
|
||||
現時点では、[Arxiv 論文](https://arxiv.org/abs/2308.00352)を引用してください:
|
||||
|
||||
```bibtex
|
||||
@misc{hong2023metagpt,
|
||||
title={MetaGPT: Meta Programming for Multi-Agent Collaborative Framework},
|
||||
|
|
@ -233,3 +250,10 @@ ## お問い合わせ先
|
|||
## デモ
|
||||
|
||||
https://github.com/geekan/MetaGPT/assets/2707039/5e8c1062-8c35-440f-bb20-2b0320f8d27d
|
||||
|
||||
## 参加する
|
||||
|
||||
📢 Discord チャンネルに参加してください!
|
||||
https://discord.gg/ZRHeExS6xv
|
||||
|
||||
お会いできることを楽しみにしています! 🎉
|
||||
|
|
|
|||
BIN
docs/resources/workspace/minecraft_tests/action_developer.png
Normal file
BIN
docs/resources/workspace/minecraft_tests/action_developer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
docs/resources/workspace/minecraft_tests/on_event.jpeg
Normal file
BIN
docs/resources/workspace/minecraft_tests/on_event.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 291 KiB |
100
examples/agent_creator.py
Normal file
100
examples/agent_creator.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
'''
|
||||
Filename: MetaGPT/examples/agent_creator.py
|
||||
Created Date: Tuesday, September 12th 2023, 3:28:37 pm
|
||||
Author: garylin2099
|
||||
'''
|
||||
import re
|
||||
|
||||
from metagpt.const import PROJECT_ROOT, WORKSPACE_ROOT
|
||||
from metagpt.actions import Action
|
||||
from metagpt.roles import Role
|
||||
from metagpt.schema import Message
|
||||
from metagpt.logs import logger
|
||||
|
||||
with open(PROJECT_ROOT / "examples/build_customized_agent.py", "r") as f:
|
||||
# use official example script to guide AgentCreator
|
||||
MULTI_ACTION_AGENT_CODE_EXAMPLE = f.read()
|
||||
|
||||
class CreateAgent(Action):
|
||||
|
||||
PROMPT_TEMPLATE = """
|
||||
### BACKGROUND
|
||||
You are using an agent framework called metagpt to write agents capable of different actions,
|
||||
the usage of metagpt can be illustrated by the following example:
|
||||
### EXAMPLE STARTS AT THIS LINE
|
||||
{example}
|
||||
### EXAMPLE ENDS AT THIS LINE
|
||||
### TASK
|
||||
Now you should create an agent with appropriate actions based on the instruction, consider carefully about
|
||||
the PROMPT_TEMPLATE of all actions and when to call self._aask()
|
||||
### INSTRUCTION
|
||||
{instruction}
|
||||
### YOUR CODE
|
||||
Return ```python your_code_here ``` with NO other texts, your code:
|
||||
"""
|
||||
|
||||
async def run(self, example: str, instruction: str):
|
||||
|
||||
prompt = self.PROMPT_TEMPLATE.format(example=example, instruction=instruction)
|
||||
# logger.info(prompt)
|
||||
|
||||
rsp = await self._aask(prompt)
|
||||
|
||||
code_text = CreateAgent.parse_code(rsp)
|
||||
|
||||
return code_text
|
||||
|
||||
@staticmethod
|
||||
def parse_code(rsp):
|
||||
pattern = r'```python(.*)```'
|
||||
match = re.search(pattern, rsp, re.DOTALL)
|
||||
code_text = match.group(1) if match else ""
|
||||
with open(WORKSPACE_ROOT / "agent_created_agent.py", "w") as f:
|
||||
f.write(code_text)
|
||||
return code_text
|
||||
|
||||
class AgentCreator(Role):
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "Matrix",
|
||||
profile: str = "AgentCreator",
|
||||
agent_template: str = MULTI_ACTION_AGENT_CODE_EXAMPLE,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(name, profile, **kwargs)
|
||||
self._init_actions([CreateAgent])
|
||||
self.agent_template = agent_template
|
||||
|
||||
async def _act(self) -> Message:
|
||||
logger.info(f"{self._setting}: ready to {self._rc.todo}")
|
||||
todo = self._rc.todo
|
||||
msg = self._rc.memory.get()[-1]
|
||||
|
||||
instruction = msg.content
|
||||
code_text = await CreateAgent().run(example=self.agent_template, instruction=instruction)
|
||||
msg = Message(content=code_text, role=self.profile, cause_by=todo)
|
||||
|
||||
return msg
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
async def main():
|
||||
|
||||
agent_template = MULTI_ACTION_AGENT_CODE_EXAMPLE
|
||||
|
||||
creator = AgentCreator(agent_template=agent_template)
|
||||
|
||||
# msg = """Write an agent called SimpleTester that will take any code snippet (str)
|
||||
# and return a testing code (str) for testing
|
||||
# the given code snippet. Use pytest as the testing framework."""
|
||||
|
||||
msg = """
|
||||
Write an agent called SimpleTester that will take any code snippet (str) and do the following:
|
||||
1. write a testing code (str) for testing the given code snippet, save the testing code as a .py file in the current working diretory;
|
||||
2. run the testing code.
|
||||
You can use pytest as the testing framework.
|
||||
"""
|
||||
await creator.run(msg)
|
||||
|
||||
asyncio.run(main())
|
||||
139
examples/build_customized_agent.py
Normal file
139
examples/build_customized_agent.py
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
'''
|
||||
Filename: MetaGPT/examples/build_customized_agent.py
|
||||
Created Date: Tuesday, September 19th 2023, 6:52:25 pm
|
||||
Author: garylin2099
|
||||
'''
|
||||
import re
|
||||
import subprocess
|
||||
import asyncio
|
||||
|
||||
import fire
|
||||
|
||||
from metagpt.actions import Action
|
||||
from metagpt.roles import Role
|
||||
from metagpt.schema import Message
|
||||
from metagpt.logs import logger
|
||||
|
||||
class SimpleWriteCode(Action):
|
||||
|
||||
PROMPT_TEMPLATE = """
|
||||
Write a python function that can {instruction} and provide two runnnable test cases.
|
||||
Return ```python your_code_here ``` with NO other texts,
|
||||
example:
|
||||
```python
|
||||
# function
|
||||
def add(a, b):
|
||||
return a + b
|
||||
# test cases
|
||||
print(add(1, 2))
|
||||
print(add(3, 4))
|
||||
```
|
||||
your code:
|
||||
"""
|
||||
|
||||
def __init__(self, name="SimpleWriteCode", context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
|
||||
async def run(self, instruction: str):
|
||||
|
||||
prompt = self.PROMPT_TEMPLATE.format(instruction=instruction)
|
||||
|
||||
rsp = await self._aask(prompt)
|
||||
|
||||
code_text = SimpleWriteCode.parse_code(rsp)
|
||||
|
||||
return code_text
|
||||
|
||||
@staticmethod
|
||||
def parse_code(rsp):
|
||||
pattern = r'```python(.*)```'
|
||||
match = re.search(pattern, rsp, re.DOTALL)
|
||||
code_text = match.group(1) if match else rsp
|
||||
return code_text
|
||||
|
||||
class SimpleRunCode(Action):
|
||||
def __init__(self, name="SimpleRunCode", context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
|
||||
async def run(self, code_text: str):
|
||||
result = subprocess.run(["python3", "-c", code_text], capture_output=True, text=True)
|
||||
code_result = result.stdout
|
||||
logger.info(f"{code_result=}")
|
||||
return code_result
|
||||
|
||||
class SimpleCoder(Role):
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "Alice",
|
||||
profile: str = "SimpleCoder",
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(name, profile, **kwargs)
|
||||
self._init_actions([SimpleWriteCode])
|
||||
|
||||
async def _act(self) -> Message:
|
||||
logger.info(f"{self._setting}: ready to {self._rc.todo}")
|
||||
todo = self._rc.todo
|
||||
|
||||
msg = self._rc.memory.get()[-1] # retrieve the latest memory
|
||||
instruction = msg.content
|
||||
|
||||
code_text = await SimpleWriteCode().run(instruction)
|
||||
msg = Message(content=code_text, role=self.profile, cause_by=todo)
|
||||
|
||||
return msg
|
||||
|
||||
class RunnableCoder(Role):
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "Alice",
|
||||
profile: str = "RunnableCoder",
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(name, profile, **kwargs)
|
||||
self._init_actions([SimpleWriteCode, SimpleRunCode])
|
||||
|
||||
async def _think(self) -> None:
|
||||
if self._rc.todo is None:
|
||||
self._set_state(0)
|
||||
return
|
||||
|
||||
if self._rc.state + 1 < len(self._states):
|
||||
self._set_state(self._rc.state + 1)
|
||||
else:
|
||||
self._rc.todo = None
|
||||
|
||||
async def _act(self) -> Message:
|
||||
logger.info(f"{self._setting}: ready to {self._rc.todo}")
|
||||
todo = self._rc.todo
|
||||
msg = self._rc.memory.get()[-1]
|
||||
|
||||
if isinstance(todo, SimpleWriteCode):
|
||||
instruction = msg.content
|
||||
result = await SimpleWriteCode().run(instruction)
|
||||
|
||||
elif isinstance(todo, SimpleRunCode):
|
||||
code_text = msg.content
|
||||
result = await SimpleRunCode().run(code_text)
|
||||
|
||||
msg = Message(content=result, role=self.profile, cause_by=todo)
|
||||
self._rc.memory.add(msg)
|
||||
return msg
|
||||
|
||||
async def _react(self) -> Message:
|
||||
while True:
|
||||
await self._think()
|
||||
if self._rc.todo is None:
|
||||
break
|
||||
await self._act()
|
||||
return Message(content="All job done", role=self.profile)
|
||||
|
||||
def main(msg="write a function that calculates the sum of a list"):
|
||||
# role = SimpleCoder()
|
||||
role = RunnableCoder()
|
||||
logger.info(msg)
|
||||
result = asyncio.run(role.run(msg))
|
||||
logger.info(result)
|
||||
|
||||
if __name__ == '__main__':
|
||||
fire.Fire(main)
|
||||
148
examples/debate.py
Normal file
148
examples/debate.py
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
'''
|
||||
Filename: MetaGPT/examples/debate.py
|
||||
Created Date: Tuesday, September 19th 2023, 6:52:25 pm
|
||||
Author: garylin2099
|
||||
'''
|
||||
import asyncio
|
||||
import platform
|
||||
import fire
|
||||
|
||||
from metagpt.software_company import SoftwareCompany
|
||||
from metagpt.actions import Action, BossRequirement
|
||||
from metagpt.roles import Role
|
||||
from metagpt.schema import Message
|
||||
from metagpt.logs import logger
|
||||
|
||||
class ShoutOut(Action):
|
||||
"""Action: Shout out loudly in a debate (quarrel)"""
|
||||
|
||||
PROMPT_TEMPLATE = """
|
||||
## BACKGROUND
|
||||
Suppose you are {name}, you are in a debate with {opponent_name}.
|
||||
## DEBATE HISTORY
|
||||
Previous rounds:
|
||||
{context}
|
||||
## YOUR TURN
|
||||
Now it's your turn, you should closely respond to your opponent's latest argument, state your position, defend your arguments, and attack your opponent's arguments,
|
||||
craft a strong and emotional response in 80 words, in {name}'s rhetoric and viewpoints, your will argue:
|
||||
"""
|
||||
|
||||
def __init__(self, name="ShoutOut", context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
|
||||
async def run(self, context: str, name: str, opponent_name: str):
|
||||
|
||||
prompt = self.PROMPT_TEMPLATE.format(context=context, name=name, opponent_name=opponent_name)
|
||||
# logger.info(prompt)
|
||||
|
||||
rsp = await self._aask(prompt)
|
||||
|
||||
return rsp
|
||||
|
||||
class Trump(Role):
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "Trump",
|
||||
profile: str = "Republican",
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(name, profile, **kwargs)
|
||||
self._init_actions([ShoutOut])
|
||||
self._watch([ShoutOut])
|
||||
self.name = "Trump"
|
||||
self.opponent_name = "Biden"
|
||||
|
||||
async def _observe(self) -> int:
|
||||
await super()._observe()
|
||||
# accept messages sent (from opponent) to self, disregard own messages from the last round
|
||||
self._rc.news = [msg for msg in self._rc.news if msg.send_to == self.name]
|
||||
return len(self._rc.news)
|
||||
|
||||
async def _act(self) -> Message:
|
||||
logger.info(f"{self._setting}: ready to {self._rc.todo}")
|
||||
|
||||
msg_history = self._rc.memory.get_by_actions([ShoutOut])
|
||||
context = []
|
||||
for m in msg_history:
|
||||
context.append(str(m))
|
||||
context = "\n".join(context)
|
||||
|
||||
rsp = await ShoutOut().run(context=context, name=self.name, opponent_name=self.opponent_name)
|
||||
|
||||
msg = Message(
|
||||
content=rsp,
|
||||
role=self.profile,
|
||||
cause_by=ShoutOut,
|
||||
sent_from=self.name,
|
||||
send_to=self.opponent_name,
|
||||
)
|
||||
|
||||
return msg
|
||||
|
||||
class Biden(Role):
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "Biden",
|
||||
profile: str = "Democrat",
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(name, profile, **kwargs)
|
||||
self._init_actions([ShoutOut])
|
||||
self._watch([BossRequirement, ShoutOut])
|
||||
self.name = "Biden"
|
||||
self.opponent_name = "Trump"
|
||||
|
||||
async def _observe(self) -> int:
|
||||
await super()._observe()
|
||||
# accept the very first human instruction (the debate topic) or messages sent (from opponent) to self,
|
||||
# disregard own messages from the last round
|
||||
self._rc.news = [msg for msg in self._rc.news if msg.cause_by == BossRequirement or msg.send_to == self.name]
|
||||
return len(self._rc.news)
|
||||
|
||||
async def _act(self) -> Message:
|
||||
logger.info(f"{self._setting}: ready to {self._rc.todo}")
|
||||
|
||||
msg_history = self._rc.memory.get_by_actions([BossRequirement, ShoutOut])
|
||||
context = []
|
||||
for m in msg_history:
|
||||
context.append(str(m))
|
||||
context = "\n".join(context)
|
||||
|
||||
rsp = await ShoutOut().run(context=context, name=self.name, opponent_name=self.opponent_name)
|
||||
|
||||
msg = Message(
|
||||
content=rsp,
|
||||
role=self.profile,
|
||||
cause_by=ShoutOut,
|
||||
sent_from=self.name,
|
||||
send_to=self.opponent_name,
|
||||
)
|
||||
|
||||
return msg
|
||||
|
||||
async def startup(idea: str, investment: float = 3.0, n_round: int = 5,
|
||||
code_review: bool = False, run_tests: bool = False):
|
||||
"""We reuse the startup paradigm for roles to interact with each other.
|
||||
Now we run a startup of presidents and watch they quarrel. :) """
|
||||
company = SoftwareCompany()
|
||||
company.hire([Biden(), Trump()])
|
||||
company.invest(investment)
|
||||
company.start_project(idea)
|
||||
await company.run(n_round=n_round)
|
||||
|
||||
|
||||
def main(idea: str, investment: float = 3.0, n_round: int = 10):
|
||||
"""
|
||||
:param idea: Debate topic, such as "Topic: The U.S. should commit more in climate change fighting"
|
||||
or "Trump: Climate change is a hoax"
|
||||
:param investment: contribute a certain dollar amount to watch the debate
|
||||
:param n_round: maximum rounds of the debate
|
||||
:return:
|
||||
"""
|
||||
if platform.system() == "Windows":
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
asyncio.run(startup(idea, investment, n_round))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
fire.Fire(main)
|
||||
82
examples/sk_agent.py
Normal file
82
examples/sk_agent.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023/9/13 12:36
|
||||
@Author : femto Zheng
|
||||
@File : sk_agent.py
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
from semantic_kernel.core_skills import FileIOSkill, MathSkill, TextSkill, TimeSkill
|
||||
from semantic_kernel.planning import SequentialPlanner
|
||||
|
||||
# from semantic_kernel.planning import SequentialPlanner
|
||||
from semantic_kernel.planning.action_planner.action_planner import ActionPlanner
|
||||
|
||||
from metagpt.actions import BossRequirement
|
||||
from metagpt.const import SKILL_DIRECTORY
|
||||
from metagpt.roles.sk_agent import SkAgent
|
||||
from metagpt.schema import Message
|
||||
from metagpt.tools.search_engine import SkSearchEngine
|
||||
|
||||
|
||||
async def main():
|
||||
# await basic_planner_example()
|
||||
# await action_planner_example()
|
||||
|
||||
# await sequential_planner_example()
|
||||
await basic_planner_web_search_example()
|
||||
|
||||
|
||||
async def basic_planner_example():
|
||||
task = """
|
||||
Tomorrow is Valentine's day. I need to come up with a few date ideas. She speaks French so write it in French.
|
||||
Convert the text to uppercase"""
|
||||
role = SkAgent()
|
||||
|
||||
# let's give the agent some skills
|
||||
role.import_semantic_skill_from_directory(SKILL_DIRECTORY, "SummarizeSkill")
|
||||
role.import_semantic_skill_from_directory(SKILL_DIRECTORY, "WriterSkill")
|
||||
role.import_skill(TextSkill(), "TextSkill")
|
||||
# using BasicPlanner
|
||||
await role.run(Message(content=task, cause_by=BossRequirement))
|
||||
|
||||
|
||||
async def sequential_planner_example():
|
||||
task = """
|
||||
Tomorrow is Valentine's day. I need to come up with a few date ideas. She speaks French so write it in French.
|
||||
Convert the text to uppercase"""
|
||||
role = SkAgent(planner_cls=SequentialPlanner)
|
||||
|
||||
# let's give the agent some skills
|
||||
role.import_semantic_skill_from_directory(SKILL_DIRECTORY, "SummarizeSkill")
|
||||
role.import_semantic_skill_from_directory(SKILL_DIRECTORY, "WriterSkill")
|
||||
role.import_skill(TextSkill(), "TextSkill")
|
||||
# using BasicPlanner
|
||||
await role.run(Message(content=task, cause_by=BossRequirement))
|
||||
|
||||
|
||||
async def basic_planner_web_search_example():
|
||||
task = """
|
||||
Question: Who made the 1989 comic book, the film version of which Jon Raymond Polito appeared in?"""
|
||||
role = SkAgent()
|
||||
|
||||
role.import_skill(SkSearchEngine(), "WebSearchSkill")
|
||||
# role.import_semantic_skill_from_directory(skills_directory, "QASkill")
|
||||
|
||||
await role.run(Message(content=task, cause_by=BossRequirement))
|
||||
|
||||
|
||||
async def action_planner_example():
|
||||
role = SkAgent(planner_cls=ActionPlanner)
|
||||
# let's give the agent 4 skills
|
||||
role.import_skill(MathSkill(), "math")
|
||||
role.import_skill(FileIOSkill(), "fileIO")
|
||||
role.import_skill(TimeSkill(), "time")
|
||||
role.import_skill(TextSkill(), "text")
|
||||
task = "What is the sum of 110 and 990?"
|
||||
await role.run(Message(content=task, cause_by=BossRequirement)) # it will choose mathskill.Add
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
18
examples/use_off_the_shelf_agent.py
Normal file
18
examples/use_off_the_shelf_agent.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
'''
|
||||
Filename: MetaGPT/examples/use_off_the_shelf_agent.py
|
||||
Created Date: Tuesday, September 19th 2023, 6:52:25 pm
|
||||
Author: garylin2099
|
||||
'''
|
||||
import asyncio
|
||||
|
||||
from metagpt.roles.product_manager import ProductManager
|
||||
from metagpt.logs import logger
|
||||
|
||||
async def main():
|
||||
msg = "Write a PRD for a snake game"
|
||||
role = ProductManager()
|
||||
result = await role.run(msg)
|
||||
logger.info(result.content[:100])
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
21
examples/write_tutorial.py
Normal file
21
examples/write_tutorial.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env python3
|
||||
# _*_ coding: utf-8 _*_
|
||||
"""
|
||||
@Time : 2023/9/4 21:40:57
|
||||
@Author : Stitch-z
|
||||
@File : tutorial_assistant.py
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
from metagpt.roles.tutorial_assistant import TutorialAssistant
|
||||
|
||||
|
||||
async def main():
|
||||
topic = "Write a tutorial about MySQL"
|
||||
role = TutorialAssistant(language="Chinese")
|
||||
await role.run(topic)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
|
||||
4
mc_requirements.txt
Normal file
4
mc_requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
javascript
|
||||
requests
|
||||
psutil
|
||||
chromadb==0.3.29
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Time : 2023/4/24 22:26
|
||||
# @Author : alexanderwu
|
||||
# @File : __init__.py
|
||||
|
||||
from metagpt import _compat as _ # noqa: F401
|
||||
|
|
|
|||
20
metagpt/_compat.py
Normal file
20
metagpt/_compat.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import platform
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
if sys.implementation.name == "cpython" and platform.system() == "Windows" and sys.version_info[:2] == (3, 9):
|
||||
import asyncio
|
||||
from asyncio.proactor_events import _ProactorBasePipeTransport
|
||||
|
||||
from semantic_kernel.orchestration import sk_function as _ # noqa: F401
|
||||
|
||||
# https://github.com/python/cpython/pull/92842
|
||||
def pacth_del(self, _warn=warnings.warn):
|
||||
if self._sock is not None:
|
||||
_warn(f"unclosed transport {self!r}", ResourceWarning, source=self)
|
||||
self._sock.close()
|
||||
|
||||
_ProactorBasePipeTransport.__del__ = pacth_del
|
||||
|
||||
# caused by https://github.com/microsoft/semantic-kernel/pull/1416
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
||||
|
|
@ -10,41 +10,32 @@ from enum import Enum
|
|||
from metagpt.actions.action import Action
|
||||
from metagpt.actions.action_output import ActionOutput
|
||||
from metagpt.actions.add_requirement import BossRequirement
|
||||
'''
|
||||
from metagpt.actions.debug_error import DebugError
|
||||
from metagpt.actions.design_api import WriteDesign
|
||||
from metagpt.actions.design_api_review import DesignReview
|
||||
from metagpt.actions.design_filenames import DesignFilenames
|
||||
from metagpt.actions.project_management import AssignTasks, WriteTasks
|
||||
from metagpt.actions.research import CollectLinks, WebBrowseAndSummarize, ConductResearch
|
||||
from metagpt.actions.run_code import RunCode
|
||||
from metagpt.actions.search_and_summarize import SearchAndSummarize
|
||||
#from metagpt.actions.search_and_summarize import SearchAndSummarize
|
||||
from metagpt.actions.write_code import WriteCode
|
||||
from metagpt.actions.write_code_review import WriteCodeReview
|
||||
from metagpt.actions.write_prd import WritePRD
|
||||
from metagpt.actions.write_prd_review import WritePRDReview
|
||||
from metagpt.actions.write_test import WriteTest
|
||||
|
||||
'''
|
||||
|
||||
class ActionType(Enum):
|
||||
"""All types of Actions, used for indexing."""
|
||||
|
||||
ADD_REQUIREMENT = BossRequirement
|
||||
WRITE_PRD = WritePRD
|
||||
WRITE_PRD_REVIEW = WritePRDReview
|
||||
WRITE_DESIGN = WriteDesign
|
||||
DESIGN_REVIEW = DesignReview
|
||||
DESIGN_FILENAMES = DesignFilenames
|
||||
WRTIE_CODE = WriteCode
|
||||
WRITE_CODE_REVIEW = WriteCodeReview
|
||||
WRITE_TEST = WriteTest
|
||||
RUN_CODE = RunCode
|
||||
DEBUG_ERROR = DebugError
|
||||
WRITE_TASKS = WriteTasks
|
||||
ASSIGN_TASKS = AssignTasks
|
||||
SEARCH_AND_SUMMARIZE = SearchAndSummarize
|
||||
COLLECT_LINKS = CollectLinks
|
||||
WEB_BROWSE_AND_SUMMARIZE = WebBrowseAndSummarize
|
||||
CONDUCT_RESEARCH = ConductResearch
|
||||
#WRITE_PRD = WritePRD
|
||||
#WRITE_DESIGN = WriteDesign
|
||||
#WRTIE_CODE = WriteCode
|
||||
#WRITE_CODE_REVIEW = WriteCodeReview
|
||||
#WRITE_TEST = WriteTest
|
||||
#RUN_CODE = RunCode
|
||||
#DEBUG_ERROR = DebugError
|
||||
#WRITE_TASKS = WriteTasks
|
||||
#ASSIGN_TASKS = AssignTasks
|
||||
# SEARCH_AND_SUMMARIZE = SearchAndSummarize
|
||||
|
||||
|
||||
__all__ = [
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
@Author : alexanderwu
|
||||
@File : action.py
|
||||
"""
|
||||
import re
|
||||
from abc import ABC
|
||||
from typing import Optional
|
||||
|
||||
|
|
@ -12,11 +13,13 @@ from tenacity import retry, stop_after_attempt, wait_fixed
|
|||
|
||||
from metagpt.actions.action_output import ActionOutput
|
||||
from metagpt.llm import LLM
|
||||
from metagpt.utils.common import OutputParser
|
||||
from metagpt.logs import logger
|
||||
from metagpt.utils.common import OutputParser
|
||||
from metagpt.utils.custom_decoder import CustomDecoder
|
||||
|
||||
|
||||
class Action(ABC):
|
||||
def __init__(self, name: str = '', context=None, llm: LLM = None):
|
||||
def __init__(self, name: str = "", context=None, llm: LLM = None):
|
||||
self.name: str = name
|
||||
if llm is None:
|
||||
llm = LLM()
|
||||
|
|
@ -46,10 +49,15 @@ class Action(ABC):
|
|||
system_msgs.append(self.prefix)
|
||||
return await self.llm.aask(prompt, system_msgs)
|
||||
|
||||
@retry(stop=stop_after_attempt(2), wait=wait_fixed(1))
|
||||
async def _aask_v1(self, prompt: str, output_class_name: str,
|
||||
output_data_mapping: dict,
|
||||
system_msgs: Optional[list[str]] = None) -> ActionOutput:
|
||||
@retry(stop=stop_after_attempt(3), wait=wait_fixed(1))
|
||||
async def _aask_v1(
|
||||
self,
|
||||
prompt: str,
|
||||
output_class_name: str,
|
||||
output_data_mapping: dict,
|
||||
system_msgs: Optional[list[str]] = None,
|
||||
format="markdown", # compatible to original format
|
||||
) -> ActionOutput:
|
||||
"""Append default prefix"""
|
||||
if not system_msgs:
|
||||
system_msgs = []
|
||||
|
|
@ -57,7 +65,21 @@ class Action(ABC):
|
|||
content = await self.llm.aask(prompt, system_msgs)
|
||||
logger.debug(content)
|
||||
output_class = ActionOutput.create_model_class(output_class_name, output_data_mapping)
|
||||
parsed_data = OutputParser.parse_data_with_mapping(content, output_data_mapping)
|
||||
|
||||
if format == "json":
|
||||
pattern = r"\[CONTENT\](\s*\{.*?\}\s*)\[/CONTENT\]"
|
||||
matches = re.findall(pattern, content, re.DOTALL)
|
||||
|
||||
for match in matches:
|
||||
if match:
|
||||
content = match
|
||||
break
|
||||
|
||||
parsed_data = CustomDecoder(strict=False).decode(content)
|
||||
|
||||
else: # using markdown parser
|
||||
parsed_data = OutputParser.parse_data_with_mapping(content, output_data_mapping)
|
||||
|
||||
logger.debug(parsed_data)
|
||||
instruct_content = output_class(**parsed_data)
|
||||
return ActionOutput(content, instruct_content)
|
||||
|
|
@ -65,4 +87,3 @@ class Action(ABC):
|
|||
async def run(self, *args, **kwargs):
|
||||
"""Run action"""
|
||||
raise NotImplementedError("The run method should be implemented in a subclass.")
|
||||
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023/5/19 12:01
|
||||
@Author : alexanderwu
|
||||
@File : analyze_dep_libs.py
|
||||
"""
|
||||
|
||||
from metagpt.actions import Action
|
||||
|
||||
PROMPT = """You are an AI developer, trying to write a program that generates code for users based on their intentions.
|
||||
|
||||
For the user's prompt:
|
||||
|
||||
---
|
||||
The API is: {prompt}
|
||||
---
|
||||
|
||||
We decide the generated files are: {filepaths_string}
|
||||
|
||||
Now that we have a file list, we need to understand the shared dependencies they have.
|
||||
Please list and briefly describe the shared contents between the files we are generating, including exported variables,
|
||||
data patterns, id names of all DOM elements that javascript functions will use, message names and function names.
|
||||
Focus only on the names of shared dependencies, do not add any other explanations.
|
||||
"""
|
||||
|
||||
|
||||
class AnalyzeDepLibs(Action):
|
||||
def __init__(self, name, context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
self.desc = "Analyze the runtime dependencies of the program based on the context"
|
||||
|
||||
async def run(self, requirement, filepaths_string):
|
||||
# prompt = f"Below is the product requirement document (PRD):\n\n{prd}\n\n{PROMPT}"
|
||||
prompt = PROMPT.format(prompt=requirement, filepaths_string=filepaths_string)
|
||||
design_filenames = await self._aask(prompt)
|
||||
return design_filenames
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023/6/9 22:22
|
||||
@Author : Leo Xiao
|
||||
@File : azure_tts.py
|
||||
"""
|
||||
from azure.cognitiveservices.speech import AudioConfig, SpeechConfig, SpeechSynthesizer
|
||||
|
||||
from metagpt.actions.action import Action
|
||||
from metagpt.config import Config
|
||||
|
||||
|
||||
class AzureTTS(Action):
|
||||
def __init__(self, name, context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
self.config = Config()
|
||||
|
||||
# Parameters reference: https://learn.microsoft.com/zh-cn/azure/cognitive-services/speech-service/language-support?tabs=tts#voice-styles-and-roles
|
||||
def synthesize_speech(self, lang, voice, role, text, output_file):
|
||||
subscription_key = self.config.get('AZURE_TTS_SUBSCRIPTION_KEY')
|
||||
region = self.config.get('AZURE_TTS_REGION')
|
||||
speech_config = SpeechConfig(
|
||||
subscription=subscription_key, region=region)
|
||||
|
||||
speech_config.speech_synthesis_voice_name = voice
|
||||
audio_config = AudioConfig(filename=output_file)
|
||||
synthesizer = SpeechSynthesizer(
|
||||
speech_config=speech_config,
|
||||
audio_config=audio_config)
|
||||
|
||||
# if voice=="zh-CN-YunxiNeural":
|
||||
ssml_string = f"""
|
||||
<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='{lang}' xmlns:mstts='http://www.w3.org/2001/mstts'>
|
||||
<voice name='{voice}'>
|
||||
<mstts:express-as style='affectionate' role='{role}'>
|
||||
{text}
|
||||
</mstts:express-as>
|
||||
</voice>
|
||||
</speak>
|
||||
"""
|
||||
|
||||
synthesizer.speak_ssml_async(ssml_string).get()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
azure_tts = AzureTTS("azure_tts")
|
||||
azure_tts.synthesize_speech(
|
||||
"zh-CN",
|
||||
"zh-CN-YunxiNeural",
|
||||
"Boy",
|
||||
"Hello, I am Kaka",
|
||||
"output.wav")
|
||||
|
|
@ -10,12 +10,69 @@ from pathlib import Path
|
|||
from typing import List
|
||||
|
||||
from metagpt.actions import Action, ActionOutput
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.const import WORKSPACE_ROOT
|
||||
from metagpt.logs import logger
|
||||
from metagpt.utils.common import CodeParser
|
||||
from metagpt.utils.get_template import get_template
|
||||
from metagpt.utils.json_to_markdown import json_to_markdown
|
||||
from metagpt.utils.mermaid import mermaid_to_file
|
||||
|
||||
PROMPT_TEMPLATE = """
|
||||
templates = {
|
||||
"json": {
|
||||
"PROMPT_TEMPLATE": """
|
||||
# Context
|
||||
{context}
|
||||
|
||||
## Format example
|
||||
{format_example}
|
||||
-----
|
||||
Role: You are an architect; the goal is to design a SOTA PEP8-compliant python system; make the best use of good open source tools
|
||||
Requirement: Fill in the following missing information based on the context, each section name is a key in json
|
||||
Max Output: 8192 chars or 2048 tokens. Try to use them up.
|
||||
|
||||
## Implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select the appropriate open-source framework.
|
||||
|
||||
## Python package name: Provide as Python str with python triple quoto, concise and clear, characters only use a combination of all lowercase and underscores
|
||||
|
||||
## File list: Provided as Python list[str], the list of ONLY REQUIRED files needed to write the program(LESS IS MORE!). Only need relative paths, comply with PEP8 standards. ALWAYS write a main.py or app.py here
|
||||
|
||||
## Data structures and interface definitions: Use mermaid classDiagram code syntax, including classes (INCLUDING __init__ method) and functions (with type annotations), CLEARLY MARK the RELATIONSHIPS between classes, and comply with PEP8 standards. The data structures SHOULD BE VERY DETAILED and the API should be comprehensive with a complete design.
|
||||
|
||||
## Program call flow: Use sequenceDiagram code syntax, COMPLETE and VERY DETAILED, using CLASSES AND API DEFINED ABOVE accurately, covering the CRUD AND INIT of each object, SYNTAX MUST BE CORRECT.
|
||||
|
||||
## Anything UNCLEAR: Provide as Plain text. Make clear here.
|
||||
|
||||
output a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example,
|
||||
and only output the json inside this tag, nothing else
|
||||
""",
|
||||
"FORMAT_EXAMPLE": """
|
||||
[CONTENT]
|
||||
{
|
||||
"Implementation approach": "We will ...",
|
||||
"Python package name": "snake_game",
|
||||
"File list": ["main.py"],
|
||||
"Data structures and interface definitions": '
|
||||
classDiagram
|
||||
class Game{
|
||||
+int score
|
||||
}
|
||||
...
|
||||
Game "1" -- "1" Food: has
|
||||
',
|
||||
"Program call flow": '
|
||||
sequenceDiagram
|
||||
participant M as Main
|
||||
...
|
||||
G->>M: end game
|
||||
',
|
||||
"Anything UNCLEAR": "The requirement is clear to me."
|
||||
}
|
||||
[/CONTENT]
|
||||
""",
|
||||
},
|
||||
"markdown": {
|
||||
"PROMPT_TEMPLATE": """
|
||||
# Context
|
||||
{context}
|
||||
|
||||
|
|
@ -39,8 +96,8 @@ Attention: Use '##' to split sections, not '#', and '## <SECTION_NAME>' SHOULD W
|
|||
|
||||
## Anything UNCLEAR: Provide as Plain text. Make clear here.
|
||||
|
||||
"""
|
||||
FORMAT_EXAMPLE = """
|
||||
""",
|
||||
"FORMAT_EXAMPLE": """
|
||||
---
|
||||
## Implementation approach
|
||||
We will ...
|
||||
|
|
@ -78,7 +135,10 @@ sequenceDiagram
|
|||
## Anything UNCLEAR
|
||||
The requirement is clear to me.
|
||||
---
|
||||
"""
|
||||
""",
|
||||
},
|
||||
}
|
||||
|
||||
OUTPUT_MAPPING = {
|
||||
"Implementation approach": (str, ...),
|
||||
"Python package name": (str, ...),
|
||||
|
|
@ -92,9 +152,11 @@ OUTPUT_MAPPING = {
|
|||
class WriteDesign(Action):
|
||||
def __init__(self, name, context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
self.desc = "Based on the PRD, think about the system design, and design the corresponding APIs, " \
|
||||
"data structures, library tables, processes, and paths. Please provide your design, feedback " \
|
||||
"clearly and in detail."
|
||||
self.desc = (
|
||||
"Based on the PRD, think about the system design, and design the corresponding APIs, "
|
||||
"data structures, library tables, processes, and paths. Please provide your design, feedback "
|
||||
"clearly and in detail."
|
||||
)
|
||||
|
||||
def recreate_workspace(self, workspace: Path):
|
||||
try:
|
||||
|
|
@ -103,42 +165,47 @@ class WriteDesign(Action):
|
|||
pass # Folder does not exist, but we don't care
|
||||
workspace.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _save_prd(self, docs_path, resources_path, prd):
|
||||
prd_file = docs_path / 'prd.md'
|
||||
quadrant_chart = CodeParser.parse_code(block="Competitive Quadrant Chart", text=prd)
|
||||
mermaid_to_file(quadrant_chart, resources_path / 'competitive_analysis')
|
||||
logger.info(f"Saving PRD to {prd_file}")
|
||||
prd_file.write_text(prd)
|
||||
async def _save_prd(self, docs_path, resources_path, context):
|
||||
prd_file = docs_path / "prd.md"
|
||||
if context[-1].instruct_content and context[-1].instruct_content.dict()["Competitive Quadrant Chart"]:
|
||||
quadrant_chart = context[-1].instruct_content.dict()["Competitive Quadrant Chart"]
|
||||
await mermaid_to_file(quadrant_chart, resources_path / "competitive_analysis")
|
||||
|
||||
def _save_system_design(self, docs_path, resources_path, content):
|
||||
data_api_design = CodeParser.parse_code(block="Data structures and interface definitions", text=content)
|
||||
seq_flow = CodeParser.parse_code(block="Program call flow", text=content)
|
||||
mermaid_to_file(data_api_design, resources_path / 'data_api_design')
|
||||
mermaid_to_file(seq_flow, resources_path / 'seq_flow')
|
||||
system_design_file = docs_path / 'system_design.md'
|
||||
if context[-1].instruct_content:
|
||||
logger.info(f"Saving PRD to {prd_file}")
|
||||
prd_file.write_text(json_to_markdown(context[-1].instruct_content.dict()))
|
||||
|
||||
async def _save_system_design(self, docs_path, resources_path, system_design):
|
||||
data_api_design = system_design.instruct_content.dict()[
|
||||
"Data structures and interface definitions"
|
||||
] # CodeParser.parse_code(block="Data structures and interface definitions", text=content)
|
||||
seq_flow = system_design.instruct_content.dict()[
|
||||
"Program call flow"
|
||||
] # CodeParser.parse_code(block="Program call flow", text=content)
|
||||
await mermaid_to_file(data_api_design, resources_path / "data_api_design")
|
||||
await mermaid_to_file(seq_flow, resources_path / "seq_flow")
|
||||
system_design_file = docs_path / "system_design.md"
|
||||
logger.info(f"Saving System Designs to {system_design_file}")
|
||||
system_design_file.write_text(content)
|
||||
system_design_file.write_text((json_to_markdown(system_design.instruct_content.dict())))
|
||||
|
||||
def _save(self, context, system_design):
|
||||
async def _save(self, context, system_design):
|
||||
if isinstance(system_design, ActionOutput):
|
||||
content = system_design.content
|
||||
ws_name = CodeParser.parse_str(block="Python package name", text=content)
|
||||
ws_name = system_design.instruct_content.dict()["Python package name"]
|
||||
else:
|
||||
content = system_design
|
||||
ws_name = CodeParser.parse_str(block="Python package name", text=system_design)
|
||||
workspace = WORKSPACE_ROOT / ws_name
|
||||
self.recreate_workspace(workspace)
|
||||
docs_path = workspace / 'docs'
|
||||
resources_path = workspace / 'resources'
|
||||
docs_path = workspace / "docs"
|
||||
resources_path = workspace / "resources"
|
||||
docs_path.mkdir(parents=True, exist_ok=True)
|
||||
resources_path.mkdir(parents=True, exist_ok=True)
|
||||
self._save_prd(docs_path, resources_path, context[-1].content)
|
||||
self._save_system_design(docs_path, resources_path, content)
|
||||
await self._save_prd(docs_path, resources_path, context)
|
||||
await self._save_system_design(docs_path, resources_path, system_design)
|
||||
|
||||
async def run(self, context):
|
||||
prompt = PROMPT_TEMPLATE.format(context=context, format_example=FORMAT_EXAMPLE)
|
||||
async def run(self, context, format=CONFIG.prompt_format):
|
||||
prompt_template, format_example = get_template(templates, format)
|
||||
prompt = prompt_template.format(context=context, format_example=format_example)
|
||||
# system_design = await self._aask(prompt)
|
||||
system_design = await self._aask_v1(prompt, "system_design", OUTPUT_MAPPING)
|
||||
self._save(context, system_design)
|
||||
system_design = await self._aask_v1(prompt, "system_design", OUTPUT_MAPPING, format=format)
|
||||
await self._save(context, system_design)
|
||||
return system_design
|
||||
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023/5/11 19:31
|
||||
@Author : alexanderwu
|
||||
@File : design_api_review.py
|
||||
"""
|
||||
from metagpt.actions.action import Action
|
||||
|
||||
|
||||
class DesignReview(Action):
|
||||
def __init__(self, name, context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
|
||||
async def run(self, prd, api_design):
|
||||
prompt = f"Here is the Product Requirement Document (PRD):\n\n{prd}\n\nHere is the list of APIs designed " \
|
||||
f"based on this PRD:\n\n{api_design}\n\nPlease review whether this API design meets the requirements" \
|
||||
f" of the PRD, and whether it complies with good design practices."
|
||||
|
||||
api_review = await self._aask(prompt)
|
||||
return api_review
|
||||
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023/5/19 11:50
|
||||
@Author : alexanderwu
|
||||
@File : design_filenames.py
|
||||
"""
|
||||
from metagpt.actions import Action
|
||||
from metagpt.logs import logger
|
||||
|
||||
PROMPT = """You are an AI developer, trying to write a program that generates code for users based on their intentions.
|
||||
When given their intentions, provide a complete and exhaustive list of file paths needed to write the program for the user.
|
||||
Only list the file paths you will write and return them as a Python string list.
|
||||
Do not add any other explanations, just return a Python string list."""
|
||||
|
||||
|
||||
class DesignFilenames(Action):
|
||||
def __init__(self, name, context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
self.desc = "Based on the PRD, consider system design, and carry out the basic design of the corresponding " \
|
||||
"APIs, data structures, and database tables. Please give your design, feedback clearly and in detail."
|
||||
|
||||
async def run(self, prd):
|
||||
prompt = f"The following is the Product Requirement Document (PRD):\n\n{prd}\n\n{PROMPT}"
|
||||
design_filenames = await self._aask(prompt)
|
||||
logger.debug(prompt)
|
||||
logger.debug(design_filenames)
|
||||
return design_filenames
|
||||
|
||||
34
metagpt/actions/minecraft/__init__.py
Normal file
34
metagpt/actions/minecraft/__init__.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# @Date : 2023/9/23 14:26
|
||||
# @Author : stellahong (stellahong@fuzhi.ai)
|
||||
# @Desc :
|
||||
from enum import Enum
|
||||
|
||||
from metagpt.actions.action import Action
|
||||
from metagpt.actions.action_output import ActionOutput
|
||||
from metagpt.actions.minecraft.design_curriculumn import DesignTask, DesignCurriculum
|
||||
from metagpt.actions.minecraft.generate_actions import GenerateActionCode
|
||||
from metagpt.actions.minecraft.manage_skills import RetrieveSkills, GenerateSkillDescription, AddNewSkills
|
||||
from metagpt.actions.minecraft.review_task import VerifyTask
|
||||
from metagpt.actions.minecraft.player_action import PlayerActions
|
||||
|
||||
|
||||
class ActionType(Enum):
|
||||
"""All types of Actions, used for indexing."""
|
||||
|
||||
Design_Task = DesignTask
|
||||
Design_Curriculum = DesignCurriculum
|
||||
Generate_Action_Code = GenerateActionCode
|
||||
Retrieve_Skills = RetrieveSkills
|
||||
Generate_Skill_Description = GenerateSkillDescription
|
||||
Add_New_Skills = AddNewSkills
|
||||
Verify_Task = VerifyTask
|
||||
Player_Actions = PlayerActions
|
||||
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ActionType",
|
||||
"Action",
|
||||
"ActionOutput",
|
||||
]
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"tabWidth": 4
|
||||
}
|
||||
16
metagpt/actions/minecraft/control_primitives/__init__.py
Normal file
16
metagpt/actions/minecraft/control_primitives/__init__.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import os
|
||||
import metagpt.utils.minecraft as utils
|
||||
from metagpt.logs import logger
|
||||
|
||||
|
||||
def load_skills_code(skill_names=None):
|
||||
skills_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
if skill_names is None:
|
||||
skill_names = [
|
||||
skill[:-3] for skill in os.listdir(f"{skills_dir}") if skill.endswith(".js")
|
||||
]
|
||||
skills = [
|
||||
utils.load_text(os.path.join(skills_dir, f"{skill_name}.js"))
|
||||
for skill_name in skill_names
|
||||
]
|
||||
return skills
|
||||
61
metagpt/actions/minecraft/control_primitives/craftHelper.js
Normal file
61
metagpt/actions/minecraft/control_primitives/craftHelper.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
function failedCraftFeedback(bot, name, item, craftingTable) {
|
||||
const recipes = bot.recipesAll(item.id, null, craftingTable);
|
||||
if (!recipes.length) {
|
||||
throw new Error(`No crafting table nearby`);
|
||||
} else {
|
||||
const recipes = bot.recipesAll(
|
||||
item.id,
|
||||
null,
|
||||
mcData.blocksByName.crafting_table.id
|
||||
);
|
||||
// find the recipe with the fewest missing ingredients
|
||||
var min = 999;
|
||||
var min_recipe = null;
|
||||
for (const recipe of recipes) {
|
||||
const delta = recipe.delta;
|
||||
var missing = 0;
|
||||
for (const delta_item of delta) {
|
||||
if (delta_item.count < 0) {
|
||||
const inventory_item = bot.inventory.findInventoryItem(
|
||||
mcData.items[delta_item.id].name,
|
||||
null
|
||||
);
|
||||
if (!inventory_item) {
|
||||
missing += -delta_item.count;
|
||||
} else {
|
||||
missing += Math.max(
|
||||
-delta_item.count - inventory_item.count,
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (missing < min) {
|
||||
min = missing;
|
||||
min_recipe = recipe;
|
||||
}
|
||||
}
|
||||
const delta = min_recipe.delta;
|
||||
let message = "";
|
||||
for (const delta_item of delta) {
|
||||
if (delta_item.count < 0) {
|
||||
const inventory_item = bot.inventory.findInventoryItem(
|
||||
mcData.items[delta_item.id].name,
|
||||
null
|
||||
);
|
||||
if (!inventory_item) {
|
||||
message += ` ${-delta_item.count} more ${
|
||||
mcData.items[delta_item.id].name
|
||||
}, `;
|
||||
} else {
|
||||
if (inventory_item.count < -delta_item.count) {
|
||||
message += `${
|
||||
-delta_item.count - inventory_item.count
|
||||
} more ${mcData.items[delta_item.id].name}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
bot.chat(`I cannot make ${name} because I need: ${message}`);
|
||||
}
|
||||
}
|
||||
43
metagpt/actions/minecraft/control_primitives/craftItem.js
Normal file
43
metagpt/actions/minecraft/control_primitives/craftItem.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
async function craftItem(bot, name, count = 1) {
|
||||
// return if name is not string
|
||||
if (typeof name !== "string") {
|
||||
throw new Error("name for craftItem must be a string");
|
||||
}
|
||||
// return if count is not number
|
||||
if (typeof count !== "number") {
|
||||
throw new Error("count for craftItem must be a number");
|
||||
}
|
||||
const itemByName = mcData.itemsByName[name];
|
||||
if (!itemByName) {
|
||||
throw new Error(`No item named ${name}`);
|
||||
}
|
||||
const craftingTable = bot.findBlock({
|
||||
matching: mcData.blocksByName.crafting_table.id,
|
||||
maxDistance: 32,
|
||||
});
|
||||
if (!craftingTable) {
|
||||
bot.chat("Craft without a crafting table");
|
||||
} else {
|
||||
await bot.pathfinder.goto(
|
||||
new GoalLookAtBlock(craftingTable.position, bot.world)
|
||||
);
|
||||
}
|
||||
const recipe = bot.recipesFor(itemByName.id, null, 1, craftingTable)[0];
|
||||
if (recipe) {
|
||||
bot.chat(`I can make ${name}`);
|
||||
try {
|
||||
await bot.craft(recipe, count, craftingTable);
|
||||
bot.chat(`I did the recipe for ${name} ${count} times`);
|
||||
} catch (err) {
|
||||
bot.chat(`I cannot do the recipe for ${name} ${count} times`);
|
||||
}
|
||||
} else {
|
||||
failedCraftFeedback(bot, name, itemByName, craftingTable);
|
||||
_craftItemFailCount++;
|
||||
if (_craftItemFailCount > 10) {
|
||||
throw new Error(
|
||||
"craftItem failed too many times, check chat log to see what happened"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
87
metagpt/actions/minecraft/control_primitives/exploreUntil.js
Normal file
87
metagpt/actions/minecraft/control_primitives/exploreUntil.js
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
// Explore downward for 60 seconds: exploreUntil(bot, new Vec3(0, -1, 0), 60);
|
||||
async function exploreUntil(
|
||||
bot,
|
||||
direction,
|
||||
maxTime = 60,
|
||||
callback = () => {
|
||||
return false;
|
||||
}
|
||||
) {
|
||||
if (typeof maxTime !== "number") {
|
||||
throw new Error("maxTime must be a number");
|
||||
}
|
||||
if (typeof callback !== "function") {
|
||||
throw new Error("callback must be a function");
|
||||
}
|
||||
const test = callback();
|
||||
if (test) {
|
||||
bot.chat("Explore success.");
|
||||
return Promise.resolve(test);
|
||||
}
|
||||
if (direction.x === 0 && direction.y === 0 && direction.z === 0) {
|
||||
throw new Error("direction cannot be 0, 0, 0");
|
||||
}
|
||||
if (
|
||||
!(
|
||||
(direction.x === 0 || direction.x === 1 || direction.x === -1) &&
|
||||
(direction.y === 0 || direction.y === 1 || direction.y === -1) &&
|
||||
(direction.z === 0 || direction.z === 1 || direction.z === -1)
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
"direction must be a Vec3 only with value of -1, 0 or 1"
|
||||
);
|
||||
}
|
||||
maxTime = Math.min(maxTime, 1200);
|
||||
return new Promise((resolve, reject) => {
|
||||
const dx = direction.x;
|
||||
const dy = direction.y;
|
||||
const dz = direction.z;
|
||||
|
||||
let explorationInterval;
|
||||
let maxTimeTimeout;
|
||||
|
||||
const cleanUp = () => {
|
||||
clearInterval(explorationInterval);
|
||||
clearTimeout(maxTimeTimeout);
|
||||
bot.pathfinder.setGoal(null);
|
||||
};
|
||||
|
||||
const explore = () => {
|
||||
const x =
|
||||
bot.entity.position.x +
|
||||
Math.floor(Math.random() * 20 + 10) * dx;
|
||||
const y =
|
||||
bot.entity.position.y +
|
||||
Math.floor(Math.random() * 20 + 10) * dy;
|
||||
const z =
|
||||
bot.entity.position.z +
|
||||
Math.floor(Math.random() * 20 + 10) * dz;
|
||||
let goal = new GoalNear(x, y, z);
|
||||
if (dy === 0) {
|
||||
goal = new GoalNearXZ(x, z);
|
||||
}
|
||||
bot.pathfinder.setGoal(goal);
|
||||
|
||||
try {
|
||||
const result = callback();
|
||||
if (result) {
|
||||
cleanUp();
|
||||
bot.chat("Explore success.");
|
||||
resolve(result);
|
||||
}
|
||||
} catch (err) {
|
||||
cleanUp();
|
||||
reject(err);
|
||||
}
|
||||
};
|
||||
|
||||
explorationInterval = setInterval(explore, 2000);
|
||||
|
||||
maxTimeTimeout = setTimeout(() => {
|
||||
cleanUp();
|
||||
bot.chat("Max exploration time reached");
|
||||
resolve(null);
|
||||
}, maxTime * 1000);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
async function givePlacedItemBack(bot, name, position) {
|
||||
await bot.chat("/gamerule doTileDrops false");
|
||||
// iterate name and position
|
||||
const history = [];
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
await givePlacedItemBackSingle(bot, name[i], position[i]);
|
||||
}
|
||||
await bot.chat("/gamerule doTileDrops true");
|
||||
|
||||
async function givePlacedItemBackSingle(bot, name, position) {
|
||||
bot.chat(`/give bot ${name} 1`);
|
||||
const x = Math.floor(position.x);
|
||||
const y = Math.floor(position.y);
|
||||
const z = Math.floor(position.z);
|
||||
// loop through 125 blocks around the block
|
||||
const size = 3;
|
||||
for (let dx = -size; dx <= size; dx++) {
|
||||
for (let dy = -size; dy <= size; dy++) {
|
||||
for (let dz = -size; dz <= size; dz++) {
|
||||
const block = bot.blockAt(new Vec3(x + dx, y + dy, z + dz));
|
||||
if (
|
||||
block?.name === name &&
|
||||
!history.includes(block.position)
|
||||
) {
|
||||
await bot.chat(
|
||||
`/setblock ${x + dx} ${y + dy} ${
|
||||
z + dz
|
||||
} air destroy`
|
||||
);
|
||||
history.push(block.position);
|
||||
await bot.waitForTicks(20);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
51
metagpt/actions/minecraft/control_primitives/killMob.js
Normal file
51
metagpt/actions/minecraft/control_primitives/killMob.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
async function killMob(bot, mobName, timeout = 300) {
|
||||
// return if mobName is not string
|
||||
if (typeof mobName !== "string") {
|
||||
throw new Error(`mobName for killMob must be a string`);
|
||||
}
|
||||
// return if timeout is not number
|
||||
if (typeof timeout !== "number") {
|
||||
throw new Error(`timeout for killMob must be a number`);
|
||||
}
|
||||
|
||||
const weaponsForShooting = [
|
||||
"bow",
|
||||
"crossbow",
|
||||
"snowball",
|
||||
"ender_pearl",
|
||||
"egg",
|
||||
"splash_potion",
|
||||
"trident",
|
||||
];
|
||||
const mainHandItem = bot.inventory.slots[bot.getEquipmentDestSlot("hand")];
|
||||
|
||||
const entity = bot.nearestEntity(
|
||||
(entity) =>
|
||||
entity.name === mobName &&
|
||||
// kill mob distance should be slightly bigger than explore distance
|
||||
entity.position.distanceTo(bot.entity.position) < 48
|
||||
);
|
||||
if (!entity) {
|
||||
bot.chat(`No ${mobName} nearby, please explore first`);
|
||||
_killMobFailCount++;
|
||||
if (_killMobFailCount > 10) {
|
||||
throw new Error(
|
||||
`killMob failed too many times, make sure you explore before calling killMob`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let droppedItem;
|
||||
if (mainHandItem && weaponsForShooting.includes(mainHandItem.name)) {
|
||||
bot.hawkEye.autoAttack(entity, mainHandItem.name);
|
||||
droppedItem = await waitForMobShot(bot, entity, timeout);
|
||||
} else {
|
||||
await bot.pvp.attack(entity);
|
||||
droppedItem = await waitForMobRemoved(bot, entity, timeout);
|
||||
}
|
||||
if (droppedItem) {
|
||||
await bot.collectBlock.collect(droppedItem, { ignoreNoPath: true });
|
||||
}
|
||||
bot.save(`${mobName}_killed`);
|
||||
}
|
||||
37
metagpt/actions/minecraft/control_primitives/mineBlock.js
Normal file
37
metagpt/actions/minecraft/control_primitives/mineBlock.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
async function mineBlock(bot, name, count = 1) {
|
||||
// return if name is not string
|
||||
if (typeof name !== "string") {
|
||||
throw new Error(`name for mineBlock must be a string`);
|
||||
}
|
||||
if (typeof count !== "number") {
|
||||
throw new Error(`count for mineBlock must be a number`);
|
||||
}
|
||||
const blockByName = mcData.blocksByName[name];
|
||||
if (!blockByName) {
|
||||
throw new Error(`No block named ${name}`);
|
||||
}
|
||||
const blocks = bot.findBlocks({
|
||||
matching: [blockByName.id],
|
||||
maxDistance: 32,
|
||||
count: 1024,
|
||||
});
|
||||
if (blocks.length === 0) {
|
||||
bot.chat(`No ${name} nearby, please explore first`);
|
||||
_mineBlockFailCount++;
|
||||
if (_mineBlockFailCount > 10) {
|
||||
throw new Error(
|
||||
"mineBlock failed too many times, make sure you explore before calling mineBlock"
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const targets = [];
|
||||
for (let i = 0; i < blocks.length; i++) {
|
||||
targets.push(bot.blockAt(blocks[i]));
|
||||
}
|
||||
await bot.collectBlock.collect(targets, {
|
||||
ignoreNoPath: true,
|
||||
count: count,
|
||||
});
|
||||
bot.save(`${name}_mined`);
|
||||
}
|
||||
79
metagpt/actions/minecraft/control_primitives/placeItem.js
Normal file
79
metagpt/actions/minecraft/control_primitives/placeItem.js
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
async function placeItem(bot, name, position) {
|
||||
// return if name is not string
|
||||
if (typeof name !== "string") {
|
||||
throw new Error(`name for placeItem must be a string`);
|
||||
}
|
||||
// return if position is not Vec3
|
||||
if (!(position instanceof Vec3)) {
|
||||
throw new Error(`position for placeItem must be a Vec3`);
|
||||
}
|
||||
const itemByName = mcData.itemsByName[name];
|
||||
if (!itemByName) {
|
||||
throw new Error(`No item named ${name}`);
|
||||
}
|
||||
const item = bot.inventory.findInventoryItem(itemByName.id);
|
||||
if (!item) {
|
||||
bot.chat(`No ${name} in inventory`);
|
||||
return;
|
||||
}
|
||||
const item_count = item.count;
|
||||
// find a reference block
|
||||
const faceVectors = [
|
||||
new Vec3(0, 1, 0),
|
||||
new Vec3(0, -1, 0),
|
||||
new Vec3(1, 0, 0),
|
||||
new Vec3(-1, 0, 0),
|
||||
new Vec3(0, 0, 1),
|
||||
new Vec3(0, 0, -1),
|
||||
];
|
||||
let referenceBlock = null;
|
||||
let faceVector = null;
|
||||
for (const vector of faceVectors) {
|
||||
const block = bot.blockAt(position.minus(vector));
|
||||
if (block?.name !== "air") {
|
||||
referenceBlock = block;
|
||||
faceVector = vector;
|
||||
bot.chat(`Placing ${name} on ${block.name} at ${block.position}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!referenceBlock) {
|
||||
bot.chat(
|
||||
`No block to place ${name} on. You cannot place a floating block.`
|
||||
);
|
||||
_placeItemFailCount++;
|
||||
if (_placeItemFailCount > 10) {
|
||||
throw new Error(
|
||||
`placeItem failed too many times. You cannot place a floating block.`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// You must use try catch to placeBlock
|
||||
try {
|
||||
// You must first go to the block position you want to place
|
||||
await bot.pathfinder.goto(new GoalPlaceBlock(position, bot.world, {}));
|
||||
// You must equip the item right before calling placeBlock
|
||||
await bot.equip(item, "hand");
|
||||
await bot.placeBlock(referenceBlock, faceVector);
|
||||
bot.chat(`Placed ${name}`);
|
||||
bot.save(`${name}_placed`);
|
||||
} catch (err) {
|
||||
const item = bot.inventory.findInventoryItem(itemByName.id);
|
||||
if (item?.count === item_count) {
|
||||
bot.chat(
|
||||
`Error placing ${name}: ${err.message}, please find another position to place`
|
||||
);
|
||||
_placeItemFailCount++;
|
||||
if (_placeItemFailCount > 10) {
|
||||
throw new Error(
|
||||
`placeItem failed too many times, please find another position to place.`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
bot.chat(`Placed ${name}`);
|
||||
bot.save(`${name}_placed`);
|
||||
}
|
||||
}
|
||||
}
|
||||
34
metagpt/actions/minecraft/control_primitives/shoot.js
Normal file
34
metagpt/actions/minecraft/control_primitives/shoot.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// shoot 1 pig with a bow: shoot(bot, "bow", "pig");
|
||||
async function shoot(bot, weapon, target) {
|
||||
const validWeapons = [
|
||||
"bow",
|
||||
"crossbow",
|
||||
"snowball",
|
||||
"ender_pearl",
|
||||
"egg",
|
||||
"splash_potion",
|
||||
"trident",
|
||||
];
|
||||
if (!validWeapons.includes(weapon)) {
|
||||
bot.chat(`${weapon} is not a valid weapon for shooting`);
|
||||
return;
|
||||
}
|
||||
|
||||
const weaponItem = mcData.itemsByName[weapon];
|
||||
if (!bot.inventory.findInventoryItem(weaponItem.id, null)) {
|
||||
bot.chat(`No ${weapon} in inventory for shooting`);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetEntity = bot.nearestEntity(
|
||||
(entity) =>
|
||||
entity.name === target
|
||||
);
|
||||
if (!targetEntity) {
|
||||
bot.chat(`No ${target} nearby`);
|
||||
return;
|
||||
}
|
||||
bot.hawkEye.autoAttack(targetEntity, "bow");
|
||||
bot.on('auto_shot_stopped', (target) => {
|
||||
})
|
||||
}
|
||||
68
metagpt/actions/minecraft/control_primitives/smeltItem.js
Normal file
68
metagpt/actions/minecraft/control_primitives/smeltItem.js
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
async function smeltItem(bot, itemName, fuelName, count = 1) {
|
||||
// return if itemName or fuelName is not string
|
||||
if (typeof itemName !== "string" || typeof fuelName !== "string") {
|
||||
throw new Error("itemName or fuelName for smeltItem must be a string");
|
||||
}
|
||||
// return if count is not a number
|
||||
if (typeof count !== "number") {
|
||||
throw new Error("count for smeltItem must be a number");
|
||||
}
|
||||
const item = mcData.itemsByName[itemName];
|
||||
const fuel = mcData.itemsByName[fuelName];
|
||||
if (!item) {
|
||||
throw new Error(`No item named ${itemName}`);
|
||||
}
|
||||
if (!fuel) {
|
||||
throw new Error(`No item named ${fuelName}`);
|
||||
}
|
||||
const furnaceBlock = bot.findBlock({
|
||||
matching: mcData.blocksByName.furnace.id,
|
||||
maxDistance: 32,
|
||||
});
|
||||
if (!furnaceBlock) {
|
||||
throw new Error("No furnace nearby");
|
||||
} else {
|
||||
await bot.pathfinder.goto(
|
||||
new GoalLookAtBlock(furnaceBlock.position, bot.world)
|
||||
);
|
||||
}
|
||||
const furnace = await bot.openFurnace(furnaceBlock);
|
||||
let success_count = 0;
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (!bot.inventory.findInventoryItem(item.id, null)) {
|
||||
bot.chat(`No ${itemName} to smelt in inventory`);
|
||||
break;
|
||||
}
|
||||
if (furnace.fuelSeconds < 15 && furnace.fuelItem()?.name !== fuelName) {
|
||||
if (!bot.inventory.findInventoryItem(fuel.id, null)) {
|
||||
bot.chat(`No ${fuelName} as fuel in inventory`);
|
||||
break;
|
||||
}
|
||||
await furnace.putFuel(fuel.id, null, 1);
|
||||
await bot.waitForTicks(20);
|
||||
if (!furnace.fuel && furnace.fuelItem()?.name !== fuelName) {
|
||||
throw new Error(`${fuelName} is not a valid fuel`);
|
||||
}
|
||||
}
|
||||
await furnace.putInput(item.id, null, 1);
|
||||
await bot.waitForTicks(12 * 20);
|
||||
if (!furnace.outputItem()) {
|
||||
throw new Error(`${itemName} is not a valid input`);
|
||||
}
|
||||
await furnace.takeOutput();
|
||||
success_count++;
|
||||
}
|
||||
furnace.close();
|
||||
if (success_count > 0) bot.chat(`Smelted ${success_count} ${itemName}.`);
|
||||
else {
|
||||
bot.chat(
|
||||
`Failed to smelt ${itemName}, please check the fuel and input.`
|
||||
);
|
||||
_smeltItemFailCount++;
|
||||
if (_smeltItemFailCount > 10) {
|
||||
throw new Error(
|
||||
`smeltItem failed too many times, please check the fuel and input.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
133
metagpt/actions/minecraft/control_primitives/useChest.js
Normal file
133
metagpt/actions/minecraft/control_primitives/useChest.js
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
async function getItemFromChest(bot, chestPosition, itemsToGet) {
|
||||
// return if chestPosition is not Vec3
|
||||
if (!(chestPosition instanceof Vec3)) {
|
||||
bot.chat("chestPosition for getItemFromChest must be a Vec3");
|
||||
return;
|
||||
}
|
||||
await moveToChest(bot, chestPosition);
|
||||
const chestBlock = bot.blockAt(chestPosition);
|
||||
const chest = await bot.openContainer(chestBlock);
|
||||
for (const name in itemsToGet) {
|
||||
const itemByName = mcData.itemsByName[name];
|
||||
if (!itemByName) {
|
||||
bot.chat(`No item named ${name}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const item = chest.findContainerItem(itemByName.id);
|
||||
if (!item) {
|
||||
bot.chat(`I don't see ${name} in this chest`);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await chest.withdraw(item.type, null, itemsToGet[name]);
|
||||
} catch (err) {
|
||||
bot.chat(`Not enough ${name} in chest.`);
|
||||
}
|
||||
}
|
||||
await closeChest(bot, chestBlock);
|
||||
}
|
||||
|
||||
async function depositItemIntoChest(bot, chestPosition, itemsToDeposit) {
|
||||
// return if chestPosition is not Vec3
|
||||
if (!(chestPosition instanceof Vec3)) {
|
||||
throw new Error(
|
||||
"chestPosition for depositItemIntoChest must be a Vec3"
|
||||
);
|
||||
}
|
||||
await moveToChest(bot, chestPosition);
|
||||
const chestBlock = bot.blockAt(chestPosition);
|
||||
const chest = await bot.openContainer(chestBlock);
|
||||
for (const name in itemsToDeposit) {
|
||||
const itemByName = mcData.itemsByName[name];
|
||||
if (!itemByName) {
|
||||
bot.chat(`No item named ${name}`);
|
||||
continue;
|
||||
}
|
||||
const item = bot.inventory.findInventoryItem(itemByName.id);
|
||||
if (!item) {
|
||||
bot.chat(`No ${name} in inventory`);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await chest.deposit(item.type, null, itemsToDeposit[name]);
|
||||
} catch (err) {
|
||||
bot.chat(`Not enough ${name} in inventory.`);
|
||||
}
|
||||
}
|
||||
await closeChest(bot, chestBlock);
|
||||
}
|
||||
|
||||
async function checkItemInsideChest(bot, chestPosition) {
|
||||
// return if chestPosition is not Vec3
|
||||
if (!(chestPosition instanceof Vec3)) {
|
||||
throw new Error(
|
||||
"chestPosition for depositItemIntoChest must be a Vec3"
|
||||
);
|
||||
}
|
||||
await moveToChest(bot, chestPosition);
|
||||
const chestBlock = bot.blockAt(chestPosition);
|
||||
await bot.openContainer(chestBlock);
|
||||
await closeChest(bot, chestBlock);
|
||||
}
|
||||
|
||||
async function moveToChest(bot, chestPosition) {
|
||||
if (!(chestPosition instanceof Vec3)) {
|
||||
throw new Error(
|
||||
"chestPosition for depositItemIntoChest must be a Vec3"
|
||||
);
|
||||
}
|
||||
if (chestPosition.distanceTo(bot.entity.position) > 32) {
|
||||
bot.chat(
|
||||
`/tp ${chestPosition.x} ${chestPosition.y} ${chestPosition.z}`
|
||||
);
|
||||
await bot.waitForTicks(20);
|
||||
}
|
||||
const chestBlock = bot.blockAt(chestPosition);
|
||||
if (chestBlock.name !== "chest") {
|
||||
bot.emit("removeChest", chestPosition);
|
||||
throw new Error(
|
||||
`No chest at ${chestPosition}, it is ${chestBlock.name}`
|
||||
);
|
||||
}
|
||||
await bot.pathfinder.goto(
|
||||
new GoalLookAtBlock(chestBlock.position, bot.world, {})
|
||||
);
|
||||
return chestBlock;
|
||||
}
|
||||
|
||||
async function listItemsInChest(bot, chestBlock) {
|
||||
const chest = await bot.openContainer(chestBlock);
|
||||
const items = chest.containerItems();
|
||||
if (items.length > 0) {
|
||||
const itemNames = items.reduce((acc, obj) => {
|
||||
if (acc[obj.name]) {
|
||||
acc[obj.name] += obj.count;
|
||||
} else {
|
||||
acc[obj.name] = obj.count;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
bot.emit("closeChest", itemNames, chestBlock.position);
|
||||
} else {
|
||||
bot.emit("closeChest", {}, chestBlock.position);
|
||||
}
|
||||
return chest;
|
||||
}
|
||||
|
||||
async function closeChest(bot, chestBlock) {
|
||||
try {
|
||||
const chest = await listItemsInChest(bot, chestBlock);
|
||||
await chest.close();
|
||||
} catch (err) {
|
||||
await bot.closeWindow(chestBlock);
|
||||
}
|
||||
}
|
||||
|
||||
function itemByName(items, name) {
|
||||
for (let i = 0; i < items.length; ++i) {
|
||||
const item = items[i];
|
||||
if (item && item.name === name) return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
function waitForMobRemoved(bot, entity, timeout = 300) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let success = false;
|
||||
let droppedItem = null;
|
||||
// Set up timeout
|
||||
const timeoutId = setTimeout(() => {
|
||||
success = false;
|
||||
bot.pvp.stop();
|
||||
}, timeout * 1000);
|
||||
|
||||
// Function to handle entityRemoved event
|
||||
function onEntityGone(e) {
|
||||
if (e === entity) {
|
||||
success = true;
|
||||
clearTimeout(timeoutId);
|
||||
bot.chat(`Killed ${entity.name}!`);
|
||||
bot.pvp.stop();
|
||||
}
|
||||
}
|
||||
|
||||
function onItemDrop(item) {
|
||||
if (entity.position.distanceTo(item.position) <= 1) {
|
||||
droppedItem = item;
|
||||
}
|
||||
}
|
||||
|
||||
function onStoppedAttacking() {
|
||||
clearTimeout(timeoutId);
|
||||
bot.removeListener("entityGone", onEntityGone);
|
||||
bot.removeListener("stoppedAttacking", onStoppedAttacking);
|
||||
bot.removeListener("itemDrop", onItemDrop);
|
||||
if (!success) reject(new Error(`Failed to kill ${entity.name}.`));
|
||||
else resolve(droppedItem);
|
||||
}
|
||||
|
||||
// Listen for entityRemoved event
|
||||
bot.on("entityGone", onEntityGone);
|
||||
bot.on("stoppedAttacking", onStoppedAttacking);
|
||||
bot.on("itemDrop", onItemDrop);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function waitForMobShot(bot, entity, timeout = 300) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let success = false;
|
||||
let droppedItem = null;
|
||||
// Set up timeout
|
||||
const timeoutId = setTimeout(() => {
|
||||
success = false;
|
||||
bot.hawkEye.stop();
|
||||
}, timeout * 1000);
|
||||
|
||||
// Function to handle entityRemoved event
|
||||
function onEntityGone(e) {
|
||||
if (e === entity) {
|
||||
success = true;
|
||||
clearTimeout(timeoutId);
|
||||
bot.chat(`Shot ${entity.name}!`);
|
||||
bot.hawkEye.stop();
|
||||
}
|
||||
}
|
||||
|
||||
function onItemDrop(item) {
|
||||
if (entity.position.distanceTo(item.position) <= 1) {
|
||||
droppedItem = item;
|
||||
}
|
||||
}
|
||||
|
||||
function onAutoShotStopped() {
|
||||
clearTimeout(timeoutId);
|
||||
bot.removeListener("entityGone", onEntityGone);
|
||||
bot.removeListener("auto_shot_stopped", onAutoShotStopped);
|
||||
bot.removeListener("itemDrop", onItemDrop);
|
||||
if (!success) reject(new Error(`Failed to shoot ${entity.name}.`));
|
||||
else resolve(droppedItem);
|
||||
}
|
||||
|
||||
// Listen for entityRemoved event
|
||||
bot.on("entityGone", onEntityGone);
|
||||
bot.on("auto_shot_stopped", onAutoShotStopped);
|
||||
bot.on("itemDrop", onItemDrop);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import os
|
||||
import metagpt.utils.minecraft as utils
|
||||
from metagpt.logs import logger
|
||||
|
||||
def load_skills_code_context(skill_names=None):
|
||||
skills_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
if skill_names is None:
|
||||
skill_names = [
|
||||
skill[:-3] for skill in os.listdir(f"{skills_dir}") if skill.endswith(".js")
|
||||
]
|
||||
skills = [
|
||||
utils.load_text(os.path.join(skills_dir, f"{skill_name}.js"))
|
||||
for skill_name in skill_names
|
||||
]
|
||||
return skills
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logger.info(load_skills_code_context(["craftItem", "exploreUntil"]))
|
||||
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
// Craft 8 oak_planks from 2 oak_log (do the recipe 2 times): craftItem(bot, "oak_planks", 2);
|
||||
// You must place a crafting table before calling this function
|
||||
async function craftItem(bot, name, count = 1) {
|
||||
const item = mcData.itemsByName[name];
|
||||
const craftingTable = bot.findBlock({
|
||||
matching: mcData.blocksByName.crafting_table.id,
|
||||
maxDistance: 32,
|
||||
});
|
||||
await bot.pathfinder.goto(
|
||||
new GoalLookAtBlock(craftingTable.position, bot.world)
|
||||
);
|
||||
const recipe = bot.recipesFor(item.id, null, 1, craftingTable)[0];
|
||||
await bot.craft(recipe, count, craftingTable);
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
Explore until find an iron_ore, use Vec3(0, -1, 0) because iron ores are usually underground
|
||||
await exploreUntil(bot, new Vec3(0, -1, 0), 60, () => {
|
||||
const iron_ore = bot.findBlock({
|
||||
matching: mcData.blocksByName["iron_ore"].id,
|
||||
maxDistance: 32,
|
||||
});
|
||||
return iron_ore;
|
||||
});
|
||||
|
||||
Explore until find a pig, use Vec3(1, 0, 1) because pigs are usually on the surface
|
||||
let pig = await exploreUntil(bot, new Vec3(1, 0, 1), 60, () => {
|
||||
const pig = bot.nearestEntity((entity) => {
|
||||
return (
|
||||
entity.name === "pig" &&
|
||||
entity.position.distanceTo(bot.entity.position) < 32
|
||||
);
|
||||
});
|
||||
return pig;
|
||||
});
|
||||
*/
|
||||
async function exploreUntil(bot, direction, maxTime = 60, callback) {
|
||||
/*
|
||||
Implementation of this function is omitted.
|
||||
direction: Vec3, can only contain value of -1, 0 or 1
|
||||
maxTime: number, the max time for exploration
|
||||
callback: function, early stop condition, will be called each second, exploration will stop if return value is not null
|
||||
|
||||
Return: null if explore timeout, otherwise return the return value of callback
|
||||
*/
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// Kill a pig and collect the dropped item: killMob(bot, "pig", 300);
|
||||
async function killMob(bot, mobName, timeout = 300) {
|
||||
const entity = bot.nearestEntity(
|
||||
(entity) =>
|
||||
entity.name === mobName &&
|
||||
entity.position.distanceTo(bot.entity.position) < 32
|
||||
);
|
||||
await bot.pvp.attack(entity);
|
||||
await bot.pathfinder.goto(
|
||||
new GoalBlock(entity.position.x, entity.position.y, entity.position.z)
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// Mine 3 cobblestone: mineBlock(bot, "stone", 3);
|
||||
async function mineBlock(bot, name, count = 1) {
|
||||
const blocks = bot.findBlocks({
|
||||
matching: (block) => {
|
||||
return block.name === name;
|
||||
},
|
||||
maxDistance: 32,
|
||||
count: count,
|
||||
});
|
||||
const targets = [];
|
||||
for (let i = 0; i < Math.min(blocks.length, count); i++) {
|
||||
targets.push(bot.blockAt(blocks[i]));
|
||||
}
|
||||
await bot.collectBlock.collect(targets, { ignoreNoPath: true });
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
await bot.pathfinder.goto(goal); // A very useful function. This function may change your main-hand equipment.
|
||||
// Following are some Goals you can use:
|
||||
new GoalNear(x, y, z, range); // Move the bot to a block within the specified range of the specified block. `x`, `y`, `z`, and `range` are `number`
|
||||
new GoalXZ(x, z); // Useful for long-range goals that don't have a specific Y level. `x` and `z` are `number`
|
||||
new GoalGetToBlock(x, y, z); // Not get into the block, but get directly adjacent to it. Useful for fishing, farming, filling bucket, and beds. `x`, `y`, and `z` are `number`
|
||||
new GoalFollow(entity, range); // Follow the specified entity within the specified range. `entity` is `Entity`, `range` is `number`
|
||||
new GoalPlaceBlock(position, bot.world, {}); // Position the bot in order to place a block. `position` is `Vec3`
|
||||
new GoalLookAtBlock(position, bot.world, {}); // Path into a position where a blockface of the block at position is visible. `position` is `Vec3`
|
||||
|
||||
// These are other Mineflayer functions you can use:
|
||||
bot.isABed(bedBlock); // Return true if `bedBlock` is a bed
|
||||
bot.blockAt(position); // Return the block at `position`. `position` is `Vec3`
|
||||
|
||||
// These are other Mineflayer async functions you can use:
|
||||
await bot.equip(item, destination); // Equip the item in the specified destination. `item` is `Item`, `destination` can only be "hand", "head", "torso", "legs", "feet", "off-hand"
|
||||
await bot.consume(); // Consume the item in the bot's hand. You must equip the item to consume first. Useful for eating food, drinking potions, etc.
|
||||
await bot.fish(); // Let bot fish. Before calling this function, you must first get to a water block and then equip a fishing rod. The bot will automatically stop fishing when it catches a fish
|
||||
await bot.sleep(bedBlock); // Sleep until sunrise. You must get to a bed block first
|
||||
await bot.activateBlock(block); // This is the same as right-clicking a block in the game. Useful for buttons, doors, etc. You must get to the block first
|
||||
await bot.lookAt(position); // Look at the specified position. You must go near the position before you look at it. To fill bucket with water, you must lookAt first. `position` is `Vec3`
|
||||
await bot.activateItem(); // This is the same as right-clicking to use the item in the bot's hand. Useful for using buckets, etc. You must equip the item to activate first
|
||||
await bot.useOn(entity); // This is the same as right-clicking an entity in the game. Useful for shearing sheep, equipping harnesses, etc. You must get to the entity first
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
// Place a crafting_table near the player, Vec3(1, 0, 0) is just an example, you shouldn't always use that: placeItem(bot, "crafting_table", bot.entity.position.offset(1, 0, 0));
|
||||
async function placeItem(bot, name, position) {
|
||||
const item = bot.inventory.findInventoryItem(mcData.itemsByName[name].id);
|
||||
// find a reference block
|
||||
const faceVectors = [
|
||||
new Vec3(0, 1, 0),
|
||||
new Vec3(0, -1, 0),
|
||||
new Vec3(1, 0, 0),
|
||||
new Vec3(-1, 0, 0),
|
||||
new Vec3(0, 0, 1),
|
||||
new Vec3(0, 0, -1),
|
||||
];
|
||||
let referenceBlock = null;
|
||||
let faceVector = null;
|
||||
for (const vector of faceVectors) {
|
||||
const block = bot.blockAt(position.minus(vector));
|
||||
if (block?.name !== "air") {
|
||||
referenceBlock = block;
|
||||
faceVector = vector;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// You must first go to the block position you want to place
|
||||
await bot.pathfinder.goto(new GoalPlaceBlock(position, bot.world, {}));
|
||||
// You must equip the item right before calling placeBlock
|
||||
await bot.equip(item, "hand");
|
||||
await bot.placeBlock(referenceBlock, faceVector);
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
// Smelt 1 raw_iron into 1 iron_ingot using 1 oak_planks as fuel: smeltItem(bot, "raw_iron", "oak_planks");
|
||||
// You must place a furnace before calling this function
|
||||
async function smeltItem(bot, itemName, fuelName, count = 1) {
|
||||
const item = mcData.itemsByName[itemName];
|
||||
const fuel = mcData.itemsByName[fuelName];
|
||||
const furnaceBlock = bot.findBlock({
|
||||
matching: mcData.blocksByName.furnace.id,
|
||||
maxDistance: 32,
|
||||
});
|
||||
await bot.pathfinder.goto(
|
||||
new GoalLookAtBlock(furnaceBlock.position, bot.world)
|
||||
);
|
||||
const furnace = await bot.openFurnace(furnaceBlock);
|
||||
for (let i = 0; i < count; i++) {
|
||||
await furnace.putFuel(fuel.id, null, 1);
|
||||
await furnace.putInput(item.id, null, 1);
|
||||
// Wait 12 seconds for the furnace to smelt the item
|
||||
await bot.waitForTicks(12 * 20);
|
||||
await furnace.takeOutput();
|
||||
}
|
||||
await furnace.close();
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// Get a torch from chest at (30, 65, 100): getItemFromChest(bot, new Vec3(30, 65, 100), {"torch": 1});
|
||||
// This function will work no matter how far the bot is from the chest.
|
||||
async function getItemFromChest(bot, chestPosition, itemsToGet) {
|
||||
await moveToChest(bot, chestPosition);
|
||||
const chestBlock = bot.blockAt(chestPosition);
|
||||
const chest = await bot.openContainer(chestBlock);
|
||||
for (const name in itemsToGet) {
|
||||
const itemByName = mcData.itemsByName[name];
|
||||
const item = chest.findContainerItem(itemByName.id);
|
||||
await chest.withdraw(item.type, null, itemsToGet[name]);
|
||||
}
|
||||
await closeChest(bot, chestBlock);
|
||||
}
|
||||
// Deposit a torch into chest at (30, 65, 100): depositItemIntoChest(bot, new Vec3(30, 65, 100), {"torch": 1});
|
||||
// This function will work no matter how far the bot is from the chest.
|
||||
async function depositItemIntoChest(bot, chestPosition, itemsToDeposit) {
|
||||
await moveToChest(bot, chestPosition);
|
||||
const chestBlock = bot.blockAt(chestPosition);
|
||||
const chest = await bot.openContainer(chestBlock);
|
||||
for (const name in itemsToDeposit) {
|
||||
const itemByName = mcData.itemsByName[name];
|
||||
const item = bot.inventory.findInventoryItem(itemByName.id);
|
||||
await chest.deposit(item.type, null, itemsToDeposit[name]);
|
||||
}
|
||||
await closeChest(bot, chestBlock);
|
||||
}
|
||||
// Check the items inside the chest at (30, 65, 100): checkItemInsideChest(bot, new Vec3(30, 65, 100));
|
||||
// You only need to call this function once without any action to finish task of checking items inside the chest.
|
||||
async function checkItemInsideChest(bot, chestPosition) {
|
||||
await moveToChest(bot, chestPosition);
|
||||
const chestBlock = bot.blockAt(chestPosition);
|
||||
await bot.openContainer(chestBlock);
|
||||
// You must close the chest after opening it if you are asked to open a chest
|
||||
await closeChest(bot, chestBlock);
|
||||
}
|
||||
247
metagpt/actions/minecraft/design_curriculumn.py
Normal file
247
metagpt/actions/minecraft/design_curriculumn.py
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# @Date : 2023/9/23 14:56
|
||||
# @Author : stellahong (stellahong@fuzhi.ai)
|
||||
# @Desc :
|
||||
import json
|
||||
import re
|
||||
|
||||
from langchain.embeddings.openai import OpenAIEmbeddings
|
||||
from langchain.vectorstores import Chroma
|
||||
from metagpt.document_store import FaissStore
|
||||
|
||||
from metagpt.logs import logger
|
||||
from metagpt.actions import Action
|
||||
from metagpt.utils.minecraft import load_prompt, fix_and_parse_json
|
||||
from metagpt.schema import HumanMessage, SystemMessage
|
||||
from metagpt.const import CKPT_DIR
|
||||
|
||||
# from metagpt.actions.minecraft import PlayerActions
|
||||
|
||||
|
||||
class DesignTask(Action):
|
||||
"""
|
||||
Action class for decomposing a task.
|
||||
Refer to the code in the voyager/agents/curriculum.py for implementation details.
|
||||
"""
|
||||
|
||||
def __init__(self, name="", context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
|
||||
async def decompose_task(self, query, events):
|
||||
system_msgs = SystemMessage(
|
||||
content=load_prompt("curriculum_task_decomposition")
|
||||
)
|
||||
prompt = self.render_human_message(
|
||||
events=events, chest_observation=""
|
||||
) + HumanMessage(content=f"Final task: {query}")
|
||||
logger.info(f"Curriculum Agent task decomposition\nFinal task: {query}")
|
||||
|
||||
rsp = await self._aask(prompt=prompt, system_msgs=system_msgs)
|
||||
logger.info(f"Curriculum Agent task decomposition\n{rsp}")
|
||||
return fix_and_parse_json(rsp)
|
||||
|
||||
def parse_llm_response(self, llm_resp):
|
||||
task = ""
|
||||
for line in llm_resp.split("\n"):
|
||||
if line.startswith("Task:"):
|
||||
task = line[5:].replace(".", "").strip()
|
||||
assert task, "Task not found in Curriculum Agent response"
|
||||
return {"next_task": task}
|
||||
|
||||
async def generate_task(self, human_msg, system_msg, max_retries=5):
|
||||
"""
|
||||
Refer to the code in the voyager/agents/curriculum.py propose_next_ai_task() for implementation details.
|
||||
Returns: task & context
|
||||
|
||||
"""
|
||||
|
||||
if max_retries == 0:
|
||||
raise RuntimeError("Max retries reached, failed to propose task.")
|
||||
curriculum = await self._aask(prompt=human_msg, system_msgs=system_msg)
|
||||
logger.info(f"Curriculum Agent message\n{curriculum}")
|
||||
try:
|
||||
response = self.parse_llm_response(
|
||||
curriculum
|
||||
) # Task: Craft 4 wooden planks.
|
||||
assert "next_task" in response
|
||||
return response["next_task"]
|
||||
except Exception as e:
|
||||
logger.info(f"Error parsing curriculum response: {e}. Trying again!")
|
||||
return self.generate_task(
|
||||
human_msg=human_msg,
|
||||
system_msg=system_msg,
|
||||
max_retries=max_retries - 1,
|
||||
)
|
||||
|
||||
async def run(self, human_msg, system_msg, *args, **kwargs):
|
||||
logger.info(f"run {self.__repr__()}")
|
||||
|
||||
# Call the language model to generate a response.
|
||||
|
||||
task = await self.generate_task(human_msg=human_msg, system_msg=system_msg)
|
||||
|
||||
return task
|
||||
|
||||
|
||||
class DesignCurriculum(Action):
|
||||
"""
|
||||
Action class for designing curriculum-related questions.
|
||||
Refer to the code in the voyager/agents/curriculum.py for implementation details.
|
||||
"""
|
||||
|
||||
def __init__(self, name="", context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
# voyager vectordb using
|
||||
self.qa_cache = {}
|
||||
self.qa_cache_questions_vectordb = Chroma(
|
||||
collection_name="qa_cache_questions_vectordb",
|
||||
embedding_function=OpenAIEmbeddings(),
|
||||
persist_directory=f"{CKPT_DIR}/curriculum/vectordb",
|
||||
)
|
||||
# TODO: change to FaissStore
|
||||
# self.qa_cache_questions_vectordb = FaissStore( {CKPT_DIR}/ 'curriculum/vectordb')
|
||||
# TODO:
|
||||
# assert self.qa_cache_questions_vectordb._collection.count() == len(
|
||||
# self.qa_cache
|
||||
# ), (
|
||||
# f"Curriculum Agent's qa cache question vectordb is not synced with qa_cache.json.\n"
|
||||
# f"There are {self.qa_cache_questions_vectordb._collection.count()} questions in vectordb "
|
||||
# f"but {len(self.qa_cache)} questions in qa_cache.json.\n"
|
||||
# f"Did you set resume=False when initializing the agent?\n"
|
||||
# f"You may need to manually delete the qa cache question vectordb directory for running from scratch.\n"
|
||||
# )
|
||||
|
||||
@classmethod
|
||||
def set_qa_cache(cls, qa_cache):
|
||||
cls.qa_cache = qa_cache
|
||||
# Check if qa_cache right using
|
||||
|
||||
@classmethod
|
||||
def generate_qa(cls, events, chest_observation):
|
||||
"""
|
||||
Generate qa for DesignTask's HumanMessage
|
||||
"""
|
||||
questions_new, _ = cls.generate_qa_step1(
|
||||
events=events, chest_observation=chest_observation
|
||||
)
|
||||
questions = []
|
||||
answers = []
|
||||
for question in questions_new:
|
||||
if cls.qa_cache_questions_vectordb._collection.count() > 0:
|
||||
docs_and_scores = (
|
||||
cls.qa_cache_questions_vectordb.similarity_search_with_score(
|
||||
question, k=1
|
||||
)
|
||||
)
|
||||
if docs_and_scores and docs_and_scores[0][1] < 0.05:
|
||||
question_cached = docs_and_scores[0][0].page_content
|
||||
assert question_cached in cls.qa_cache
|
||||
answer_cached = cls.qa_cache[question_cached]
|
||||
questions.append(question_cached)
|
||||
answers.append(answer_cached)
|
||||
continue
|
||||
answer = cls.generate_qa_step2(question=question)
|
||||
assert question not in cls.qa_cache
|
||||
cls.qa_cache[question] = answer
|
||||
cls.qa_cache_questions_vectordb.add_texts(
|
||||
texts=[question],
|
||||
)
|
||||
with open(f"{CKPT_DIR}/curriculum/qa_cache.json", "w") as f:
|
||||
json.dump(cls.qa_cache, f)
|
||||
cls.qa_cache_questions_vectordb.persist()
|
||||
questions.append(question)
|
||||
answers.append(answer)
|
||||
assert len(questions_new) == len(questions) == len(answers)
|
||||
return questions, answers
|
||||
|
||||
async def generate_qa_step1(self, events, human_msg, system_msg):
|
||||
biome = events[-1][1]["status"]["biome"].replace("_", " ")
|
||||
questions = [
|
||||
f"What are the blocks that I can find in the {biome} in Minecraft?",
|
||||
f"What are the items that I can find in the {biome} in Minecraft?",
|
||||
f"What are the mobs that I can find in the {biome} in Minecraft?",
|
||||
]
|
||||
qa_response = await self._aask(prompt=human_msg, system_msgs=system_msg)
|
||||
|
||||
try:
|
||||
# Regex pattern to extract question and concept pairs
|
||||
pattern = r"Question \d+: (.+)\nConcept \d+: (.+)"
|
||||
# Extracting all question and concept pairs from the text
|
||||
pairs = re.findall(pattern, qa_response)
|
||||
# Storing each question and concept in separate lists
|
||||
questions_new = [pair[0] for pair in pairs]
|
||||
questions.extend(questions_new)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error parsing curriculum response for "
|
||||
f"QA step 1 ask questions: {e}."
|
||||
)
|
||||
return questions
|
||||
|
||||
async def generate_qa_step2(self, question):
|
||||
# Implement the logic for another specific step in generating questions and answers.
|
||||
logger.info(f"Curriculum Agent Question: {question}")
|
||||
human_msg = HumanMessage(content=f"Question: {question}").content
|
||||
system_msg = [
|
||||
SystemMessage(
|
||||
content=load_prompt("curriculum_qa_step2_answer_questions")
|
||||
).content
|
||||
]
|
||||
answer = await self._aask(prompt=human_msg, system_msgs=system_msg)
|
||||
logger.info(f"Curriculum Agent {answer}")
|
||||
return answer
|
||||
|
||||
async def get_context_from_task(self, task):
|
||||
"""
|
||||
Args: task
|
||||
Returns: context: "Question: {question}\n{answer}"
|
||||
if include ore in question, gpt will try to use tool with skill touch enhancement to mine
|
||||
"""
|
||||
|
||||
question = (
|
||||
f"How to {task.replace('_', ' ').replace(' ore', '').replace(' ores', '').replace('.', '').strip().lower()}"
|
||||
f" in Minecraft?"
|
||||
)
|
||||
if question in self.qa_cache:
|
||||
answer = self.qa_cache[question]
|
||||
else:
|
||||
answer = await self.generate_qa_step2(question=question)
|
||||
self.qa_cache[question] = answer
|
||||
self.qa_cache_questions_vectordb.add_texts(
|
||||
texts=[question],
|
||||
)
|
||||
with open(f"{CKPT_DIR}/curriculum/qa_cache.json", "w") as f:
|
||||
json.dump(self.qa_cache, f)
|
||||
self.qa_cache_questions_vectordb.persist()
|
||||
context = f"Question: {question}\n{answer}"
|
||||
return context
|
||||
|
||||
async def generate_context(self, task, max_retries=5):
|
||||
"""
|
||||
Refer to the code in the voyager/agents/curriculum.py propose_next_ai_task() for implementation details.
|
||||
Returns: context
|
||||
|
||||
"""
|
||||
|
||||
if max_retries == 0:
|
||||
raise RuntimeError("Max retries reached, failed to propose context.")
|
||||
try:
|
||||
context = await self.get_context_from_task(
|
||||
task=task
|
||||
) # Curriculum Agent Question: How to craft 4 wooden planks in Minecraft? & Curriculum Agent Answer: ...
|
||||
return context
|
||||
except Exception as e:
|
||||
logger.info(f"Error parsing curriculum response: {e}. Trying again!")
|
||||
return self.generate_context(
|
||||
task=task,
|
||||
max_retries=max_retries - 1,
|
||||
)
|
||||
|
||||
async def run(self, task, human_msg, system_msg, *args, **kwargs):
|
||||
logger.info(f"run {self.__repr__()}")
|
||||
# Generate curriculum-related questions and answers.
|
||||
# curriculum_qustion = await self.generate_qa_step1(events, human_msg, system_msg)
|
||||
curriculum_context = await self.generate_context(task)
|
||||
|
||||
# Return the generated questions and answers.
|
||||
return curriculum_context
|
||||
46
metagpt/actions/minecraft/generate_actions.py
Normal file
46
metagpt/actions/minecraft/generate_actions.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# @Date : 2023/9/23 15:44
|
||||
# @Author : stellahong (stellahong@fuzhi.ai)
|
||||
# @Desc :
|
||||
from metagpt.logs import logger
|
||||
from metagpt.actions import Action
|
||||
from metagpt.utils.minecraft import parse_action_response
|
||||
|
||||
|
||||
class GenerateActionCode(Action):
|
||||
"""
|
||||
Action class for generating action code.
|
||||
Refer to the code in the voyager/agents/action.py for implementation details.
|
||||
"""
|
||||
|
||||
def __init__(self, name="", context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
|
||||
async def generate_code(self, human_msg, system_msg=[]):
|
||||
"""
|
||||
Generate action code logic.
|
||||
|
||||
Implement the logic for generating action code here.
|
||||
"""
|
||||
rsp = await self._aask(prompt=human_msg, system_msgs=system_msg)
|
||||
parsed_result = parse_action_response(rsp)
|
||||
# logger.info(f"parsed_result is HERE: {parsed_result}")
|
||||
|
||||
try:
|
||||
return (
|
||||
parsed_result["program_code"] + "\n" + parsed_result["exec_code"],
|
||||
parsed_result["program_name"],
|
||||
)
|
||||
except:
|
||||
logger.error(f"Failed to parse response: {parsed_result}")
|
||||
return None, None
|
||||
|
||||
async def run(self, human_msg, system_msg, *args, **kwargs):
|
||||
logger.info(f"run {self.__repr__()}")
|
||||
# Generate action code.
|
||||
generated_code, program_name = await self.generate_code(
|
||||
human_msg=human_msg, system_msg=system_msg
|
||||
)
|
||||
|
||||
# Return the generated code.
|
||||
return generated_code, program_name
|
||||
139
metagpt/actions/minecraft/manage_skills.py
Normal file
139
metagpt/actions/minecraft/manage_skills.py
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# @Date : 2023/9/23 14:56
|
||||
# @Author : stellahong (stellahong@fuzhi.ai)
|
||||
# @Desc :
|
||||
import os
|
||||
import json
|
||||
|
||||
from langchain.embeddings.openai import OpenAIEmbeddings
|
||||
from langchain.vectorstores import Chroma
|
||||
from metagpt.document_store import FaissStore
|
||||
from metagpt.logs import logger
|
||||
from metagpt.actions import Action
|
||||
from metagpt.const import CKPT_DIR
|
||||
|
||||
|
||||
class RetrieveSkills(Action):
|
||||
"""
|
||||
Action class for retrieving skills.
|
||||
Refer to the code in the voyager/agents/skill.py for implementation details.
|
||||
"""
|
||||
|
||||
def __init__(self, name="", context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
# TODO: mv to PlayerAction
|
||||
self.retrieval_top_k = 5
|
||||
self.vectordb = Chroma(
|
||||
collection_name="skill_vectordb",
|
||||
embedding_function=OpenAIEmbeddings(),
|
||||
persist_directory=f"{CKPT_DIR}/skill/vectordb",
|
||||
)
|
||||
# Check if skills right using
|
||||
# TODO:
|
||||
# assert self.vectordb._collection.count() == len(self.skills), (
|
||||
# f"Skill Manager's vectordb is not synced with skills.json.\n"
|
||||
# f"There are {self.vectordb._collection.count()} skills in vectordb but {len(self.skills)} skills in skills.json.\n"
|
||||
# f"Did you set resume=False when initializing the manager?\n"
|
||||
# f"You may need to manually delete the vectordb directory for running from scratch."
|
||||
# )
|
||||
|
||||
async def run(self, query, skills, *args, **kwargs):
|
||||
# Implement the logic for retrieving skills here.
|
||||
k = min(self.vectordb._collection.count(), self.retrieval_top_k)
|
||||
if k == 0:
|
||||
return []
|
||||
logger.info(f"Skill Manager retrieving for {k} skills")
|
||||
docs_and_scores = self.vectordb.similarity_search_with_score(query, k=k)
|
||||
logger.info(
|
||||
f"Skill Manager retrieved skills: "
|
||||
f"{', '.join([doc.metadata['name'] for doc, _ in docs_and_scores])}"
|
||||
)
|
||||
retrieve_skills = []
|
||||
for doc, _ in docs_and_scores:
|
||||
retrieve_skills.append(skills[doc.metadata["name"]]["code"])
|
||||
return retrieve_skills
|
||||
|
||||
|
||||
class AddNewSkills(Action):
|
||||
"""
|
||||
Action class for adding new skills.
|
||||
Refer to the code in the voyager/agents/skill.py for implementation details.
|
||||
"""
|
||||
|
||||
def __init__(self, name="", context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
# TODO: mv to PlayerAction
|
||||
self.vectordb = Chroma(
|
||||
collection_name="skill_vectordb",
|
||||
embedding_function=OpenAIEmbeddings(),
|
||||
persist_directory=f"{CKPT_DIR}/skill/vectordb",
|
||||
)
|
||||
# TODO: change to FaissStore
|
||||
# self.qa_cache_questions_vectordb = FaissStore( {CKPT_DIR}/ 'skill/vectordb')
|
||||
# TODO:
|
||||
# Check if skills right using
|
||||
# assert self.vectordb._collection.count() == len(self.skills), (
|
||||
# f"Skill Manager's vectordb is not synced with skills.json.\n"
|
||||
# f"There are {self.vectordb._collection.count()} skills in vectordb but {len(self.skills)} skills in skills.json.\n"
|
||||
# f"Did you set resume=False when initializing the manager?\n"
|
||||
# f"You may need to manually delete the vectordb directory for running from scratch."
|
||||
# )
|
||||
|
||||
async def run(
|
||||
self, task, program_name, program_code, skills, skill_desp, *args, **kwargs
|
||||
):
|
||||
# Implement the logic for adding new skills here.
|
||||
# TODO: Fix this
|
||||
if task.startswith("Deposit useless items into the chest at"):
|
||||
# No need to reuse the deposit skill
|
||||
return {}
|
||||
logger.info(
|
||||
f"Skill Manager generated description for {program_name}:\n{skill_desp}\033[0m"
|
||||
)
|
||||
if program_name in skills:
|
||||
logger.info(f"Skill {program_name} already exists. Rewriting!")
|
||||
self.vectordb._collection.delete(ids=[program_name])
|
||||
i = 2
|
||||
while f"{program_name}V{i}.js" in os.listdir(f"{CKPT_DIR}/skill/code"):
|
||||
i += 1
|
||||
dumped_program_name = f"{program_name}V{i}"
|
||||
else:
|
||||
dumped_program_name = program_name
|
||||
self.vectordb.add_texts(
|
||||
texts=[skill_desp],
|
||||
ids=[program_name],
|
||||
metadatas=[{"name": program_name}],
|
||||
)
|
||||
|
||||
# FIXME
|
||||
# assert self.vectordb._collection.count() == len(
|
||||
# skills
|
||||
# ), "vectordb is not synced with skills.json"
|
||||
|
||||
with open(f"{CKPT_DIR}/skill/code/{dumped_program_name}.js", "w") as f:
|
||||
f.write(program_code)
|
||||
with open(f"{CKPT_DIR}/skill/description/{dumped_program_name}.txt", "w") as f:
|
||||
f.write(skill_desp)
|
||||
with open(f"{CKPT_DIR}/skill/skills.json", "w") as f:
|
||||
json.dump(skills, f)
|
||||
self.vectordb.persist()
|
||||
return {
|
||||
"code": program_code,
|
||||
"description": skill_desp,
|
||||
}
|
||||
|
||||
|
||||
class GenerateSkillDescription(Action):
|
||||
"""
|
||||
Action class for generating skill descriptions.
|
||||
Refer to the code in the voyager/agents/skill.py for implementation details.
|
||||
"""
|
||||
|
||||
def __init__(self, name="", context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
|
||||
async def run(self, program_name, human_message, system_message, *args, **kwargs):
|
||||
# Implement the logic for generating skill descriptions here.
|
||||
rsp = await self._aask(prompt=human_message, system_msgs=system_message)
|
||||
skill_description = f" // { rsp}"
|
||||
return f"async function {program_name}(bot) {{\n{skill_description}\n}}"
|
||||
10
metagpt/actions/minecraft/player_action.py
Normal file
10
metagpt/actions/minecraft/player_action.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# @Date : 2023/9/23 17:06
|
||||
# @Author : stellahong (stellahong@fuzhi.ai)
|
||||
# @Desc :
|
||||
from metagpt.actions import Action
|
||||
|
||||
class PlayerActions(Action):
|
||||
"""Minecraft player info without any implementation details"""
|
||||
async def run(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
46
metagpt/actions/minecraft/review_task.py
Normal file
46
metagpt/actions/minecraft/review_task.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# @Date : 2023/9/23 14:56
|
||||
# @Author : stellahong (stellahong@fuzhi.ai)
|
||||
# @Desc :
|
||||
from metagpt.logs import logger
|
||||
from metagpt.actions import Action
|
||||
from metagpt.utils.minecraft import fix_and_parse_json
|
||||
|
||||
|
||||
class VerifyTask(Action):
|
||||
"""
|
||||
Action class for verifying a task.
|
||||
Refer to the code in the voyager/agents/critic.py for implementation details.
|
||||
"""
|
||||
|
||||
def __init__(self, name="", context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
self.vect_db = ""
|
||||
|
||||
async def run(self,human_msg, system_msg, max_retries=5, *args, **kwargs):
|
||||
# Implement the logic to verify the task here.
|
||||
|
||||
# Example: Verify the completion of a task.
|
||||
|
||||
# If verification is successful, return a success message.
|
||||
# task, status, review_info = "", True, "Task verified successfully."
|
||||
|
||||
if max_retries == 0:
|
||||
logger.info(f"Failed to parse Critic Agent response. Consider updating your prompt.")
|
||||
return False, ""
|
||||
|
||||
if human_msg or system_msg is None:
|
||||
return False, ""
|
||||
critic = await self._aask(prompt=human_msg, system_msgs=system_msg)
|
||||
try:
|
||||
response = fix_and_parse_json(critic)
|
||||
assert response["success"] in [True, False]
|
||||
if "critique" not in response:
|
||||
response["critique"] = ""
|
||||
logger.info("Task verified successfully.")
|
||||
return response["success"], response["critique"]
|
||||
except Exception as e:
|
||||
logger.error(f"Error verifying the task: {str(e)}")
|
||||
return await self.run(human_msg, system_msg, max_retries=max_retries-1)
|
||||
|
||||
|
||||
|
|
@ -5,13 +5,74 @@
|
|||
@Author : alexanderwu
|
||||
@File : project_management.py
|
||||
"""
|
||||
from typing import List, Tuple
|
||||
from typing import List
|
||||
|
||||
from metagpt.actions.action import Action
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.const import WORKSPACE_ROOT
|
||||
from metagpt.utils.common import CodeParser
|
||||
from metagpt.utils.get_template import get_template
|
||||
from metagpt.utils.json_to_markdown import json_to_markdown
|
||||
|
||||
PROMPT_TEMPLATE = '''
|
||||
templates = {
|
||||
"json": {
|
||||
"PROMPT_TEMPLATE": """
|
||||
# Context
|
||||
{context}
|
||||
|
||||
## Format example
|
||||
{format_example}
|
||||
-----
|
||||
Role: You are a project manager; the goal is to break down tasks according to PRD/technical design, give a task list, and analyze task dependencies to start with the prerequisite modules
|
||||
Requirements: Based on the context, fill in the following missing information, each section name is a key in json. Here the granularity of the task is a file, if there are any missing files, you can supplement them
|
||||
Attention: Use '##' to split sections, not '#', and '## <SECTION_NAME>' SHOULD WRITE BEFORE the code and triple quote.
|
||||
|
||||
## Required Python third-party packages: Provided in requirements.txt format
|
||||
|
||||
## Required Other language third-party packages: Provided in requirements.txt format
|
||||
|
||||
## Full API spec: Use OpenAPI 3.0. Describe all APIs that may be used by both frontend and backend.
|
||||
|
||||
## Logic Analysis: Provided as a Python list[list[str]. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first
|
||||
|
||||
## Task list: Provided as Python list[str]. Each str is a filename, the more at the beginning, the more it is a prerequisite dependency, should be done first
|
||||
|
||||
## Shared Knowledge: Anything that should be public like utils' functions, config's variables details that should make clear first.
|
||||
|
||||
## Anything UNCLEAR: Provide as Plain text. Make clear here. For example, don't forget a main entry. don't forget to init 3rd party libs.
|
||||
|
||||
output a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example,
|
||||
and only output the json inside this tag, nothing else
|
||||
""",
|
||||
"FORMAT_EXAMPLE": '''
|
||||
{
|
||||
"Required Python third-party packages": [
|
||||
"flask==1.1.2",
|
||||
"bcrypt==3.2.0"
|
||||
],
|
||||
"Required Other language third-party packages": [
|
||||
"No third-party ..."
|
||||
],
|
||||
"Full API spec": """
|
||||
openapi: 3.0.0
|
||||
...
|
||||
description: A JSON object ...
|
||||
""",
|
||||
"Logic Analysis": [
|
||||
["game.py","Contains..."]
|
||||
],
|
||||
"Task list": [
|
||||
"game.py"
|
||||
],
|
||||
"Shared Knowledge": """
|
||||
'game.py' contains ...
|
||||
""",
|
||||
"Anything UNCLEAR": "We need ... how to start."
|
||||
}
|
||||
''',
|
||||
},
|
||||
"markdown": {
|
||||
"PROMPT_TEMPLATE": """
|
||||
# Context
|
||||
{context}
|
||||
|
||||
|
|
@ -28,7 +89,7 @@ Attention: Use '##' to split sections, not '#', and '## <SECTION_NAME>' SHOULD W
|
|||
|
||||
## Full API spec: Use OpenAPI 3.0. Describe all APIs that may be used by both frontend and backend.
|
||||
|
||||
## Logic Analysis: Provided as a Python list[str, str]. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first
|
||||
## Logic Analysis: Provided as a Python list[list[str]. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first
|
||||
|
||||
## Task list: Provided as Python list[str]. Each str is a filename, the more at the beginning, the more it is a prerequisite dependency, should be done first
|
||||
|
||||
|
|
@ -36,9 +97,8 @@ Attention: Use '##' to split sections, not '#', and '## <SECTION_NAME>' SHOULD W
|
|||
|
||||
## Anything UNCLEAR: Provide as Plain text. Make clear here. For example, don't forget a main entry. don't forget to init 3rd party libs.
|
||||
|
||||
'''
|
||||
|
||||
FORMAT_EXAMPLE = '''
|
||||
""",
|
||||
"FORMAT_EXAMPLE": '''
|
||||
---
|
||||
## Required Python third-party packages
|
||||
```python
|
||||
|
|
@ -67,7 +127,7 @@ description: A JSON object ...
|
|||
## Logic Analysis
|
||||
```python
|
||||
[
|
||||
("game.py", "Contains ..."),
|
||||
["game.py", "Contains ..."],
|
||||
]
|
||||
```
|
||||
|
||||
|
|
@ -88,13 +148,14 @@ description: A JSON object ...
|
|||
## Anything UNCLEAR
|
||||
We need ... how to start.
|
||||
---
|
||||
'''
|
||||
|
||||
''',
|
||||
},
|
||||
}
|
||||
OUTPUT_MAPPING = {
|
||||
"Required Python third-party packages": (str, ...),
|
||||
"Required Other language third-party packages": (str, ...),
|
||||
"Required Python third-party packages": (List[str], ...),
|
||||
"Required Other language third-party packages": (List[str], ...),
|
||||
"Full API spec": (str, ...),
|
||||
"Logic Analysis": (List[Tuple[str, str]], ...),
|
||||
"Logic Analysis": (List[List[str]], ...),
|
||||
"Task list": (List[str], ...),
|
||||
"Shared Knowledge": (str, ...),
|
||||
"Anything UNCLEAR": (str, ...),
|
||||
|
|
@ -102,22 +163,25 @@ OUTPUT_MAPPING = {
|
|||
|
||||
|
||||
class WriteTasks(Action):
|
||||
|
||||
def __init__(self, name="CreateTasks", context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
|
||||
def _save(self, context, rsp):
|
||||
ws_name = CodeParser.parse_str(block="Python package name", text=context[-1].content)
|
||||
file_path = WORKSPACE_ROOT / ws_name / 'docs/api_spec_and_tasks.md'
|
||||
file_path.write_text(rsp.content)
|
||||
if context[-1].instruct_content:
|
||||
ws_name = context[-1].instruct_content.dict()["Python package name"]
|
||||
else:
|
||||
ws_name = CodeParser.parse_str(block="Python package name", text=context[-1].content)
|
||||
file_path = WORKSPACE_ROOT / ws_name / "docs/api_spec_and_tasks.md"
|
||||
file_path.write_text(json_to_markdown(rsp.instruct_content.dict()))
|
||||
|
||||
# Write requirements.txt
|
||||
requirements_path = WORKSPACE_ROOT / ws_name / 'requirements.txt'
|
||||
requirements_path.write_text(rsp.instruct_content.dict().get("Required Python third-party packages").strip('"\n'))
|
||||
requirements_path = WORKSPACE_ROOT / ws_name / "requirements.txt"
|
||||
requirements_path.write_text("\n".join(rsp.instruct_content.dict().get("Required Python third-party packages")))
|
||||
|
||||
async def run(self, context):
|
||||
prompt = PROMPT_TEMPLATE.format(context=context, format_example=FORMAT_EXAMPLE)
|
||||
rsp = await self._aask_v1(prompt, "task", OUTPUT_MAPPING)
|
||||
async def run(self, context, format=CONFIG.prompt_format):
|
||||
prompt_template, format_example = get_template(templates, format)
|
||||
prompt = prompt_template.format(context=context, format_example=format_example)
|
||||
rsp = await self._aask_v1(prompt, "task", OUTPUT_MAPPING, format=format)
|
||||
self._save(context, rsp)
|
||||
return rsp
|
||||
|
||||
|
|
@ -126,4 +190,3 @@ class AssignTasks(Action):
|
|||
async def run(self, *args, **kwargs):
|
||||
# Here you should implement the actual action
|
||||
pass
|
||||
|
||||
|
|
@ -1,277 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Callable
|
||||
|
||||
from pydantic import parse_obj_as
|
||||
|
||||
from metagpt.actions import Action
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.logs import logger
|
||||
from metagpt.tools.search_engine import SearchEngine
|
||||
from metagpt.tools.web_browser_engine import WebBrowserEngine, WebBrowserEngineType
|
||||
from metagpt.utils.text import generate_prompt_chunk, reduce_message_length
|
||||
|
||||
LANG_PROMPT = "Please respond in {language}."
|
||||
|
||||
RESEARCH_BASE_SYSTEM = """You are an AI critical thinker research assistant. Your sole purpose is to write well \
|
||||
written, critically acclaimed, objective and structured reports on the given text."""
|
||||
|
||||
RESEARCH_TOPIC_SYSTEM = "You are an AI researcher assistant, and your research topic is:\n#TOPIC#\n{topic}"
|
||||
|
||||
SEARCH_TOPIC_PROMPT = """Please provide up to 2 necessary keywords related to your research topic for Google search. \
|
||||
Your response must be in JSON format, for example: ["keyword1", "keyword2"]."""
|
||||
|
||||
SUMMARIZE_SEARCH_PROMPT = """### Requirements
|
||||
1. The keywords related to your research topic and the search results are shown in the "Search Result Information" section.
|
||||
2. Provide up to {decomposition_nums} queries related to your research topic base on the search results.
|
||||
3. Please respond in the following JSON format: ["query1", "query2", "query3", ...].
|
||||
|
||||
### Search Result Information
|
||||
{search_results}
|
||||
"""
|
||||
|
||||
COLLECT_AND_RANKURLS_PROMPT = """### Topic
|
||||
{topic}
|
||||
### Query
|
||||
{query}
|
||||
|
||||
### The online search results
|
||||
{results}
|
||||
|
||||
### Requirements
|
||||
Please remove irrelevant search results that are not related to the query or topic. Then, sort the remaining search results \
|
||||
based on the link credibility. If two results have equal credibility, prioritize them based on the relevance. Provide the
|
||||
ranked results' indices in JSON format, like [0, 1, 3, 4, ...], without including other words.
|
||||
"""
|
||||
|
||||
WEB_BROWSE_AND_SUMMARIZE_PROMPT = '''### Requirements
|
||||
1. Utilize the text in the "Reference Information" section to respond to the question "{query}".
|
||||
2. If the question cannot be directly answered using the text, but the text is related to the research topic, please provide \
|
||||
a comprehensive summary of the text.
|
||||
3. If the text is entirely unrelated to the research topic, please reply with a simple text "Not relevant."
|
||||
4. Include all relevant factual information, numbers, statistics, etc., if available.
|
||||
|
||||
### Reference Information
|
||||
{content}
|
||||
'''
|
||||
|
||||
|
||||
CONDUCT_RESEARCH_PROMPT = '''### Reference Information
|
||||
{content}
|
||||
|
||||
### Requirements
|
||||
Please provide a detailed research report in response to the following topic: "{topic}", using the information provided \
|
||||
above. The report must meet the following requirements:
|
||||
|
||||
- Focus on directly addressing the chosen topic.
|
||||
- Ensure a well-structured and in-depth presentation, incorporating relevant facts and figures where available.
|
||||
- Present data and findings in an intuitive manner, utilizing feature comparative tables, if applicable.
|
||||
- The report should have a minimum word count of 2,000 and be formatted with Markdown syntax following APA style guidelines.
|
||||
- Include all source URLs in APA format at the end of the report.
|
||||
'''
|
||||
|
||||
|
||||
class CollectLinks(Action):
|
||||
"""Action class to collect links from a search engine."""
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "",
|
||||
*args,
|
||||
rank_func: Callable[[list[str]], None] | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(name, *args, **kwargs)
|
||||
self.desc = "Collect links from a search engine."
|
||||
self.search_engine = SearchEngine()
|
||||
self.rank_func = rank_func
|
||||
|
||||
async def run(
|
||||
self,
|
||||
topic: str,
|
||||
decomposition_nums: int = 4,
|
||||
url_per_query: int = 4,
|
||||
system_text: str | None = None,
|
||||
) -> dict[str, list[str]]:
|
||||
"""Run the action to collect links.
|
||||
|
||||
Args:
|
||||
topic: The research topic.
|
||||
decomposition_nums: The number of search questions to generate.
|
||||
url_per_query: The number of URLs to collect per search question.
|
||||
system_text: The system text.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the search questions as keys and the collected URLs as values.
|
||||
"""
|
||||
system_text = system_text if system_text else RESEARCH_TOPIC_SYSTEM.format(topic=topic)
|
||||
keywords = await self._aask(SEARCH_TOPIC_PROMPT, [system_text])
|
||||
try:
|
||||
keywords = json.loads(keywords)
|
||||
keywords = parse_obj_as(list[str], keywords)
|
||||
except Exception as e:
|
||||
logger.exception(f"fail to get keywords related to the research topic \"{topic}\" for {e}")
|
||||
keywords = [topic]
|
||||
results = await asyncio.gather(*(self.search_engine.run(i, as_string=False) for i in keywords))
|
||||
|
||||
def gen_msg():
|
||||
while True:
|
||||
search_results = "\n".join(f"#### Keyword: {i}\n Search Result: {j}\n" for (i, j) in zip(keywords, results))
|
||||
prompt = SUMMARIZE_SEARCH_PROMPT.format(decomposition_nums=decomposition_nums, search_results=search_results)
|
||||
yield prompt
|
||||
remove = max(results, key=len)
|
||||
remove.pop()
|
||||
if len(remove) == 0:
|
||||
break
|
||||
prompt = reduce_message_length(gen_msg(), self.llm.model, system_text, CONFIG.max_tokens_rsp)
|
||||
logger.debug(prompt)
|
||||
queries = await self._aask(prompt, [system_text])
|
||||
try:
|
||||
queries = json.loads(queries)
|
||||
queries = parse_obj_as(list[str], queries)
|
||||
except Exception as e:
|
||||
logger.exception(f"fail to break down the research question due to {e}")
|
||||
queries = keywords
|
||||
ret = {}
|
||||
for query in queries:
|
||||
ret[query] = await self._search_and_rank_urls(topic, query, url_per_query)
|
||||
return ret
|
||||
|
||||
async def _search_and_rank_urls(self, topic: str, query: str, num_results: int = 4) -> list[str]:
|
||||
"""Search and rank URLs based on a query.
|
||||
|
||||
Args:
|
||||
topic: The research topic.
|
||||
query: The search query.
|
||||
num_results: The number of URLs to collect.
|
||||
|
||||
Returns:
|
||||
A list of ranked URLs.
|
||||
"""
|
||||
max_results = max(num_results * 2, 6)
|
||||
results = await self.search_engine.run(query, max_results=max_results, as_string=False)
|
||||
_results = "\n".join(f"{i}: {j}" for i, j in zip(range(max_results), results))
|
||||
prompt = COLLECT_AND_RANKURLS_PROMPT.format(topic=topic, query=query, results=_results)
|
||||
logger.debug(prompt)
|
||||
indices = await self._aask(prompt)
|
||||
try:
|
||||
indices = json.loads(indices)
|
||||
assert all(isinstance(i, int) for i in indices)
|
||||
except Exception as e:
|
||||
logger.exception(f"fail to rank results for {e}")
|
||||
indices = list(range(max_results))
|
||||
results = [results[i] for i in indices]
|
||||
if self.rank_func:
|
||||
results = self.rank_func(results)
|
||||
return [i["link"] for i in results[:num_results]]
|
||||
|
||||
|
||||
class WebBrowseAndSummarize(Action):
|
||||
"""Action class to explore the web and provide summaries of articles and webpages."""
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
browse_func: Callable[[list[str]], None] | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(*args, **kwargs)
|
||||
if CONFIG.model_for_researcher_summary:
|
||||
self.llm.model = CONFIG.model_for_researcher_summary
|
||||
self.web_browser_engine = WebBrowserEngine(
|
||||
engine=WebBrowserEngineType.CUSTOM if browse_func else None,
|
||||
run_func=browse_func,
|
||||
)
|
||||
self.desc = "Explore the web and provide summaries of articles and webpages."
|
||||
|
||||
async def run(
|
||||
self,
|
||||
url: str,
|
||||
*urls: str,
|
||||
query: str,
|
||||
system_text: str = RESEARCH_BASE_SYSTEM,
|
||||
) -> dict[str, str]:
|
||||
"""Run the action to browse the web and provide summaries.
|
||||
|
||||
Args:
|
||||
url: The main URL to browse.
|
||||
urls: Additional URLs to browse.
|
||||
query: The research question.
|
||||
system_text: The system text.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the URLs as keys and their summaries as values.
|
||||
"""
|
||||
contents = await self.web_browser_engine.run(url, *urls)
|
||||
if not urls:
|
||||
contents = [contents]
|
||||
|
||||
summaries = {}
|
||||
prompt_template = WEB_BROWSE_AND_SUMMARIZE_PROMPT.format(query=query, content="{}")
|
||||
for u, content in zip([url, *urls], contents):
|
||||
content = content.inner_text
|
||||
chunk_summaries = []
|
||||
for prompt in generate_prompt_chunk(content, prompt_template, self.llm.model, system_text, CONFIG.max_tokens_rsp):
|
||||
logger.debug(prompt)
|
||||
summary = await self._aask(prompt, [system_text])
|
||||
if summary == "Not relevant.":
|
||||
continue
|
||||
chunk_summaries.append(summary)
|
||||
|
||||
if not chunk_summaries:
|
||||
summaries[u] = None
|
||||
continue
|
||||
|
||||
if len(chunk_summaries) == 1:
|
||||
summaries[u] = chunk_summaries[0]
|
||||
continue
|
||||
|
||||
content = "\n".join(chunk_summaries)
|
||||
prompt = WEB_BROWSE_AND_SUMMARIZE_PROMPT.format(query=query, content=content)
|
||||
summary = await self._aask(prompt, [system_text])
|
||||
summaries[u] = summary
|
||||
return summaries
|
||||
|
||||
|
||||
class ConductResearch(Action):
|
||||
"""Action class to conduct research and generate a research report."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if CONFIG.model_for_researcher_report:
|
||||
self.llm.model = CONFIG.model_for_researcher_report
|
||||
|
||||
async def run(
|
||||
self,
|
||||
topic: str,
|
||||
content: str,
|
||||
system_text: str = RESEARCH_BASE_SYSTEM,
|
||||
) -> str:
|
||||
"""Run the action to conduct research and generate a research report.
|
||||
|
||||
Args:
|
||||
topic: The research topic.
|
||||
content: The content for research.
|
||||
system_text: The system text.
|
||||
|
||||
Returns:
|
||||
The generated research report.
|
||||
"""
|
||||
prompt = CONDUCT_RESEARCH_PROMPT.format(topic=topic, content=content)
|
||||
logger.debug(prompt)
|
||||
self.llm.auto_max_tokens = True
|
||||
return await self._aask(prompt, [system_text])
|
||||
|
||||
|
||||
def get_research_system_text(topic: str, language: str):
|
||||
"""Get the system text for conducting research.
|
||||
|
||||
Args:
|
||||
topic: The research topic.
|
||||
language: The language for the system text.
|
||||
|
||||
Returns:
|
||||
The system text for conducting research.
|
||||
"""
|
||||
return " ".join((RESEARCH_TOPIC_SYSTEM.format(topic=topic), LANG_PROMPT.format(language=language)))
|
||||
|
|
@ -1,214 +0,0 @@
|
|||
"""Code Docstring Generator.
|
||||
|
||||
This script provides a tool to automatically generate docstrings for Python code. It uses the specified style to create
|
||||
docstrings for the given code and system text.
|
||||
|
||||
Usage:
|
||||
python3 -m metagpt.actions.write_docstring <filename> [--overwrite] [--style=<docstring_style>]
|
||||
|
||||
Arguments:
|
||||
filename The path to the Python file for which you want to generate docstrings.
|
||||
|
||||
Options:
|
||||
--overwrite If specified, overwrite the original file with the code containing docstrings.
|
||||
--style=<docstring_style> Specify the style of the generated docstrings.
|
||||
Valid values: 'google', 'numpy', or 'sphinx'.
|
||||
Default: 'google'
|
||||
|
||||
Example:
|
||||
python3 -m metagpt.actions.write_docstring startup.py --overwrite False --style=numpy
|
||||
|
||||
This script uses the 'fire' library to create a command-line interface. It generates docstrings for the given Python code using
|
||||
the specified docstring style and adds them to the code.
|
||||
"""
|
||||
import ast
|
||||
from typing import Literal
|
||||
|
||||
from metagpt.actions.action import Action
|
||||
from metagpt.utils.common import OutputParser
|
||||
from metagpt.utils.pycst import merge_docstring
|
||||
|
||||
PYTHON_DOCSTRING_SYSTEM = '''### Requirements
|
||||
1. Add docstrings to the given code following the {style} style.
|
||||
2. Replace the function body with an Ellipsis object(...) to reduce output.
|
||||
3. If the types are already annotated, there is no need to include them in the docstring.
|
||||
4. Extract only class, function or the docstrings for the module parts from the given Python code, avoiding any other text.
|
||||
|
||||
### Input Example
|
||||
```python
|
||||
def function_with_pep484_type_annotations(param1: int) -> bool:
|
||||
return isinstance(param1, int)
|
||||
|
||||
class ExampleError(Exception):
|
||||
def __init__(self, msg: str):
|
||||
self.msg = msg
|
||||
```
|
||||
|
||||
### Output Example
|
||||
```python
|
||||
{example}
|
||||
```
|
||||
'''
|
||||
|
||||
# https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html
|
||||
|
||||
PYTHON_DOCSTRING_EXAMPLE_GOOGLE = '''
|
||||
def function_with_pep484_type_annotations(param1: int) -> bool:
|
||||
"""Example function with PEP 484 type annotations.
|
||||
|
||||
Extended description of function.
|
||||
|
||||
Args:
|
||||
param1: The first parameter.
|
||||
|
||||
Returns:
|
||||
The return value. True for success, False otherwise.
|
||||
"""
|
||||
...
|
||||
|
||||
class ExampleError(Exception):
|
||||
"""Exceptions are documented in the same way as classes.
|
||||
|
||||
The __init__ method was documented in the class level docstring.
|
||||
|
||||
Args:
|
||||
msg: Human readable string describing the exception.
|
||||
|
||||
Attributes:
|
||||
msg: Human readable string describing the exception.
|
||||
"""
|
||||
...
|
||||
'''
|
||||
|
||||
PYTHON_DOCSTRING_EXAMPLE_NUMPY = '''
|
||||
def function_with_pep484_type_annotations(param1: int) -> bool:
|
||||
"""
|
||||
Example function with PEP 484 type annotations.
|
||||
|
||||
Extended description of function.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
param1
|
||||
The first parameter.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
The return value. True for success, False otherwise.
|
||||
"""
|
||||
...
|
||||
|
||||
class ExampleError(Exception):
|
||||
"""
|
||||
Exceptions are documented in the same way as classes.
|
||||
|
||||
The __init__ method was documented in the class level docstring.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
msg
|
||||
Human readable string describing the exception.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
msg
|
||||
Human readable string describing the exception.
|
||||
"""
|
||||
...
|
||||
'''
|
||||
|
||||
PYTHON_DOCSTRING_EXAMPLE_SPHINX = '''
|
||||
def function_with_pep484_type_annotations(param1: int) -> bool:
|
||||
"""Example function with PEP 484 type annotations.
|
||||
|
||||
Extended description of function.
|
||||
|
||||
:param param1: The first parameter.
|
||||
:type param1: int
|
||||
|
||||
:return: The return value. True for success, False otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
...
|
||||
|
||||
class ExampleError(Exception):
|
||||
"""Exceptions are documented in the same way as classes.
|
||||
|
||||
The __init__ method was documented in the class level docstring.
|
||||
|
||||
:param msg: Human-readable string describing the exception.
|
||||
:type msg: str
|
||||
"""
|
||||
...
|
||||
'''
|
||||
|
||||
_python_docstring_style = {
|
||||
"google": PYTHON_DOCSTRING_EXAMPLE_GOOGLE.strip(),
|
||||
"numpy": PYTHON_DOCSTRING_EXAMPLE_NUMPY.strip(),
|
||||
"sphinx": PYTHON_DOCSTRING_EXAMPLE_SPHINX.strip(),
|
||||
}
|
||||
|
||||
|
||||
class WriteDocstring(Action):
|
||||
"""This class is used to write docstrings for code.
|
||||
|
||||
Attributes:
|
||||
desc: A string describing the action.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.desc = "Write docstring for code."
|
||||
|
||||
async def run(
|
||||
self, code: str,
|
||||
system_text: str = PYTHON_DOCSTRING_SYSTEM,
|
||||
style: Literal["google", "numpy", "sphinx"] = "google",
|
||||
) -> str:
|
||||
"""Writes docstrings for the given code and system text in the specified style.
|
||||
|
||||
Args:
|
||||
code: A string of Python code.
|
||||
system_text: A string of system text.
|
||||
style: A string specifying the style of the docstring. Can be 'google', 'numpy', or 'sphinx'.
|
||||
|
||||
Returns:
|
||||
The Python code with docstrings added.
|
||||
"""
|
||||
system_text = system_text.format(style=style, example=_python_docstring_style[style])
|
||||
simplified_code = _simplify_python_code(code)
|
||||
documented_code = await self._aask(f"```python\n{simplified_code}\n```", [system_text])
|
||||
documented_code = OutputParser.parse_python_code(documented_code)
|
||||
return merge_docstring(code, documented_code)
|
||||
|
||||
|
||||
def _simplify_python_code(code: str) -> None:
|
||||
"""Simplifies the given Python code by removing expressions and the last if statement.
|
||||
|
||||
Args:
|
||||
code: A string of Python code.
|
||||
|
||||
Returns:
|
||||
The simplified Python code.
|
||||
"""
|
||||
code_tree = ast.parse(code)
|
||||
code_tree.body = [i for i in code_tree.body if not isinstance(i, ast.Expr)]
|
||||
if isinstance(code_tree.body[-1], ast.If):
|
||||
code_tree.body.pop()
|
||||
return ast.unparse(code_tree)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import fire
|
||||
|
||||
async def run(filename: str, overwrite: bool = False, style: Literal["google", "numpy", "sphinx"] = "google"):
|
||||
with open(filename) as f:
|
||||
code = f.read()
|
||||
code = await WriteDocstring().run(code, style=style)
|
||||
if overwrite:
|
||||
with open(filename, "w") as f:
|
||||
f.write(code)
|
||||
return code
|
||||
|
||||
fire.Fire(run)
|
||||
|
|
@ -5,13 +5,102 @@
|
|||
@Author : alexanderwu
|
||||
@File : write_prd.py
|
||||
"""
|
||||
from typing import List, Tuple
|
||||
from typing import List
|
||||
|
||||
from metagpt.actions import Action, ActionOutput
|
||||
from metagpt.actions.search_and_summarize import SearchAndSummarize
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.logs import logger
|
||||
from metagpt.utils.get_template import get_template
|
||||
|
||||
PROMPT_TEMPLATE = """
|
||||
templates = {
|
||||
"json": {
|
||||
"PROMPT_TEMPLATE": """
|
||||
# Context
|
||||
## Original Requirements
|
||||
{requirements}
|
||||
|
||||
## Search Information
|
||||
{search_information}
|
||||
|
||||
## mermaid quadrantChart code syntax example. DONT USE QUOTO IN CODE DUE TO INVALID SYNTAX. Replace the <Campain X> with REAL COMPETITOR NAME
|
||||
```mermaid
|
||||
quadrantChart
|
||||
title Reach and engagement of campaigns
|
||||
x-axis Low Reach --> High Reach
|
||||
y-axis Low Engagement --> High Engagement
|
||||
quadrant-1 We should expand
|
||||
quadrant-2 Need to promote
|
||||
quadrant-3 Re-evaluate
|
||||
quadrant-4 May be improved
|
||||
"Campaign: A": [0.3, 0.6]
|
||||
"Campaign B": [0.45, 0.23]
|
||||
"Campaign C": [0.57, 0.69]
|
||||
"Campaign D": [0.78, 0.34]
|
||||
"Campaign E": [0.40, 0.34]
|
||||
"Campaign F": [0.35, 0.78]
|
||||
"Our Target Product": [0.5, 0.6]
|
||||
```
|
||||
|
||||
## Format example
|
||||
{format_example}
|
||||
-----
|
||||
Role: You are a professional product manager; the goal is to design a concise, usable, efficient product
|
||||
Requirements: According to the context, fill in the following missing information, each section name is a key in json ,If the requirements are unclear, ensure minimum viability and avoid excessive design
|
||||
|
||||
## Original Requirements: Provide as Plain text, place the polished complete original requirements here
|
||||
|
||||
## Product Goals: Provided as Python list[str], up to 3 clear, orthogonal product goals. If the requirement itself is simple, the goal should also be simple
|
||||
|
||||
## User Stories: Provided as Python list[str], up to 5 scenario-based user stories, If the requirement itself is simple, the user stories should also be less
|
||||
|
||||
## Competitive Analysis: Provided as Python list[str], up to 7 competitive product analyses, consider as similar competitors as possible
|
||||
|
||||
## Competitive Quadrant Chart: Use mermaid quadrantChart code syntax. up to 14 competitive products. Translation: Distribute these competitor scores evenly between 0 and 1, trying to conform to a normal distribution centered around 0.5 as much as possible.
|
||||
|
||||
## Requirement Analysis: Provide as Plain text. Be simple. LESS IS MORE. Make your requirements less dumb. Delete the parts unnessasery.
|
||||
|
||||
## Requirement Pool: Provided as Python list[list[str], the parameters are requirement description, priority(P0/P1/P2), respectively, comply with PEP standards; no more than 5 requirements and consider to make its difficulty lower
|
||||
|
||||
## UI Design draft: Provide as Plain text. Be simple. Describe the elements and functions, also provide a simple style description and layout description.
|
||||
## Anything UNCLEAR: Provide as Plain text. Make clear here.
|
||||
|
||||
output a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example,
|
||||
and only output the json inside this tag, nothing else
|
||||
""",
|
||||
"FORMAT_EXAMPLE": """
|
||||
[CONTENT]
|
||||
{
|
||||
"Original Requirements": "",
|
||||
"Search Information": "",
|
||||
"Requirements": "",
|
||||
"Product Goals": [],
|
||||
"User Stories": [],
|
||||
"Competitive Analysis": [],
|
||||
"Competitive Quadrant Chart": "quadrantChart
|
||||
title Reach and engagement of campaigns
|
||||
x-axis Low Reach --> High Reach
|
||||
y-axis Low Engagement --> High Engagement
|
||||
quadrant-1 We should expand
|
||||
quadrant-2 Need to promote
|
||||
quadrant-3 Re-evaluate
|
||||
quadrant-4 May be improved
|
||||
Campaign A: [0.3, 0.6]
|
||||
Campaign B: [0.45, 0.23]
|
||||
Campaign C: [0.57, 0.69]
|
||||
Campaign D: [0.78, 0.34]
|
||||
Campaign E: [0.40, 0.34]
|
||||
Campaign F: [0.35, 0.78]",
|
||||
"Requirement Analysis": "",
|
||||
"Requirement Pool": [["P0","P0 requirement"],["P1","P1 requirement"]],
|
||||
"UI Design draft": "",
|
||||
"Anything UNCLEAR": "",
|
||||
}
|
||||
[/CONTENT]
|
||||
""",
|
||||
},
|
||||
"markdown": {
|
||||
"PROMPT_TEMPLATE": """
|
||||
# Context
|
||||
## Original Requirements
|
||||
{requirements}
|
||||
|
|
@ -57,12 +146,12 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. AND '## <SECTION_NAME>' SHOULD W
|
|||
|
||||
## Requirement Analysis: Provide as Plain text. Be simple. LESS IS MORE. Make your requirements less dumb. Delete the parts unnessasery.
|
||||
|
||||
## Requirement Pool: Provided as Python list[str, str], the parameters are requirement description, priority(P0/P1/P2), respectively, comply with PEP standards; no more than 5 requirements and consider to make its difficulty lower
|
||||
## Requirement Pool: Provided as Python list[list[str], the parameters are requirement description, priority(P0/P1/P2), respectively, comply with PEP standards; no more than 5 requirements and consider to make its difficulty lower
|
||||
|
||||
## UI Design draft: Provide as Plain text. Be simple. Describe the elements and functions, also provide a simple style description and layout description.
|
||||
## Anything UNCLEAR: Provide as Plain text. Make clear here.
|
||||
"""
|
||||
FORMAT_EXAMPLE = """
|
||||
""",
|
||||
"FORMAT_EXAMPLE": """
|
||||
---
|
||||
## Original Requirements
|
||||
The boss ...
|
||||
|
|
@ -102,7 +191,7 @@ The product should be a ...
|
|||
## Requirement Pool
|
||||
```python
|
||||
[
|
||||
("End game ...", "P0")
|
||||
["End game ...", "P0"]
|
||||
]
|
||||
```
|
||||
|
||||
|
|
@ -112,7 +201,10 @@ Give a basic function description, and a draft
|
|||
## Anything UNCLEAR
|
||||
There are no unclear points.
|
||||
---
|
||||
"""
|
||||
""",
|
||||
},
|
||||
}
|
||||
|
||||
OUTPUT_MAPPING = {
|
||||
"Original Requirements": (str, ...),
|
||||
"Product Goals": (List[str], ...),
|
||||
|
|
@ -120,8 +212,8 @@ OUTPUT_MAPPING = {
|
|||
"Competitive Analysis": (List[str], ...),
|
||||
"Competitive Quadrant Chart": (str, ...),
|
||||
"Requirement Analysis": (str, ...),
|
||||
"Requirement Pool": (List[Tuple[str, str]], ...),
|
||||
"UI Design draft":(str, ...),
|
||||
"Requirement Pool": (List[List[str]], ...),
|
||||
"UI Design draft": (str, ...),
|
||||
"Anything UNCLEAR": (str, ...),
|
||||
}
|
||||
|
||||
|
|
@ -130,7 +222,7 @@ class WritePRD(Action):
|
|||
def __init__(self, name="", context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
|
||||
async def run(self, requirements, *args, **kwargs) -> ActionOutput:
|
||||
async def run(self, requirements, format=CONFIG.prompt_format, *args, **kwargs) -> ActionOutput:
|
||||
sas = SearchAndSummarize()
|
||||
# rsp = await sas.run(context=requirements, system_text=SEARCH_AND_SUMMARIZE_SYSTEM_EN_US)
|
||||
rsp = ""
|
||||
|
|
@ -139,9 +231,11 @@ class WritePRD(Action):
|
|||
logger.info(sas.result)
|
||||
logger.info(rsp)
|
||||
|
||||
prompt = PROMPT_TEMPLATE.format(requirements=requirements, search_information=info,
|
||||
format_example=FORMAT_EXAMPLE)
|
||||
prompt_template, format_example = get_template(templates, format)
|
||||
prompt = prompt_template.format(
|
||||
requirements=requirements, search_information=info, format_example=format_example
|
||||
)
|
||||
logger.debug(prompt)
|
||||
prd = await self._aask_v1(prompt, "prd", OUTPUT_MAPPING)
|
||||
# prd = await self._aask_v1(prompt, "prd", OUTPUT_MAPPING)
|
||||
prd = await self._aask_v1(prompt, "prd", OUTPUT_MAPPING, format=format)
|
||||
return prd
|
||||
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023/5/11 17:45
|
||||
@Author : alexanderwu
|
||||
@File : write_prd_review.py
|
||||
"""
|
||||
from metagpt.actions.action import Action
|
||||
|
||||
|
||||
class WritePRDReview(Action):
|
||||
def __init__(self, name, context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
self.prd = None
|
||||
self.desc = "Based on the PRD, conduct a PRD Review, providing clear and detailed feedback"
|
||||
self.prd_review_prompt_template = """
|
||||
Given the following Product Requirement Document (PRD):
|
||||
{prd}
|
||||
|
||||
As a project manager, please review it and provide your feedback and suggestions.
|
||||
"""
|
||||
|
||||
async def run(self, prd):
|
||||
self.prd = prd
|
||||
prompt = self.prd_review_prompt_template.format(prd=self.prd)
|
||||
review = await self._aask(prompt)
|
||||
return review
|
||||
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
@File : environment.py
|
||||
"""
|
||||
from metagpt.actions.action import Action
|
||||
from metagpt.logs import logger
|
||||
from metagpt.utils.common import CodeParser
|
||||
|
||||
PROMPT_TEMPLATE = """
|
||||
|
|
@ -35,7 +36,15 @@ class WriteTest(Action):
|
|||
|
||||
async def write_code(self, prompt):
|
||||
code_rsp = await self._aask(prompt)
|
||||
code = CodeParser.parse_code(block="", text=code_rsp)
|
||||
|
||||
try:
|
||||
code = CodeParser.parse_code(block="", text=code_rsp)
|
||||
except Exception:
|
||||
# Handle the exception if needed
|
||||
logger.error(f"Can't parse the code: {code_rsp}")
|
||||
|
||||
# Return code_rsp in case of an exception, assuming llm just returns code as it is and doesn't wrap it inside ```
|
||||
code = code_rsp
|
||||
return code
|
||||
|
||||
async def run(self, code_to_test, test_file_name, source_file_path, workspace):
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ class Config(metaclass=Singleton):
|
|||
self.openai_api_rpm = self._get("RPM", 3)
|
||||
self.openai_api_model = self._get("OPENAI_API_MODEL", "gpt-4")
|
||||
self.max_tokens_rsp = self._get("MAX_TOKENS", 2048)
|
||||
self.deployment_name = self._get('DEPLOYMENT_NAME')
|
||||
self.deployment_name = self._get("DEPLOYMENT_NAME")
|
||||
self.deployment_id = self._get("DEPLOYMENT_ID")
|
||||
|
||||
self.claude_api_key = self._get("Anthropic_API_KEY")
|
||||
|
|
@ -83,6 +83,10 @@ class Config(metaclass=Singleton):
|
|||
self.calc_usage = self._get("CALC_USAGE", True)
|
||||
self.model_for_researcher_summary = self._get("MODEL_FOR_RESEARCHER_SUMMARY")
|
||||
self.model_for_researcher_report = self._get("MODEL_FOR_RESEARCHER_REPORT")
|
||||
self.mermaid_engine = self._get("MERMAID_ENGINE", "nodejs")
|
||||
self.pyppeteer_executable_path = self._get("PYPPETEER_EXECUTABLE_PATH", "")
|
||||
|
||||
self.prompt_format = self._get("PROMPT_FORMAT", "markdown")
|
||||
|
||||
def _init_with_config_files_and_env(self, configs: dict, yaml_file):
|
||||
"""Load from config/key.yaml, config/config.yaml, and env in decreasing order of priority"""
|
||||
|
|
@ -111,4 +115,4 @@ class Config(metaclass=Singleton):
|
|||
return value
|
||||
|
||||
|
||||
CONFIG = Config()
|
||||
CONFIG = Config()
|
||||
|
|
|
|||
|
|
@ -12,9 +12,11 @@ def get_project_root():
|
|||
"""Search upwards to find the project root directory."""
|
||||
current_path = Path.cwd()
|
||||
while True:
|
||||
if (current_path / '.git').exists() or \
|
||||
(current_path / '.project_root').exists() or \
|
||||
(current_path / '.gitignore').exists():
|
||||
if (
|
||||
(current_path / ".git").exists()
|
||||
or (current_path / ".project_root").exists()
|
||||
or (current_path / ".gitignore").exists()
|
||||
):
|
||||
return current_path
|
||||
parent_path = current_path.parent
|
||||
if parent_path == current_path:
|
||||
|
|
@ -23,15 +25,61 @@ def get_project_root():
|
|||
|
||||
|
||||
PROJECT_ROOT = get_project_root()
|
||||
DATA_PATH = PROJECT_ROOT / 'data'
|
||||
WORKSPACE_ROOT = PROJECT_ROOT / 'workspace'
|
||||
PROMPT_PATH = PROJECT_ROOT / 'metagpt/prompts'
|
||||
UT_PATH = PROJECT_ROOT / 'data/ut'
|
||||
DATA_PATH = PROJECT_ROOT / "data"
|
||||
WORKSPACE_ROOT = PROJECT_ROOT / "workspace"
|
||||
PROMPT_PATH = PROJECT_ROOT / "metagpt/prompts"
|
||||
UT_PATH = PROJECT_ROOT / "data/ut"
|
||||
SWAGGER_PATH = UT_PATH / "files/api/"
|
||||
UT_PY_PATH = UT_PATH / "files/ut/"
|
||||
API_QUESTIONS_PATH = UT_PATH / "files/question/"
|
||||
YAPI_URL = "http://yapi.deepwisdomai.com/"
|
||||
TMP = PROJECT_ROOT / 'tmp'
|
||||
TMP = PROJECT_ROOT / "tmp"
|
||||
RESEARCH_PATH = DATA_PATH / "research"
|
||||
TUTORIAL_PATH = DATA_PATH / "tutorial_docx"
|
||||
|
||||
SKILL_DIRECTORY = PROJECT_ROOT / "metagpt/skills"
|
||||
|
||||
MEM_TTL = 24 * 30 * 3600
|
||||
|
||||
### MineCraft ###
|
||||
CKPT_DIR = PROJECT_ROOT / "metagpt/ckpt"
|
||||
LOG_DIR = PROJECT_ROOT / "logs"
|
||||
|
||||
DEFAULT_WARMUP = {
|
||||
"context": 15,
|
||||
"biome": 10,
|
||||
"time": 15,
|
||||
"nearby_blocks": 0,
|
||||
"other_blocks": 10,
|
||||
"nearby_entities": 5,
|
||||
"health": 15,
|
||||
"hunger": 15,
|
||||
"position": 0,
|
||||
"equipment": 0,
|
||||
"inventory": 0,
|
||||
"optional_inventory_items": 7,
|
||||
"chests": 0,
|
||||
"completed_tasks": 0,
|
||||
"failed_tasks": 0,
|
||||
}
|
||||
|
||||
CURRICULUM_OB = [
|
||||
"context",
|
||||
"biome",
|
||||
"time",
|
||||
"nearby_blocks",
|
||||
"other_blocks",
|
||||
"nearby_entities",
|
||||
"health",
|
||||
"hunger",
|
||||
"position",
|
||||
"equipment",
|
||||
"inventory",
|
||||
"chests",
|
||||
"completed_tasks",
|
||||
"failed_tasks",
|
||||
]
|
||||
|
||||
|
||||
CORE_INVENTORY_ITEMS = r".*_log|.*_planks|stick|crafting_table|furnace"
|
||||
r"|cobblestone|dirt|coal|.*_pickaxe|.*_sword|.*_axe", # curriculum_agent: only show these items in inventory before optional_inventory_items reached in warm up
|
||||
|
|
@ -5,13 +5,15 @@
|
|||
@Author : unkn-wn (Leon Yee)
|
||||
@File : lancedb_store.py
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
|
||||
import lancedb
|
||||
import shutil, os
|
||||
|
||||
|
||||
class LanceStore:
|
||||
def __init__(self, name):
|
||||
db = lancedb.connect('./data/lancedb')
|
||||
db = lancedb.connect("./data/lancedb")
|
||||
self.db = db
|
||||
self.name = name
|
||||
self.table = None
|
||||
|
|
@ -23,16 +25,18 @@ class LanceStore:
|
|||
# .where - SQL syntax filtering for metadata (e.g. where("price > 100"))
|
||||
# .metric - specifies the distance metric to use
|
||||
# .nprobes - values will yield better recall (more likely to find vectors if they exist) at the expense of latency.
|
||||
if self.table == None: raise Exception("Table not created yet, please add data first.")
|
||||
if self.table is None:
|
||||
raise Exception("Table not created yet, please add data first.")
|
||||
|
||||
results = self.table \
|
||||
.search(query) \
|
||||
.limit(n_results) \
|
||||
.select(kwargs.get('select')) \
|
||||
.where(kwargs.get('where')) \
|
||||
.metric(metric) \
|
||||
.nprobes(nprobes) \
|
||||
results = (
|
||||
self.table.search(query)
|
||||
.limit(n_results)
|
||||
.select(kwargs.get("select"))
|
||||
.where(kwargs.get("where"))
|
||||
.metric(metric)
|
||||
.nprobes(nprobes)
|
||||
.to_df()
|
||||
)
|
||||
return results
|
||||
|
||||
def persist(self):
|
||||
|
|
@ -45,14 +49,11 @@ class LanceStore:
|
|||
|
||||
documents = []
|
||||
for i in range(len(data)):
|
||||
row = {
|
||||
'vector': data[i],
|
||||
'id': ids[i]
|
||||
}
|
||||
row = {"vector": data[i], "id": ids[i]}
|
||||
row.update(metadatas[i])
|
||||
documents.append(row)
|
||||
|
||||
if self.table != None:
|
||||
if self.table is not None:
|
||||
self.table.add(documents)
|
||||
else:
|
||||
self.table = self.db.create_table(self.name, documents)
|
||||
|
|
@ -61,13 +62,10 @@ class LanceStore:
|
|||
# This function is for adding individual documents
|
||||
# It assumes you're passing in a single vector embedding, metadata, and id
|
||||
|
||||
row = {
|
||||
'vector': data,
|
||||
'id': _id
|
||||
}
|
||||
row = {"vector": data, "id": _id}
|
||||
row.update(metadata)
|
||||
|
||||
if self.table != None:
|
||||
if self.table is not None:
|
||||
self.table.add([row])
|
||||
else:
|
||||
self.table = self.db.create_table(self.name, [row])
|
||||
|
|
@ -75,7 +73,8 @@ class LanceStore:
|
|||
def delete(self, _id):
|
||||
# This function deletes a row by id.
|
||||
# LanceDB delete syntax uses SQL syntax, so you can use "in" or "="
|
||||
if self.table == None: raise Exception("Table not created yet, please add data first")
|
||||
if self.table is None:
|
||||
raise Exception("Table not created yet, please add data first")
|
||||
|
||||
if isinstance(_id, str):
|
||||
return self.table.delete(f"id = '{_id}'")
|
||||
|
|
@ -85,6 +84,6 @@ class LanceStore:
|
|||
def drop(self, name):
|
||||
# This function drops a table, if it exists.
|
||||
|
||||
path = os.path.join(self.db.uri, name + '.lance')
|
||||
path = os.path.join(self.db.uri, name + ".lance")
|
||||
if os.path.exists(path):
|
||||
shutil.rmtree(path)
|
||||
shutil.rmtree(path)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
"""
|
||||
import asyncio
|
||||
from typing import Iterable
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from metagpt.memory import Memory
|
||||
|
|
|
|||
386
metagpt/minecraft_team.py
Normal file
386
metagpt/minecraft_team.py
Normal file
|
|
@ -0,0 +1,386 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# @Date : 2023/9/23 14:14
|
||||
# @Author : stellahong (stellahong@fuzhi.ai)
|
||||
# @Desc :
|
||||
from typing import Iterable, Dict, Any
|
||||
from pydantic import BaseModel, Field
|
||||
import requests
|
||||
import json
|
||||
import re
|
||||
|
||||
from metagpt.logs import logger
|
||||
from metagpt.roles import Role
|
||||
from metagpt.schema import Message
|
||||
from metagpt.software_company import SoftwareCompany
|
||||
|
||||
from metagpt.actions.minecraft.player_action import PlayerActions
|
||||
from metagpt.roles.minecraft.minecraft_base import Minecraft
|
||||
from metagpt.environment import Environment
|
||||
from metagpt.mineflayer_environment import MineflayerEnv
|
||||
from metagpt.const import CKPT_DIR
|
||||
from metagpt.actions.minecraft.control_primitives import load_skills_code
|
||||
|
||||
|
||||
class GameEnvironment(BaseModel, arbitrary_types_allowed=True):
|
||||
"""
|
||||
游戏环境的记忆,用于多个agent进行信息的共享和缓存,而不需要重复在自己的角色内维护缓存
|
||||
"""
|
||||
|
||||
event: dict[str, Any] = Field(default_factory=dict)
|
||||
current_task: str = Field(default="Mine 1 wood log")
|
||||
task_execution_time: float = Field(default=float)
|
||||
context: str = Field(
|
||||
default="You can mine one of oak, birch, spruce, jungle, acacia, dark oak, or mangrove logs."
|
||||
)
|
||||
code: str = Field(default="")
|
||||
program_name: str = Field(default="")
|
||||
critique: str = Field(default="")
|
||||
skills: dict = Field(default_factory=dict) # for skills.json
|
||||
retrieve_skills: list[str] = Field(default_factory=list)
|
||||
event_summary: str = Field(default="")
|
||||
|
||||
qa_cache: dict[str, str] = Field(default_factory=dict)
|
||||
completed_tasks: list[str] = Field(default_factory=list) # Critique things
|
||||
failed_tasks: list[str] = Field(default_factory=list)
|
||||
|
||||
skill_desp: str = Field(default="")
|
||||
|
||||
chest_memory: dict[str, Any] = Field(
|
||||
default_factory=dict
|
||||
) # eg: {'(1344, 64, 1381)': 'Unknown'}
|
||||
chest_observation: str = Field(default="") # eg: "Chests: None\n\n"
|
||||
|
||||
mf_instance: MineflayerEnv = Field(default_factory=MineflayerEnv)
|
||||
|
||||
@property
|
||||
def progress(self):
|
||||
# return len(self.completed_tasks) + 10 # Test only
|
||||
return len(self.completed_tasks)
|
||||
|
||||
@property
|
||||
def programs(self):
|
||||
programs = ""
|
||||
if self.code == "":
|
||||
return programs # TODO: maybe fix 10054 now, a better way is isolating env.step() like voyager
|
||||
for skill_name, entry in self.skills.items():
|
||||
programs += f"{entry['code']}\n\n"
|
||||
for primitives in load_skills_code():
|
||||
programs += f"{primitives}\n\n"
|
||||
return programs
|
||||
|
||||
@property
|
||||
def warm_up(self):
|
||||
return self.mf_instance.warm_up
|
||||
|
||||
@property
|
||||
def core_inv_items_regex(self):
|
||||
return self.mf_instance.core_inv_items_regex
|
||||
|
||||
def set_mc_port(self, mc_port):
|
||||
self.mf_instance.set_mc_port(mc_port)
|
||||
|
||||
def set_mc_resume(self, resume: bool = False): # TODO: mv to config
|
||||
if resume:
|
||||
logger.info(f"Loading Action Developer from {CKPT_DIR}/action")
|
||||
with open(f"{CKPT_DIR}/action/chest_memory.json", "r") as f:
|
||||
self.chest_memory = json.load(f)
|
||||
|
||||
logger.info(f"Loading Curriculum Agent from {CKPT_DIR}/curriculum")
|
||||
with open(f"{CKPT_DIR}/curriculum/completed_tasks.json", "r") as f:
|
||||
self.completed_tasks = json.load(f)
|
||||
with open(f"{CKPT_DIR}/curriculum/failed_tasks.json", "r") as f:
|
||||
self.failed_tasks = json.load(f)
|
||||
with open(f"{CKPT_DIR}/curriculum/qa_cache.json", "r") as f:
|
||||
self.qa_cache = json.load(f)
|
||||
|
||||
logger.info(f"Loading Skill Manager from {CKPT_DIR}/skill\033[0m")
|
||||
with open(f"{CKPT_DIR}/skill/skills.json", "r") as f:
|
||||
self.skills = json.load(f)
|
||||
|
||||
def register_roles(self, roles: Iterable[Minecraft]):
|
||||
for role in roles:
|
||||
role.set_memory(self)
|
||||
|
||||
def update_event(self, event: Dict):
|
||||
if self.event == event:
|
||||
return
|
||||
self.event = event
|
||||
self.update_chest_memory(event)
|
||||
self.event_summary = self.summarize_chatlog(event)
|
||||
|
||||
def update_task(self, task: str):
|
||||
self.current_task = task
|
||||
|
||||
def update_context(self, context: str):
|
||||
self.context = context
|
||||
|
||||
def update_code(self, code: str):
|
||||
self.code = code # action_developer.gen_action_code to HERE
|
||||
|
||||
def update_program_name(self, program_name: str):
|
||||
self.program_name = program_name
|
||||
|
||||
def update_critique(self, critique: str):
|
||||
self.critique = critique # critic_agent.check_task_success to HERE
|
||||
|
||||
def append_skill(self, skill: dict):
|
||||
self.skills[self.program_name] = skill # skill_manager.retrieve_skills to HERE
|
||||
|
||||
def update_retrieve_skills(self, retrieve_skills: list):
|
||||
self.retrieve_skills = retrieve_skills
|
||||
|
||||
def update_skill_desp(self, skill_desp: str):
|
||||
self.skill_desp = skill_desp
|
||||
|
||||
def update_chest_memory(self, events: Dict):
|
||||
"""
|
||||
Input: events: Dict
|
||||
Result: self.chest_memory update & save to json
|
||||
"""
|
||||
nearbyChests = events[-1][1]["nearbyChests"]
|
||||
for position, chest in nearbyChests.items():
|
||||
if position in self.chest_memory:
|
||||
if isinstance(chest, dict):
|
||||
self.chest_memory[position] = chest
|
||||
if chest == "Invalid":
|
||||
logger.info(f"Action Developer removing chest {position}: {chest}")
|
||||
self.chest_memory.pop(position)
|
||||
else:
|
||||
if chest != "Invalid":
|
||||
logger.info(f"Action Developer saving chest {position}: {chest}")
|
||||
self.chest_memory[position] = chest
|
||||
with open(f"{CKPT_DIR}/action/chest_memory.json", "w") as f:
|
||||
json.dump(self.chest_memory, f)
|
||||
|
||||
def update_chest_observation(self):
|
||||
"""
|
||||
update chest_memory to chest_observation.
|
||||
Refer to @ https://github.com/MineDojo/Voyager/blob/main/voyager/agents/action.py
|
||||
"""
|
||||
|
||||
chests = []
|
||||
for chest_position, chest in self.chest_memory.items():
|
||||
if isinstance(chest, dict) and len(chest) > 0:
|
||||
chests.append(f"{chest_position}: {chest}")
|
||||
for chest_position, chest in self.chest_memory.items():
|
||||
if isinstance(chest, dict) and len(chest) == 0:
|
||||
chests.append(f"{chest_position}: Empty")
|
||||
for chest_position, chest in self.chest_memory.items():
|
||||
if isinstance(chest, str):
|
||||
assert chest == "Unknown"
|
||||
chests.append(f"{chest_position}: Unknown items inside")
|
||||
assert len(chests) == len(self.chest_memory)
|
||||
if chests:
|
||||
chests = "\n".join(chests)
|
||||
self.chest_observation = f"Chests:\n{chests}\n\n"
|
||||
else:
|
||||
self.chest_observation = f"Chests: None\n\n"
|
||||
|
||||
def summarize_chatlog(self, events):
|
||||
def filter_item(message: str):
|
||||
craft_pattern = r"I cannot make \w+ because I need: (.*)"
|
||||
craft_pattern2 = (
|
||||
r"I cannot make \w+ because there is no crafting table nearby"
|
||||
)
|
||||
mine_pattern = r"I need at least a (.*) to mine \w+!"
|
||||
if re.match(craft_pattern, message):
|
||||
return re.match(craft_pattern, message).groups()[0]
|
||||
elif re.match(craft_pattern2, message):
|
||||
return "a nearby crafting table"
|
||||
elif re.match(mine_pattern, message):
|
||||
return re.match(mine_pattern, message).groups()[0]
|
||||
else:
|
||||
return ""
|
||||
|
||||
chatlog = set()
|
||||
for event_type, event in events:
|
||||
if event_type == "onChat":
|
||||
item = filter_item(event["onChat"])
|
||||
if item:
|
||||
chatlog.add(item)
|
||||
return "I also need " + ", ".join(chatlog) + "." if chatlog else ""
|
||||
|
||||
def update_exploration_progress(self, success: bool):
|
||||
"""
|
||||
Split task into completed_tasks or failed_tasks
|
||||
Args: info = {
|
||||
"task": self.task,
|
||||
"success": success,
|
||||
"conversations": self.conversations,
|
||||
}
|
||||
"""
|
||||
task = self.current_task
|
||||
if task.startswith("Deposit useless items into the chest at"):
|
||||
return
|
||||
if success:
|
||||
logger.info(f"Completed task {task}.")
|
||||
self.completed_tasks.append(task)
|
||||
else:
|
||||
logger.info(f"Failed to complete task {task}. Skipping to next task.")
|
||||
self.failed_tasks.append(task)
|
||||
# TODO: when not success, transform code below to update event!(isolate step soon!)
|
||||
# if self.reset_placed_if_failed and not success:
|
||||
# # revert all the placing event in the last step
|
||||
# blocks = []
|
||||
# positions = []
|
||||
# for event_type, event in events:
|
||||
# if event_type == "onSave" and event["onSave"].endswith("_placed"):
|
||||
# block = event["onSave"].split("_placed")[0]
|
||||
# position = event["status"]["position"]
|
||||
# blocks.append(block)
|
||||
# positions.append(position)
|
||||
# new_events = self.env.step(
|
||||
# f"await givePlacedItemBack(bot, {U.json_dumps(blocks)}, {U.json_dumps(positions)})",
|
||||
# programs=self.skill_manager.programs,
|
||||
# )
|
||||
# events[-1][1]["inventory"] = new_events[-1][1]["inventory"]
|
||||
# events[-1][1]["voxels"] = new_events[-1][1]["voxels"]
|
||||
|
||||
self.save_sorted_tasks()
|
||||
|
||||
def save_sorted_tasks(self):
|
||||
updated_completed_tasks = []
|
||||
# record repeated failed tasks
|
||||
updated_failed_tasks = self.failed_tasks
|
||||
# dedup but keep order
|
||||
for task in self.completed_tasks:
|
||||
if task not in updated_completed_tasks:
|
||||
updated_completed_tasks.append(task)
|
||||
|
||||
# remove completed tasks from failed tasks
|
||||
for task in updated_completed_tasks:
|
||||
while task in updated_failed_tasks:
|
||||
updated_failed_tasks.remove(task)
|
||||
|
||||
self.completed_tasks = updated_completed_tasks
|
||||
self.failed_tasks = updated_failed_tasks
|
||||
|
||||
# dump to json
|
||||
with open(f"{CKPT_DIR}/curriculum/completed_tasks.json", "w") as f:
|
||||
json.dump(self.completed_tasks, f)
|
||||
with open(f"{CKPT_DIR}/curriculum/failed_tasks.json", "w") as f:
|
||||
json.dump(self.failed_tasks, f)
|
||||
|
||||
async def on_event(self, *args):
|
||||
"""
|
||||
Retrieve Minecraft events.
|
||||
|
||||
This function is used to obtain events from the Minecraft environment. Check the implementation in
|
||||
the 'voyager/env/bridge.py step()' function to capture events generated within the game.
|
||||
|
||||
Returns:
|
||||
list: A list of Minecraft events.
|
||||
|
||||
Raises:
|
||||
Exception: If there is an issue retrieving events.
|
||||
"""
|
||||
try:
|
||||
if not self.mf_instance.has_reset:
|
||||
# TODO Modify
|
||||
logger.info("Environment has not been reset yet, is resetting")
|
||||
self.mf_instance.reset(
|
||||
options={
|
||||
"mode": "soft",
|
||||
"wait_ticks": 20,
|
||||
}
|
||||
)
|
||||
# raise {}
|
||||
self.mf_instance.check_process()
|
||||
self.mf_instance.unpause()
|
||||
data = {
|
||||
"code": self.code,
|
||||
"programs": self.programs,
|
||||
}
|
||||
res = requests.post(
|
||||
f"{self.mf_instance.server}/step",
|
||||
json=data,
|
||||
timeout=self.mf_instance.request_timeout,
|
||||
)
|
||||
if res.status_code != 200:
|
||||
logger.error("Failed to step Minecraft server")
|
||||
raise {}
|
||||
returned_data = res.json()
|
||||
self.mf_instance.pause()
|
||||
events = json.loads(returned_data)
|
||||
logger.info(f"Get Current Event: {events}")
|
||||
return events
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to retrieve Minecraft events: {str(e)}")
|
||||
raise {}
|
||||
|
||||
|
||||
class MinecraftPlayer(SoftwareCompany):
|
||||
"""
|
||||
Software Company: Possesses a team, SOP (Standard Operating Procedures), and a platform for instant messaging,
|
||||
dedicated to writing executable code.
|
||||
"""
|
||||
|
||||
environment: Environment = Field(default_factory=Environment)
|
||||
game_memory: GameEnvironment = Field(default_factory=GameEnvironment)
|
||||
investment: float = Field(default=50.0)
|
||||
task: str = Field(default="")
|
||||
game_info: dict = Field(default={})
|
||||
|
||||
def set_port(self, mc_port):
|
||||
self.game_memory.set_mc_port(mc_port)
|
||||
|
||||
def set_resume(self, resume: bool = False):
|
||||
self.game_memory.set_mc_resume(resume=resume)
|
||||
|
||||
def check_complete_round(self):
|
||||
complete_round = []
|
||||
for role in self.environment.roles.values():
|
||||
status = role.finish_step
|
||||
complete_round.append(status)
|
||||
#if not status:
|
||||
# return complete_round
|
||||
#complete_round = True
|
||||
complete_round_tag = all(complete_round)
|
||||
logger.info(f"complete_round {complete_round}")
|
||||
return complete_round_tag
|
||||
|
||||
def update_round(self):
|
||||
for role in self.environment.roles.values():
|
||||
role.finish_step = False
|
||||
role.round_id+=1
|
||||
role._rc.todo = None
|
||||
logger.info(f"round_id:{role.round_id}")
|
||||
|
||||
def hire(self, roles: list[Role]):
|
||||
self.environment.add_roles(roles)
|
||||
self.game_memory.register_roles(roles)
|
||||
|
||||
def start(self, task, round=0):
|
||||
"""Start a project from publishing boss requirement."""
|
||||
self.task = task
|
||||
self.environment.publish_message(
|
||||
Message(role="Player", content=task, cause_by=PlayerActions, round_id=round)
|
||||
)
|
||||
logger.info(self.game_info)
|
||||
|
||||
def _save(self):
|
||||
logger.info(self.json())
|
||||
|
||||
def _reset(self):
|
||||
for role_profile, role in self.environment.roles.items():
|
||||
role.reset_state()
|
||||
|
||||
async def run(self, n_round=3):
|
||||
"""Run company until target round or no money"""
|
||||
round_id=0
|
||||
while n_round > 0:
|
||||
# self._save()
|
||||
if self.check_complete_round():
|
||||
n_round -= 1
|
||||
self.update_round()
|
||||
round_id+=1
|
||||
# add new task into env and continue
|
||||
#fixme: update self.task
|
||||
self.start(task=self.task, round=round_id)
|
||||
|
||||
logger.info(f"{n_round=}")
|
||||
self._check_balance()
|
||||
await self.environment.run()
|
||||
#self.environment.memory.clear()
|
||||
#self._reset()
|
||||
return self.environment.history
|
||||
294
metagpt/mineflayer_env/.gitignore
vendored
Normal file
294
metagpt/mineflayer_env/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
# MCP-Reborn
|
||||
MCP-Reborn/
|
||||
run/
|
||||
*.jar
|
||||
config.json
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
.idea/
|
||||
|
||||
# Logs
|
||||
logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
package-lock.json
|
||||
3
metagpt/mineflayer_env/mineflayer/.prettierignore
Normal file
3
metagpt/mineflayer_env/mineflayer/.prettierignore
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Ignore artifacts:
|
||||
build
|
||||
coverage
|
||||
3
metagpt/mineflayer_env/mineflayer/.prettierrc.json
Normal file
3
metagpt/mineflayer_env/mineflayer/.prettierrc.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"tabWidth": 4
|
||||
}
|
||||
425
metagpt/mineflayer_env/mineflayer/index.js
Normal file
425
metagpt/mineflayer_env/mineflayer/index.js
Normal file
|
|
@ -0,0 +1,425 @@
|
|||
const fs = require("fs");
|
||||
const express = require("express");
|
||||
const bodyParser = require("body-parser");
|
||||
const mineflayer = require("mineflayer");
|
||||
|
||||
const skills = require("./lib/skillLoader");
|
||||
const { initCounter, getNextTime } = require("./lib/utils");
|
||||
const obs = require("./lib/observation/base");
|
||||
const OnChat = require("./lib/observation/onChat");
|
||||
const OnError = require("./lib/observation/onError");
|
||||
const { Voxels, BlockRecords } = require("./lib/observation/voxels");
|
||||
const Status = require("./lib/observation/status");
|
||||
const Inventory = require("./lib/observation/inventory");
|
||||
const OnSave = require("./lib/observation/onSave");
|
||||
const Chests = require("./lib/observation/chests");
|
||||
const { plugin: tool } = require("mineflayer-tool");
|
||||
|
||||
let bot = null;
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(bodyParser.json({ limit: "50mb" }));
|
||||
app.use(bodyParser.urlencoded({ limit: "50mb", extended: false }));
|
||||
|
||||
app.post("/start", (req, res) => {
|
||||
if (bot) onDisconnect("Restarting bot");
|
||||
bot = null;
|
||||
console.log(req.body);
|
||||
bot = mineflayer.createBot({
|
||||
host: "localhost", // minecraft server ip
|
||||
port: req.body.port, // minecraft server port
|
||||
username: "bot",
|
||||
disableChatSigning: true,
|
||||
checkTimeoutInterval: 60 * 60 * 1000,
|
||||
});
|
||||
bot.once("error", onConnectionFailed);
|
||||
|
||||
// Event subscriptions
|
||||
bot.waitTicks = req.body.waitTicks;
|
||||
bot.globalTickCounter = 0;
|
||||
bot.stuckTickCounter = 0;
|
||||
bot.stuckPosList = [];
|
||||
bot.iron_pickaxe = false;
|
||||
|
||||
bot.on("kicked", onDisconnect);
|
||||
|
||||
// mounting will cause physicsTick to stop
|
||||
bot.on("mount", () => {
|
||||
bot.dismount();
|
||||
});
|
||||
|
||||
bot.once("spawn", async () => {
|
||||
bot.removeListener("error", onConnectionFailed);
|
||||
let itemTicks = 1;
|
||||
if (req.body.reset === "hard") {
|
||||
bot.chat("/clear @s");
|
||||
bot.chat("/kill @s");
|
||||
const inventory = req.body.inventory ? req.body.inventory : {};
|
||||
const equipment = req.body.equipment
|
||||
? req.body.equipment
|
||||
: [null, null, null, null, null, null];
|
||||
for (let key in inventory) {
|
||||
bot.chat(`/give @s minecraft:${key} ${inventory[key]}`);
|
||||
itemTicks += 1;
|
||||
}
|
||||
const equipmentNames = [
|
||||
"armor.head",
|
||||
"armor.chest",
|
||||
"armor.legs",
|
||||
"armor.feet",
|
||||
"weapon.mainhand",
|
||||
"weapon.offhand",
|
||||
];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
if (i === 4) continue;
|
||||
if (equipment[i]) {
|
||||
bot.chat(
|
||||
`/item replace entity @s ${equipmentNames[i]} with minecraft:${equipment[i]}`
|
||||
);
|
||||
itemTicks += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (req.body.position) {
|
||||
bot.chat(
|
||||
`/tp @s ${req.body.position.x} ${req.body.position.y} ${req.body.position.z}`
|
||||
);
|
||||
}
|
||||
|
||||
// if iron_pickaxe is in bot's inventory
|
||||
if (
|
||||
bot.inventory.items().find((item) => item.name === "iron_pickaxe")
|
||||
) {
|
||||
bot.iron_pickaxe = true;
|
||||
}
|
||||
|
||||
const { pathfinder } = require("mineflayer-pathfinder");
|
||||
const tool = require("mineflayer-tool").plugin;
|
||||
const collectBlock = require("mineflayer-collectblock").plugin;
|
||||
const pvp = require("mineflayer-pvp").plugin;
|
||||
const minecraftHawkEye = require("minecrafthawkeye");
|
||||
bot.loadPlugin(pathfinder);
|
||||
bot.loadPlugin(tool);
|
||||
bot.loadPlugin(collectBlock);
|
||||
bot.loadPlugin(pvp);
|
||||
bot.loadPlugin(minecraftHawkEye);
|
||||
|
||||
// bot.collectBlock.movements.digCost = 0;
|
||||
// bot.collectBlock.movements.placeCost = 0;
|
||||
|
||||
obs.inject(bot, [
|
||||
OnChat,
|
||||
OnError,
|
||||
Voxels,
|
||||
Status,
|
||||
Inventory,
|
||||
OnSave,
|
||||
Chests,
|
||||
BlockRecords,
|
||||
]);
|
||||
skills.inject(bot);
|
||||
|
||||
if (req.body.spread) {
|
||||
bot.chat(`/spreadplayers ~ ~ 0 300 under 80 false @s`);
|
||||
await bot.waitForTicks(bot.waitTicks);
|
||||
}
|
||||
|
||||
await bot.waitForTicks(bot.waitTicks * itemTicks);
|
||||
res.json(bot.observe());
|
||||
|
||||
initCounter(bot);
|
||||
bot.chat("/gamerule keepInventory true");
|
||||
bot.chat("/gamerule doDaylightCycle false");
|
||||
});
|
||||
|
||||
function onConnectionFailed(e) {
|
||||
console.log(e);
|
||||
bot = null;
|
||||
res.status(400).json({ error: e });
|
||||
}
|
||||
function onDisconnect(message) {
|
||||
if (bot.viewer) {
|
||||
bot.viewer.close();
|
||||
}
|
||||
bot.end();
|
||||
console.log(message);
|
||||
bot = null;
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/step", async (req, res) => {
|
||||
// import useful package
|
||||
let response_sent = false;
|
||||
function otherError(err) {
|
||||
console.log("Uncaught Error");
|
||||
bot.emit("error", handleError(err));
|
||||
bot.waitForTicks(bot.waitTicks).then(() => {
|
||||
if (!response_sent) {
|
||||
response_sent = true;
|
||||
res.json(bot.observe());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
process.on("uncaughtException", otherError);
|
||||
|
||||
const mcData = require("minecraft-data")(bot.version);
|
||||
mcData.itemsByName["leather_cap"] = mcData.itemsByName["leather_helmet"];
|
||||
mcData.itemsByName["leather_tunic"] =
|
||||
mcData.itemsByName["leather_chestplate"];
|
||||
mcData.itemsByName["leather_pants"] =
|
||||
mcData.itemsByName["leather_leggings"];
|
||||
mcData.itemsByName["leather_boots"] = mcData.itemsByName["leather_boots"];
|
||||
mcData.itemsByName["lapis_lazuli_ore"] = mcData.itemsByName["lapis_ore"];
|
||||
mcData.blocksByName["lapis_lazuli_ore"] = mcData.blocksByName["lapis_ore"];
|
||||
const {
|
||||
Movements,
|
||||
goals: {
|
||||
Goal,
|
||||
GoalBlock,
|
||||
GoalNear,
|
||||
GoalXZ,
|
||||
GoalNearXZ,
|
||||
GoalY,
|
||||
GoalGetToBlock,
|
||||
GoalLookAtBlock,
|
||||
GoalBreakBlock,
|
||||
GoalCompositeAny,
|
||||
GoalCompositeAll,
|
||||
GoalInvert,
|
||||
GoalFollow,
|
||||
GoalPlaceBlock,
|
||||
},
|
||||
pathfinder,
|
||||
Move,
|
||||
ComputedPath,
|
||||
PartiallyComputedPath,
|
||||
XZCoordinates,
|
||||
XYZCoordinates,
|
||||
SafeBlock,
|
||||
GoalPlaceBlockOptions,
|
||||
} = require("mineflayer-pathfinder");
|
||||
const { Vec3 } = require("vec3");
|
||||
|
||||
// Set up pathfinder
|
||||
const movements = new Movements(bot, mcData);
|
||||
bot.pathfinder.setMovements(movements);
|
||||
|
||||
bot.globalTickCounter = 0;
|
||||
bot.stuckTickCounter = 0;
|
||||
bot.stuckPosList = [];
|
||||
|
||||
function onTick() {
|
||||
bot.globalTickCounter++;
|
||||
if (bot.pathfinder.isMoving()) {
|
||||
bot.stuckTickCounter++;
|
||||
if (bot.stuckTickCounter >= 100) {
|
||||
onStuck(1.5);
|
||||
bot.stuckTickCounter = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bot.on("physicTick", onTick);
|
||||
|
||||
// initialize fail count
|
||||
let _craftItemFailCount = 0;
|
||||
let _killMobFailCount = 0;
|
||||
let _mineBlockFailCount = 0;
|
||||
let _placeItemFailCount = 0;
|
||||
let _smeltItemFailCount = 0;
|
||||
|
||||
// Retrieve array form post bod
|
||||
const code = req.body.code;
|
||||
const programs = req.body.programs;
|
||||
bot.cumulativeObs = [];
|
||||
await bot.waitForTicks(bot.waitTicks);
|
||||
const r = await evaluateCode(code, programs);
|
||||
process.off("uncaughtException", otherError);
|
||||
if (r !== "success") {
|
||||
bot.emit("error", handleError(r));
|
||||
}
|
||||
await returnItems();
|
||||
// wait for last message
|
||||
await bot.waitForTicks(bot.waitTicks);
|
||||
if (!response_sent) {
|
||||
response_sent = true;
|
||||
res.json(bot.observe());
|
||||
}
|
||||
bot.removeListener("physicTick", onTick);
|
||||
|
||||
async function evaluateCode(code, programs) {
|
||||
// Echo the code produced for players to see it. Don't echo when the bot code is already producing dialog or it will double echo
|
||||
try {
|
||||
await eval("(async () => {" + programs + "\n" + code + "})()");
|
||||
return "success";
|
||||
} catch (err) {
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
function onStuck(posThreshold) {
|
||||
const currentPos = bot.entity.position;
|
||||
bot.stuckPosList.push(currentPos);
|
||||
|
||||
// Check if the list is full
|
||||
if (bot.stuckPosList.length === 5) {
|
||||
const oldestPos = bot.stuckPosList[0];
|
||||
const posDifference = currentPos.distanceTo(oldestPos);
|
||||
|
||||
if (posDifference < posThreshold) {
|
||||
teleportBot(); // execute the function
|
||||
}
|
||||
|
||||
// Remove the oldest time from the list
|
||||
bot.stuckPosList.shift();
|
||||
}
|
||||
}
|
||||
|
||||
function teleportBot() {
|
||||
const blocks = bot.findBlocks({
|
||||
matching: (block) => {
|
||||
return block.type === 0;
|
||||
},
|
||||
maxDistance: 1,
|
||||
count: 27,
|
||||
});
|
||||
|
||||
if (blocks) {
|
||||
// console.log(blocks.length);
|
||||
const randomIndex = Math.floor(Math.random() * blocks.length);
|
||||
const block = blocks[randomIndex];
|
||||
bot.chat(`/tp @s ${block.x} ${block.y} ${block.z}`);
|
||||
} else {
|
||||
bot.chat("/tp @s ~ ~1.25 ~");
|
||||
}
|
||||
}
|
||||
|
||||
function returnItems() {
|
||||
bot.chat("/gamerule doTileDrops false");
|
||||
const crafting_table = bot.findBlock({
|
||||
matching: mcData.blocksByName.crafting_table.id,
|
||||
maxDistance: 128,
|
||||
});
|
||||
if (crafting_table) {
|
||||
bot.chat(
|
||||
`/setblock ${crafting_table.position.x} ${crafting_table.position.y} ${crafting_table.position.z} air destroy`
|
||||
);
|
||||
bot.chat("/give @s crafting_table");
|
||||
}
|
||||
const furnace = bot.findBlock({
|
||||
matching: mcData.blocksByName.furnace.id,
|
||||
maxDistance: 128,
|
||||
});
|
||||
if (furnace) {
|
||||
bot.chat(
|
||||
`/setblock ${furnace.position.x} ${furnace.position.y} ${furnace.position.z} air destroy`
|
||||
);
|
||||
bot.chat("/give @s furnace");
|
||||
}
|
||||
if (bot.inventoryUsed() >= 32) {
|
||||
// if chest is not in bot's inventory
|
||||
if (!bot.inventory.items().find((item) => item.name === "chest")) {
|
||||
bot.chat("/give @s chest");
|
||||
}
|
||||
}
|
||||
// if iron_pickaxe not in bot's inventory and bot.iron_pickaxe
|
||||
if (
|
||||
bot.iron_pickaxe &&
|
||||
!bot.inventory.items().find((item) => item.name === "iron_pickaxe")
|
||||
) {
|
||||
bot.chat("/give @s iron_pickaxe");
|
||||
}
|
||||
bot.chat("/gamerule doTileDrops true");
|
||||
}
|
||||
|
||||
function handleError(err) {
|
||||
let stack = err.stack;
|
||||
if (!stack) {
|
||||
return err;
|
||||
}
|
||||
console.log(stack);
|
||||
const final_line = stack.split("\n")[1];
|
||||
const regex = /<anonymous>:(\d+):\d+\)/;
|
||||
|
||||
const programs_length = programs.split("\n").length;
|
||||
let match_line = null;
|
||||
for (const line of stack.split("\n")) {
|
||||
const match = regex.exec(line);
|
||||
if (match) {
|
||||
const line_num = parseInt(match[1]);
|
||||
if (line_num >= programs_length) {
|
||||
match_line = line_num - programs_length;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!match_line) {
|
||||
return err.message;
|
||||
}
|
||||
let f_line = final_line.match(
|
||||
/\((?<file>.*):(?<line>\d+):(?<pos>\d+)\)/
|
||||
);
|
||||
if (f_line && f_line.groups && fs.existsSync(f_line.groups.file)) {
|
||||
const { file, line, pos } = f_line.groups;
|
||||
const f = fs.readFileSync(file, "utf8").split("\n");
|
||||
// let filename = file.match(/(?<=node_modules\\)(.*)/)[1];
|
||||
let source = file + `:${line}\n${f[line - 1].trim()}\n `;
|
||||
|
||||
const code_source =
|
||||
"at " +
|
||||
code.split("\n")[match_line - 1].trim() +
|
||||
" in your code";
|
||||
return source + err.message + "\n" + code_source;
|
||||
} else if (
|
||||
f_line &&
|
||||
f_line.groups &&
|
||||
f_line.groups.file.includes("<anonymous>")
|
||||
) {
|
||||
const { file, line, pos } = f_line.groups;
|
||||
let source =
|
||||
"Your code" +
|
||||
`:${match_line}\n${code.split("\n")[match_line - 1].trim()}\n `;
|
||||
let code_source = "";
|
||||
if (line < programs_length) {
|
||||
source =
|
||||
"In your program code: " +
|
||||
programs.split("\n")[line - 1].trim() +
|
||||
"\n";
|
||||
code_source = `at line ${match_line}:${code
|
||||
.split("\n")
|
||||
[match_line - 1].trim()} in your code`;
|
||||
}
|
||||
return source + err.message + "\n" + code_source;
|
||||
}
|
||||
return err.message;
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/stop", (req, res) => {
|
||||
bot.end();
|
||||
res.json({
|
||||
message: "Bot stopped",
|
||||
});
|
||||
});
|
||||
|
||||
app.post("/pause", (req, res) => {
|
||||
if (!bot) {
|
||||
res.status(400).json({ error: "Bot not spawned" });
|
||||
return;
|
||||
}
|
||||
bot.chat("/pause");
|
||||
bot.waitForTicks(bot.waitTicks).then(() => {
|
||||
res.json({ message: "Success" });
|
||||
});
|
||||
});
|
||||
|
||||
// Server listening to PORT 3000
|
||||
|
||||
const DEFAULT_PORT = 3000;
|
||||
const PORT = process.argv[2] || DEFAULT_PORT;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server started on port ${PORT}`);
|
||||
});
|
||||
45
metagpt/mineflayer_env/mineflayer/lib/observation/base.js
Normal file
45
metagpt/mineflayer_env/mineflayer/lib/observation/base.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
class Observation {
|
||||
constructor(bot) {
|
||||
if (new.target === Observation) {
|
||||
throw new TypeError(
|
||||
"Cannot instantiate abstract class Observation"
|
||||
);
|
||||
}
|
||||
|
||||
this.bot = bot;
|
||||
this.name = "Observation";
|
||||
}
|
||||
|
||||
observe() {
|
||||
throw new TypeError("Method 'observe()' must be implemented.");
|
||||
}
|
||||
|
||||
reset() {}
|
||||
}
|
||||
|
||||
function inject(bot, obs_list) {
|
||||
bot.obsList = [];
|
||||
bot.cumulativeObs = [];
|
||||
bot.eventMemory = {};
|
||||
obs_list.forEach((obs) => {
|
||||
bot.obsList.push(new obs(bot));
|
||||
});
|
||||
bot.event = function (event_name) {
|
||||
let result = {};
|
||||
bot.obsList.forEach((obs) => {
|
||||
if (obs.name.startsWith("on") && obs.name !== event_name) {
|
||||
return;
|
||||
}
|
||||
result[obs.name] = obs.observe();
|
||||
});
|
||||
bot.cumulativeObs.push([event_name, result]);
|
||||
};
|
||||
bot.observe = function () {
|
||||
bot.event("observe");
|
||||
const result = bot.cumulativeObs;
|
||||
bot.cumulativeObs = [];
|
||||
return JSON.stringify(result);
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { Observation, inject };
|
||||
31
metagpt/mineflayer_env/mineflayer/lib/observation/chests.js
Normal file
31
metagpt/mineflayer_env/mineflayer/lib/observation/chests.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
const { Observation } = require("./base");
|
||||
|
||||
class Chests extends Observation {
|
||||
constructor(bot) {
|
||||
super(bot);
|
||||
this.name = "nearbyChests";
|
||||
this.chestsItems = {};
|
||||
bot.on("closeChest", (chestItems, position) => {
|
||||
this.chestsItems[position] = chestItems;
|
||||
});
|
||||
bot.on("removeChest", (chestPosition) => {
|
||||
this.chestsItems[chestPosition] = "Invalid";
|
||||
});
|
||||
}
|
||||
|
||||
observe() {
|
||||
const chests = this.bot.findBlocks({
|
||||
matching: this.bot.registry.blocksByName.chest.id,
|
||||
maxDistance: 16,
|
||||
count: 999,
|
||||
});
|
||||
chests.forEach((chest) => {
|
||||
if (!this.chestsItems.hasOwnProperty(chest)) {
|
||||
this.chestsItems[chest] = "Unknown";
|
||||
}
|
||||
});
|
||||
return this.chestsItems;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Chests;
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
const { Observation } = require("./base");
|
||||
|
||||
class Inventory extends Observation {
|
||||
constructor(bot) {
|
||||
super(bot);
|
||||
this.name = "inventory";
|
||||
}
|
||||
|
||||
observe() {
|
||||
return listItems(this.bot);
|
||||
}
|
||||
}
|
||||
|
||||
function listItems(bot) {
|
||||
const items = getInventoryItems(bot);
|
||||
return items.reduce(itemToDict, {});
|
||||
}
|
||||
|
||||
function getInventoryItems(bot) {
|
||||
const inventory = bot.currentWindow || bot.inventory;
|
||||
return inventory.items();
|
||||
}
|
||||
|
||||
function itemToDict(acc, cur) {
|
||||
if (cur.name && cur.count) {
|
||||
//if both name and count property are defined
|
||||
if (acc[cur.name]) {
|
||||
//if the item is already in the dict
|
||||
acc[cur.name] += cur.count;
|
||||
} else {
|
||||
//if the item is not in the dict
|
||||
acc[cur.name] = cur.count;
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
//export modules
|
||||
module.exports = Inventory;
|
||||
26
metagpt/mineflayer_env/mineflayer/lib/observation/onChat.js
Normal file
26
metagpt/mineflayer_env/mineflayer/lib/observation/onChat.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
const Observation = require("./base.js").Observation;
|
||||
|
||||
class onChat extends Observation {
|
||||
constructor(bot) {
|
||||
super(bot);
|
||||
this.name = "onChat";
|
||||
this.obs = "";
|
||||
bot.on("chatEvent", (username, message) => {
|
||||
// Save entity status to local variable
|
||||
if (message.startsWith("/")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.obs += message;
|
||||
this.bot.event(this.name);
|
||||
});
|
||||
}
|
||||
|
||||
observe() {
|
||||
const result = this.obs;
|
||||
this.obs = "";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = onChat;
|
||||
22
metagpt/mineflayer_env/mineflayer/lib/observation/onError.js
Normal file
22
metagpt/mineflayer_env/mineflayer/lib/observation/onError.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
const Observation = require("./base.js").Observation;
|
||||
|
||||
class onError extends Observation {
|
||||
constructor(bot) {
|
||||
super(bot);
|
||||
this.name = "onError";
|
||||
this.obs = null;
|
||||
bot.on("error", (err) => {
|
||||
// Save entity status to local variable
|
||||
this.obs = err;
|
||||
this.bot.event(this.name);
|
||||
});
|
||||
}
|
||||
|
||||
observe() {
|
||||
const result = this.obs;
|
||||
this.obs = null;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = onError;
|
||||
22
metagpt/mineflayer_env/mineflayer/lib/observation/onSave.js
Normal file
22
metagpt/mineflayer_env/mineflayer/lib/observation/onSave.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
const Observation = require("./base.js").Observation;
|
||||
|
||||
class onSave extends Observation {
|
||||
constructor(bot) {
|
||||
super(bot);
|
||||
this.name = "onSave";
|
||||
this.obs = null;
|
||||
bot.on("save", (eventName) => {
|
||||
// Save entity status to local variable
|
||||
this.obs = eventName;
|
||||
this.bot.event(this.name);
|
||||
});
|
||||
}
|
||||
|
||||
observe() {
|
||||
const result = this.obs;
|
||||
this.obs = null;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = onSave;
|
||||
103
metagpt/mineflayer_env/mineflayer/lib/observation/status.js
Normal file
103
metagpt/mineflayer_env/mineflayer/lib/observation/status.js
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
const Observation = require("./base.js").Observation;
|
||||
|
||||
class Status extends Observation {
|
||||
constructor(bot) {
|
||||
super(bot);
|
||||
this.name = "status";
|
||||
}
|
||||
|
||||
observe() {
|
||||
return {
|
||||
health: this.bot.health,
|
||||
food: this.bot.food,
|
||||
saturation: this.bot.foodSaturation,
|
||||
oxygen: this.bot.oxygenLevel,
|
||||
position: this.bot.entity.position,
|
||||
velocity: this.bot.entity.velocity,
|
||||
yaw: this.bot.entity.yaw,
|
||||
pitch: this.bot.entity.pitch,
|
||||
onGround: this.bot.entity.onGround,
|
||||
equipment: this.getEquipment(),
|
||||
name: this.bot.entity.username,
|
||||
timeSinceOnGround: this.bot.entity.timeSinceOnGround,
|
||||
isInWater: this.bot.entity.isInWater,
|
||||
isInLava: this.bot.entity.isInLava,
|
||||
isInWeb: this.bot.entity.isInWeb,
|
||||
isCollidedHorizontally: this.bot.entity.isCollidedHorizontally,
|
||||
isCollidedVertically: this.bot.entity.isCollidedVertically,
|
||||
biome: this.bot.blockAt(this.bot.entity.position)
|
||||
? this.bot.blockAt(this.bot.entity.position).biome.name
|
||||
: "None",
|
||||
entities: this.getEntities(),
|
||||
timeOfDay: this.getTime(),
|
||||
inventoryUsed: this.bot.inventoryUsed(),
|
||||
elapsedTime: this.bot.globalTickCounter,
|
||||
};
|
||||
}
|
||||
|
||||
itemToObs(item) {
|
||||
if (!item) return null;
|
||||
return item.name;
|
||||
}
|
||||
|
||||
getTime() {
|
||||
const timeOfDay = this.bot.time.timeOfDay;
|
||||
let time = "";
|
||||
if (timeOfDay < 1000) {
|
||||
time = "sunrise";
|
||||
} else if (timeOfDay < 6000) {
|
||||
time = "day";
|
||||
} else if (timeOfDay < 12000) {
|
||||
time = "noon";
|
||||
} else if (timeOfDay < 13000) {
|
||||
time = "sunset";
|
||||
} else if (timeOfDay < 18000) {
|
||||
time = "night";
|
||||
} else if (timeOfDay < 22000) {
|
||||
time = "midnight";
|
||||
} else {
|
||||
time = "sunrise";
|
||||
}
|
||||
return time;
|
||||
}
|
||||
|
||||
// For each item in equipment, if it exists, return the name of the item
|
||||
// otherwise return null
|
||||
getEquipment() {
|
||||
const slots = this.bot.inventory.slots;
|
||||
const mainHand = this.bot.heldItem;
|
||||
return slots
|
||||
.slice(5, 9)
|
||||
.concat(mainHand, slots[45])
|
||||
.map(this.itemToObs);
|
||||
}
|
||||
|
||||
getEntities() {
|
||||
const entities = this.bot.entities;
|
||||
if (!entities) return {};
|
||||
// keep all monsters in one list, keep other mobs in another list
|
||||
const mobs = {};
|
||||
for (const id in entities) {
|
||||
const entity = entities[id];
|
||||
if (!entity.displayName) continue;
|
||||
if (entity.name === "player" || entity.name === "item") continue;
|
||||
if (entity.position.distanceTo(this.bot.entity.position) < 32) {
|
||||
if (!mobs[entity.name]) {
|
||||
mobs[entity.name] = entity.position.distanceTo(
|
||||
this.bot.entity.position
|
||||
);
|
||||
} else if (
|
||||
mobs[entity.name] >
|
||||
entity.position.distanceTo(this.bot.entity.position)
|
||||
) {
|
||||
mobs[entity.name] = entity.position.distanceTo(
|
||||
this.bot.entity.position
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return mobs;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Status;
|
||||
67
metagpt/mineflayer_env/mineflayer/lib/observation/voxels.js
Normal file
67
metagpt/mineflayer_env/mineflayer/lib/observation/voxels.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
// Blocks = require("./blocks")
|
||||
const { Observation } = require("./base");
|
||||
|
||||
class Voxels extends Observation {
|
||||
constructor(bot) {
|
||||
super(bot);
|
||||
this.name = "voxels";
|
||||
}
|
||||
|
||||
observe() {
|
||||
return Array.from(getSurroundingBlocks(this.bot, 8, 2, 8));
|
||||
}
|
||||
}
|
||||
|
||||
class BlockRecords extends Observation {
|
||||
constructor(bot) {
|
||||
super(bot);
|
||||
this.name = "blockRecords";
|
||||
this.records = new Set();
|
||||
this.tick = 0;
|
||||
bot.on("physicsTick", () => {
|
||||
this.tick++;
|
||||
if (this.tick >= 100) {
|
||||
const items = getInventoryItems(this.bot);
|
||||
getSurroundingBlocks(this.bot, 8, 2, 8).forEach((block) => {
|
||||
if (!items.has(block)) this.records.add(block);
|
||||
});
|
||||
this.tick = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
observe() {
|
||||
return Array.from(this.records);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.records = new Set();
|
||||
}
|
||||
}
|
||||
|
||||
function getSurroundingBlocks(bot, x_distance, y_distance, z_distance) {
|
||||
const surroundingBlocks = new Set();
|
||||
|
||||
for (let x = -x_distance; x <= x_distance; x++) {
|
||||
for (let y = -y_distance; y <= y_distance; y++) {
|
||||
for (let z = -z_distance; z <= z_distance; z++) {
|
||||
const block = bot.blockAt(bot.entity.position.offset(x, y, z));
|
||||
if (block && block.type !== 0) {
|
||||
surroundingBlocks.add(block.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// console.log(surroundingBlocks);
|
||||
return surroundingBlocks;
|
||||
}
|
||||
|
||||
function getInventoryItems(bot) {
|
||||
const items = new Set();
|
||||
bot.inventory.items().forEach((item) => {
|
||||
if (item) items.add(item.name);
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
module.exports = { Voxels, BlockRecords };
|
||||
79
metagpt/mineflayer_env/mineflayer/lib/skillLoader.js
Normal file
79
metagpt/mineflayer_env/mineflayer/lib/skillLoader.js
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
function inject(bot) {
|
||||
bot._sleep = bot.sleep;
|
||||
bot.sleep = async (bedBlock) => {
|
||||
await bot.waitForTicks(20);
|
||||
await bot._sleep(bedBlock);
|
||||
await bot.waitForTicks(135);
|
||||
};
|
||||
|
||||
bot._fish = bot.fish;
|
||||
bot.fish = async () => {
|
||||
if (bot.heldItem?.name !== "fishing_rod") {
|
||||
bot.chat("I'm not holding a fishing rod!");
|
||||
return;
|
||||
}
|
||||
let timeout = null;
|
||||
await Promise.race([
|
||||
bot._fish(),
|
||||
new Promise(
|
||||
(resolve, reject) =>
|
||||
(timeout = setTimeout(() => {
|
||||
bot.activateItem();
|
||||
reject(
|
||||
new Error(
|
||||
"Finishing timeout, make sure you get to and look at a water block!"
|
||||
)
|
||||
);
|
||||
}, 60000))
|
||||
),
|
||||
]);
|
||||
clearTimeout(timeout);
|
||||
await bot.waitForTicks(20);
|
||||
};
|
||||
|
||||
bot._consume = bot.consume;
|
||||
bot.consume = async () => {
|
||||
// action_count.activateItem++;
|
||||
await bot._consume();
|
||||
await bot.waitForTicks(20);
|
||||
};
|
||||
|
||||
bot._useOn = bot.useOn;
|
||||
bot.useOn = async (entity) => {
|
||||
if (entity.position.distanceTo(bot.entity.position) > 6) {
|
||||
bot.chat("Please goto a place near the entity first!");
|
||||
return;
|
||||
}
|
||||
await bot._useOn(entity);
|
||||
await bot.waitForTicks(20);
|
||||
};
|
||||
|
||||
bot._activateBlock = bot.activateBlock;
|
||||
bot.activateBlock = async (block) => {
|
||||
if (block.position.distanceTo(bot.entity.position) > 6) {
|
||||
bot.chat("Please goto a place near the block first!");
|
||||
return;
|
||||
}
|
||||
// action_count.activateBlock++;
|
||||
await bot._activateBlock(block);
|
||||
};
|
||||
|
||||
bot._chat = bot.chat;
|
||||
bot.chat = (message) => {
|
||||
// action_count.chat++;
|
||||
bot.emit("chatEvent", "bot", message);
|
||||
bot._chat(message);
|
||||
};
|
||||
|
||||
bot.inventoryUsed = () => {
|
||||
return bot.inventory.slots.slice(9, 45).filter((item) => item !== null)
|
||||
.length;
|
||||
};
|
||||
|
||||
bot.save = function (eventName) {
|
||||
bot.emit("save", eventName);
|
||||
};
|
||||
}
|
||||
|
||||
// export all control_primitives
|
||||
module.exports = { inject };
|
||||
31
metagpt/mineflayer_env/mineflayer/lib/utils.js
Normal file
31
metagpt/mineflayer_env/mineflayer/lib/utils.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
let gameTimeCounter = 0;
|
||||
let gameTimeList = [];
|
||||
const initCounter = (bot) => {
|
||||
gameTimeList = [];
|
||||
for (let i = 0; i < 13000; i += 1000) {
|
||||
gameTimeList.push(i);
|
||||
}
|
||||
for (let i = 13000; i < 24000; i += 2000) {
|
||||
gameTimeList.push(i);
|
||||
}
|
||||
const timeOfDay = bot.time.timeOfDay;
|
||||
for (let i = 0; i < gameTimeList.length; i++) {
|
||||
if (gameTimeList[i] > timeOfDay) {
|
||||
gameTimeCounter = i - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getNextTime = () => {
|
||||
gameTimeCounter++;
|
||||
if (gameTimeCounter >= gameTimeList.length) {
|
||||
gameTimeCounter = 0;
|
||||
}
|
||||
return gameTimeList[gameTimeCounter];
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
initCounter,
|
||||
getNextTime,
|
||||
};
|
||||
107
metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/.gitignore
vendored
Normal file
107
metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
lib/
|
||||
package-lock.json
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 TheDudeFromCI
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
<h1 align="center">mineflayer-collectblock</h1>
|
||||
<p align="center"><i>A small utility plugin for allowing users to collect blocks using a higher level API.</i></p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://github.com/TheDudeFromCI/mineflayer-collectblock/workflows/Build/badge.svg" />
|
||||
<a href="https://www.npmjs.com/package/mineflayer-collectblock"><img src="https://img.shields.io/npm/v/mineflayer-collectblock" /></a>
|
||||
<img src="https://img.shields.io/github/repo-size/TheDudeFromCI/mineflayer-collectblock" />
|
||||
<img src="https://img.shields.io/npm/dm/mineflayer-collectblock" />
|
||||
<img src="https://img.shields.io/github/contributors/TheDudeFromCI/mineflayer-collectblock" />
|
||||
<img src="https://img.shields.io/github/license/TheDudeFromCI/mineflayer-collectblock" />
|
||||
</p>
|
||||
|
||||
---
|
||||
## This is a modified version to better support Voyager
|
||||
|
||||
## Showcase
|
||||
|
||||
You can see a video of the plugin in action, [here.](https://youtu.be/5T_rcCnNnf4)
|
||||
The source code of the bot in the video can be seen in the examples folder, [here.](https://github.com/TheDudeFromCI/mineflayer-collectblock/blob/master/examples/collector.js)
|
||||
|
||||
### Description
|
||||
|
||||
This plugin is a wrapper for mineflayer that allows for easier API usage when collecting blocks or item drops. This plugin is designed to reduce some of the boilerplate code based around the act of pathfinding to a block _(handled by_ ***mineflayer-pathfinder***_)_, selecting the best tool to mine that block _(handled by_ ***mineflayer-tool***_)_, actually mining it, then moving to collect the item drops from that block. This plugin allows for all of that basic concept to be wrapped up into a single API function.
|
||||
|
||||
In addition to the usage above, some additional quality of life features are available in this plugin. These include the ability to automatically deposit items into a chest when the bot's inventory is full, collecting new tools from a chest if the bot doesn't currently have a required tool _(also handled by_ ***mineflayer-tool***_)_, and allowing for queueing of multiple blocks or item drops to the collection task, so they can be processed later.
|
||||
|
||||
### Getting Started
|
||||
|
||||
This plugin is built using Node and can be installed using:
|
||||
```bash
|
||||
npm install --save mineflayer-collectblock
|
||||
```
|
||||
|
||||
### Simple Bot
|
||||
|
||||
The brief description goes here.
|
||||
|
||||
```js
|
||||
// Create your bot
|
||||
const mineflayer = require("mineflayer")
|
||||
const bot = mineflayer.createBot({
|
||||
host: 'localhost',
|
||||
username: 'Player',
|
||||
})
|
||||
let mcData
|
||||
|
||||
// Load collect block
|
||||
bot.loadPlugin(require('mineflayer-collectblock').plugin)
|
||||
|
||||
async function collectGrass() {
|
||||
// Find a nearby grass block
|
||||
const grass = bot.findBlock({
|
||||
matching: mcData.blocksByName.grass_block.id,
|
||||
maxDistance: 64
|
||||
})
|
||||
|
||||
if (grass) {
|
||||
// If we found one, collect it.
|
||||
try {
|
||||
await bot.collectBlock.collect(grass)
|
||||
collectGrass() // Collect another grass block
|
||||
} catch (err) {
|
||||
console.log(err) // Handle errors, if any
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// On spawn, start collecting all nearby grass
|
||||
bot.once('spawn', () => {
|
||||
mcData = require('minecraft-data')(bot.version)
|
||||
collectGrass()
|
||||
})
|
||||
```
|
||||
|
||||
### Documentation
|
||||
|
||||
[API](https://github.com/TheDudeFromCI/mineflayer-collectblock/blob/master/docs/api.md)
|
||||
|
||||
[Examples](https://github.com/TheDudeFromCI/mineflayer-collectblock/tree/master/examples)
|
||||
|
||||
### License
|
||||
|
||||
This project uses the [MIT](https://github.com/TheDudeFromCI/mineflayer-collectblock/blob/master/LICENSE) license.
|
||||
|
||||
### Contributions
|
||||
|
||||
This project is accepting PRs and Issues. See something you think can be improved? Go for it! Any and all help is highly appreciated!
|
||||
|
||||
For larger changes, it is recommended to discuss these changes in the issues tab before writing any code. It's also preferred to make many smaller PRs than one large one, where applicable.
|
||||
|
|
@ -0,0 +1 @@
|
|||
theme: jekyll-theme-cayman
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
# API <!-- omit in toc -->
|
||||
|
||||
Welcome to the *mineflayer-collectblock* API documentation page.
|
||||
|
||||
## Table of Contents <!-- omit in toc -->
|
||||
|
||||
- [1. Summary](#1-summary)
|
||||
- [Properties](#properties)
|
||||
- [`bot.collectblock.movements: Movements`](#botcollectblockmovements-movements)
|
||||
- [Functions](#functions)
|
||||
- [collect](#collect)
|
||||
- [Options:](#options)
|
||||
|
||||
## 1. Summary
|
||||
|
||||
The collect block plugin is a utility plugin that can be used to help make collecting blocks and item drops very easy, using only a single API call. No need to worry about pathfinding to the block, selecting the right tool, or moving to pick up the item drop after mining.
|
||||
|
||||
## Properties
|
||||
|
||||
### `bot.collectblock.movements: Movements`
|
||||
|
||||
The movements object used by the pathfinder plugin to define the movement configuration. This object is passed to the pathfinder plugin when any API from this plugin is called in order to control how pathfinding should work when collecting the given blocks or item.
|
||||
|
||||
If set to null, the pathfinder plugin movements is not updated.
|
||||
|
||||
Defaults to a new movements object instance.
|
||||
|
||||
## Functions
|
||||
|
||||
### collect
|
||||
|
||||
Usage: `bot.collectblock.collect(target: Collectable | Collectable[], options?: CollectOptions, cb: (err?: Error) => void): void`
|
||||
|
||||
Causes the bot to collect the given block, item drop, or list of those. If the target is a block, the bot will move to the block, mine it, and pick up the item drop. If the target is an item drop, the bot will move to the item drop and pick it up. If the target is a list of collectables, the bot will move from target to target in order of closest to furthest and collect each target in turn.
|
||||
|
||||
#### Options:
|
||||
|
||||
* `append: boolean`
|
||||
|
||||
If true, the target(s) will be appended to the existing target list instead of starting a new task. Defaults to false.
|
||||
|
||||
* `ignoreNoPath: boolean`
|
||||
|
||||
If true, errors will not be thrown when a path to the target block cannot be found. The bot will attempt to choose the best available position it can find, instead. Errors are still thrown if the bot cannot interact with the block from it's final location. Defaults to false.
|
||||
|
||||
* `chestLocations: Vec3[]`
|
||||
|
||||
Gets the list of chest locations to use when storing items after the bot's inventory becomes full. If undefined, it defaults to the chest location list on the bot.collectBlock plugin.
|
||||
|
||||
* `itemFilter: ItemFilter`
|
||||
|
||||
When transferring items to a chest, this filter is used to determine what items are allowed to be moved, and what items aren't allowed to be moved. Defaults to the item filter specified on the bot.collectBlock plugin.
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* This bot example show how to direct a bot to collect a specific block type
|
||||
* or a group of nearby blocks of that type.
|
||||
*/
|
||||
|
||||
const mineflayer = require('mineflayer')
|
||||
const collectBlock = require('mineflayer-collectblock').plugin
|
||||
|
||||
if (process.argv.length < 4 || process.argv.length > 6) {
|
||||
console.log('Usage : node collector.js <host> <port> [<name>] [<password>]')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const bot = mineflayer.createBot({
|
||||
host: process.argv[2],
|
||||
port: process.argv[3],
|
||||
username: process.argv[4] || 'collector',
|
||||
password: process.argv[5]
|
||||
})
|
||||
|
||||
bot.loadPlugin(collectBlock)
|
||||
|
||||
let mcData
|
||||
bot.once('spawn', () => {
|
||||
mcData = require('minecraft-data')(bot.version)
|
||||
})
|
||||
|
||||
bot.on('chat', async (username, message) => {
|
||||
const args = message.split(' ')
|
||||
if (args[0] !== 'collect') return
|
||||
|
||||
let count = 1
|
||||
if (args.length === 3) count = parseInt(args[1])
|
||||
|
||||
let type = args[1]
|
||||
if (args.length === 3) type = args[2]
|
||||
|
||||
const blockType = mcData.blocksByName[type]
|
||||
if (!blockType) {
|
||||
return
|
||||
}
|
||||
|
||||
const blocks = bot.findBlocks({
|
||||
matching: blockType.id,
|
||||
maxDistance: 64,
|
||||
count: count
|
||||
})
|
||||
|
||||
if (blocks.length === 0) {
|
||||
bot.chat("I don't see that block nearby.")
|
||||
return
|
||||
}
|
||||
|
||||
const targets = []
|
||||
for (let i = 0; i < Math.min(blocks.length, count); i++) {
|
||||
targets.push(bot.blockAt(blocks[i]))
|
||||
}
|
||||
|
||||
bot.chat(`Found ${targets.length} ${type}(s)`)
|
||||
|
||||
try {
|
||||
await bot.collectBlock.collect(targets)
|
||||
// All blocks have been collected.
|
||||
bot.chat('Done')
|
||||
} catch (err) {
|
||||
// An error occurred, report it.
|
||||
bot.chat(err.message)
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* This bot example shows how to collect a vein of ores quickly after only finding a single block.
|
||||
* This makes it easy to collect a vein of ores or mine a tree without looking for every block in the
|
||||
* area.
|
||||
*/
|
||||
|
||||
const mineflayer = require('mineflayer')
|
||||
const collectBlock = require('mineflayer-collectblock').plugin
|
||||
|
||||
if (process.argv.length < 4 || process.argv.length > 6) {
|
||||
console.log('Usage : node oreMiner.js <host> <port> [<name>] [<password>]')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const bot = mineflayer.createBot({
|
||||
host: process.argv[2],
|
||||
port: process.argv[3],
|
||||
username: process.argv[4] || 'oreMiner',
|
||||
password: process.argv[5]
|
||||
})
|
||||
|
||||
bot.loadPlugin(collectBlock)
|
||||
|
||||
let mcData
|
||||
bot.once('spawn', () => {
|
||||
mcData = require('minecraft-data')(bot.version)
|
||||
})
|
||||
|
||||
bot.on('chat', async (username, message) => {
|
||||
const args = message.split(' ')
|
||||
if (args[0] !== 'collect') return
|
||||
|
||||
const blockType = mcData.blocksByName[args[1]]
|
||||
if (!blockType) {
|
||||
bot.chat(`I don't know any blocks named ${args[1]}.`)
|
||||
return
|
||||
}
|
||||
|
||||
const block = bot.findBlock({
|
||||
matching: blockType.id,
|
||||
maxDistance: 64
|
||||
})
|
||||
|
||||
if (!block) {
|
||||
bot.chat("I don't see that block nearby.")
|
||||
return
|
||||
}
|
||||
|
||||
const targets = bot.collectBlock.findFromVein(block)
|
||||
try {
|
||||
await bot.collectBlock.collect(targets)
|
||||
// All blocks have been collected.
|
||||
bot.chat('Done')
|
||||
} catch (err) {
|
||||
// An error occurred, report it.
|
||||
bot.chat(err.message)
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
/**
|
||||
* This bot example shows how to use the chest filling mechanic of the plugin.
|
||||
* Simply provide a given storage chest, and the bot will automatically try and
|
||||
* store it's inventory in that chest when the bot's inventory becomes full.
|
||||
*/
|
||||
|
||||
if (process.argv.length < 4 || process.argv.length > 6) {
|
||||
console.log('Usage : node storageBot.js <host> <port> [<name>] [<password>]')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Load your libraries
|
||||
const mineflayer = require('mineflayer')
|
||||
const collectBlock = require('mineflayer-collectblock').plugin
|
||||
|
||||
// Create your bot
|
||||
const bot = mineflayer.createBot({
|
||||
host: process.argv[2],
|
||||
port: parseInt(process.argv[3]),
|
||||
username: process.argv[4] ? process.argv[4] : 'storageBot',
|
||||
password: process.argv[5]
|
||||
})
|
||||
|
||||
// Load the collect block plugin
|
||||
bot.loadPlugin(collectBlock)
|
||||
|
||||
// Load mcData on login
|
||||
let mcData
|
||||
bot.once('login', () => {
|
||||
mcData = require('minecraft-data')(bot.version)
|
||||
})
|
||||
|
||||
// On spawn, try to find any nearby chests and save those as storage locations.
|
||||
// When the bot's inventory becomes too full, it will empty it's inventory into
|
||||
// these chests before collecting more resources. If a chest gets full, it moves
|
||||
// to the next one in order until it's inventory is empty or it runs out of chests.
|
||||
bot.once('spawn', () => {
|
||||
bot.collectBlock.chestLocations = bot.findBlocks({
|
||||
matching: mcData.blocksByName.chest.id,
|
||||
maxDistance: 16,
|
||||
count: 999999 // Get as many chests as we can
|
||||
})
|
||||
|
||||
if (bot.collectBlock.chestLocations.length === 0) {
|
||||
bot.chat("I don't see any chests nearby.")
|
||||
} else {
|
||||
for (const chestPos of bot.collectBlock.chestLocations) {
|
||||
bot.chat(`I found a chest at ${chestPos}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Wait for someone to say something
|
||||
bot.on('chat', async (username, message) => {
|
||||
// If the player says something start starts with "collect"
|
||||
// Otherwise, do nothing
|
||||
const args = message.split(' ')
|
||||
if (args[0] !== 'collect') return
|
||||
|
||||
// If the player specifies a number, collect that many. Otherwise, default to 1.
|
||||
let count = 1
|
||||
if (args.length === 3) count = parseInt(args[1])
|
||||
|
||||
// If a number was given the item number is the 3rd arg, not the 2nd.
|
||||
let type = args[1]
|
||||
if (args.length === 3) type = args[2]
|
||||
|
||||
// Get the id of that block type for this version of Minecraft.
|
||||
const blockType = mcData.blocksByName[type]
|
||||
if (!blockType) {
|
||||
bot.chat(`I don't know any blocks named ${type}.`)
|
||||
return
|
||||
}
|
||||
|
||||
// Find all nearby blocks of that type, up to the given count, within 64 blocks.
|
||||
const blocks = bot.findBlocks({
|
||||
matching: blockType.id,
|
||||
maxDistance: 64,
|
||||
count: count
|
||||
})
|
||||
|
||||
// Complain if we can't find any nearby blocks of that type.
|
||||
if (blocks.length === 0) {
|
||||
bot.chat("I don't see that block nearby.")
|
||||
return
|
||||
}
|
||||
|
||||
// Convert the block position array into a block array to pass to collect block.
|
||||
const targets = []
|
||||
for (let i = 0; i < Math.min(blocks.length, count); i++) {
|
||||
targets.push(bot.blockAt(blocks[i]))
|
||||
}
|
||||
|
||||
// Announce what we found.
|
||||
bot.chat(`Found ${targets.length} ${type}(s)`)
|
||||
|
||||
// Tell the bot to collect all of the given blocks in the block list.
|
||||
try {
|
||||
await bot.collectBlock.collect(targets)
|
||||
// All blocks have been collected.
|
||||
bot.chat('Done')
|
||||
} catch (err) {
|
||||
// An error occurred, report it.
|
||||
bot.chat(err.message)
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"name": "mineflayer-collectblock",
|
||||
"version": "1.4.1",
|
||||
"description": "A simple utility plugin for Mineflayer that add a higher level API for collecting blocks.",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "ts-standard && tsc && require-self",
|
||||
"clean": "rm -rf lib",
|
||||
"test": "test"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/TheDudeFromCI/mineflayer-collectblock.git"
|
||||
},
|
||||
"keywords": [
|
||||
"mineflayer",
|
||||
"plugin",
|
||||
"api",
|
||||
"utility",
|
||||
"helper",
|
||||
"collect"
|
||||
],
|
||||
"author": "TheDudeFromCI",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/TheDudeFromCI/mineflayer-collectblock/issues"
|
||||
},
|
||||
"homepage": "https://github.com/TheDudeFromCI/mineflayer-collectblock#readme",
|
||||
"dependencies": {
|
||||
"mineflayer": "^4.0.0",
|
||||
"mineflayer-pathfinder": "^2.1.1",
|
||||
"mineflayer-tool": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.6.4",
|
||||
"require-self": "^0.2.3",
|
||||
"ts-standard": "^11.0.0",
|
||||
"typescript": "^4.1.3"
|
||||
},
|
||||
"files": [
|
||||
"lib/**/*"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { Bot } from 'mineflayer'
|
||||
import { Block } from 'prismarine-block'
|
||||
|
||||
export function findFromVein (bot: Bot, block: Block, maxBlocks: number, maxDistance: number, floodRadius: number): Block[] {
|
||||
const targets: Block[] = []
|
||||
const open: Block[] = [block]
|
||||
const type = block.type
|
||||
const center = block.position
|
||||
|
||||
for (let i = 0; i < maxBlocks; i++) {
|
||||
const next = open.pop()
|
||||
if (next == null) break
|
||||
|
||||
targets.push(next)
|
||||
|
||||
for (let x = -floodRadius; x <= floodRadius; x++) {
|
||||
for (let y = -floodRadius; y <= floodRadius; y++) {
|
||||
for (let z = -floodRadius; z <= floodRadius; z++) {
|
||||
const neighborPos = next.position.offset(x, y, z)
|
||||
if (neighborPos.manhattanDistanceTo(center) > maxDistance) continue
|
||||
|
||||
const neighbor = bot.blockAt(neighborPos)
|
||||
if (neighbor == null || neighbor.type !== type) continue
|
||||
|
||||
if (targets.includes(neighbor)) continue
|
||||
if (open.includes(neighbor)) continue
|
||||
|
||||
open.push(neighbor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return targets
|
||||
}
|
||||
|
|
@ -0,0 +1,451 @@
|
|||
import { Bot } from "mineflayer";
|
||||
import { Block } from "prismarine-block";
|
||||
import { Movements, goals } from "mineflayer-pathfinder";
|
||||
import { TemporarySubscriber } from "./TemporarySubscriber";
|
||||
import { Entity } from "prismarine-entity";
|
||||
import { error } from "./Util";
|
||||
import { Vec3 } from "vec3";
|
||||
import { emptyInventoryIfFull, ItemFilter } from "./Inventory";
|
||||
import { findFromVein } from "./BlockVeins";
|
||||
import { Collectable, Targets } from "./Targets";
|
||||
import { Item } from "prismarine-item";
|
||||
import mcDataLoader from "minecraft-data";
|
||||
import { once } from "events";
|
||||
import { callbackify } from "util";
|
||||
|
||||
export type Callback = (err?: Error) => void;
|
||||
|
||||
async function collectAll(
|
||||
bot: Bot,
|
||||
options: CollectOptionsFull
|
||||
): Promise<void> {
|
||||
let success_count = 0;
|
||||
while (!options.targets.empty) {
|
||||
await emptyInventoryIfFull(
|
||||
bot,
|
||||
options.chestLocations,
|
||||
options.itemFilter
|
||||
);
|
||||
const closest = options.targets.getClosest();
|
||||
if (closest == null) break;
|
||||
switch (closest.constructor.name) {
|
||||
case "Block": {
|
||||
try {
|
||||
if (success_count >= options.count) {
|
||||
break;
|
||||
}
|
||||
await bot.tool.equipForBlock(
|
||||
closest as Block,
|
||||
equipToolOptions
|
||||
);
|
||||
const goal = new goals.GoalLookAtBlock(
|
||||
closest.position,
|
||||
bot.world
|
||||
);
|
||||
await bot.pathfinder.goto(goal);
|
||||
await mineBlock(bot, closest as Block, options);
|
||||
success_count++;
|
||||
// TODO: options.ignoreNoPath
|
||||
} catch (err) {
|
||||
// @ts-ignore
|
||||
// console.log(err.stack)
|
||||
// bot.pathfinder.stop()
|
||||
// bot.waitForTicks(10)
|
||||
try {
|
||||
bot.pathfinder.setGoal(null);
|
||||
} catch (err) {}
|
||||
if (options.ignoreNoPath) {
|
||||
// @ts-ignore
|
||||
if (err.name === "Invalid block") {
|
||||
console.log(
|
||||
`Block ${closest.name} at ${closest.position} is not valid! Skip it!`
|
||||
);
|
||||
} // @ts-ignore
|
||||
else if (err.name === "Unsafe block") {
|
||||
console.log(
|
||||
`${closest.name} at ${closest.position} is not safe to break! Skip it!`
|
||||
);
|
||||
// @ts-ignore
|
||||
} else if (err.name === "NoItem") {
|
||||
const properties =
|
||||
bot.registry.blocksByName[closest.name];
|
||||
const leastTool = Object.keys(
|
||||
properties.harvestTools
|
||||
)[0];
|
||||
const item = bot.registry.items[leastTool];
|
||||
bot.chat(
|
||||
`I need at least a ${item.name} to mine ${closest.name}! Skip it!`
|
||||
);
|
||||
return;
|
||||
} else if (
|
||||
// @ts-ignore
|
||||
err.name === "NoPath" ||
|
||||
// @ts-ignore
|
||||
err.name === "Timeout"
|
||||
) {
|
||||
if (
|
||||
bot.entity.position.distanceTo(
|
||||
closest.position
|
||||
) < 0.5
|
||||
) {
|
||||
await mineBlock(bot, closest as Block, options);
|
||||
break;
|
||||
}
|
||||
console.log(
|
||||
`No path to ${closest.name} at ${closest.position}! Skip it!`
|
||||
);
|
||||
// @ts-ignore
|
||||
} else if (err.message === "Digging aborted") {
|
||||
console.log(`Digging aborted! Skip it!`);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
bot.chat(`Error: ${err.message}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "Entity": {
|
||||
// Don't collect any entities that are marked as 'invalid'
|
||||
if (!(closest as Entity).isValid) break;
|
||||
try {
|
||||
const tempEvents = new TemporarySubscriber(bot);
|
||||
const waitForPickup = new Promise<void>(
|
||||
(resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
// After 10 seconds, reject the promise
|
||||
clearTimeout(timeout);
|
||||
tempEvents.cleanup();
|
||||
reject(new Error("Failed to pickup item"));
|
||||
}, 10000);
|
||||
tempEvents.subscribeTo(
|
||||
"entityGone",
|
||||
(entity: Entity) => {
|
||||
if (entity === closest) {
|
||||
clearTimeout(timeout);
|
||||
tempEvents.cleanup();
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
bot.pathfinder.setGoal(
|
||||
new goals.GoalFollow(closest as Entity, 0)
|
||||
);
|
||||
// await bot.pathfinder.goto(new goals.GoalBlock(closest.position.x, closest.position.y, closest.position.z))
|
||||
await waitForPickup;
|
||||
} catch (err) {
|
||||
// @ts-ignore
|
||||
console.log(err.stack);
|
||||
try {
|
||||
bot.pathfinder.setGoal(null);
|
||||
} catch (err) {}
|
||||
if (options.ignoreNoPath) {
|
||||
// @ts-ignore
|
||||
if (err.message === "Failed to pickup item") {
|
||||
bot.chat(`Failed to pickup item! Skip it!`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw error(
|
||||
"UnknownType",
|
||||
`Target ${closest.constructor.name} is not a Block or Entity!`
|
||||
);
|
||||
}
|
||||
}
|
||||
options.targets.removeTarget(closest);
|
||||
}
|
||||
bot.chat(`Collect finish!`);
|
||||
}
|
||||
|
||||
const equipToolOptions = {
|
||||
requireHarvest: true,
|
||||
getFromChest: false,
|
||||
maxTools: 2,
|
||||
};
|
||||
|
||||
async function mineBlock(
|
||||
bot: Bot,
|
||||
block: Block,
|
||||
options: CollectOptionsFull
|
||||
): Promise<void> {
|
||||
if (
|
||||
bot.blockAt(block.position)?.type !== block.type ||
|
||||
bot.blockAt(block.position)?.type === 0
|
||||
) {
|
||||
options.targets.removeTarget(block);
|
||||
throw error("Invalid block", "Block is not valid!");
|
||||
// @ts-expect-error
|
||||
} else if (!bot.pathfinder.movements.safeToBreak(block)) {
|
||||
options.targets.removeTarget(block);
|
||||
throw error("Unsafe block", "Block is not safe to break!");
|
||||
}
|
||||
|
||||
await bot.tool.equipForBlock(block, equipToolOptions);
|
||||
|
||||
if (!block.canHarvest(bot.heldItem ? bot.heldItem.type : bot.heldItem)) {
|
||||
options.targets.removeTarget(block);
|
||||
throw error("NoItem", "Bot does not have a harvestable tool!");
|
||||
}
|
||||
|
||||
const tempEvents = new TemporarySubscriber(bot);
|
||||
tempEvents.subscribeTo("itemDrop", (entity: Entity) => {
|
||||
if (
|
||||
entity.position.distanceTo(block.position.offset(0.5, 0.5, 0.5)) <=
|
||||
0.5
|
||||
) {
|
||||
options.targets.appendTarget(entity);
|
||||
}
|
||||
});
|
||||
try {
|
||||
await bot.dig(block);
|
||||
// Waiting for items to drop
|
||||
await new Promise<void>((resolve) => {
|
||||
let remainingTicks = 10;
|
||||
tempEvents.subscribeTo("physicTick", () => {
|
||||
remainingTicks--;
|
||||
if (remainingTicks <= 0) {
|
||||
tempEvents.cleanup();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
tempEvents.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A set of options to apply when collecting the given targets.
|
||||
*/
|
||||
export interface CollectOptions {
|
||||
/**
|
||||
* If true, the target(s) will be appended to the existing target list instead of
|
||||
* starting a new task. Defaults to false.
|
||||
*/
|
||||
append?: boolean;
|
||||
|
||||
/**
|
||||
* If true, errors will not be thrown when a path to the target block cannot
|
||||
* be found. The bot will attempt to choose the best available position it
|
||||
* can find, instead. Errors are still thrown if the bot cannot interact with
|
||||
* the block from it's final location. Defaults to false.
|
||||
*/
|
||||
ignoreNoPath?: boolean;
|
||||
|
||||
/**
|
||||
* Gets the list of chest locations to use when storing items after the bot's
|
||||
* inventory becomes full. If undefined, it defaults to the chest location
|
||||
* list on the bot.collectBlock plugin.
|
||||
*/
|
||||
chestLocations?: Vec3[];
|
||||
|
||||
/**
|
||||
* When transferring items to a chest, this filter is used to determine what
|
||||
* items are allowed to be moved, and what items aren't allowed to be moved.
|
||||
* Defaults to the item filter specified on the bot.collectBlock plugin.
|
||||
*/
|
||||
itemFilter?: ItemFilter;
|
||||
|
||||
/**
|
||||
* The total number of items to collect
|
||||
*/
|
||||
count?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A version of collect options where all values are assigned.
|
||||
*/
|
||||
interface CollectOptionsFull {
|
||||
append: boolean;
|
||||
ignoreNoPath: boolean;
|
||||
chestLocations: Vec3[];
|
||||
itemFilter: ItemFilter;
|
||||
targets: Targets;
|
||||
count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The collect block plugin.
|
||||
*/
|
||||
export class CollectBlock {
|
||||
/**
|
||||
* The bot.
|
||||
*/
|
||||
private readonly bot: Bot;
|
||||
|
||||
/**
|
||||
* The list of active targets being collected.
|
||||
*/
|
||||
private readonly targets: Targets;
|
||||
|
||||
/**
|
||||
* The movements configuration to be sent to the pathfinder plugin.
|
||||
*/
|
||||
movements?: Movements;
|
||||
|
||||
/**
|
||||
* A list of chest locations which the bot is allowed to empty their inventory into
|
||||
* if it becomes full while the bot is collecting resources.
|
||||
*/
|
||||
chestLocations: Vec3[] = [];
|
||||
|
||||
/**
|
||||
* When collecting items, this filter is used to determine what items should be placed
|
||||
* into a chest if the bot's inventory becomes full. By default, returns true for all
|
||||
* items except for tools, weapons, and armor.
|
||||
*
|
||||
* @param item - The item stack in the bot's inventory to check.
|
||||
*
|
||||
* @returns True if the item should be moved into the chest. False otherwise.
|
||||
*/
|
||||
itemFilter: ItemFilter = (item: Item) => {
|
||||
if (item.name.includes("helmet")) return false;
|
||||
if (item.name.includes("chestplate")) return false;
|
||||
if (item.name.includes("leggings")) return false;
|
||||
if (item.name.includes("boots")) return false;
|
||||
if (item.name.includes("shield")) return false;
|
||||
if (item.name.includes("sword")) return false;
|
||||
if (item.name.includes("pickaxe")) return false;
|
||||
if (item.name.includes("axe")) return false;
|
||||
if (item.name.includes("shovel")) return false;
|
||||
if (item.name.includes("hoe")) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new instance of the create block plugin.
|
||||
*
|
||||
* @param bot - The bot this plugin is acting on.
|
||||
*/
|
||||
constructor(bot: Bot) {
|
||||
this.bot = bot;
|
||||
this.targets = new Targets(bot);
|
||||
// @ts-ignore
|
||||
this.movements = new Movements(bot, mcDataLoader(bot.version));
|
||||
}
|
||||
|
||||
/**
|
||||
* If target is a block:
|
||||
* Causes the bot to break and collect the target block.
|
||||
*
|
||||
* If target is an item drop:
|
||||
* Causes the bot to collect the item drop.
|
||||
*
|
||||
* If target is an array containing items or blocks, preforms the correct action for
|
||||
* all targets in that array sorting dynamically by distance.
|
||||
*
|
||||
* @param target - The block(s) or item(s) to collect.
|
||||
* @param options - The set of options to use when handling these targets
|
||||
* @param cb - The callback that is called finished.
|
||||
*/
|
||||
async collect(
|
||||
target: Collectable | Collectable[],
|
||||
options: CollectOptions | Callback = {},
|
||||
cb?: Callback
|
||||
): Promise<void> {
|
||||
if (typeof options === "function") {
|
||||
cb = options;
|
||||
options = {};
|
||||
}
|
||||
// @ts-expect-error
|
||||
if (cb != null) return callbackify(this.collect)(target, options, cb);
|
||||
|
||||
const optionsFull: CollectOptionsFull = {
|
||||
append: options.append ?? false,
|
||||
ignoreNoPath: options.ignoreNoPath ?? false,
|
||||
chestLocations: options.chestLocations ?? this.chestLocations,
|
||||
itemFilter: options.itemFilter ?? this.itemFilter,
|
||||
targets: this.targets,
|
||||
count: options.count ?? Infinity,
|
||||
};
|
||||
|
||||
if (this.bot.pathfinder == null) {
|
||||
throw error(
|
||||
"UnresolvedDependency",
|
||||
"The mineflayer-collectblock plugin relies on the mineflayer-pathfinder plugin to run!"
|
||||
);
|
||||
}
|
||||
|
||||
if (this.bot.tool == null) {
|
||||
throw error(
|
||||
"UnresolvedDependency",
|
||||
"The mineflayer-collectblock plugin relies on the mineflayer-tool plugin to run!"
|
||||
);
|
||||
}
|
||||
|
||||
if (this.movements != null) {
|
||||
this.bot.pathfinder.setMovements(this.movements);
|
||||
}
|
||||
|
||||
if (!optionsFull.append) await this.cancelTask();
|
||||
if (Array.isArray(target)) {
|
||||
this.targets.appendTargets(target);
|
||||
} else {
|
||||
this.targets.appendTarget(target);
|
||||
}
|
||||
|
||||
try {
|
||||
await collectAll(this.bot, optionsFull);
|
||||
this.targets.clear();
|
||||
} catch (err) {
|
||||
this.targets.clear();
|
||||
// Ignore path stopped error for cancelTask to work properly (imo we shouldn't throw any pathing errors)
|
||||
// @ts-expect-error
|
||||
if (err.name !== "PathStopped") throw err;
|
||||
} finally {
|
||||
// @ts-expect-error
|
||||
this.bot.emit("collectBlock_finished");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all touching blocks of the same type to the given block and returns them as an array.
|
||||
* This effectively acts as a flood fill algorithm to retrieve blocks in the same ore vein and similar.
|
||||
*
|
||||
* @param block - The starting block.
|
||||
* @param maxBlocks - The maximum number of blocks to look for before stopping.
|
||||
* @param maxDistance - The max distance from the starting block to look.
|
||||
* @param floodRadius - The max distance distance from block A to block B to be considered "touching"
|
||||
*/
|
||||
findFromVein(
|
||||
block: Block,
|
||||
maxBlocks = 100,
|
||||
maxDistance = 16,
|
||||
floodRadius = 1
|
||||
): Block[] {
|
||||
return findFromVein(
|
||||
this.bot,
|
||||
block,
|
||||
maxBlocks,
|
||||
maxDistance,
|
||||
floodRadius
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the current collection task, if still active.
|
||||
*
|
||||
* @param cb - The callback to use when the task is stopped.
|
||||
*/
|
||||
async cancelTask(cb?: Callback): Promise<void> {
|
||||
if (this.targets.empty) {
|
||||
if (cb != null) cb();
|
||||
return await Promise.resolve();
|
||||
}
|
||||
this.bot.pathfinder.stop();
|
||||
if (cb != null) {
|
||||
// @ts-expect-error
|
||||
this.bot.once("collectBlock_finished", cb);
|
||||
}
|
||||
await once(this.bot, "collectBlock_finished");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import { Bot } from 'mineflayer'
|
||||
import { Callback } from './CollectBlock'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { error } from './Util'
|
||||
import { Item } from 'prismarine-item'
|
||||
import { goals } from 'mineflayer-pathfinder'
|
||||
import { callbackify } from 'util'
|
||||
|
||||
export type ItemFilter = (item: Item) => boolean
|
||||
|
||||
function getClosestChest (bot: Bot, chestLocations: Vec3[]): Vec3 | null {
|
||||
let chest = null
|
||||
let distance = 0
|
||||
|
||||
for (const c of chestLocations) {
|
||||
const dist = c.distanceTo(bot.entity.position)
|
||||
if (chest == null || dist < distance) {
|
||||
chest = c
|
||||
distance = dist
|
||||
}
|
||||
}
|
||||
|
||||
if (chest != null) {
|
||||
chestLocations.splice(chestLocations.indexOf(chest), 1)
|
||||
}
|
||||
|
||||
return chest
|
||||
}
|
||||
|
||||
export async function emptyInventoryIfFull (bot: Bot, chestLocations: Vec3[], itemFilter: ItemFilter, cb?: Callback): Promise<void> {
|
||||
// @ts-expect-error
|
||||
if (cb != null) return callbackify(emptyInventoryIfFull)(bot, chestLocations, cb)
|
||||
if (bot.inventory.emptySlotCount() > 0) return
|
||||
return await emptyInventory(bot, chestLocations, itemFilter)
|
||||
}
|
||||
|
||||
export async function emptyInventory (bot: Bot, chestLocations: Vec3[], itemFilter: ItemFilter, cb?: Callback): Promise<void> {
|
||||
// @ts-expect-error
|
||||
if (cb != null) return callbackify(emptyInventory)(bot, chestLocations, cb)
|
||||
if (chestLocations.length === 0) {
|
||||
throw error('NoChests', 'There are no defined chest locations!')
|
||||
}
|
||||
|
||||
// Shallow clone so we can safely remove chests from the list that are full.
|
||||
chestLocations = [...chestLocations]
|
||||
|
||||
while (true) {
|
||||
const chest = getClosestChest(bot, chestLocations)
|
||||
if (chest == null) {
|
||||
throw error('NoChests', 'All chests are full.')
|
||||
}
|
||||
const hasRemaining = await tryEmptyInventory(bot, chest, itemFilter)
|
||||
if (!hasRemaining) return
|
||||
}
|
||||
}
|
||||
|
||||
async function tryEmptyInventory (bot: Bot, chestLocation: Vec3, itemFilter: ItemFilter, cb?: (err: Error | undefined, hasRemaining: boolean) => void): Promise<boolean> {
|
||||
// @ts-expect-error
|
||||
if (cb != null) return callbackify(tryEmptyInventory)(bot, chestLocation, itemFilter, cb)
|
||||
await gotoChest(bot, chestLocation)
|
||||
return await placeItems(bot, chestLocation, itemFilter)
|
||||
}
|
||||
|
||||
async function gotoChest (bot: Bot, location: Vec3, cb?: Callback): Promise<void> {
|
||||
// @ts-expect-error
|
||||
if (cb != null) return callbackify(gotoChest)(bot, location)
|
||||
await bot.pathfinder.goto(new goals.GoalGetToBlock(location.x, location.y, location.z))
|
||||
}
|
||||
|
||||
async function placeItems (bot: Bot, chestPos: Vec3, itemFilter: ItemFilter, cb?: (err: Error | undefined, hasRemaining: boolean) => void): Promise<boolean> {
|
||||
// @ts-expect-error
|
||||
if (cb != null) return callbackify(placeItems)(bot, chestPos, itemFilter, cb)
|
||||
const chestBlock = bot.blockAt(chestPos)
|
||||
if (chestBlock == null) {
|
||||
throw error('UnloadedChunk', 'Chest is in an unloaded chunk!')
|
||||
}
|
||||
const chest = await bot.openChest(chestBlock)
|
||||
for (const item of bot.inventory.items()) {
|
||||
if (!itemFilter(item)) continue
|
||||
if (chest.firstEmptyContainerSlot() === null) {
|
||||
// We have items that didn't fit.
|
||||
return true
|
||||
}
|
||||
await chest.deposit(item.type, item.metadata, item.count)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import { Bot } from 'mineflayer'
|
||||
import { Block } from 'prismarine-block'
|
||||
import { Entity } from 'prismarine-entity'
|
||||
|
||||
export type Collectable = Block | Entity
|
||||
|
||||
export class Targets {
|
||||
private readonly bot: Bot
|
||||
private targets: Collectable[] = []
|
||||
|
||||
constructor (bot: Bot) {
|
||||
this.bot = bot
|
||||
}
|
||||
|
||||
appendTargets (targets: Collectable[]): void {
|
||||
for (const target of targets) {
|
||||
this.appendTarget(target)
|
||||
}
|
||||
}
|
||||
|
||||
appendTarget (target: Collectable): void {
|
||||
if (this.targets.includes(target)) return
|
||||
this.targets.push(target)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the closest target to the bot in this list.
|
||||
*
|
||||
* @returns The closest target, or null if there are no targets.
|
||||
*/
|
||||
getClosest (): Collectable | null {
|
||||
let closest: Collectable | null = null
|
||||
let distance: number = 0
|
||||
|
||||
for (const target of this.targets) {
|
||||
const dist = target.position.distanceTo(this.bot.entity.position)
|
||||
|
||||
if (closest == null || dist < distance) {
|
||||
closest = target
|
||||
distance = dist
|
||||
}
|
||||
}
|
||||
|
||||
return closest
|
||||
}
|
||||
|
||||
get empty (): boolean {
|
||||
return this.targets.length === 0
|
||||
}
|
||||
|
||||
clear (): void {
|
||||
this.targets.length = 0
|
||||
}
|
||||
|
||||
removeTarget (target: Collectable): void {
|
||||
const index = this.targets.indexOf(target)
|
||||
if (index < 0) return
|
||||
this.targets.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import type { Callback } from './index'
|
||||
export type Task = (cb: Callback) => void
|
||||
export type SyncTask = () => void
|
||||
|
||||
/**
|
||||
* A simple utility class for queuing up a series of async tasks to execute.
|
||||
*/
|
||||
export class TaskQueue {
|
||||
private tasks: Task[] = []
|
||||
|
||||
/**
|
||||
* If true, the task list will stop executing if one of the tasks throws an error.
|
||||
*/
|
||||
readonly stopOnError: boolean = true
|
||||
|
||||
/**
|
||||
* Adds a new async task to this queue. The provided callback should be executed when
|
||||
* the async task is complete.
|
||||
*
|
||||
* @param task - The async task to add.
|
||||
*/
|
||||
add (task: Task): void {
|
||||
this.tasks.push(task)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a synchronous task toi this queue.
|
||||
*
|
||||
* @param task - The sync task to add.
|
||||
*/
|
||||
addSync (task: SyncTask): void {
|
||||
this.add((cb) => {
|
||||
try {
|
||||
task()
|
||||
cb()
|
||||
} catch (err: any) {
|
||||
cb(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs all tasks currently in this queue and empties the queue.
|
||||
*
|
||||
* @param cb - The optional callback to be executed when all tasks in this queue have
|
||||
* finished executing.
|
||||
*/
|
||||
runAll (cb?: Callback): void {
|
||||
const taskList = this.tasks
|
||||
this.tasks = []
|
||||
|
||||
let index = -1
|
||||
const runNext: () => void = () => {
|
||||
index++
|
||||
if (index >= taskList.length) {
|
||||
if (cb !== undefined) cb()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
taskList[index]((err) => {
|
||||
if (err !== undefined) {
|
||||
if (cb !== undefined) cb(err)
|
||||
|
||||
if (this.stopOnError) return
|
||||
}
|
||||
|
||||
runNext()
|
||||
})
|
||||
} catch (err: any) {
|
||||
if (cb !== undefined) cb(err)
|
||||
}
|
||||
}
|
||||
|
||||
runNext()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { Bot } from 'mineflayer'
|
||||
|
||||
class Subscription {
|
||||
constructor (readonly eventName: string, readonly callback: Function) {}
|
||||
}
|
||||
|
||||
export class TemporarySubscriber {
|
||||
private readonly subscriptions: Subscription[] = []
|
||||
|
||||
constructor (readonly bot: Bot) {}
|
||||
|
||||
/**
|
||||
* Adds a new temporary event listener to the bot.
|
||||
*
|
||||
* @param event - The event to subscribe to.
|
||||
* @param callback - The function to execute.
|
||||
*/
|
||||
subscribeTo (event: string, callback: Function): void {
|
||||
this.subscriptions.push(new Subscription(event, callback))
|
||||
|
||||
// @ts-expect-error
|
||||
this.bot.on(event, callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all attached event listeners from the bot.
|
||||
*/
|
||||
cleanup (): void {
|
||||
for (const sub of this.subscriptions) {
|
||||
// @ts-expect-error
|
||||
this.bot.removeListener(sub.eventName, sub.callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* Creates a new error object with the given type and message.
|
||||
*
|
||||
* @param type - The error type.
|
||||
* @param message - The error message.
|
||||
*
|
||||
* @returns The error object.
|
||||
*/
|
||||
export function error (type: string, message: string): Error {
|
||||
const e = new Error(message)
|
||||
e.name = type
|
||||
return e
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { Bot } from 'mineflayer'
|
||||
import { CollectBlock } from './CollectBlock'
|
||||
import { pathfinder as pathfinderPlugin } from 'mineflayer-pathfinder'
|
||||
import { plugin as toolPlugin } from 'mineflayer-tool'
|
||||
|
||||
export function plugin (bot: Bot): void {
|
||||
// @ts-expect-error
|
||||
bot.collectBlock = new CollectBlock(bot)
|
||||
|
||||
// Load plugins if not loaded manually.
|
||||
setTimeout(() => loadPathfinderPlugin(bot), 0)
|
||||
setTimeout(() => loadToolPlugin(bot), 0)
|
||||
}
|
||||
|
||||
function loadPathfinderPlugin (bot: Bot): void {
|
||||
if (bot.pathfinder != null) return
|
||||
bot.loadPlugin(pathfinderPlugin)
|
||||
}
|
||||
|
||||
function loadToolPlugin (bot: Bot): void {
|
||||
if (bot.tool != null) return
|
||||
bot.loadPlugin(toolPlugin)
|
||||
}
|
||||
|
||||
export { CollectBlock, Callback, CollectOptions } from './CollectBlock'
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||
/* Basic Options */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
"target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||
"allowJs": true, /* Allow javascript files to be compiled. */
|
||||
"checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
"declaration": true,
|
||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"outDir": "./lib",
|
||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
// "composite": true, /* Enable project compilation */
|
||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||
// "removeComments": true, /* Do not emit comments to output. */
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
"strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
"alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
/* Additional Checks */
|
||||
"noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
/* Module Resolution Options */
|
||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
/* Advanced Options */
|
||||
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/__tests__/*"
|
||||
]
|
||||
}
|
||||
38
metagpt/mineflayer_env/mineflayer/package.json
Normal file
38
metagpt/mineflayer_env/mineflayer/package.json
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"name": "voyager",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"body-parser": "^1.20.2",
|
||||
"express": "^4.18.2",
|
||||
"magic-string": "^0.30.0",
|
||||
"minecraft-data": "^3.31.0",
|
||||
"minecrafthawkeye": "^1.3.6",
|
||||
"mineflayer": "^4.8.1",
|
||||
"mineflayer-collectblock": "file:mineflayer-collectblock",
|
||||
"mineflayer-pathfinder": "^2.4.2",
|
||||
"mineflayer-pvp": "^1.3.2",
|
||||
"mineflayer-tool": "^1.2.0",
|
||||
"mocha": "^10.2.0",
|
||||
"prismarine-biome": "^1.3.0",
|
||||
"prismarine-block": "=1.16.3",
|
||||
"prismarine-entity": "^2.2.0",
|
||||
"prismarine-item": "^1.12.1",
|
||||
"prismarine-nbt": "^2.2.1",
|
||||
"prismarine-recipe": "^1.3.1",
|
||||
"prismarine-viewer": "^1.24.0",
|
||||
"typescript": "^4.9.5",
|
||||
"vec3": "^0.1.8",
|
||||
"graceful-fs": "^4.2.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "2.8.5"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue