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