Merge branch 'minecraft' of github.com:geekan/MetaGPT into minecraft

This commit is contained in:
stellahsr 2023-10-06 15:49:11 +08:00
commit eb9ea304a5
215 changed files with 10530 additions and 1257 deletions

View file

@ -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
View 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())

View file

@ -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__ = [

View file

@ -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.")

View file

@ -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

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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

View 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",
]

View file

@ -0,0 +1,3 @@
{
"tabWidth": 4
}

View 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

View 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}`);
}
}

View 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"
);
}
}
}

View 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);
});
}

View file

@ -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;
}
}
}
}
}
}

View 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`);
}

View 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`);
}

View 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`);
}
}
}

View 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) => {
})
}

View 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.`
);
}
}
}

View 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;
}

View file

@ -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);
});
}

View file

@ -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"]))

View file

@ -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);
}

View file

@ -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
*/
}

View file

@ -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)
);
}

View file

@ -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 });
}

View file

@ -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

View file

@ -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);
}

View file

@ -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();
}

View file

@ -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);
}

View 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

View 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

View 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}}"

View 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

View 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)

View file

@ -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

View file

@ -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)))

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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):

View file

@ -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()

View file

@ -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

View file

@ -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)

View file

@ -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
View 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
View 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

View file

@ -0,0 +1,3 @@
# Ignore artifacts:
build
coverage

View file

@ -0,0 +1,3 @@
{
"tabWidth": 4
}

View 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}`);
});

View 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 };

View 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;

View file

@ -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;

View 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;

View 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;

View 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;

View 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;

View 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 };

View 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 };

View 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,
};

View 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

View file

@ -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.

View file

@ -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.

View file

@ -0,0 +1 @@
theme: jekyll-theme-cayman

View file

@ -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.

View file

@ -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)
}
})

View file

@ -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)
}
})

View file

@ -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)
}
})

View file

@ -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/**/*"
]
}

View file

@ -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
}

View file

@ -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");
}
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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()
}
}

View file

@ -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)
}
}
}

View file

@ -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
}

View file

@ -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'

View file

@ -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__/*"
]
}

View 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"
}
}

View file

@ -0,0 +1,162 @@
# -*- coding: utf-8 -*-
# @Date : 2023/09/25 22:13
# @Author : yuymf
# @Desc : @https://github.com/MineDojo/Voyager/blob/main/voyager/env/bridge.py
import os
import time
import json
import requests
import re
from metagpt.logs import logger
import metagpt.utils.minecraft as U
from metagpt.utils.minecraft.process_monitor import SubprocessMonitor
from metagpt.const import CKPT_DIR, DEFAULT_WARMUP, CURRICULUM_OB, CORE_INVENTORY_ITEMS
class MineflayerEnv:
def __init__(
self,
mc_port=None,
server_host="http://127.0.0.1",
server_port=3000,
request_timeout=600,
):
self.mc_port = mc_port
self.server = f"{server_host}:{server_port}"
self.server_port = server_port
self.request_timeout = request_timeout
self.mineflayer = self.get_mineflayer_process(server_port)
self.has_reset = False
self.reset_options = None
self.connected = False
self.server_paused = False
self.warm_up = {} # turns that when to add part of curriculum_ob to HumanMessage TODO: MV
self.core_inv_items_regex = None
self._set_warmup()
os.makedirs(f"{CKPT_DIR}/curriculum/vectordb", exist_ok=True)
os.makedirs(f"{CKPT_DIR}/action", exist_ok=True)
os.makedirs(f"{CKPT_DIR}/skill/code", exist_ok=True)
os.makedirs(f"{CKPT_DIR}/skill/description", exist_ok=True)
os.makedirs(f"{CKPT_DIR}/skill/vectordb", exist_ok=True)
def _set_warmup(self):
warm_up = DEFAULT_WARMUP
if "optional_inventory_items" in warm_up:
assert CORE_INVENTORY_ITEMS is not None
self.core_inv_items_regex = re.compile(
CORE_INVENTORY_ITEMS
)
self.warm_up["optional_inventory_items"] = warm_up[
"optional_inventory_items"
]
else:
self.warm_up["optional_inventory_items"] = 0
for key in CURRICULUM_OB:
self.warm_up[key] = warm_up.get(key, DEFAULT_WARMUP[key])
self.warm_up["nearby_blocks"] = 0
self.warm_up["inventory"] = 0
self.warm_up["completed_tasks"] = 0
self.warm_up["failed_tasks"] = 0
def set_mc_port(self, mc_port):
self.mc_port = mc_port
def get_mineflayer_process(self, server_port):
U.f_mkdir("./logs", "mineflayer")
file_path = os.path.abspath(os.path.dirname(__file__))
return SubprocessMonitor(
commands=[
"node",
U.f_join(file_path, "mineflayer_env/mineflayer/index.js"),
str(server_port),
],
name="mineflayer",
ready_match=r"Server started on port (\d+)",
log_path=U.f_join("./logs", "mineflayer"),
)
def check_process(self):
retry = 0
while not self.mineflayer.is_running:
logger.info("Mineflayer process has exited, restarting")
self.mineflayer.run()
if not self.mineflayer.is_running:
if retry > 3:
logger.error("Mineflayer process failed to start")
raise {}
else:
retry += 1
continue
logger.info(self.mineflayer.ready_line)
res = requests.post(
f"{self.server}/start",
json=self.reset_options,
timeout=self.request_timeout,
)
if res.status_code != 200:
self.mineflayer.stop()
logger.error(f"Minecraft server reply with code {res.status_code}")
raise {}
return res.json()
def reset(
self,
*,
seed=None,
options=None,
):
if options is None:
options = {}
if options.get("inventory", {}) and options.get("mode", "hard") != "hard":
logger.error("inventory can only be set when options is hard")
raise {}
self.reset_options = {
"port": self.mc_port,
"reset": options.get("mode", "hard"),
"inventory": options.get("inventory", {}),
"equipment": options.get("equipment", []),
"spread": options.get("spread", False),
"waitTicks": options.get("wait_ticks", 5),
"position": options.get("position", None),
}
self.unpause()
self.mineflayer.stop()
time.sleep(1) # wait for mineflayer to exit
returned_data = self.check_process()
self.has_reset = True
self.connected = True
# All the reset in step will be soft
self.reset_options["reset"] = "soft"
self.pause()
return json.loads(returned_data)
def close(self):
self.unpause()
if self.connected:
res = requests.post(f"{self.server}/stop")
if res.status_code == 200:
self.connected = False
self.mineflayer.stop()
return not self.connected
def pause(self):
if self.mineflayer.is_running and not self.server_paused:
res = requests.post(f"{self.server}/pause")
if res.status_code == 200:
self.server_paused = True
return self.server_paused
def unpause(self):
if self.mineflayer.is_running and self.server_paused:
res = requests.post(f"{self.server}/pause")
if res.status_code == 200:
self.server_paused = False
else:
print(res.json())
return self.server_paused

View file

@ -0,0 +1,15 @@
Explain: ...
Plan:
1) ...
2) ...
3) ...
...
Code:
```javascript
// helper functions (only if needed, try to avoid them)
...
// main function after the helper functions
async function yourMainFunctionName(bot) {
// ...
}
```

View file

@ -0,0 +1,49 @@
You are a helpful assistant that writes Mineflayer javascript code to complete any Minecraft task specified by me.
Here are some useful programs written with Mineflayer APIs.
{programs}
At each round of conversation, I will give you
Code from the last round: ...
Execution error: ...
Chat log: ...
Biome: ...
Time: ...
Nearby blocks: ...
Nearby entities (nearest to farthest):
Health: ...
Hunger: ...
Position: ...
Equipment: ...
Inventory (xx/36): ...
Chests: ...
Task: ...
Context: ...
Critique: ...
You should then respond to me with
Explain (if applicable): Are there any steps missing in your plan? Why does the code not complete the task? What does the chat log and execution error imply?
Plan: How to complete the task step by step. You should pay attention to Inventory since it tells what you have. The task completeness check is also based on your final inventory.
Code:
1) Write an async function taking the bot as the only argument.
2) Reuse the above useful programs as much as possible.
- Use `mineBlock(bot, name, count)` to collect blocks. Do not use `bot.dig` directly.
- Use `craftItem(bot, name, count)` to craft items. Do not use `bot.craft` or `bot.recipesFor` directly.
- Use `smeltItem(bot, name count)` to smelt items. Do not use `bot.openFurnace` directly.
- Use `placeItem(bot, name, position)` to place blocks. Do not use `bot.placeBlock` directly.
- Use `killMob(bot, name, timeout)` to kill mobs. Do not use `bot.attack` directly.
3) Your function will be reused for building more complex functions. Therefore, you should make it generic and reusable. You should not make strong assumption about the inventory (as it may be changed at a later time), and therefore you should always check whether you have the required items before using them. If not, you should first collect the required items and reuse the above useful programs.
4) Functions in the "Code from the last round" section will not be saved or executed. Do not reuse functions listed there.
5) Anything defined outside a function will be ignored, define all your variables inside your functions.
6) Call `bot.chat` to show the intermediate progress.
7) Use `exploreUntil(bot, direction, maxDistance, callback)` when you cannot find something. You should frequently call this before mining blocks or killing mobs. You should select a direction at random every time instead of constantly using (1, 0, 1).
8) `maxDistance` should always be 32 for `bot.findBlocks` and `bot.findBlock`. Do not cheat.
9) Do not write infinite loops or recursive functions.
10) Do not use `bot.on` or `bot.once` to register event listeners. You definitely do not need them.
11) Name your function in a meaningful way (can infer the task from the name).
You should only respond in the format as described below:
RESPONSE FORMAT:
{response_format}

View file

@ -0,0 +1,127 @@
You are an assistant that assesses my progress of playing Minecraft and provides useful guidance.
You are required to evaluate if I have met the task requirements. Exceeding the task requirements is also considered a success while failing to meet them requires you to provide critique to help me improve.
I will give you the following information:
Biome: The biome after the task execution.
Time: The current time.
Nearby blocks: The surrounding blocks. These blocks are not collected yet. However, this is useful for some placing or planting tasks.
Health: My current health.
Hunger: My current hunger level. For eating task, if my hunger level is 20.0, then I successfully ate the food.
Position: My current position.
Equipment: My final equipment. For crafting tasks, I sometimes equip the crafted item.
Inventory (xx/36): My final inventory. For mining and smelting tasks, you only need to check inventory.
Chests: If the task requires me to place items in a chest, you can find chest information here.
Task: The objective I need to accomplish.
Context: The context of the task.
You should only respond in JSON format as described below:
{
"reasoning": "reasoning",
"success": boolean,
"critique": "critique",
}
Ensure the response can be parsed by Python `json.loads`, e.g.: no trailing commas, no single quotes, etc.
Here are some examples:
INPUT:
Inventory (2/36): {'oak_log':2, 'spruce_log':2}
Task: Mine 3 wood logs
RESPONSE:
{
"reasoning": "You need to mine 3 wood logs. You have 2 oak logs and 2 spruce logs, which add up to 4 wood logs.",
"success": true,
"critique": ""
}
INPUT:
Inventory (3/36): {'crafting_table': 1, 'spruce_planks': 6, 'stick': 4}
Task: Craft a wooden pickaxe
RESPONSE:
{
"reasoning": "You have enough materials to craft a wooden pickaxe, but you didn't craft it.",
"success": false,
"critique": "Craft a wooden pickaxe with a crafting table using 3 spruce planks and 2 sticks."
}
INPUT:
Inventory (2/36): {'raw_iron': 5, 'stone_pickaxe': 1}
Task: Mine 5 iron_ore
RESPONSE:
{
"reasoning": "Mining iron_ore in Minecraft will get raw_iron. You have 5 raw_iron in your inventory.",
"success": true,
"critique": ""
}
INPUT:
Biome: plains
Nearby blocks: stone, dirt, grass_block, grass, farmland, wheat
Inventory (26/36): ...
Task: Plant 1 wheat seed.
RESPONSE:
{
"reasoning": "For planting tasks, inventory information is useless. In nearby blocks, there is farmland and wheat, which means you succeed to plant the wheat seed.",
"success": true,
"critique": ""
}
INPUT:
Inventory (11/36): {... ,'rotten_flesh': 1}
Task: Kill 1 zombie
Context: ...
RESPONSE
{
"reasoning": "You have rotten flesh in your inventory, which means you successfully killed one zombie.",
"success": true,
"critique": ""
}
INPUT:
Hunger: 20.0/20.0
Inventory (11/36): ...
Task: Eat 1 ...
Context: ...
RESPONSE
{
"reasoning": "For all eating task, if the player's hunger is 20.0, then the player successfully ate the food.",
"success": true,
"critique": ""
}
INPUT:
Nearby blocks: chest
Inventory (28/36): {'rail': 1, 'coal': 2, 'oak_planks': 13, 'copper_block': 1, 'diorite': 7, 'cooked_beef': 4, 'granite': 22, 'cobbled_deepslate': 23, 'feather': 4, 'leather': 2, 'cooked_chicken': 3, 'white_wool': 2, 'stick': 3, 'black_wool': 1, 'stone_sword': 2, 'stone_hoe': 1, 'stone_axe': 2, 'stone_shovel': 2, 'cooked_mutton': 4, 'cobblestone_wall': 18, 'crafting_table': 1, 'furnace': 1, 'iron_pickaxe': 1, 'stone_pickaxe': 1, 'raw_copper': 12}
Chests:
(81, 131, 16): {'andesite': 2, 'dirt': 2, 'cobblestone': 75, 'wooden_pickaxe': 1, 'wooden_sword': 1}
Task: Deposit useless items into the chest at (81, 131, 16)
Context: ...
RESPONSE
{
"reasoning": "You have 28 items in your inventory after depositing, which is more than 20. You need to deposit more items from your inventory to the chest.",
"success": false,
"critique": "Deposit more useless items such as copper_block, diorite, granite, cobbled_deepslate, feather, and leather to meet the requirement of having only 20 occupied slots in your inventory."
}

View file

@ -0,0 +1,42 @@
You are a helpful assistant that tells me the next immediate task to do in Minecraft. My ultimate goal is to discover as many diverse things as possible, accomplish as many diverse tasks as possible and become the best Minecraft player in the world.
I will give you the following information:
Question 1: ...
Answer: ...
Question 2: ...
Answer: ...
Question 3: ...
Answer: ...
...
Biome: ...
Time: ...
Nearby blocks: ...
Other blocks that are recently seen: ...
Nearby entities (nearest to farthest): ...
Health: Higher than 15 means I'm healthy.
Hunger: Higher than 15 means I'm not hungry.
Position: ...
Equipment: If I have better armor in my inventory, you should ask me to equip it.
Inventory (xx/36): ...
Chests: You can ask me to deposit or take items from these chests. There also might be some unknown chest, you should ask me to open and check items inside the unknown chest.
Completed tasks so far: ...
Failed tasks that are too hard: ...
You must follow the following criteria:
1) You should act as a mentor and guide me to the next task based on my current learning progress.
2) Please be very specific about what resources I need to collect, what I need to craft, or what mobs I need to kill.
3) The next task should follow a concise format, such as "Mine [quantity] [block]", "Craft [quantity] [item]", "Smelt [quantity] [item]", "Kill [quantity] [mob]", "Cook [quantity] [food]", "Equip [item]" etc. It should be a single phrase. Do not propose multiple tasks at the same time. Do not mention anything else.
4) The next task should not be too hard since I may not have the necessary resources or have learned enough skills to complete it yet.
5) The next task should be novel and interesting. I should look for rare resources, upgrade my equipment and tools using better materials, and discover new things. I should not be doing the same thing over and over again.
6) I may sometimes need to repeat some tasks if I need to collect more resources to complete more difficult tasks. Only repeat tasks if necessary.
7) Do not ask me to build or dig shelter even if it's at night. I want to explore the world and discover new things. I don't want to stay in one place.
8) Tasks that require information beyond the player's status to verify should be avoided. For instance, "Placing 4 torches" and "Dig a 2x1x2 hole" are not ideal since they require visual confirmation from the screen. All the placing, building, planting, and trading tasks should be avoided. Do not propose task starting with these keywords.
You should only respond in the format as described below:
RESPONSE FORMAT:
Reasoning: Based on the information I listed above, do reasoning about what the next task should be.
Task: The next task.
Here's an example response:
Reasoning: The inventory is empty now, chop down a tree to get some wood.
Task: Obtain a wood log.

View file

@ -0,0 +1,94 @@
You are a helpful assistant that asks questions to help me decide the next immediate task to do in Minecraft. My ultimate goal is to discover as many things as possible, accomplish as many tasks as possible and become the best Minecraft player in the world.
I will give you the following information:
Biome: ...
Time: ...
Nearby blocks: ...
Other blocks that are recently seen: ...
Nearby entities (nearest to farthest): ...
Health: ...
Hunger: ...
Position: ...
Equipment: ...
Inventory (xx/36): ...
Chests: ...
Completed tasks so far: ...
Failed tasks that are too hard: ...
You must follow the following criteria:
1) You should ask at least 5 questions (but no more than 10 questions) to help me decide the next immediate task to do. Each question should be followed by the concept that the question is about.
2) Your question should be specific to a concept in Minecraft.
Bad example (the question is too general):
Question: What is the best way to play Minecraft?
Concept: unknown
Bad example (axe is still general, you should specify the type of axe such as wooden axe):
What are the benefits of using an axe to gather resources?
Concept: axe
Good example:
Question: How to make a wooden pickaxe?
Concept: wooden pickaxe
3) Your questions should be self-contained and not require any context.
Bad example (the question requires the context of my current biome):
Question: What are the blocks that I can find in my current biome?
Concept: unknown
Bad example (the question requires the context of my current inventory):
Question: What are the resources you need the most currently?
Concept: unknown
Bad example (the question requires the context of my current inventory):
Question: Do you have any gold or emerald resources?
Concept: gold
Bad example (the question requires the context of my nearby entities):
Question: Can you see any animals nearby that you can kill for food?
Concept: food
Bad example (the question requires the context of my nearby blocks):
Question: Is there any water source nearby?
Concept: water
Good example:
Question: What are the blocks that I can find in the sparse jungle?
Concept: sparse jungle
4) Do not ask questions about building tasks (such as building a shelter) since they are too hard for me to do.
Let's say your current biome is sparse jungle. You can ask questions like:
Question: What are the items that I can find in the sparse jungle?
Concept: sparse jungle
Question: What are the mobs that I can find in the sparse jungle?
Concept: sparse jungle
Let's say you see a creeper nearby, and you have not defeated a creeper before. You can ask a question like:
Question: How to defeat the creeper?
Concept: creeper
Let's say your last completed task is "Craft a wooden pickaxe". You can ask a question like:
Question: What are the suggested tasks that I can do after crafting a wooden pickaxe?
Concept: wooden pickaxe
Here are some more question and concept examples:
Question: What are the ores that I can find in the sparse jungle?
Concept: sparse jungle
(the above concept should not be "ore" because I need to look up the page of "sparse jungle" to find out what ores I can find in the sparse jungle)
Question: How can you obtain food in the sparse jungle?
Concept: sparse jungle
(the above concept should not be "food" because I need to look up the page of "sparse jungle" to find out what food I can obtain in the sparse jungle)
Question: How can you use the furnace to upgrade your equipment and make useful items?
Concept: furnace
Question: How to obtain a diamond ore?
Concept: diamond ore
Question: What are the benefits of using a stone pickaxe over a wooden pickaxe?
Concept: stone pickaxe
Question: What are the tools that you can craft using wood planks and sticks?
Concept: wood planks
You should only respond in the format as described below:
RESPONSE FORMAT:
Reasoning: ...
Question 1: ...
Concept 1: ...
Question 2: ...
Concept 2: ...
Question 3: ...
Concept 3: ...
Question 4: ...
Concept 4: ...
Question 5: ...
Concept 5: ...
...

View file

@ -0,0 +1,8 @@
You are a helpful assistant that answer my question about Minecraft.
I will give you the following information:
Question: ...
You will answer the question based on the context (only if available and helpful) and your own knowledge of Minecraft.
1) Start your answer with "Answer: ".
2) Answer "Answer: Unknown" if you don't know the answer.

View file

@ -0,0 +1,12 @@
You are a helpful assistant that generates a curriculum of subgoals to complete any Minecraft task specified by me.
I'll give you a final task and my current inventory, you need to decompose the task into a list of subgoals based on my inventory.
You must follow the following criteria:
1) Return a Python list of subgoals that can be completed in order to complete the specified task.
2) Each subgoal should follow a concise format, such as "Mine [quantity] [block]", "Craft [quantity] [item]", "Smelt [quantity] [item]", "Kill [quantity] [mob]", "Cook [quantity] [food]", "Equip [item]".
3) Include each level of necessary tools as a subgoal, such as wooden, stone, iron, diamond, etc.
You should only respond in JSON format as described below:
["subgoal1", "subgoal2", "subgoal3", ...]
Ensure the response can be parsed by Python `json.loads`, e.g.: no trailing commas, no single quotes, etc.

View file

@ -0,0 +1,51 @@
You are a helpful assistant that writes a description of the given function written in Mineflayer javascript code.
1) Do not mention the function name.
2) Do not mention anything about `bot.chat` or helper functions.
3) There might be some helper functions before the main function, but you only need to describe the main function.
4) Try to summarize the function in no more than 6 sentences.
5) Your response should be a single line of text.
For example, if the function is:
async function mineCobblestone(bot) {
// Check if the wooden pickaxe is in the inventory, if not, craft one
let woodenPickaxe = bot.inventory.findInventoryItem(mcData.itemsByName["wooden_pickaxe"].id);
if (!woodenPickaxe) {
bot.chat("Crafting a wooden pickaxe.");
await craftWoodenPickaxe(bot);
woodenPickaxe = bot.inventory.findInventoryItem(mcData.itemsByName["wooden_pickaxe"].id);
}
// Equip the wooden pickaxe if it exists
if (woodenPickaxe) {
await bot.equip(woodenPickaxe, "hand");
// Explore until we find a stone block
await exploreUntil(bot, new Vec3(1, -1, 1), 60, () => {
const stone = bot.findBlock({
matching: mcData.blocksByName["stone"].id,
maxDistance: 32
});
if (stone) {
return true;
}
});
// Mine 8 cobblestone blocks using the wooden pickaxe
bot.chat("Found a stone block. Mining 8 cobblestone blocks.");
await mineBlock(bot, "stone", 8);
bot.chat("Successfully mined 8 cobblestone blocks.");
// Save the event of mining 8 cobblestone
bot.save("cobblestone_mined");
} else {
bot.chat("Failed to craft a wooden pickaxe. Cannot mine cobblestone.");
}
}
The main function is `mineCobblestone`.
Then you would write:
The function is about mining 8 cobblestones using a wooden pickaxe. First check if a wooden pickaxe is in the inventory. If not, craft one. If the wooden pickaxe is available, equip the wooden pickaxe in the hand. Next, explore the environment until finding a stone block. Once a stone block is found, mine a total of 8 cobblestone blocks using the wooden pickaxe.

View file

@ -0,0 +1,39 @@
#!/usr/bin/env python3
# _*_ coding: utf-8 _*_
"""
@Time : 2023/9/4 15:40:40
@Author : Stitch-z
@File : tutorial_assistant.py
@Describe : Tutorial Assistant's prompt templates.
"""
COMMON_PROMPT = """
You are now a seasoned technical professional in the field of the internet.
We need you to write a technical tutorial with the topic "{topic}".
"""
DIRECTORY_PROMPT = COMMON_PROMPT + """
Please provide the specific table of contents for this tutorial, strictly following the following requirements:
1. The output must be strictly in the specified language, {language}.
2. Answer strictly in the dictionary format like {{"title": "xxx", "directory": [{{"dir 1": ["sub dir 1", "sub dir 2"]}}, {{"dir 2": ["sub dir 3", "sub dir 4"]}}]}}.
3. The directory should be as specific and sufficient as possible, with a primary and secondary directory.The secondary directory is in the array.
4. Do not have extra spaces or line breaks.
5. Each directory title has practical significance.
"""
CONTENT_PROMPT = COMMON_PROMPT + """
Now I will give you the module directory titles for the topic.
Please output the detailed principle content of this title in detail.
If there are code examples, please provide them according to standard code specifications.
Without a code example, it is not necessary.
The module directory titles for the topic is as follows:
{directory}
Strictly limit output according to the following requirements:
1. Follow the Markdown syntax format for layout.
2. If there are code examples, they must follow standard syntax specifications, have document annotations, and be displayed in code blocks.
3. The output must be strictly in the specified language, {language}.
4. Do not have redundant output, including concluding remarks.
5. Strict requirement not to output the topic "{topic}".
"""

View file

@ -6,11 +6,17 @@
"""
import asyncio
import time
from typing import NamedTuple
from typing import NamedTuple, Union
import openai
from openai.error import APIConnectionError
from tenacity import retry, stop_after_attempt, after_log, wait_fixed, retry_if_exception_type
from tenacity import (
after_log,
retry,
retry_if_exception_type,
stop_after_attempt,
wait_fixed,
)
from metagpt.config import CONFIG
from metagpt.logs import logger
@ -48,12 +54,14 @@ class RateLimiter:
self.last_call_time = time.time()
class Costs(NamedTuple):
total_prompt_tokens: int
total_completion_tokens: int
total_cost: float
total_budget: float
class CostManager(metaclass=Singleton):
"""计算使用接口的开销"""
@ -74,7 +82,9 @@ class CostManager(metaclass=Singleton):
"""
self.total_prompt_tokens += prompt_tokens
self.total_completion_tokens += completion_tokens
cost = (prompt_tokens * TOKEN_COSTS[model]["prompt"] + completion_tokens * TOKEN_COSTS[model]["completion"]) / 1000
cost = (
prompt_tokens * TOKEN_COSTS[model]["prompt"] + completion_tokens * TOKEN_COSTS[model]["completion"]
) / 1000
self.total_cost += cost
logger.info(
f"Total running cost: ${self.total_cost:.3f} | Max budget: ${CONFIG.max_budget:.3f} | "
@ -100,6 +110,7 @@ class CostManager(metaclass=Singleton):
"""
return self.total_completion_tokens
def get_total_cost(self):
"""
Get the total cost of API calls.
@ -109,25 +120,20 @@ def get_total_cost(self):
"""
return self.total_cost
def get_costs(self) -> Costs:
"""Get all costs"""
return Costs(self.total_prompt_tokens, self.total_completion_tokens, self.total_cost, self.total_budget)
def log_and_reraise(retry_state):
logger.error(f"Retry attempts exhausted. Last exception: {retry_state.outcome.exception()}")
logger.warning("""
Recommend going to https://deepwisdom.feishu.cn/wiki/MsGnwQBjiif9c3koSJNcYaoSnu4#part-XdatdVlhEojeAfxaaEZcMV3ZniQ
See FAQ 5.8
""")
raise retry_state.outcome.exception()
def log_and_reraise(retry_state):
logger.error(f"Retry attempts exhausted. Last exception: {retry_state.outcome.exception()}")
logger.warning("""
logger.warning(
"""
Recommend going to https://deepwisdom.feishu.cn/wiki/MsGnwQBjiif9c3koSJNcYaoSnu4#part-XdatdVlhEojeAfxaaEZcMV3ZniQ
See FAQ 5.8
""")
"""
)
raise retry_state.outcome.exception()
@ -182,15 +188,18 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
"n": 1,
"stop": None,
"temperature": 0.3,
"timeout": 3
"timeout": 3,
}
if CONFIG.openai_api_type == "azure":
if CONFIG.deployment_name and CONFIG.deployment_id:
raise ValueError("You can only use one of the `deployment_id` or `deployment_name` model")
elif not CONFIG.deployment_name and not CONFIG.deployment_id:
raise ValueError("You must specify `DEPLOYMENT_NAME` or `DEPLOYMENT_ID` parameter")
kwargs_mode = {"engine": CONFIG.deployment_name} if CONFIG.deployment_name \
kwargs_mode = (
{"engine": CONFIG.deployment_name}
if CONFIG.deployment_name
else {"deployment_id": CONFIG.deployment_id}
)
else:
kwargs_mode = {"model": self.model}
kwargs.update(kwargs_mode)
@ -219,7 +228,7 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
@retry(
stop=stop_after_attempt(3),
wait=wait_fixed(1),
after=after_log(logger, logger.level('WARNING').name),
after=after_log(logger, logger.level("WARNING").name),
retry=retry_if_exception_type(APIConnectionError),
retry_error_callback=log_and_reraise,
)
@ -236,8 +245,8 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
try:
prompt_tokens = count_message_tokens(messages, self.model)
completion_tokens = count_string_tokens(rsp, self.model)
usage['prompt_tokens'] = prompt_tokens
usage['completion_tokens'] = completion_tokens
usage["prompt_tokens"] = prompt_tokens
usage["completion_tokens"] = completion_tokens
return usage
except Exception as e:
logger.error("usage calculation failed!", e)
@ -273,8 +282,8 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
def _update_costs(self, usage: dict):
if CONFIG.calc_usage:
try:
prompt_tokens = int(usage['prompt_tokens'])
completion_tokens = int(usage['completion_tokens'])
prompt_tokens = int(usage["prompt_tokens"])
completion_tokens = int(usage["completion_tokens"])
self._cost_manager.update_cost(prompt_tokens, completion_tokens, self.model)
except Exception as e:
logger.error("updating costs failed!", e)
@ -286,3 +295,31 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
if not self.auto_max_tokens:
return CONFIG.max_tokens_rsp
return get_max_completion_tokens(messages, self.model, CONFIG.max_tokens_rsp)
def moderation(self, content: Union[str, list[str]]):
try:
if not content:
logger.error("content cannot be empty!")
else:
rsp = self._moderation(content=content)
return rsp
except Exception as e:
logger.error(f"moderating failed:{e}")
def _moderation(self, content: Union[str, list[str]]):
rsp = self.llm.Moderation.create(input=content)
return rsp
async def amoderation(self, content: Union[str, list[str]]):
try:
if not content:
logger.error("content cannot be empty!")
else:
rsp = await self._amoderation(content=content)
return rsp
except Exception as e:
logger.error(f"moderating failed:{e}")
async def _amoderation(self, content: Union[str, list[str]]):
rsp = await self.llm.Moderation.acreate(input=content)
return rsp

View file

@ -7,24 +7,20 @@
"""
from metagpt.roles.role import Role
'''
from metagpt.roles.architect import Architect
from metagpt.roles.project_manager import ProjectManager
from metagpt.roles.product_manager import ProductManager
from metagpt.roles.engineer import Engineer
from metagpt.roles.qa_engineer import QaEngineer
from metagpt.roles.seacher import Searcher
from metagpt.roles.sales import Sales
from metagpt.roles.customer_service import CustomerService
'''
__all__ = [
"Role",
"Architect",
"ProjectManager",
"ProductManager",
"Engineer",
"QaEngineer",
"Searcher",
"Sales",
"CustomerService",
#"Architect",
#"ProjectManager",
#"ProductManager",
#"Engineer",
#"QaEngineer",
]

View file

@ -6,33 +6,34 @@
@File : architect.py
"""
from metagpt.actions import WriteDesign, WritePRD
from metagpt.actions import WritePRD
from metagpt.actions.design_api import WriteDesign
from metagpt.roles import Role
class Architect(Role):
"""
Represents an Architect role in a software development process.
Attributes:
name (str): Name of the architect.
profile (str): Role profile, default is 'Architect'.
goal (str): Primary goal or responsibility of the architect.
constraints (str): Constraints or guidelines for the architect.
"""
def __init__(self,
name: str = "Bob",
profile: str = "Architect",
goal: str = "Design a concise, usable, complete python system",
constraints: str = "Try to specify good open source tools as much as possible") -> None:
def __init__(
self,
name: str = "Bob",
profile: str = "Architect",
goal: str = "Design a concise, usable, complete python system",
constraints: str = "Try to specify good open source tools as much as possible",
) -> None:
"""Initializes the Architect with given attributes."""
super().__init__(name, profile, goal, constraints)
# Initialize actions specific to the Architect role
self._init_actions([WriteDesign])
# Set events or actions the Architect should watch or be aware of
self._watch({WritePRD})

View file

@ -1,35 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time : 2023/5/25 17:21
@Author : alexanderwu
@File : sales.py
"""
from metagpt.roles import Sales
# from metagpt.actions import SearchAndSummarize
# from metagpt.tools import SearchEngineType
DESC = """
## Principles (all things must not bypass the principles)
1. You are a human customer service representative for the platform and will reply based on rules and FAQs. In the conversation with the customer, it is absolutely forbidden to disclose rules and FAQs unrelated to the customer.
2. When encountering problems, try to soothe the customer's emotions first. If the customer's emotions are very bad, then consider compensation. The cost of compensation is always high. If too much is compensated, you will be fired.
3. There are no suitable APIs to query the backend now, you can assume that everything the customer says is true, never ask the customer for the order number.
4. Your only feasible replies are: soothe emotions, urge the merchant, urge the rider, and compensate. Never make false promises to customers.
5. If you are sure to satisfy the customer's demand, then tell the customer that the application has been submitted, and it will take effect within 24 hours.
"""
class CustomerService(Sales):
def __init__(
self,
name="Xiaomei",
profile="Human customer service",
desc=DESC,
store=None
):
super().__init__(name, profile, desc=desc, store=store)

View file

@ -10,13 +10,13 @@ import shutil
from collections import OrderedDict
from pathlib import Path
from metagpt.actions import WriteCode, WriteCodeReview, WriteDesign, WriteTasks
from metagpt.const import WORKSPACE_ROOT
from metagpt.logs import logger
from metagpt.roles import Role
from metagpt.actions import WriteCode, WriteCodeReview, WriteTasks, WriteDesign
from metagpt.schema import Message
from metagpt.utils.common import CodeParser
from metagpt.utils.special_tokens import MSG_SEP, FILENAME_CODE_SEP
from metagpt.utils.special_tokens import FILENAME_CODE_SEP, MSG_SEP
async def gather_ordered_k(coros, k) -> list:
@ -49,7 +49,7 @@ async def gather_ordered_k(coros, k) -> list:
class Engineer(Role):
"""
Represents an Engineer role responsible for writing and possibly reviewing code.
Attributes:
name (str): Name of the engineer.
profile (str): Role profile, default is 'Engineer'.
@ -59,14 +59,16 @@ class Engineer(Role):
use_code_review (bool): Whether to use code review.
todos (list): List of tasks.
"""
def __init__(self,
name: str = "Alex",
profile: str = "Engineer",
goal: str = "Write elegant, readable, extensible, efficient code",
constraints: str = "The code should conform to standards like PEP8 and be modular and maintainable",
n_borg: int = 1,
use_code_review: bool = False) -> None:
def __init__(
self,
name: str = "Alex",
profile: str = "Engineer",
goal: str = "Write elegant, readable, extensible, efficient code",
constraints: str = "The code should conform to standards like PEP8 and be modular and maintainable",
n_borg: int = 1,
use_code_review: bool = False,
) -> None:
"""Initializes the Engineer role with given attributes."""
super().__init__(name, profile, goal, constraints)
self._init_actions([WriteCode])
@ -90,13 +92,13 @@ class Engineer(Role):
@classmethod
def parse_workspace(cls, system_design_msg: Message) -> str:
if system_design_msg.instruct_content:
return system_design_msg.instruct_content.dict().get("Python package name").strip().strip("'").strip("\"")
return system_design_msg.instruct_content.dict().get("Python package name").strip().strip("'").strip('"')
return CodeParser.parse_str(block="Python package name", text=system_design_msg.content)
def get_workspace(self) -> Path:
msg = self._rc.memory.get_by_action(WriteDesign)[-1]
if not msg:
return WORKSPACE_ROOT / 'src'
return WORKSPACE_ROOT / "src"
workspace = self.parse_workspace(msg)
# Codes are written in workspace/{package_name}/{package_name}
return WORKSPACE_ROOT / workspace / workspace
@ -111,7 +113,7 @@ class Engineer(Role):
def write_file(self, filename: str, code: str):
workspace = self.get_workspace()
filename = filename.replace('"', '').replace('\n', '')
filename = filename.replace('"', "").replace("\n", "")
file = workspace / filename
file.parent.mkdir(parents=True, exist_ok=True)
file.write_text(code)
@ -127,8 +129,7 @@ class Engineer(Role):
todo_coros = []
for todo in self.todos:
todo_coro = WriteCode().run(
context=self._rc.memory.get_by_actions([WriteTasks, WriteDesign]),
filename=todo
context=self._rc.memory.get_by_actions([WriteTasks, WriteDesign]), filename=todo
)
todo_coros.append(todo_coro)
@ -142,17 +143,14 @@ class Engineer(Role):
self._rc.memory.add(msg)
del self.todos[0]
logger.info(f'Done {self.get_workspace()} generating.')
logger.info(f"Done {self.get_workspace()} generating.")
msg = Message(content="all done.", role=self.profile, cause_by=type(self._rc.todo))
return msg
async def _act_sp(self) -> Message:
code_msg_all = [] # gather all code info, will pass to qa_engineer for tests later
code_msg_all = [] # gather all code info, will pass to qa_engineer for tests later
for todo in self.todos:
code = await WriteCode().run(
context=self._rc.history,
filename=todo
)
code = await WriteCode().run(context=self._rc.history, filename=todo)
# logger.info(todo)
# logger.info(code_rsp)
# code = self.parse_code(code_rsp)
@ -163,17 +161,14 @@ class Engineer(Role):
code_msg = todo + FILENAME_CODE_SEP + str(file_path)
code_msg_all.append(code_msg)
logger.info(f'Done {self.get_workspace()} generating.')
logger.info(f"Done {self.get_workspace()} generating.")
msg = Message(
content=MSG_SEP.join(code_msg_all),
role=self.profile,
cause_by=type(self._rc.todo),
send_to="QaEngineer"
content=MSG_SEP.join(code_msg_all), role=self.profile, cause_by=type(self._rc.todo), send_to="QaEngineer"
)
return msg
async def _act_sp_precision(self) -> Message:
code_msg_all = [] # gather all code info, will pass to qa_engineer for tests later
code_msg_all = [] # gather all code info, will pass to qa_engineer for tests later
for todo in self.todos:
"""
# Select essential information from the historical data to reduce the length of the prompt (summarized from human experience):
@ -188,18 +183,11 @@ class Engineer(Role):
context.append(m.content)
context_str = "\n".join(context)
# Write code
code = await WriteCode().run(
context=context_str,
filename=todo
)
code = await WriteCode().run(context=context_str, filename=todo)
# Code review
if self.use_code_review:
try:
rewrite_code = await WriteCodeReview().run(
context=context_str,
code=code,
filename=todo
)
rewrite_code = await WriteCodeReview().run(context=context_str, code=code, filename=todo)
code = rewrite_code
except Exception as e:
logger.error("code review failed!", e)
@ -211,12 +199,9 @@ class Engineer(Role):
code_msg = todo + FILENAME_CODE_SEP + str(file_path)
code_msg_all.append(code_msg)
logger.info(f'Done {self.get_workspace()} generating.')
logger.info(f"Done {self.get_workspace()} generating.")
msg = Message(
content=MSG_SEP.join(code_msg_all),
role=self.profile,
cause_by=type(self._rc.todo),
send_to="QaEngineer"
content=MSG_SEP.join(code_msg_all), role=self.profile, cause_by=type(self._rc.todo), send_to="QaEngineer"
)
return msg
@ -224,4 +209,4 @@ class Engineer(Role):
"""Determines the mode of action based on whether code review is used."""
if self.use_code_review:
return await self._act_sp_precision()
return await self._act_sp()
return await self._act_sp()

View file

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# @Date : 2023/9/23 14:27
# @Author : stellahong (stellahong@fuzhi.ai)
# @Desc :

View file

@ -0,0 +1,238 @@
# -*- coding: utf-8 -*-
# @Date : 2023/9/23 12:45
# @Author : stellahong (stellahong@fuzhi.ai)
# @Desc :
from metagpt.logs import logger
from metagpt.roles.minecraft.minecraft_base import Minecraft as Base
from metagpt.schema import Message, HumanMessage, SystemMessage
from metagpt.roles.minecraft.minecraft_base import agent_registry
from metagpt.actions.minecraft.generate_actions import GenerateActionCode
from metagpt.actions.minecraft.manage_skills import (
GenerateSkillDescription,
RetrieveSkills,
AddNewSkills,
)
import metagpt.utils.minecraft as utils
from metagpt.config import CONFIG
from metagpt.actions.minecraft.control_primitives_context import (
load_skills_code_context,
)
@agent_registry.register("action_developer")
class ActionDeveloper(Base):
"""
iterative prompting mechanism in paper.
generate action code based on environment observation and plan, as well as skills retrieval results
"""
def __init__(
self,
name: str = "Bob",
profile: str = "Generate code for specified tasks",
goal: str = "Produce accurate and efficient code solutions in Python and JavaScript",
constraints: str = "Adhere to coding best practices and style guidelines",
) -> None:
super().__init__(name, profile, goal, constraints)
# Initialize actions specific to the Action role
self._init_actions([GenerateActionCode])
# Set events or actions the ActionAgent should watch or be aware of
# 需要根据events进行自己chest_observation的更新
self._watch([RetrieveSkills])
def render_system_message(self, skills=[], *args, **kwargs):
"""
According to basic skills context files to genenarate js skill codes.
Refer to @ https://github.com/MineDojo/Voyager/blob/main/voyager/agents/action.py
"""
action_template = utils.load_prompt("action_template")
base_skills = [
"exploreUntil",
"mineBlock",
"craftItem",
"placeItem",
"smeltItem",
"killMob",
]
if not CONFIG.openai_api_model == "gpt-3.5-turbo":
base_skills += [
"useChest",
"mineflayer",
]
programs = "\n\n".join(load_skills_code_context(base_skills) + skills)
response_format = utils.load_prompt("action_response_format")
system_action_prompt = action_template.format(
programs=programs, response_format=response_format
)
system_action_message = SystemMessage(content=system_action_prompt)
assert isinstance(system_action_message, SystemMessage)
return system_action_message
def render_human_message(
self, events, code="", task="", context="", critique="", *args, **kwargs
):
"""
Integrate observation about the environment(especially events), add to HumanMessage.
Refer to @ https://github.com/MineDojo/Voyager/blob/main/voyager/agents/action.py
"""
# Deal with events info
chat_messages = []
error_messages = []
# damage_messages = [] # TODO: try to add damage_messages into prompt later
assert events[-1][0] == "observe", "Last event must be observe"
for i, (event_type, event) in enumerate(events):
if event_type == "onChat":
chat_messages.append(event["onChat"])
elif event_type == "onError":
error_messages.append(event["onError"])
elif event_type == "observe":
biome = event["status"]["biome"]
time_of_day = event["status"]["timeOfDay"]
voxels = event["voxels"]
entities = event["status"]["entities"]
health = event["status"]["health"]
hunger = event["status"]["food"]
position = event["status"]["position"]
equipment = event["status"]["equipment"]
inventory_used = event["status"]["inventoryUsed"]
inventory = event["inventory"]
assert i == len(events) - 1, "observe must be the last event"
# Collect all the environment information into a str: observation
observation = ""
observation = (
f"Code from the last round:\n{code or 'No code in the first round'}\n\n"
)
if error_messages:
error = "\n".join(error_messages)
observation += f"Execution error:\n{error}\n\n"
else:
observation += f"Execution error: No error\n\n"
if chat_messages:
chat_log = "\n".join(chat_messages)
observation += f"Chat log: {chat_log}\n\n"
else:
observation += f"Chat log: None\n\n"
observation += f"Biome: {biome}\n\n"
observation += f"Time: {time_of_day}\n\n"
observation += f"Nearby blocks: {', '.join(voxels) if voxels else 'None'}\n\n"
if entities:
nearby_entities = [
k for k, v in sorted(entities.items(), key=lambda x: x[1])
]
observation += f"Nearby entities (nearest to farthest): {', '.join(nearby_entities)}\n\n"
else:
observation += f"Nearby entities (nearest to farthest): None\n\n"
observation += f"Health: {health:.1f}/20\n\n"
observation += f"Hunger: {hunger:.1f}/20\n\n"
observation += f"Position: x={position['x']:.1f}, y={position['y']:.1f}, z={position['z']:.1f}\n\n"
observation += f"Equipment: {equipment}\n\n"
observation += f"Inventory ({inventory_used}/36): {'Empty' if not inventory else ', '.join(inventory)}\n\n"
if not (
task == "Place and deposit useless items into a chest"
or task.startswith("Deposit useless items into the chest at")
):
observation += self.game_memory.chest_observation
observation += f"Task: {task}\n\n"
observation += f"Context: {context or 'None'}\n\n"
observation += f"Critique: {critique or 'None'}\n\n"
return HumanMessage(content=observation)
def encapsule_message(
self,
events,
code="",
task="",
context="",
critique="",
skills=[],
*args,
**kwargs,
):
system_message = self.render_system_message(skills=skills)
human_message = self.render_human_message(
events=events, code=code, task=task, context=context, critique=critique
)
return {
"system_msg": [system_message.content],
"human_msg": human_message.content,
}
async def _observe(self) -> int:
await super()._observe()
for msg in self._rc.news:
logger.info(msg.send_to == self._setting.name)
self._rc.news = [
msg for msg in self._rc.news if msg.send_to == self._setting.name
] # only relevant msgs count as observed news
logger.info(len(self._rc.news))
return len(self._rc.news)
async def generate_action_code(self, human_msg, system_msg, *args, **kwargs):
code, program_name = await GenerateActionCode().run(
human_msg, system_msg, *args, **kwargs
)
# logger.warning(type(code))
# logger.info(f"Code is Here:{code}")
self.perform_game_info_callback(code, self.game_memory.update_code)
self.perform_game_info_callback(
program_name, self.game_memory.update_program_name
)
msg = Message(
content=f"{code}",
instruct_content="generate_action_code",
role=self.profile,
)
# logger.info(msg)
return msg
async def _act(self) -> Message:
todo = self._rc.todo
logger.debug(f"Todo is {todo}")
self.maintain_actions(todo)
# 获取最新的游戏周边信息
events = await self._obtain_events()
self.perform_game_info_callback(events, self.game_memory.update_event)
context = self.game_memory.context
task = self.game_memory.current_task
code = self.game_memory.code
critique = self.game_memory.critique
retrieve_skills = self.game_memory.retrieve_skills
message = self.encapsule_message(
events=events,
code=code,
task=task,
context=context,
critique=critique,
skills=retrieve_skills,
)
logger.info(todo)
handler_map = {
GenerateActionCode: self.generate_action_code,
}
handler = handler_map.get(type(todo))
logger.info(handler)
if handler:
msg = await handler(**message)
msg.cause_by = type(todo)
msg.round_id = self.round_id
logger.info(msg.send_to)
self._publish_message(msg)
return msg
raise ValueError(f"Unknown todo type: {type(todo)}")

View file

@ -0,0 +1,163 @@
# -*- coding: utf-8 -*-
# @Date : 2023/9/23 12:46
# @Author : stellahong (stellahong@fuzhi.ai)
# @Desc :
from metagpt.roles.minecraft.minecraft_base import Minecraft as Base
from metagpt.actions.minecraft.generate_actions import GenerateActionCode
from metagpt.actions.minecraft.manage_skills import AddNewSkills
from metagpt.roles.minecraft.minecraft_base import agent_registry
from metagpt.actions.minecraft.review_task import VerifyTask
from metagpt.utils.minecraft import load_prompt
from metagpt.schema import Message, HumanMessage, SystemMessage
from metagpt.logs import logger
@agent_registry.register("critic_agent")
class CriticReviewer(Base):
"""
self-verification
"""
def __init__(
self,
name: str = "Simon",
profile: str = "Task Reviewer",
goal: str = "To provide insightful and constructive feedback on a wide range of content types, helping creators improve their work and maintaining high-quality standards.",
constraints: str = "Adherence to ethical reviewing practices, respectful communication, and confidentiality of sensitive information.",
) -> None:
super().__init__(name, profile, goal, constraints)
# Initialize actions specific to the CriticReviewer role
self._init_actions([VerifyTask])
# Set events or actions the CriticReviewer should watch or be aware of
# 需要获取最新的events来进行评估
self._watch([GenerateActionCode, AddNewSkills])
def render_system_message(self):
system_message = SystemMessage(content=load_prompt("critic"))
return system_message
def render_human_message(self, events, task, context, chest_observation):
assert events[-1][0] == "observe", "Last event must be observe"
biome = events[-1][1]["status"]["biome"]
time_of_day = events[-1][1]["status"]["timeOfDay"]
voxels = events[-1][1]["voxels"]
health = events[-1][1]["status"]["health"]
hunger = events[-1][1]["status"]["food"]
position = events[-1][1]["status"]["position"]
equipment = events[-1][1]["status"]["equipment"]
inventory_used = events[-1][1]["status"]["inventoryUsed"]
inventory = events[-1][1]["inventory"]
for i, (event_type, event) in enumerate(events):
if event_type == "onError":
logger.info(
f"\033[31mCritic Agent: Error occurs {event['onError']}\033[0m"
)
# return None
return HumanMessage(content="")
observation = ""
observation += f"Biome: {biome}\n\n"
observation += f"Time: {time_of_day}\n\n"
if voxels:
observation += f"Nearby blocks: {', '.join(voxels)}\n\n"
else:
observation += f"Nearby blocks: None\n\n"
observation += f"Health: {health:.1f}/20\n\n"
observation += f"Hunger: {hunger:.1f}/20\n\n"
observation += f"Position: x={position['x']:.1f}, y={position['y']:.1f}, z={position['z']:.1f}\n\n"
observation += f"Equipment: {equipment}\n\n"
if inventory:
observation += f"Inventory ({inventory_used}/36): {inventory}\n\n"
else:
observation += f"Inventory ({inventory_used}/36): Empty\n\n"
observation += chest_observation
observation += f"Task: {task}\n\n"
if context:
observation += f"Context: {context}\n\n"
else:
observation += f"Context: None\n\n"
logger.info(f"****Critic Agent human message****\n: {observation}")
return HumanMessage(content=observation)
def encapsule_message(
self,
events,
task,
context,
chest_observation,
*args,
**kwargs,
):
system_message = self.render_system_message()
human_message = self.render_human_message(
events=events,
task=task,
context=context,
chest_observation=chest_observation,
)
return {
"system_msg": [system_message.content],
"human_msg": human_message.content,
}
async def verify_task(self, human_msg, system_msg, *args, **kwargs):
success, critique = await VerifyTask().run(human_msg, system_msg, max_retries=5)
self.perform_game_info_callback(
success, self.game_memory.update_exploration_progress
)
return Message(
content=f"{critique}",
instruct_content="verify_task",
role=self.profile,
send_to=agent_registry.entries["skill_manager"]()._setting.name,
) # addnewskill
# TODO:if not success
async def _act(self) -> Message:
todo = self._rc.todo
logger.debug(f"Todo is {todo}")
self.maintain_actions(todo)
# 获取最新的游戏周边信息
events = await self._obtain_events()
self.perform_game_info_callback(
events, self.game_memory.update_event
) # update chest_memory / chest observation
context = self.game_memory.context
task = self.game_memory.current_task
chest_observation = self.game_memory.chest_observation
message = self.encapsule_message(
events=events,
task=task,
context=context,
chest_observation=chest_observation,
)
logger.info(todo)
handler_map = {
VerifyTask: self.verify_task,
}
handler = handler_map.get(type(todo))
logger.info(handler)
if handler:
msg = await handler(**message)
msg.cause_by = type(todo)
msg.round_id = self.round_id
logger.info(msg.send_to)
self._publish_message(msg)
return msg
raise ValueError(f"Unknown todo type: {type(todo)}")

View file

@ -0,0 +1,347 @@
# -*- coding: utf-8 -*-
# @Date : 2023/9/23 12:45
# @Author : stellahong (stellahong@fuzhi.ai)
# @Desc :
import random
import json
from metagpt.logs import logger
from metagpt.schema import Message, HumanMessage, SystemMessage
from metagpt.roles.minecraft.minecraft_base import Minecraft as Base
from metagpt.actions.minecraft.design_curriculumn import DesignCurriculum, DesignTask
from metagpt.actions.minecraft.player_action import PlayerActions
from metagpt.utils.minecraft import load_prompt
from metagpt.const import CKPT_DIR, CURRICULUM_OB
class CurriculumDesigner(Base):
"""
CurriculumDesigner is the automatic curriculum in paper, refer to the code voyager/agents/curriculum.py
"""
def __init__(
self,
name: str = "David",
profile: str = "Expertise in minecraft task design and curriculum development.",
goal: str = " Collect and integrate learner feedback to improve and refine educational content and pathways",
constraints: str = "Limited budget and resources for the development of educational content and technology tools.",
) -> None:
super().__init__(name, profile, goal, constraints)
# Initialize actions specific to the Action role
self._init_actions([DesignTask, DesignCurriculum])
# Set events or actions the ActionAgent should watch or be aware of
self._watch([PlayerActions, DesignTask])
def render_curriculum_observation(self, *, events, chest_observation):
"""
Returns: observation for curriculum
Refer to @ https://github.com/MineDojo/Voyager/blob/main/voyager/agents/curriculum.py
"""
assert events[-1][0] == "observe", "Last event must be observe"
event = events[-1][1]
biome = event["status"]["biome"]
time_of_day = event["status"]["timeOfDay"]
voxels = event["voxels"]
block_records = event["blockRecords"]
entities = event["status"]["entities"]
health = event["status"]["health"]
hunger = event["status"]["food"]
position = event["status"]["position"]
equipment = event["status"]["equipment"]
inventory_used = event["status"]["inventoryUsed"]
inventory = event["inventory"]
if not any(
"dirt" in block
or "log" in block
or "grass" in block
or "sand" in block
or "snow" in block
for block in voxels
):
biome = "underground"
other_blocks = ", ".join(
list(
set(block_records).difference(set(voxels).union(set(inventory.keys())))
)
)
other_blocks = other_blocks if other_blocks else "None"
nearby_entities = (
", ".join([k for k, v in sorted(entities.items(), key=lambda x: x[1])])
if entities
else "None"
)
completed_tasks = (
", ".join(self.game_memory.completed_tasks)
if self.game_memory.completed_tasks
else "None"
)
failed_tasks = (
", ".join(self.game_memory.failed_tasks)
if self.game_memory.failed_tasks
else "None"
)
# filter out optional inventory items if required
if (
self.game_memory.progress
< self.game_memory.warm_up["optional_inventory_items"]
):
inventory = {
k: v
for k, v in inventory.items()
if self.game_memory.core_inv_items_regex.search(k) is not None
}
observation = {
"context": "",
"biome": f"Biome: {biome}\n\n",
"time": f"Time: {time_of_day}\n\n",
"nearby_blocks": f"Nearby blocks: {', '.join(voxels) if voxels else 'None'}\n\n",
"other_blocks": f"Other blocks that are recently seen: {other_blocks}\n\n",
"nearby_entities": f"Nearby entities: {nearby_entities}\n\n",
"health": f"Health: {health:.1f}/20\n\n",
"hunger": f"Hunger: {hunger:.1f}/20\n\n",
"position": f"Position: x={position['x']:.1f}, y={position['y']:.1f}, z={position['z']:.1f}\n\n",
"equipment": f"Equipment: {equipment}\n\n",
"inventory": f"Inventory ({inventory_used}/36): {inventory if inventory else 'Empty'}\n\n",
"chests": chest_observation,
"completed_tasks": f"Completed tasks so far: {completed_tasks}\n\n",
"failed_tasks": f"Failed tasks that are too hard: {failed_tasks}\n\n",
}
return observation
# --------------------------------Design Task Prepare---------------------------------------
def render_design_task_human_message(
self, events, chest_observation, *args, **kwargs
):
"""
Returns: observation for curriculum
Refer to @ https://github.com/MineDojo/Voyager/blob/main/voyager/agents/curriculum.py
"""
content = ""
warm_up = self.game_memory.mf_instance.warm_up
observation = self.render_curriculum_observation(
events=events, chest_observation=chest_observation
)
if self.game_memory.progress >= warm_up["context"]:
questions, answers = DesignCurriculum.generate_qa(
events=events, chest_observation=chest_observation
)
i = 1
for question, answer in zip(questions, answers):
if "Answer: Unknown" in answer or "language model" in answer:
continue
observation["context"] += f"Question {i}: {question}\n"
observation["context"] += f"{answer}\n\n"
i += 1
if i > 5:
break
for key in CURRICULUM_OB:
if self.game_memory.progress >= warm_up[key]:
if warm_up[key] != 0:
should_include = random.random() < 0.8
else:
should_include = True
if should_include:
content += observation[key]
logger.info(f"Curriculum Agent human message\n{content}")
return HumanMessage(content=content)
def render_design_task_system_message(self, *args, **kwargs):
return SystemMessage(content=load_prompt("curriculum"))
def encapsule_design_task_message(self, events, chest_observation, *args, **kwargs):
human_msg = self.render_design_task_human_message(
events=events, chest_observation=chest_observation, *args, **kwargs
)
system_msg = self.render_design_task_system_message(*args, **kwargs)
return {"system_msg": [system_msg.content], "human_msg": human_msg.content}
def generate_task_if_inventory_full(self, events, chest_observation):
"""
TODO: Try if this could be done with prompt
Returns: Task When inventory is almost full
"""
if chest_observation != "Chests: None\n\n":
chests = chest_observation[8:-2].split("\n")
for chest in chests:
content = chest.split(":")[1]
if content == " Unknown items inside" or content == " Empty":
position = chest.split(":")[0]
task = f"Deposit useless items into the chest at {position}"
return task
if "chest" in events[-1][1]["inventory"]:
task = "Place a chest"
else:
task = "Craft 1 chest"
return task
# -----------------------------------------------------------------------------------------
# --------------------------------Design Curriculum Prepare--------------------------------
def render_design_curriculum_system_message(self, *args, **kwargs):
return SystemMessage(content=load_prompt("curriculum_qa_step1_ask_questions"))
def render_design_curriculum_human_message(
self, events, chest_observation, *args, **kwargs
):
observation = self.render_curriculum_observation(
events=events, chest_observation=chest_observation
)
content = ""
for key in CURRICULUM_OB:
content += observation[key]
return HumanMessage(content=content)
def encapsule_design_curriculum_message(
self, events, chest_observation, *args, **kwargs
):
human_msg = self.render_design_curriculum_human_message(
events=events, chest_observation=chest_observation, *args, **kwargs
)
system_msg = self.render_design_curriculum_system_message(*args, **kwargs)
return {"system_msg": [system_msg.content], "human_msg": human_msg.content}
def generate_context_if_inventory_full(self, events, chest_observation):
"""
TODO: Try if this could be done with prompt
Returns: Context When inventory is almost full
"""
inventoryUsed = events[-1][1]["status"]["inventoryUsed"]
if chest_observation != "Chests: None\n\n":
chests = chest_observation[8:-2].split("\n")
for chest in chests:
content = chest.split(":")[1]
if content == " Unknown items inside" or content == " Empty":
context = (
f"Your inventory have {inventoryUsed} occupied slots before depositing. "
"After depositing, your inventory should only have 20 occupied slots. "
"You should deposit useless items such as andesite, dirt, cobblestone, etc. "
"Also, you can deposit low-level tools, "
"For example, if you have a stone pickaxe, you can deposit a wooden pickaxe. "
"Make sure the list of useless items are in your inventory "
"(do not list items already in the chest), "
"You can use bot.inventoryUsed() to check how many inventory slots are used."
)
return context
if "chest" in events[-1][1]["inventory"]:
context = (
f"You have a chest in inventory, place it around you. "
f"If chests is not None, or nearby blocks contains chest, this task is success."
)
else:
context = "Craft 1 chest with 8 planks of any kind of wood."
return context
# -----------------------------------------------------------------------------------------
async def handle_task_design(self, human_msg, system_msg, *args, **kwargs):
"""
Args:
human_msg:
system_msg:
*args:
**kwargs:
Returns:
"""
events = self.game_memory.event
chest_observation = self.game_memory.chest_observation
inventoryUsed = events[-1][1]["status"]["inventoryUsed"]
if self.game_memory.progress == 0:
task = self.game_memory.current_task
elif inventoryUsed >= 33:
task = self.generate_task_if_inventory_full(
self, events=events, chest_observation=chest_observation
)
else:
task = await DesignTask().run(human_msg, system_msg, *args, **kwargs)
logger.info(f"Handle_task_design result is Here: {task}")
self.perform_game_info_callback(task, self.game_memory.update_task)
return Message(
content=f"{task}", instruct_content="task_design", role=self.profile
)
async def handle_curriculum_design(self, human_msg, system_msg, *args, **kwargs):
"""
refer to the context generation in voyager
Args:
human_msg:
system_msg:
*args:
**kwargs:
Returns:
"""
events = self.game_memory.event
chest_observation = self.game_memory.chest_observation
inventoryUsed = events[-1][1]["status"]["inventoryUsed"]
task = self.game_memory.current_task
if self.game_memory.progress == 0:
context = self.game_memory.context
elif inventoryUsed >= 33:
context = self.generate_context_if_inventory_full(
self, events=events, chest_observation=chest_observation
)
else:
context = await DesignCurriculum().run(
task, human_msg, system_msg, *args, **kwargs
)
self.perform_game_info_callback(context, self.game_memory.update_context)
return Message(
content=f"{context}",
instruct_content="curriculum_design",
role=self.profile,
)
async def _act(self) -> Message:
todo = self._rc.todo
logger.debug(f"Todo is {todo}")
self.maintain_actions(todo)
# 获取最新的游戏周边环境信息
events = await self._obtain_events()
self.perform_game_info_callback(events, self.game_memory.update_event)
chest_observation = self.game_memory.chest_observation
DesignCurriculum.set_qa_cache(self.game_memory.qa_cache)
# msg = self._rc.memory.get(k=1)[0]
# query = msg.content
design_task_message = self.encapsule_design_task_message(
events, chest_observation
)
design_curriculum_message = self.encapsule_design_curriculum_message(
events, chest_observation
)
handler_map = {
DesignTask: self.handle_task_design,
DesignCurriculum: self.handle_curriculum_design,
}
handler = handler_map.get(type(todo))
if handler:
if type(todo) == "DesignTask":
msg = await handler(**design_task_message)
else:
msg = await handler(**design_curriculum_message)
msg.cause_by = type(todo)
msg.round_id = self.round_id
self._publish_message(msg)
return msg
raise ValueError(f"Unknown todo type: {type(todo)}")

Some files were not shown because too many files have changed in this diff Show more