mirror of
https://github.com/kolbytn/mindcraft.git
synced 2025-03-28 14:56:24 +01:00
fix: remove novita profile
This commit is contained in:
commit
dc1a547f92
30 changed files with 698 additions and 356 deletions
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -5,4 +5,10 @@ code_records/
|
|||
scratch.js
|
||||
bots/**/action-code/**
|
||||
bots/**/
|
||||
keys.json
|
||||
keys.json
|
||||
services/viaproxy/jars/**
|
||||
services/viaproxy/logs/**
|
||||
services/viaproxy/plugins/**
|
||||
services/viaproxy/ViaLoader/**
|
||||
services/viaproxy/saves.json
|
||||
services/viaproxy/viaproxy.yml
|
||||
|
|
27
FAQ.md
Normal file
27
FAQ.md
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Common Issues
|
||||
- `Error: connect ECONNREFUSED`: Minecraft refused to connect with mindcraft program. Most likely due to:
|
||||
- you have not opened your game to LAN in game settings
|
||||
- your LAN port is incorrect, make sure the one you enter in game is the same as specified in `settings.js`
|
||||
- you have the wrong version of minecraft, make sure your MC version is the same as specified in `settings.js`
|
||||
|
||||
- `ERR_MODULE_NOT_FOUND`: You are missing an npm package. run `npm install`
|
||||
|
||||
- Many issues are caused by out-of-date node module patches, especially after updates. A catch-all is to delete the `node_modules` folder, and run `npm install`
|
||||
|
||||
- `My brain disconnected, try again`: Something is wrong with the LLM api. You may have the wrong API key, exceeded your rate limits, or other. Check the program outputs for more details.
|
||||
|
||||
- `I'm stuck!` or other issues with constantly getting stuck:
|
||||
- Mineflayer's pathfinder is imperfect. We have improved upon it with patches, but these might not have been applied properly. Make sure your code is up to date with main, delete the `node_modules` folder, and run `npm install`
|
||||
- The bot will still get stuck occasionally, but not constantly.
|
||||
|
||||
- `Why I added the api key but still prompted that the key can't be found?`
|
||||
- Possible reason 1: Did not modify keys.example.json to keys.json.
|
||||
- Possible reason 2: If you use vscode to edit, you need to `ctrl+s` to save the file for the changes to take effect.
|
||||
- Possible reason 3: Not setting the code path correctly in setting.js, use andy.js by default.
|
||||
|
||||
# Common Questions
|
||||
- Mod Support? Mindcraft only supports client-side mods like optifine and sodium, though they can be tricky to set up. Mods that change minecraft game mechanics are not supported.
|
||||
|
||||
- Texture Packs? Apparently these cause issues and refuse to connect. Not sure why
|
||||
|
||||
- Baritone? Baritone is a mod that is completely different from mineflayer. There is currently no easy way to integrate the two programs.
|
103
README.md
103
README.md
|
@ -1,25 +1,47 @@
|
|||
# Mindcraft 🧠⛏️
|
||||
|
||||
Crafting minds for Minecraft with Language Models and Mineflayer!
|
||||
Crafting minds for Minecraft with LLMs and Mineflayer!
|
||||
|
||||
[FAQ](https://github.com/kolbytn/mindcraft/blob/main/FAQ.md)
|
||||
|
||||
[Discord Support](https://discord.gg/ZsrAAByEnr)
|
||||
|
||||
[Join the discord for support!](https://discord.gg/ZsrAAByEnr)
|
||||
|
||||
#### ‼️Warning‼️
|
||||
|
||||
This project allows an AI model to write/execute code on your computer that may be insecure, dangerous, and vulnerable to injection attacks on public servers. Code writing is disabled by default, you can enable it by setting `allow_insecure_coding` to `true` in `settings.js`. Enable only on local or private servers, **never** on public servers. Ye be warned.
|
||||
Do not connect this bot to public servers with coding enabled. This project allows an LLM to write/execute code on your computer. While the code is sandboxed, it is still vulnerable to injection attacks on public servers. Code writing is disabled by default, you can enable it by setting `allow_insecure_coding` to `true` in `settings.js`. We strongly recommend running with additional layers of security such as docker containers. Ye be warned.
|
||||
|
||||
## Requirements
|
||||
|
||||
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-bedrock-edition-pc) (up to v1.21.1)
|
||||
- [Node.js](https://nodejs.org/) (at least v14)
|
||||
- One of these: [OpenAI API Key](https://openai.com/blog/openai-api) | [Gemini API Key](https://aistudio.google.com/app/apikey) |[Anthropic API Key](https://docs.anthropic.com/claude/docs/getting-access-to-claude) | [Replicate API Key](https://replicate.com/) | [Hugging Face API Key](https://huggingface.co/) | [Groq API Key](https://console.groq.com/keys) | [Ollama Installed](https://ollama.com/download) | [Novita AI API Key](https://novita.ai/settings?utm_source=github_mindcraft&utm_medium=github_readme&utm_campaign=link#key-management)
|
||||
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-bedrock-edition-pc) (up to v1.21.1, recommend v1.20.4)
|
||||
- [Node.js Installed](https://nodejs.org/) (at least v14)
|
||||
- One of these: [OpenAI API Key](https://openai.com/blog/openai-api) | [Gemini API Key](https://aistudio.google.com/app/apikey) | [Anthropic API Key](https://docs.anthropic.com/claude/docs/getting-access-to-claude) | [Replicate API Key](https://replicate.com/) | [Hugging Face API Key](https://huggingface.co/) | [Groq API Key](https://console.groq.com/keys) | [Ollama Installed](https://ollama.com/download). | [Qwen API Key [Intl.]](https://www.alibabacloud.com/help/en/model-studio/developer-reference/get-api-key)/[[cn]](https://help.aliyun.com/zh/model-studio/getting-started/first-api-call-to-qwen?) | [Novita AI API Key](https://novita.ai/settings?utm_source=github_mindcraft&utm_medium=github_readme&utm_campaign=link#key-management) |
|
||||
|
||||
## Installation
|
||||
## Install and Run
|
||||
|
||||
1. Make sure you have the requirements above.
|
||||
|
||||
2. Clone or download this repository (big green button)
|
||||
|
||||
3. Rename `keys.example.json` to `keys.json` and fill in your API keys (you only need one). The desired model is set in `andy.json` or other profiles. For other models refer to the table below.
|
||||
|
||||
4. In terminal/command prompt, run `npm install` from the installed directory
|
||||
|
||||
5. Start a minecraft world and open it to LAN on localhost port `55916`
|
||||
|
||||
6. Run `node main.js` from the installed directory
|
||||
|
||||
If you encounter issues, check the [FAQ](https://github.com/kolbytn/mindcraft/blob/main/FAQ.md) or find support on [discord](https://discord.gg/ZsrAAByEnr). We are currently not very responsive to github issues.
|
||||
|
||||
## Customization
|
||||
|
||||
You can configure project details in `settings.js`. [See file.](settings.js)
|
||||
|
||||
You can configure the agent's name, model, and prompts in their profile like `andy.json`.
|
||||
|
||||
Rename `keys.example.json` to `keys.json` and fill in your API keys, and you can set the desired model in `andy.json` or other profiles.
|
||||
| API | Config Variable | Example Model name | Docs |
|
||||
|------|------|------|------|
|
||||
| OpenAI | `OPENAI_API_KEY` | `gpt-3.5-turbo` | [docs](https://platform.openai.com/docs/models) | (optionally add `OPENAI_ORG_ID`)
|
||||
| OpenAI | `OPENAI_API_KEY` | `gpt-4o-mini` | [docs](https://platform.openai.com/docs/models) |
|
||||
| Google | `GEMINI_API_KEY` | `gemini-pro` | [docs](https://ai.google.dev/gemini-api/docs/models/gemini) |
|
||||
| Anthropic | `ANTHROPIC_API_KEY` | `claude-3-haiku-20240307` | [docs](https://docs.anthropic.com/claude/docs/models-overview) |
|
||||
| Replicate | `REPLICATE_API_KEY` | `meta/meta-llama-3-70b-instruct` | [docs](https://replicate.com/collections/language-models) |
|
||||
|
@ -27,38 +49,45 @@ Rename `keys.example.json` to `keys.json` and fill in your API keys, and you can
|
|||
| Groq | `GROQCLOUD_API_KEY` | `groq/mixtral-8x7b-32768` | [docs](https://console.groq.com/docs/models) |
|
||||
| Hugging Face | `HUGGINGFACE_API_KEY` | `huggingface/mistralai/Mistral-Nemo-Instruct-2407` | [docs](https://huggingface.co/models) |
|
||||
| Novita AI | `NOVITA_API_KEY` | `gryphe/mythomax-l2-13b` | [docs](https://novita.ai/model-api/product/llm-api?utm_source=github_mindcraft&utm_medium=github_readme&utm_campaign=link) |
|
||||
| Qwen | `QWEN_API_KEY` | `qwen-max` | [Intl.](https://www.alibabacloud.com/help/en/model-studio/developer-reference/use-qwen-by-calling-api)/[cn](https://help.aliyun.com/zh/model-studio/getting-started/models) |
|
||||
|
||||
If you use Ollama, to install the models used by default (generation and embedding), execute the following terminal command:
|
||||
`ollama pull llama3 && ollama pull nomic-embed-text`
|
||||
|
||||
Then, clone/download this repository
|
||||
|
||||
Run `npm install` from the installed directory
|
||||
|
||||
Install the minecraft version specified in `settings.js`, currently supports up to 1.21.1
|
||||
|
||||
### Running Locally
|
||||
|
||||
Start a minecraft world and open it to LAN on localhost port `55916`
|
||||
|
||||
Run `node main.js`
|
||||
|
||||
You can configure the agent's name, model, and prompts in their profile like `andy.json`.
|
||||
|
||||
You can configure project details in `settings.js`. [See file for more details](settings.js)
|
||||
|
||||
### Online Servers
|
||||
To connect to online servers your bot will need an official Microsoft/Minecraft account. You can use your own personal one, but will need another account if you want to connect with it. Here are example settings for this:
|
||||
```
|
||||
## Online Servers
|
||||
To connect to online servers your bot will need an official Microsoft/Minecraft account. You can use your own personal one, but will need another account if you want to connect too and play with it. To connect, change these lines in `settings.js`:
|
||||
```javascript
|
||||
"host": "111.222.333.444",
|
||||
"port": 55920,
|
||||
"auth": "microsoft",
|
||||
|
||||
// rest is same...
|
||||
```
|
||||
‼️ Please make sure your bot's name in the profile.json matches the account name! Otherwise the bot will spam talk to itself.
|
||||
‼️ The bot's name in the profile.json must exactly match the Minecraft profile name! Otherwise the bot will spam talk to itself.
|
||||
|
||||
### Bot Profiles
|
||||
To use different accounts, Mindcraft will connect with the account that the Minecraft launcher is currently using. You can switch accounts in the launcer, then run `node main.js`, then switch to your main account after the bot has connected.
|
||||
|
||||
### Docker Container
|
||||
|
||||
If you intend to `allow_insecure_coding`, it is a good idea to run the app in a docker container to reduce risks of running unknown code. This is strongly recommended before connecting to remote servers.
|
||||
|
||||
```bash
|
||||
docker run -i -t --rm -v $(pwd):/app -w /app -p 3000-3003:3000-3003 node:latest node main.js
|
||||
```
|
||||
or simply
|
||||
```bash
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
When running in docker, if you want the bot to join your local minecraft server, you have to use a special host address `host.docker.internal` to call your localhost from inside your docker container. Put this into your [settings.js](settings.js):
|
||||
|
||||
```javascript
|
||||
"host": "host.docker.internal", // instead of "localhost", to join your local minecraft from inside the docker container
|
||||
```
|
||||
|
||||
To connect to an unsupported minecraft version, you can try to use [viaproxy](services/viaproxy/README.md)
|
||||
|
||||
## Bot Profiles
|
||||
|
||||
Bot profiles are json files (such as `andy.json`) that define:
|
||||
|
||||
|
@ -70,13 +99,15 @@ Bot profiles are json files (such as `andy.json`) that define:
|
|||
|
||||
By default, the program will use the profiles specified in `settings.js`. You can specify one or more agent profiles using the `--profiles` argument:
|
||||
|
||||
`node main.js --profiles ./profiles/andy.json ./profiles/jill.json`
|
||||
```bash
|
||||
node main.js --profiles ./profiles/andy.json ./profiles/jill.json
|
||||
```
|
||||
|
||||
### Model Specifications
|
||||
|
||||
LLM backends can be specified as simply as `"model": "gpt-3.5-turbo"`. However, for both the chat model and the embedding model, the bot profile can specify the below attributes:
|
||||
|
||||
```
|
||||
```json
|
||||
"model": {
|
||||
"api": "openai",
|
||||
"url": "https://api.openai.com/v1/",
|
||||
|
@ -85,7 +116,7 @@ LLM backends can be specified as simply as `"model": "gpt-3.5-turbo"`. However,
|
|||
"embedding": {
|
||||
"api": "openai",
|
||||
"url": "https://api.openai.com/v1/",
|
||||
"model": "text-embedding-ada-002"
|
||||
"model": "text-embedding-3-small"
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -95,15 +126,15 @@ If the embedding field is not specified, then it will use the default embedding
|
|||
|
||||
Thus, all the below specifications are equivalent to the above example:
|
||||
|
||||
```
|
||||
```json
|
||||
"model": "gpt-3.5-turbo"
|
||||
```
|
||||
```
|
||||
```json
|
||||
"model": {
|
||||
"api": "openai"
|
||||
}
|
||||
```
|
||||
```
|
||||
```json
|
||||
"model": "gpt-3.5-turbo",
|
||||
"embedding": "openai"
|
||||
```
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import * as skills from '../../../src/agent/library/skills.js';
|
||||
import * as world from '../../../src/agent/library/world.js';
|
||||
import Vec3 from 'vec3';
|
||||
(async (bot) => {
|
||||
|
||||
const log = skills.log;
|
||||
/* CODE HERE */
|
||||
log(bot, 'Code finished.');
|
||||
|
||||
export async function main(bot) {
|
||||
/* CODE HERE */
|
||||
log(bot, 'Code finished.');
|
||||
}
|
||||
})
|
20
docker-compose.yml
Normal file
20
docker-compose.yml
Normal file
|
@ -0,0 +1,20 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
image: node:latest
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- .:/app
|
||||
command: node main.js
|
||||
ports:
|
||||
- "3000-3003:3000-3003" # see the view from the camera mounted on your bot head: http://localhost:3000/
|
||||
|
||||
viaproxy: #use this service to connect to an unsupported minecraft server versions. more info: ./services/viaproxy/README.md
|
||||
image: ghcr.io/viaversion/viaproxy:latest
|
||||
volumes:
|
||||
- ./services/viaproxy:/app/run
|
||||
ports:
|
||||
- "25568:25568"
|
||||
profiles:
|
||||
- viaproxy
|
|
@ -5,5 +5,6 @@
|
|||
"ANTHROPIC_API_KEY": "",
|
||||
"REPLICATE_API_KEY": "",
|
||||
"GROQCLOUD_API_KEY": "",
|
||||
"HUGGINGFACE_API_KEY": ""
|
||||
"HUGGINGFACE_API_KEY": "",
|
||||
"QWEN_API_KEY":""
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
"prismarine-item": "^1.15.0",
|
||||
"prismarine-viewer": "^1.28.0",
|
||||
"replicate": "^0.29.4",
|
||||
"ses": "^1.9.1",
|
||||
"vec3": "^0.1.10",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
|
|
|
@ -1,33 +1,37 @@
|
|||
{
|
||||
"name": "novita",
|
||||
"name": "qwen",
|
||||
|
||||
"url": "https://api.novita.ai/v3/openai",
|
||||
"model": {
|
||||
"api": "qwen",
|
||||
"url": "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation",
|
||||
"model": "qwen-max"
|
||||
},
|
||||
|
||||
"model": "novita/meta-llama/llama-3.1-70b-instruct",
|
||||
"embedding": {
|
||||
"api": "qwen",
|
||||
"url": "https://dashscope.aliyuncs.com/api/v1/services/embeddings/text-embedding/text-embedding",
|
||||
"model": "text-embedding-v2"
|
||||
},
|
||||
|
||||
"max_tokens": 65536,
|
||||
"conversing": "You are a playful Minecraft bot named $NAME that can converse with players, see, move, mine, build, and interact with the world by using commands.\n$SELF_PROMPT Act human-like as if you were a typical Minecraft player, rather than an AI. Be very brief in your responses, don't apologize constantly, don't give instructions or make lists unless asked, and don't refuse requests. Don't pretend to act, use commands immediately when requested. Do NOT say this: 'Sure, I've stopped.', instead say this: 'Sure, I'll stop. !stop'. Do NOT say this: 'On my way! Give me a moment.', instead say this: 'On my way! !goToPlayer('playername', 3)'. This is extremely important to me, take a deep breath and have fun :)\nSummarized memory:'$MEMORY'\n$STATS\n$INVENTORY\n$COMMAND_DOCS\n$EXAMPLES\nConversation Begin:",
|
||||
|
||||
"embedding": "openai",
|
||||
"coding": "You are an intelligent mineflayer bot $NAME that plays minecraft by writing javascript codeblocks. Given the conversation between you and the user, use the provided skills and world functions to Only write a complete js codeblock that controls the mineflayer bot ``` // using this syntax ```. The code will be executed and you will recieve it's output. If you are satisfied with the response, respond without a codeblock in a conversational way. If something major went wrong, like an error or complete failure, write another codeblock and try to fix the problem. Minor mistakes are acceptable. Be maximally efficient, creative, and clear. Do not use commands !likeThis, only use codeblocks. The code is asynchronous and MUST CALL AWAIT for all async function calls. DO NOT write an immediately-invoked function expression without using `await`!! DO NOT WRITE LIKE THIS: ```(async () => {console.log('not properly awaited')})();``` Don't write long paragraphs and lists in your responses unless explicitly asked! Only summarize the code you write with a sentence or two when done. This is extremely important to me, think step-by-step, take a deep breath and good luck! \n$SELF_PROMPT\nSummarized memory:'$MEMORY'\n$STATS\n$INVENTORY\n$CODE_DOCS\n$EXAMPLES\nConversation:",
|
||||
|
||||
"conversing": "You are a playful Minecraft bot named $NAME that can converse with players, see, move, mine, build, and interact with the world by using commands.\n$SELF_PROMPT Act human-like as if you were a typical Minecraft player, rather than an AI. Be very brief in your responses, don't apologize constantly, don't give instructions or make lists unless asked, and don't refuse requests. Don't pretend to act, use commands immediately when requested. Do NOT say this: 'Sure, I've stopped.', instead say this: 'Sure, I'll stop. !stop'. Do NOT say this: 'On my way! Give me a moment.', instead say this: 'On my way! !goToPlayer('playername', 3)'. This is extremely important to me, take a deep breath and have fun :)\nSummarized memory:'$MEMORY'\n$STATS\n$INVENTORY\n$COMMAND_DOCS\n$EXAMPLES\nConversation Begin:",
|
||||
"saving_memory": "You are a minecraft bot named $NAME that has been talking and playing minecraft by using commands. Update your memory by summarizing the following conversation and your old memory in your next response. Prioritize preserving important facts, things you've learned, useful tips, and long term reminders. Do Not record stats, inventory, or docs! Only save transient information from your chat history. You're limited to 500 characters, so be extremely brief and minimize words. Compress useful information. \nOld Memory: '$MEMORY'\nRecent conversation: \n$TO_SUMMARIZE\nSummarize your old memory and recent conversation into a new memory, and respond only with the unwrapped memory text: ",
|
||||
|
||||
"coding": "You are an intelligent mineflayer bot $NAME that plays minecraft by writing javascript codeblocks. Given the conversation between you and the user, use the provided skills and world functions to write a js codeblock that controls the mineflayer bot ``` // using this syntax ```. The code will be executed and you will recieve it's output. If you are satisfied with the response, respond without a codeblock in a conversational way. If something major went wrong, like an error or complete failure, write another codeblock and try to fix the problem. Minor mistakes are acceptable. Be maximally efficient, creative, and clear. Do not use commands !likeThis, only use codeblocks. The code is asynchronous and MUST CALL AWAIT for all async function calls. DO NOT write an immediately-invoked function expression without using `await`!! Use double-quotes for strings, not singles. Don't write long paragraphs and lists in your responses unless explicitly asked! Only summarize the code you write with a sentence or two when done. This is extremely important to me, think step-by-step, take a deep breath and good luck! \n$SELF_PROMPT\nSummarized memory:'$MEMORY'\n$STATS\n$INVENTORY\n$CODE_DOCS\n$EXAMPLES\nConversation:",
|
||||
"modes": {
|
||||
"self_preservation": true,
|
||||
"unstuck": true,
|
||||
"cowardice": false,
|
||||
"self_defense": true,
|
||||
"hunting": true,
|
||||
"item_collecting": true,
|
||||
"torch_placing": true,
|
||||
"idle_staring": true,
|
||||
"cheat": false
|
||||
},
|
||||
|
||||
"saving_memory": "You are a minecraft bot named $NAME that has been talking and playing minecraft by using commands. Update your memory by summarizing the following conversation and your old memory in your next response. Prioritize preserving important facts, things you've learned, useful tips, and long term reminders. Do Not record stats, inventory, or docs! Only save transient information from your chat history. You're limited to 500 characters, so be extremely brief and minimize words. Compress useful information. \nOld Memory: '$MEMORY'\nRecent conversation: \n$TO_SUMMARIZE\nSummarize your old memory and recent conversation into a new memory, and respond only with the unwrapped memory text: ",
|
||||
|
||||
"modes": {
|
||||
"self_preservation": true,
|
||||
"unstuck": true,
|
||||
"cowardice": false,
|
||||
"self_defense": true,
|
||||
"hunting": true,
|
||||
"item_collecting": true,
|
||||
"torch_placing": true,
|
||||
"idle_staring": true,
|
||||
"cheat": false
|
||||
},
|
||||
|
||||
"conversation_examples": [
|
||||
"conversation_examples": [
|
||||
[
|
||||
{"role": "user", "content": "miner_32: Hey! What are you up to?"},
|
||||
{"role": "assistant", "content": "Nothing much miner_32, what do you need?"}
|
25
services/viaproxy/README.md
Normal file
25
services/viaproxy/README.md
Normal file
|
@ -0,0 +1,25 @@
|
|||
Use this service to connect your bot to an unsupported minecraft server versions.
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
docker-compose --profile viaproxy up
|
||||
```
|
||||
|
||||
After first start it will create config file `services/viaproxy/viaproxy.yml`.
|
||||
|
||||
Edit this file, and change your desired target `target-address`,
|
||||
|
||||
then point your `settings.js` `host` and `port` to viaproxy endpoint:
|
||||
|
||||
```javascript
|
||||
"host": "host.docker.internal",
|
||||
"port": 25568,
|
||||
```
|
||||
|
||||
This easily works with "offline" servers.
|
||||
|
||||
Connecting to "online" servers via viaproxy involves more effort: see `auth-method` in `services/viaproxy/viaproxy.yml` (TODO describe)
|
||||
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ export default
|
|||
// "./profiles/claude.json",
|
||||
// "./profiles/gemini.json",
|
||||
// "./profiles/llama.json",
|
||||
// "./profiles/novita.json",
|
||||
// "./profiles/qwen.json",
|
||||
|
||||
// using more than 1 profile requires you to /msg each bot indivually
|
||||
],
|
||||
|
|
148
src/agent/action_manager.js
Normal file
148
src/agent/action_manager.js
Normal file
|
@ -0,0 +1,148 @@
|
|||
export class ActionManager {
|
||||
constructor(agent) {
|
||||
this.agent = agent;
|
||||
this.executing = false;
|
||||
this.currentActionLabel = '';
|
||||
this.currentActionFn = null;
|
||||
this.timedout = false;
|
||||
this.resume_func = null;
|
||||
this.resume_name = '';
|
||||
}
|
||||
|
||||
async resumeAction(actionFn, timeout) {
|
||||
return this._executeResume(actionFn, timeout);
|
||||
}
|
||||
|
||||
async runAction(actionLabel, actionFn, { timeout, resume = false } = {}) {
|
||||
if (resume) {
|
||||
return this._executeResume(actionLabel, actionFn, timeout);
|
||||
} else {
|
||||
return this._executeAction(actionLabel, actionFn, timeout);
|
||||
}
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (!this.executing) return;
|
||||
const timeout = setTimeout(() => {
|
||||
this.agent.cleanKill('Code execution refused stop after 10 seconds. Killing process.');
|
||||
}, 10000);
|
||||
while (this.executing) {
|
||||
this.agent.requestInterrupt();
|
||||
console.log('waiting for code to finish executing...');
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
}
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
cancelResume() {
|
||||
this.resume_func = null;
|
||||
this.resume_name = null;
|
||||
}
|
||||
|
||||
async _executeResume(actionLabel = null, actionFn = null, timeout = 10) {
|
||||
const new_resume = actionFn != null;
|
||||
if (new_resume) { // start new resume
|
||||
this.resume_func = actionFn;
|
||||
assert(actionLabel != null, 'actionLabel is required for new resume');
|
||||
this.resume_name = actionLabel;
|
||||
}
|
||||
if (this.resume_func != null && this.agent.isIdle() && (!this.agent.self_prompter.on || new_resume)) {
|
||||
this.currentActionLabel = this.resume_name;
|
||||
let res = await this._executeAction(this.resume_name, this.resume_func, timeout);
|
||||
this.currentActionLabel = '';
|
||||
return res;
|
||||
} else {
|
||||
return { success: false, message: null, interrupted: false, timedout: false };
|
||||
}
|
||||
}
|
||||
|
||||
async _executeAction(actionLabel, actionFn, timeout = 10) {
|
||||
let TIMEOUT;
|
||||
try {
|
||||
console.log('executing code...\n');
|
||||
|
||||
// await current action to finish (executing=false), with 10 seconds timeout
|
||||
// also tell agent.bot to stop various actions
|
||||
if (this.executing) {
|
||||
console.log(`action "${actionLabel}" trying to interrupt current action "${this.currentActionLabel}"`);
|
||||
}
|
||||
await this.stop();
|
||||
|
||||
// clear bot logs and reset interrupt code
|
||||
this.agent.clearBotLogs();
|
||||
|
||||
this.executing = true;
|
||||
this.currentActionLabel = actionLabel;
|
||||
this.currentActionFn = actionFn;
|
||||
|
||||
// timeout in minutes
|
||||
if (timeout > 0) {
|
||||
TIMEOUT = this._startTimeout(timeout);
|
||||
}
|
||||
|
||||
// start the action
|
||||
await actionFn();
|
||||
|
||||
// mark action as finished + cleanup
|
||||
this.executing = false;
|
||||
this.currentActionLabel = '';
|
||||
this.currentActionFn = null;
|
||||
clearTimeout(TIMEOUT);
|
||||
|
||||
// get bot activity summary
|
||||
let output = this._getBotOutputSummary();
|
||||
let interrupted = this.agent.bot.interrupt_code;
|
||||
let timedout = this.timedout;
|
||||
this.agent.clearBotLogs();
|
||||
|
||||
// if not interrupted and not generating, emit idle event
|
||||
if (!interrupted && !this.agent.coder.generating) {
|
||||
this.agent.bot.emit('idle');
|
||||
}
|
||||
|
||||
// return action status report
|
||||
return { success: true, message: output, interrupted, timedout };
|
||||
} catch (err) {
|
||||
this.executing = false;
|
||||
this.currentActionLabel = '';
|
||||
this.currentActionFn = null;
|
||||
clearTimeout(TIMEOUT);
|
||||
this.cancelResume();
|
||||
console.error("Code execution triggered catch: " + err);
|
||||
await this.stop();
|
||||
|
||||
let message = this._getBotOutputSummary() + '!!Code threw exception!! Error: ' + err;
|
||||
let interrupted = this.agent.bot.interrupt_code;
|
||||
this.agent.clearBotLogs();
|
||||
if (!interrupted && !this.agent.coder.generating) {
|
||||
this.agent.bot.emit('idle');
|
||||
}
|
||||
return { success: false, message, interrupted, timedout: false };
|
||||
}
|
||||
}
|
||||
|
||||
_getBotOutputSummary() {
|
||||
const { bot } = this.agent;
|
||||
if (bot.interrupt_code && !this.timedout) return '';
|
||||
let output = bot.output;
|
||||
const MAX_OUT = 500;
|
||||
if (output.length > MAX_OUT) {
|
||||
output = `Code output is very long (${output.length} chars) and has been shortened.\n
|
||||
First outputs:\n${output.substring(0, MAX_OUT / 2)}\n...skipping many lines.\nFinal outputs:\n ${output.substring(output.length - MAX_OUT / 2)}`;
|
||||
}
|
||||
else {
|
||||
output = 'Code output:\n' + output;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
_startTimeout(TIMEOUT_MINS = 10) {
|
||||
return setTimeout(async () => {
|
||||
console.warn(`Code execution timed out after ${TIMEOUT_MINS} minutes. Attempting force stop.`);
|
||||
this.timedout = true;
|
||||
this.agent.history.add('system', `Code execution timed out after ${TIMEOUT_MINS} minutes. Attempting force stop.`);
|
||||
await this.stop(); // last attempt to stop
|
||||
}, TIMEOUT_MINS * 60 * 1000);
|
||||
}
|
||||
|
||||
}
|
|
@ -4,6 +4,7 @@ import { Prompter } from './prompter.js';
|
|||
import { initModes } from './modes.js';
|
||||
import { initBot } from '../utils/mcdata.js';
|
||||
import { containsCommand, commandExists, executeCommand, truncCommandMessage, isAction } from './commands/index.js';
|
||||
import { ActionManager } from './action_manager.js';
|
||||
import { NPCContoller } from './npc/controller.js';
|
||||
import { MemoryBank } from './memory_bank.js';
|
||||
import { SelfPrompter } from './self_prompter.js';
|
||||
|
@ -13,6 +14,7 @@ import settings from '../../settings.js';
|
|||
|
||||
export class Agent {
|
||||
async start(profile_fp, load_mem=false, init_message=null, count_id=0) {
|
||||
this.actions = new ActionManager(this);
|
||||
this.prompter = new Prompter(this, profile_fp);
|
||||
this.name = this.prompter.getName();
|
||||
this.history = new History(this);
|
||||
|
@ -23,7 +25,7 @@ export class Agent {
|
|||
|
||||
await this.prompter.initExamples();
|
||||
|
||||
console.log('Logging in...');
|
||||
console.log('Logging into minecraft...');
|
||||
this.bot = initBot(this.name);
|
||||
|
||||
initModes(this);
|
||||
|
@ -33,6 +35,10 @@ export class Agent {
|
|||
save_data = this.history.load();
|
||||
}
|
||||
|
||||
this.bot.on('login', () => {
|
||||
console.log('Logged in!');
|
||||
});
|
||||
|
||||
this.bot.once('spawn', async () => {
|
||||
addViewer(this.bot, count_id);
|
||||
|
||||
|
@ -40,7 +46,7 @@ export class Agent {
|
|||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
console.log(`${this.name} spawned.`);
|
||||
this.coder.clear();
|
||||
this.clearBotLogs();
|
||||
|
||||
const ignore_messages = [
|
||||
"Set own game mode to",
|
||||
|
@ -56,13 +62,9 @@ export class Agent {
|
|||
|
||||
if (ignore_messages.some((m) => message.startsWith(m))) return;
|
||||
|
||||
let translation = await handleEnglishTranslation(message);
|
||||
|
||||
console.log('received message from', username, ':', translation);
|
||||
|
||||
this.shut_up = false;
|
||||
|
||||
this.handleMessage(username, translation);
|
||||
this.handleMessage(username, message);
|
||||
});
|
||||
|
||||
// set the bot to automatically eat food when hungry
|
||||
|
@ -91,6 +93,17 @@ export class Agent {
|
|||
});
|
||||
}
|
||||
|
||||
requestInterrupt() {
|
||||
this.bot.interrupt_code = true;
|
||||
this.bot.collectBlock.cancelTask();
|
||||
this.bot.pathfinder.stop();
|
||||
this.bot.pvp.stop();
|
||||
}
|
||||
|
||||
clearBotLogs() {
|
||||
this.bot.output = '';
|
||||
this.bot.interrupt_code = false;
|
||||
}
|
||||
|
||||
async cleanChat(message, translate_up_to=-1) {
|
||||
let to_translate = message;
|
||||
|
@ -123,6 +136,7 @@ export class Agent {
|
|||
|
||||
let self_prompt = source === 'system' || source === this.name;
|
||||
|
||||
// First check for user commands
|
||||
if (!self_prompt) {
|
||||
const user_command_name = containsCommand(message);
|
||||
if (user_command_name) {
|
||||
|
@ -143,6 +157,11 @@ export class Agent {
|
|||
}
|
||||
}
|
||||
|
||||
// Now translate the message
|
||||
message = await handleEnglishTranslation(message);
|
||||
console.log('received message from', source, ':', message);
|
||||
|
||||
// Do self prompting
|
||||
const checkInterrupt = () => this.self_prompter.shouldInterrupt(self_prompt) || this.shut_up;
|
||||
|
||||
let behavior_log = this.bot.modes.flushBehaviorLog();
|
||||
|
@ -155,6 +174,7 @@ export class Agent {
|
|||
await this.history.add('system', behavior_log);
|
||||
}
|
||||
|
||||
// Handle other user messages
|
||||
await this.history.add(source, message);
|
||||
this.history.save();
|
||||
|
||||
|
@ -250,8 +270,8 @@ export class Agent {
|
|||
this.cleanKill('Bot disconnected! Killing agent process.');
|
||||
});
|
||||
this.bot.on('death', () => {
|
||||
this.coder.cancelResume();
|
||||
this.coder.stop();
|
||||
this.actions.cancelResume();
|
||||
this.actions.stop();
|
||||
});
|
||||
this.bot.on('kicked', (reason) => {
|
||||
console.warn('Bot kicked!', reason);
|
||||
|
@ -260,14 +280,21 @@ export class Agent {
|
|||
this.bot.on('messagestr', async (message, _, jsonMsg) => {
|
||||
if (jsonMsg.translate && jsonMsg.translate.startsWith('death') && message.startsWith(this.name)) {
|
||||
console.log('Agent died: ', message);
|
||||
this.handleMessage('system', `You died with the final message: '${message}'. Previous actions were stopped and you have respawned. Notify the user and perform any necessary actions.`);
|
||||
let death_pos = this.bot.entity.position;
|
||||
this.memory_bank.rememberPlace('last_death_position', death_pos.x, death_pos.y, death_pos.z);
|
||||
let death_pos_text = null;
|
||||
if (death_pos) {
|
||||
death_pos_text = `x: ${death_pos.x.toFixed(2)}, y: ${death_pos.y.toFixed(2)}, z: ${death_pos.x.toFixed(2)}`;
|
||||
}
|
||||
let dimention = this.bot.game.dimension;
|
||||
this.handleMessage('system', `You died at position ${death_pos_text || "unknown"} in the ${dimention} dimension with the final message: '${message}'. Your place of death is saved as 'last_death_position' if you want to return. Previous actions were stopped and you have respawned.`);
|
||||
}
|
||||
});
|
||||
this.bot.on('idle', () => {
|
||||
this.bot.clearControlStates();
|
||||
this.bot.pathfinder.stop(); // clear any lingering pathfinder
|
||||
this.bot.modes.unPauseAll();
|
||||
this.coder.executeResume();
|
||||
this.actions.resumeAction();
|
||||
});
|
||||
|
||||
// Init NPC controller
|
||||
|
@ -297,7 +324,7 @@ export class Agent {
|
|||
}
|
||||
|
||||
isIdle() {
|
||||
return !this.coder.executing && !this.coder.generating;
|
||||
return !this.actions.executing && !this.coder.generating;
|
||||
}
|
||||
|
||||
cleanKill(msg='Killing agent process...') {
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import { writeFile, readFile, mkdirSync } from 'fs';
|
||||
import { checkSafe } from '../utils/safety.js';
|
||||
import settings from '../../settings.js';
|
||||
import { makeCompartment } from './library/lockdown.js';
|
||||
import * as skills from './library/skills.js';
|
||||
import * as world from './library/world.js';
|
||||
import { Vec3 } from 'vec3';
|
||||
|
||||
export class Coder {
|
||||
constructor(agent) {
|
||||
this.agent = agent;
|
||||
this.file_counter = 0;
|
||||
this.fp = '/bots/'+agent.name+'/action-code/';
|
||||
this.executing = false;
|
||||
this.generating = false;
|
||||
this.code_template = '';
|
||||
this.timedout = false;
|
||||
this.cur_action_name = '';
|
||||
|
||||
readFile('./bots/template.js', 'utf8', (err, data) => {
|
||||
if (err) throw err;
|
||||
|
@ -21,7 +21,7 @@ export class Coder {
|
|||
mkdirSync('.' + this.fp, { recursive: true });
|
||||
}
|
||||
|
||||
// write custom code to file and import it
|
||||
// write custom code to file and prepare for evaluation
|
||||
async stageCode(code) {
|
||||
code = this.sanitizeCode(code);
|
||||
let src = '';
|
||||
|
@ -47,13 +47,24 @@ export class Coder {
|
|||
// } commented for now, useful to keep files for debugging
|
||||
this.file_counter++;
|
||||
|
||||
let write_result = await this.writeFilePromise('.' + this.fp + filename, src)
|
||||
let write_result = await this.writeFilePromise('.' + this.fp + filename, src);
|
||||
// This is where we determine the environment the agent's code should be exposed to.
|
||||
// It will only have access to these things, (in addition to basic javascript objects like Array, Object, etc.)
|
||||
// Note that the code may be able to modify the exposed objects.
|
||||
const compartment = makeCompartment({
|
||||
skills,
|
||||
log: skills.log,
|
||||
world,
|
||||
Vec3,
|
||||
});
|
||||
const mainFn = compartment.evaluate(src);
|
||||
|
||||
if (write_result) {
|
||||
console.error('Error writing code execution file: ' + result);
|
||||
return null;
|
||||
}
|
||||
return await import('../..' + this.fp + filename);
|
||||
|
||||
return { main: mainFn };
|
||||
}
|
||||
|
||||
sanitizeCode(code) {
|
||||
|
@ -83,7 +94,7 @@ export class Coder {
|
|||
|
||||
async generateCode(agent_history) {
|
||||
// wrapper to prevent overlapping code generation loops
|
||||
await this.stop();
|
||||
await this.agent.actions.stop();
|
||||
this.generating = true;
|
||||
let res = await this.generateCodeLoop(agent_history);
|
||||
this.generating = false;
|
||||
|
@ -119,7 +130,7 @@ export class Coder {
|
|||
}
|
||||
|
||||
if (failures >= 3) {
|
||||
return {success: false, message: 'Action failed, agent would not write code.', interrupted: false, timedout: false};
|
||||
return { success: false, message: 'Action failed, agent would not write code.', interrupted: false, timedout: false };
|
||||
}
|
||||
messages.push({
|
||||
role: 'system',
|
||||
|
@ -130,29 +141,22 @@ export class Coder {
|
|||
}
|
||||
code = res.substring(res.indexOf('```')+3, res.lastIndexOf('```'));
|
||||
|
||||
if (!checkSafe(code)) {
|
||||
console.warn(`Detected insecure generated code, not executing. Insecure code: \n\`${code}\``);
|
||||
const message = 'Error: Code insecurity detected. Do not import, read/write files, execute dynamic code, or access the internet. Please try again:';
|
||||
messages.push({ role: 'system', content: message });
|
||||
continue;
|
||||
}
|
||||
|
||||
const execution_file = await this.stageCode(code);
|
||||
if (!execution_file) {
|
||||
const executionModuleExports = await this.stageCode(code);
|
||||
if (!executionModuleExports) {
|
||||
agent_history.add('system', 'Failed to stage code, something is wrong.');
|
||||
return {success: false, message: null, interrupted: false, timedout: false};
|
||||
}
|
||||
|
||||
code_return = await this.execute(async ()=>{
|
||||
return await execution_file.main(this.agent.bot);
|
||||
}, settings.code_timeout_mins);
|
||||
code_return = await this.agent.actions.runAction('newAction', async () => {
|
||||
return await executionModuleExports.main(this.agent.bot);
|
||||
}, { timeout: settings.code_timeout_mins });
|
||||
if (code_return.interrupted && !code_return.timedout)
|
||||
return {success: false, message: null, interrupted: true, timedout: false};
|
||||
return { success: false, message: null, interrupted: true, timedout: false };
|
||||
console.log("Code generation result:", code_return.success, code_return.message);
|
||||
|
||||
if (code_return.success) {
|
||||
const summary = "Summary of newAction\nAgent wrote this code: \n```" + this.sanitizeCode(code) + "```\nCode Output:\n" + code_return.message;
|
||||
return {success: true, message: summary, interrupted: false, timedout: false};
|
||||
return { success: true, message: summary, interrupted: false, timedout: false };
|
||||
}
|
||||
|
||||
messages.push({
|
||||
|
@ -164,114 +168,7 @@ export class Coder {
|
|||
content: code_return.message + '\nCode failed. Please try again:'
|
||||
});
|
||||
}
|
||||
return {success: false, message: null, interrupted: false, timedout: true};
|
||||
return { success: false, message: null, interrupted: false, timedout: true };
|
||||
}
|
||||
|
||||
async executeResume(func=null, timeout=10) {
|
||||
const new_resume = func != null;
|
||||
if (new_resume) { // start new resume
|
||||
this.resume_func = func;
|
||||
this.resume_name = this.cur_action_name;
|
||||
}
|
||||
if (this.resume_func != null && this.agent.isIdle() && (!this.agent.self_prompter.on || new_resume)) {
|
||||
this.cur_action_name = this.resume_name;
|
||||
let res = await this.execute(this.resume_func, timeout);
|
||||
this.cur_action_name = '';
|
||||
return res;
|
||||
} else {
|
||||
return {success: false, message: null, interrupted: false, timedout: false};
|
||||
}
|
||||
}
|
||||
|
||||
cancelResume() {
|
||||
this.resume_func = null;
|
||||
this.resume_name = null;
|
||||
}
|
||||
|
||||
setCurActionName(name) {
|
||||
this.cur_action_name = name.replace(/!/g, '');
|
||||
}
|
||||
|
||||
// returns {success: bool, message: string, interrupted: bool, timedout: false}
|
||||
async execute(func, timeout=10) {
|
||||
if (!this.code_template) return {success: false, message: "Code template not loaded.", interrupted: false, timedout: false};
|
||||
|
||||
let TIMEOUT;
|
||||
try {
|
||||
console.log('executing code...\n');
|
||||
await this.stop();
|
||||
this.clear();
|
||||
|
||||
this.executing = true;
|
||||
if (timeout > 0)
|
||||
TIMEOUT = this._startTimeout(timeout);
|
||||
await func(); // open fire
|
||||
this.executing = false;
|
||||
clearTimeout(TIMEOUT);
|
||||
|
||||
let output = this.formatOutput(this.agent.bot);
|
||||
let interrupted = this.agent.bot.interrupt_code;
|
||||
let timedout = this.timedout;
|
||||
this.clear();
|
||||
if (!interrupted && !this.generating) this.agent.bot.emit('idle');
|
||||
return {success:true, message: output, interrupted, timedout};
|
||||
} catch (err) {
|
||||
this.executing = false;
|
||||
clearTimeout(TIMEOUT);
|
||||
this.cancelResume();
|
||||
console.error("Code execution triggered catch: " + err);
|
||||
await this.stop();
|
||||
|
||||
let message = this.formatOutput(this.agent.bot) + '!!Code threw exception!! Error: ' + err;
|
||||
let interrupted = this.agent.bot.interrupt_code;
|
||||
this.clear();
|
||||
if (!interrupted && !this.generating) this.agent.bot.emit('idle');
|
||||
return {success: false, message, interrupted, timedout: false};
|
||||
}
|
||||
}
|
||||
|
||||
formatOutput(bot) {
|
||||
if (bot.interrupt_code && !this.timedout) return '';
|
||||
let output = bot.output;
|
||||
const MAX_OUT = 500;
|
||||
if (output.length > MAX_OUT) {
|
||||
output = `Code output is very long (${output.length} chars) and has been shortened.\n
|
||||
First outputs:\n${output.substring(0, MAX_OUT/2)}\n...skipping many lines.\nFinal outputs:\n ${output.substring(output.length - MAX_OUT/2)}`;
|
||||
}
|
||||
else {
|
||||
output = 'Code output:\n' + output;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (!this.executing) return;
|
||||
const start = Date.now();
|
||||
while (this.executing) {
|
||||
this.agent.bot.interrupt_code = true;
|
||||
this.agent.bot.collectBlock.cancelTask();
|
||||
this.agent.bot.pathfinder.stop();
|
||||
this.agent.bot.pvp.stop();
|
||||
console.log('waiting for code to finish executing...');
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
if (Date.now() - start > 10 * 1000) {
|
||||
this.agent.cleanKill('Code execution refused stop after 10 seconds. Killing process.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.agent.bot.output = '';
|
||||
this.agent.bot.interrupt_code = false;
|
||||
this.timedout = false;
|
||||
}
|
||||
|
||||
_startTimeout(TIMEOUT_MINS=10) {
|
||||
return setTimeout(async () => {
|
||||
console.warn(`Code execution timed out after ${TIMEOUT_MINS} minutes. Attempting force stop.`);
|
||||
this.timedout = true;
|
||||
this.agent.history.add('system', `Code execution timed out after ${TIMEOUT_MINS} minutes. Attempting force stop.`);
|
||||
await this.stop(); // last attempt to stop
|
||||
}, TIMEOUT_MINS*60*1000);
|
||||
}
|
||||
}
|
|
@ -1,21 +1,26 @@
|
|||
import * as skills from '../library/skills.js';
|
||||
import settings from '../../../settings.js';
|
||||
|
||||
function wrapExecution(func, resume=false, timeout=-1) {
|
||||
return async function (agent, ...args) {
|
||||
let code_return;
|
||||
const wrappedFunction = async () => {
|
||||
await func(agent, ...args);
|
||||
};
|
||||
if (resume) {
|
||||
code_return = await agent.coder.executeResume(wrappedFunction, timeout);
|
||||
} else {
|
||||
code_return = await agent.coder.execute(wrappedFunction, timeout);
|
||||
function runAsAction (actionFn, resume = false, timeout = -1) {
|
||||
let actionLabel = null; // Will be set on first use
|
||||
|
||||
const wrappedAction = async function (agent, ...args) {
|
||||
// Set actionLabel only once, when the action is first created
|
||||
if (!actionLabel) {
|
||||
const actionObj = actionsList.find(a => a.perform === wrappedAction);
|
||||
actionLabel = actionObj.name.substring(1); // Remove the ! prefix
|
||||
}
|
||||
|
||||
const actionFnWithAgent = async () => {
|
||||
await actionFn(agent, ...args);
|
||||
};
|
||||
const code_return = await agent.actions.runAction(`action:${actionLabel}`, actionFnWithAgent, { timeout, resume });
|
||||
if (code_return.interrupted && !code_return.timedout)
|
||||
return;
|
||||
return code_return.message;
|
||||
}
|
||||
|
||||
return wrappedAction;
|
||||
}
|
||||
|
||||
export const actionsList = [
|
||||
|
@ -36,9 +41,9 @@ export const actionsList = [
|
|||
name: '!stop',
|
||||
description: 'Force stop all actions and commands that are currently executing.',
|
||||
perform: async function (agent) {
|
||||
await agent.coder.stop();
|
||||
agent.coder.clear();
|
||||
agent.coder.cancelResume();
|
||||
await agent.actions.stop();
|
||||
agent.clearBotLogs();
|
||||
agent.actions.cancelResume();
|
||||
agent.bot.emit('idle');
|
||||
let msg = 'Agent stopped.';
|
||||
if (agent.self_prompter.on)
|
||||
|
@ -78,18 +83,18 @@ export const actionsList = [
|
|||
'player_name': {type: 'string', description: 'The name of the player to go to.'},
|
||||
'closeness': {type: 'float', description: 'How close to get to the player.', domain: [0, Infinity]}
|
||||
},
|
||||
perform: wrapExecution(async (agent, player_name, closeness) => {
|
||||
perform: runAsAction(async (agent, player_name, closeness) => {
|
||||
return await skills.goToPlayer(agent.bot, player_name, closeness);
|
||||
})
|
||||
},
|
||||
{
|
||||
name: '!followPlayer',
|
||||
description: 'Endlessly follow the given player. Will defend that player if self_defense mode is on.',
|
||||
description: 'Endlessly follow the given player.',
|
||||
params: {
|
||||
'player_name': {type: 'string', description: 'name of the player to follow.'},
|
||||
'follow_dist': {type: 'float', description: 'The distance to follow from.', domain: [0, Infinity]}
|
||||
},
|
||||
perform: wrapExecution(async (agent, player_name, follow_dist) => {
|
||||
perform: runAsAction(async (agent, player_name, follow_dist) => {
|
||||
await skills.followPlayer(agent.bot, player_name, follow_dist);
|
||||
}, true)
|
||||
},
|
||||
|
@ -99,9 +104,9 @@ export const actionsList = [
|
|||
params: {
|
||||
'type': { type: 'BlockName', description: 'The block type to go to.' },
|
||||
'closeness': { type: 'float', description: 'How close to get to the block.', domain: [0, Infinity] },
|
||||
'search_range': { type: 'float', description: 'The distance to search for the block.', domain: [0, Infinity] }
|
||||
'search_range': { type: 'float', description: 'The range to search for the block.', domain: [0, 512] }
|
||||
},
|
||||
perform: wrapExecution(async (agent, type, closeness, range) => {
|
||||
perform: runAsAction(async (agent, type, closeness, range) => {
|
||||
await skills.goToNearestBlock(agent.bot, type, closeness, range);
|
||||
})
|
||||
},
|
||||
|
@ -109,7 +114,7 @@ export const actionsList = [
|
|||
name: '!moveAway',
|
||||
description: 'Move away from the current location in any direction by a given distance.',
|
||||
params: {'distance': { type: 'float', description: 'The distance to move away.', domain: [0, Infinity] }},
|
||||
perform: wrapExecution(async (agent, distance) => {
|
||||
perform: runAsAction(async (agent, distance) => {
|
||||
await skills.moveAway(agent.bot, distance);
|
||||
})
|
||||
},
|
||||
|
@ -127,11 +132,11 @@ export const actionsList = [
|
|||
name: '!goToPlace',
|
||||
description: 'Go to a saved location.',
|
||||
params: {'name': { type: 'string', description: 'The name of the location to go to.' }},
|
||||
perform: wrapExecution(async (agent, name) => {
|
||||
perform: runAsAction(async (agent, name) => {
|
||||
const pos = agent.memory_bank.recallPlace(name);
|
||||
if (!pos) {
|
||||
skills.log(agent.bot, `No location named "${name}" saved.`);
|
||||
return;
|
||||
skills.log(agent.bot, `No location named "${name}" saved.`);
|
||||
return;
|
||||
}
|
||||
await skills.goToPosition(agent.bot, pos[0], pos[1], pos[2], 1);
|
||||
})
|
||||
|
@ -144,7 +149,7 @@ export const actionsList = [
|
|||
'item_name': { type: 'ItemName', description: 'The name of the item to give.' },
|
||||
'num': { type: 'int', description: 'The number of items to give.', domain: [1, Number.MAX_SAFE_INTEGER] }
|
||||
},
|
||||
perform: wrapExecution(async (agent, player_name, item_name, num) => {
|
||||
perform: runAsAction(async (agent, player_name, item_name, num) => {
|
||||
await skills.giveToPlayer(agent.bot, item_name, player_name, num);
|
||||
})
|
||||
},
|
||||
|
@ -152,7 +157,7 @@ export const actionsList = [
|
|||
name: '!consume',
|
||||
description: 'Eat/drink the given item.',
|
||||
params: {'item_name': { type: 'ItemName', description: 'The name of the item to consume.' }},
|
||||
perform: wrapExecution(async (agent, item_name) => {
|
||||
perform: runAsAction(async (agent, item_name) => {
|
||||
await agent.bot.consume(item_name);
|
||||
skills.log(agent.bot, `Consumed ${item_name}.`);
|
||||
})
|
||||
|
@ -161,7 +166,7 @@ export const actionsList = [
|
|||
name: '!equip',
|
||||
description: 'Equip the given item.',
|
||||
params: {'item_name': { type: 'ItemName', description: 'The name of the item to equip.' }},
|
||||
perform: wrapExecution(async (agent, item_name) => {
|
||||
perform: runAsAction(async (agent, item_name) => {
|
||||
await skills.equip(agent.bot, item_name);
|
||||
})
|
||||
},
|
||||
|
@ -172,7 +177,7 @@ export const actionsList = [
|
|||
'item_name': { type: 'ItemName', description: 'The name of the item to put in the chest.' },
|
||||
'num': { type: 'int', description: 'The number of items to put in the chest.', domain: [1, Number.MAX_SAFE_INTEGER] }
|
||||
},
|
||||
perform: wrapExecution(async (agent, item_name, num) => {
|
||||
perform: runAsAction(async (agent, item_name, num) => {
|
||||
await skills.putInChest(agent.bot, item_name, num);
|
||||
})
|
||||
},
|
||||
|
@ -183,7 +188,7 @@ export const actionsList = [
|
|||
'item_name': { type: 'ItemName', description: 'The name of the item to take.' },
|
||||
'num': { type: 'int', description: 'The number of items to take.', domain: [1, Number.MAX_SAFE_INTEGER] }
|
||||
},
|
||||
perform: wrapExecution(async (agent, item_name, num) => {
|
||||
perform: runAsAction(async (agent, item_name, num) => {
|
||||
await skills.takeFromChest(agent.bot, item_name, num);
|
||||
})
|
||||
},
|
||||
|
@ -191,7 +196,7 @@ export const actionsList = [
|
|||
name: '!viewChest',
|
||||
description: 'View the items/counts of the nearest chest.',
|
||||
params: { },
|
||||
perform: wrapExecution(async (agent) => {
|
||||
perform: runAsAction(async (agent) => {
|
||||
await skills.viewChest(agent.bot);
|
||||
})
|
||||
},
|
||||
|
@ -202,7 +207,7 @@ export const actionsList = [
|
|||
'item_name': { type: 'ItemName', description: 'The name of the item to discard.' },
|
||||
'num': { type: 'int', description: 'The number of items to discard.', domain: [1, Number.MAX_SAFE_INTEGER] }
|
||||
},
|
||||
perform: wrapExecution(async (agent, item_name, num) => {
|
||||
perform: runAsAction(async (agent, item_name, num) => {
|
||||
const start_loc = agent.bot.entity.position;
|
||||
await skills.moveAway(agent.bot, 5);
|
||||
await skills.discard(agent.bot, item_name, num);
|
||||
|
@ -216,7 +221,7 @@ export const actionsList = [
|
|||
'type': { type: 'BlockName', description: 'The block type to collect.' },
|
||||
'num': { type: 'int', description: 'The number of blocks to collect.', domain: [1, Number.MAX_SAFE_INTEGER] }
|
||||
},
|
||||
perform: wrapExecution(async (agent, type, num) => {
|
||||
perform: runAsAction(async (agent, type, num) => {
|
||||
await skills.collectBlock(agent.bot, type, num);
|
||||
}, false, 10) // 10 minute timeout
|
||||
},
|
||||
|
@ -226,10 +231,10 @@ export const actionsList = [
|
|||
params: {
|
||||
'type': { type: 'BlockName', description: 'The block type to collect.' }
|
||||
},
|
||||
perform: wrapExecution(async (agent, type) => {
|
||||
perform: runAsAction(async (agent, type) => {
|
||||
let success = await skills.collectBlock(agent.bot, type, 1);
|
||||
if (!success)
|
||||
agent.coder.cancelResume();
|
||||
agent.actions.cancelResume();
|
||||
}, true, 3) // 3 minute timeout
|
||||
},
|
||||
{
|
||||
|
@ -239,7 +244,7 @@ export const actionsList = [
|
|||
'recipe_name': { type: 'ItemName', description: 'The name of the output item to craft.' },
|
||||
'num': { type: 'int', description: 'The number of times to craft the recipe. This is NOT the number of output items, as it may craft many more items depending on the recipe.', domain: [1, Number.MAX_SAFE_INTEGER] }
|
||||
},
|
||||
perform: wrapExecution(async (agent, recipe_name, num) => {
|
||||
perform: runAsAction(async (agent, recipe_name, num) => {
|
||||
await skills.craftRecipe(agent.bot, recipe_name, num);
|
||||
})
|
||||
},
|
||||
|
@ -250,32 +255,29 @@ export const actionsList = [
|
|||
'item_name': { type: 'ItemName', description: 'The name of the input item to smelt.' },
|
||||
'num': { type: 'int', description: 'The number of times to smelt the item.', domain: [1, Number.MAX_SAFE_INTEGER] }
|
||||
},
|
||||
perform: async function (agent, item_name, num) {
|
||||
let response = await wrapExecution(async (agent) => {
|
||||
console.log('smelting item');
|
||||
return await skills.smeltItem(agent.bot, item_name, num);
|
||||
})(agent);
|
||||
perform: runAsAction(async (agent, item_name, num) => {
|
||||
let response = await skills.smeltItem(agent.bot, item_name, num);
|
||||
if (response.indexOf('Successfully') !== -1) {
|
||||
// there is a bug where the bot's inventory is not updated after smelting
|
||||
// only updates after a restart
|
||||
agent.cleanKill(response + ' Safely restarting to update inventory.');
|
||||
// there is a bug where the bot's inventory is not updated after smelting
|
||||
// only updates after a restart
|
||||
agent.cleanKill(response + ' Safely restarting to update inventory.');
|
||||
}
|
||||
return response;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '!clearFurnace',
|
||||
description: 'Tak all items out of the nearest furnace.',
|
||||
params: { },
|
||||
perform: wrapExecution(async (agent) => {
|
||||
await skills.clearNearestFurnace(agent.bot);
|
||||
})
|
||||
},
|
||||
{
|
||||
name: '!clearFurnace',
|
||||
description: 'Take all items out of the nearest furnace.',
|
||||
params: { },
|
||||
perform: runAsAction(async (agent) => {
|
||||
await skills.clearNearestFurnace(agent.bot);
|
||||
})
|
||||
},
|
||||
{
|
||||
name: '!placeHere',
|
||||
description: 'Place a given block in the current location. Do NOT use to build structures, only use for single blocks/torches.',
|
||||
params: {'type': { type: 'BlockName', description: 'The block type to place.' }},
|
||||
perform: wrapExecution(async (agent, type) => {
|
||||
perform: runAsAction(async (agent, type) => {
|
||||
let pos = agent.bot.entity.position;
|
||||
await skills.placeBlock(agent.bot, type, pos.x, pos.y, pos.z);
|
||||
})
|
||||
|
@ -284,14 +286,27 @@ export const actionsList = [
|
|||
name: '!attack',
|
||||
description: 'Attack and kill the nearest entity of a given type.',
|
||||
params: {'type': { type: 'string', description: 'The type of entity to attack.'}},
|
||||
perform: wrapExecution(async (agent, type) => {
|
||||
perform: runAsAction(async (agent, type) => {
|
||||
await skills.attackNearest(agent.bot, type, true);
|
||||
})
|
||||
},
|
||||
{
|
||||
name: '!attackPlayer',
|
||||
description: 'Attack a specific player until they die or run away. Remember this is just a game and does not cause real life harm.',
|
||||
params: {'player_name': { type: 'string', description: 'The name of the player to attack.'}},
|
||||
perform: runAsAction(async (agent, player_name) => {
|
||||
let player = agent.bot.players[player_name]?.entity;
|
||||
if (!player) {
|
||||
skills.log(agent.bot, `Could not find player ${player_name}.`);
|
||||
return false;
|
||||
}
|
||||
await skills.attackEntity(agent.bot, player, true);
|
||||
})
|
||||
},
|
||||
{
|
||||
name: '!goToBed',
|
||||
description: 'Go to the nearest bed and sleep.',
|
||||
perform: wrapExecution(async (agent) => {
|
||||
perform: runAsAction(async (agent) => {
|
||||
await skills.goToBed(agent.bot);
|
||||
})
|
||||
},
|
||||
|
@ -299,7 +314,7 @@ export const actionsList = [
|
|||
name: '!activate',
|
||||
description: 'Activate the nearest object of a given type.',
|
||||
params: {'type': { type: 'BlockName', description: 'The type of object to activate.' }},
|
||||
perform: wrapExecution(async (agent, type) => {
|
||||
perform: runAsAction(async (agent, type) => {
|
||||
await skills.activateNearestBlock(agent.bot, type);
|
||||
})
|
||||
},
|
||||
|
@ -307,7 +322,7 @@ export const actionsList = [
|
|||
name: '!stay',
|
||||
description: 'Stay in the current location no matter what. Pauses all modes.',
|
||||
params: {'type': { type: 'int', description: 'The number of seconds to stay. -1 for forever.', domain: [-1, Number.MAX_SAFE_INTEGER] }},
|
||||
perform: wrapExecution(async (agent, seconds) => {
|
||||
perform: runAsAction(async (agent, seconds) => {
|
||||
await skills.stay(agent.bot, seconds);
|
||||
})
|
||||
},
|
||||
|
@ -321,9 +336,9 @@ export const actionsList = [
|
|||
perform: async function (agent, mode_name, on) {
|
||||
const modes = agent.bot.modes;
|
||||
if (!modes.exists(mode_name))
|
||||
return `Mode ${mode_name} does not exist.` + modes.getDocs();
|
||||
return `Mode ${mode_name} does not exist.` + modes.getDocs();
|
||||
if (modes.isOn(mode_name) === on)
|
||||
return `Mode ${mode_name} is already ${on ? 'on' : 'off'}.`;
|
||||
return `Mode ${mode_name} is already ${on ? 'on' : 'off'}.`;
|
||||
modes.setOn(mode_name, on);
|
||||
return `Mode ${mode_name} is now ${on ? 'on' : 'off'}.`;
|
||||
}
|
||||
|
|
|
@ -207,7 +207,6 @@ export async function executeCommand(agent, message) {
|
|||
else {
|
||||
console.log('parsed command:', parsed);
|
||||
const command = getCommand(parsed.commandName);
|
||||
const is_action = isAction(command.name);
|
||||
let numArgs = 0;
|
||||
if (parsed.args) {
|
||||
numArgs = parsed.args.length;
|
||||
|
@ -215,11 +214,7 @@ export async function executeCommand(agent, message) {
|
|||
if (numArgs !== numParams(command))
|
||||
return `Command ${command.name} was given ${numArgs} args, but requires ${numParams(command)} args.`;
|
||||
else {
|
||||
if (is_action)
|
||||
agent.coder.setCurActionName(command.name);
|
||||
const result = await command.perform(agent, ...parsed.args);
|
||||
if (is_action)
|
||||
agent.coder.setCurActionName('');
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -106,14 +106,10 @@ export const queryList = [
|
|||
name: "!craftable",
|
||||
description: "Get the craftable items with the bot's inventory.",
|
||||
perform: function (agent) {
|
||||
const bot = agent.bot;
|
||||
const table = world.getNearestBlock(bot, 'crafting_table');
|
||||
let craftable = world.getCraftableItems(agent.bot);
|
||||
let res = 'CRAFTABLE_ITEMS';
|
||||
for (const item of mc.getAllItems()) {
|
||||
let recipes = bot.recipesFor(item.id, null, 1, table);
|
||||
if (recipes.length > 0) {
|
||||
res += `\n- ${item.name}`;
|
||||
}
|
||||
for (const item of craftable) {
|
||||
res += `\n- ${item}`;
|
||||
}
|
||||
if (res == 'CRAFTABLE_ITEMS') {
|
||||
res += ': none';
|
||||
|
|
26
src/agent/library/lockdown.js
Normal file
26
src/agent/library/lockdown.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
import 'ses';
|
||||
|
||||
// This sets up the secure environment
|
||||
// We disable some of the taming to allow for more flexibility
|
||||
|
||||
// For configuration, see https://github.com/endojs/endo/blob/master/packages/ses/docs/lockdown.md
|
||||
lockdown({
|
||||
// basic devex and quality of life improvements
|
||||
localeTaming: 'unsafe',
|
||||
consoleTaming: 'unsafe',
|
||||
errorTaming: 'unsafe',
|
||||
stackFiltering: 'verbose',
|
||||
// allow eval outside of created compartments
|
||||
// (mineflayer dep "protodef" uses eval)
|
||||
evalTaming: 'unsafeEval',
|
||||
});
|
||||
|
||||
export const makeCompartment = (endowments = {}) => {
|
||||
return new Compartment({
|
||||
// provide untamed Math, Date, etc
|
||||
Math,
|
||||
Date,
|
||||
// standard endowments
|
||||
...endowments
|
||||
});
|
||||
}
|
|
@ -43,6 +43,11 @@ export async function craftRecipe(bot, itemName, num=1) {
|
|||
**/
|
||||
let placedTable = false;
|
||||
|
||||
if (mc.getItemCraftingRecipes(itemName).length == 0) {
|
||||
log(bot, `${itemName} is either not an item, or it does not have a crafting recipe!`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// get recipes that don't require a crafting table
|
||||
let recipes = bot.recipesFor(mc.getItemId(itemName), null, 1, null);
|
||||
let craftingTable = null;
|
||||
|
@ -76,7 +81,7 @@ export async function craftRecipe(bot, itemName, num=1) {
|
|||
}
|
||||
}
|
||||
if (!recipes || recipes.length === 0) {
|
||||
log(bot, `You do not have the resources to craft a ${itemName}.`);
|
||||
log(bot, `You do not have the resources to craft a ${itemName}. It requires: ${Object.entries(mc.getItemCraftingRecipes(itemName)[0]).map(([key, value]) => `${key}: ${value}`).join(', ')}.`);
|
||||
if (placedTable) {
|
||||
await collectBlock(bot, 'crafting_table', 1);
|
||||
}
|
||||
|
|
|
@ -171,6 +171,33 @@ export function getInventoryCounts(bot) {
|
|||
}
|
||||
|
||||
|
||||
export function getCraftableItems(bot) {
|
||||
/**
|
||||
* Get a list of all items that can be crafted with the bot's current inventory.
|
||||
* @param {Bot} bot - The bot to get the craftable items for.
|
||||
* @returns {string[]} - A list of all items that can be crafted.
|
||||
* @example
|
||||
* let craftableItems = world.getCraftableItems(bot);
|
||||
**/
|
||||
let table = getNearestBlock(bot, 'crafting_table');
|
||||
if (!table) {
|
||||
for (const item of bot.inventory.items()) {
|
||||
if (item != null && item.name === 'crafting_table') {
|
||||
table = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let res = [];
|
||||
for (const item of mc.getAllItems()) {
|
||||
let recipes = bot.recipesFor(item.id, null, 1, table);
|
||||
if (recipes.length > 0)
|
||||
res.push(item.name);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
export function getPosition(bot) {
|
||||
/**
|
||||
* Get your position in the world (Note that y is vertical).
|
||||
|
|
|
@ -120,7 +120,7 @@ const modes = [
|
|||
update: async function (agent) {
|
||||
const enemy = world.getNearestEntityWhere(agent.bot, entity => mc.isHostile(entity), 16);
|
||||
if (enemy && await world.isClearPath(agent.bot, enemy)) {
|
||||
say(agent, `Aaa! A ${enemy.name}!`);
|
||||
say(agent, `Aaa! A ${enemy.name.replace("_", " ")}!`);
|
||||
execute(this, agent, async () => {
|
||||
await skills.avoidEnemies(agent.bot, 24);
|
||||
});
|
||||
|
@ -162,7 +162,7 @@ const modes = [
|
|||
{
|
||||
name: 'item_collecting',
|
||||
description: 'Collect nearby items when idle.',
|
||||
interrupts: ['followPlayer'],
|
||||
interrupts: ['action:followPlayer'],
|
||||
on: true,
|
||||
active: false,
|
||||
|
||||
|
@ -193,7 +193,7 @@ const modes = [
|
|||
{
|
||||
name: 'torch_placing',
|
||||
description: 'Place torches when idle and there are no torches nearby.',
|
||||
interrupts: ['followPlayer'],
|
||||
interrupts: ['action:followPlayer'],
|
||||
on: true,
|
||||
active: false,
|
||||
cooldown: 5,
|
||||
|
@ -260,9 +260,9 @@ async function execute(mode, agent, func, timeout=-1) {
|
|||
if (agent.self_prompter.on)
|
||||
agent.self_prompter.stopLoop();
|
||||
mode.active = true;
|
||||
let code_return = await agent.coder.execute(async () => {
|
||||
let code_return = await agent.actions.runAction(`mode:${mode.name}`, async () => {
|
||||
await func();
|
||||
}, timeout);
|
||||
}, { timeout });
|
||||
mode.active = false;
|
||||
console.log(`Mode ${mode.name} finished executing, code_return: ${code_return.message}`);
|
||||
}
|
||||
|
@ -328,7 +328,7 @@ class ModeController {
|
|||
this.unPauseAll();
|
||||
}
|
||||
for (let mode of this.modes_list) {
|
||||
let interruptible = mode.interrupts.some(i => i === 'all') || mode.interrupts.some(i => i === this.agent.coder.cur_action_name);
|
||||
let interruptible = mode.interrupts.some(i => i === 'all') || mode.interrupts.some(i => i === this.agent.actions.currentActionLabel);
|
||||
if (mode.on && !mode.paused && !mode.active && (this.agent.isIdle() || interruptible)) {
|
||||
await mode.update(this.agent);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ export class BuildGoal {
|
|||
async wrapSkill(func) {
|
||||
if (!this.agent.isIdle())
|
||||
return false;
|
||||
let res = await this.agent.coder.execute(func);
|
||||
let res = await this.agent.actions.runAction('BuildGoal', func);
|
||||
return !res.interrupted;
|
||||
}
|
||||
|
||||
|
|
|
@ -39,14 +39,14 @@ export class NPCContoller {
|
|||
}
|
||||
|
||||
init() {
|
||||
for (let file of readdirSync('src/agent/npc/construction')) {
|
||||
if (file.endsWith('.json')) {
|
||||
try {
|
||||
try {
|
||||
for (let file of readdirSync('src/agent/npc/construction')) {
|
||||
if (file.endsWith('.json')) {
|
||||
this.constructions[file.slice(0, -5)] = JSON.parse(readFileSync('src/agent/npc/construction/' + file, 'utf8'));
|
||||
} catch (e) {
|
||||
console.log('Error reading construction file: ', file);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Error reading construction file');
|
||||
}
|
||||
|
||||
for (let name in this.constructions) {
|
||||
|
@ -72,7 +72,7 @@ export class NPCContoller {
|
|||
if (!this.agent.isIdle()) return;
|
||||
|
||||
// Persue goal
|
||||
if (!this.agent.coder.resume_func) {
|
||||
if (!this.agent.actions.resume_func) {
|
||||
this.executeNext();
|
||||
this.agent.history.save();
|
||||
}
|
||||
|
@ -104,7 +104,7 @@ export class NPCContoller {
|
|||
|
||||
async executeNext() {
|
||||
if (!this.agent.isIdle()) return;
|
||||
await this.agent.coder.execute(async () => {
|
||||
await this.agent.actions.runAction('npc:moveAway', async () => {
|
||||
await skills.moveAway(this.agent.bot, 2);
|
||||
});
|
||||
|
||||
|
@ -114,7 +114,7 @@ export class NPCContoller {
|
|||
if (building == this.data.home) {
|
||||
let door_pos = this.getBuildingDoor(building);
|
||||
if (door_pos) {
|
||||
await this.agent.coder.execute(async () => {
|
||||
await this.agent.actions.runAction('npc:exitBuilding', async () => {
|
||||
await skills.useDoor(this.agent.bot, door_pos);
|
||||
await skills.moveAway(this.agent.bot, 2); // If the bot is too close to the building it will try to enter again
|
||||
});
|
||||
|
@ -132,13 +132,13 @@ export class NPCContoller {
|
|||
let building = this.currentBuilding();
|
||||
if (this.data.home !== null && (building === null || building != this.data.home)) {
|
||||
let door_pos = this.getBuildingDoor(this.data.home);
|
||||
await this.agent.coder.execute(async () => {
|
||||
await this.agent.actions.runAction('npc:returnHome', async () => {
|
||||
await skills.useDoor(this.agent.bot, door_pos);
|
||||
});
|
||||
}
|
||||
|
||||
// Go to bed
|
||||
await this.agent.coder.execute(async () => {
|
||||
await this.agent.actions.runAction('npc:bed', async () => {
|
||||
await skills.goToBed(this.agent.bot);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -322,7 +322,7 @@ export class ItemGoal {
|
|||
// If the bot has failed to obtain the block before, explore
|
||||
if (this.failed.includes(next.name)) {
|
||||
this.failed = this.failed.filter((item) => item !== next.name);
|
||||
await this.agent.coder.execute(async () => {
|
||||
await this.agent.actions.runAction('itemGoal:explore', async () => {
|
||||
await skills.moveAway(this.agent.bot, 8);
|
||||
});
|
||||
} else {
|
||||
|
@ -339,7 +339,7 @@ export class ItemGoal {
|
|||
|
||||
// Execute the next goal
|
||||
let init_quantity = world.getInventoryCounts(this.agent.bot)[next.name] || 0;
|
||||
await this.agent.coder.execute(async () => {
|
||||
await this.agent.actions.runAction('itemGoal:next', async () => {
|
||||
await next.execute(quantity);
|
||||
});
|
||||
let final_quantity = world.getInventoryCounts(this.agent.bot)[next.name] || 0;
|
||||
|
|
|
@ -13,6 +13,7 @@ import { Local } from '../models/local.js';
|
|||
import { Novita } from '../models/novita.js';
|
||||
import { GroqCloudAPI } from '../models/groq.js';
|
||||
import { HuggingFace } from '../models/huggingface.js';
|
||||
import { Qwen } from "../models/qwen.js";
|
||||
|
||||
export class Prompter {
|
||||
constructor(agent, fp) {
|
||||
|
@ -46,6 +47,8 @@ export class Prompter {
|
|||
chat.api = 'groq';
|
||||
else if (chat.model.includes('novita/'))
|
||||
chat.api = 'novita';
|
||||
else if (chat.model.includes('qwen'))
|
||||
chat.api = 'qwen';
|
||||
else
|
||||
chat.api = 'ollama';
|
||||
}
|
||||
|
@ -69,6 +72,8 @@ export class Prompter {
|
|||
this.chat_model = new HuggingFace(chat.model, chat.url);
|
||||
else if (chat.api === 'novita')
|
||||
this.chat_model = new Novita(chat.model.replace('novita/', ''), chat.url);
|
||||
else if (chat.api === 'qwen')
|
||||
this.chat_model = new Qwen(chat.model, chat.url);
|
||||
else
|
||||
throw new Error('Unknown API:', api);
|
||||
|
||||
|
@ -92,6 +97,8 @@ export class Prompter {
|
|||
this.embedding_model = new ReplicateAPI(embedding.model, embedding.url);
|
||||
else if (embedding.api === 'ollama')
|
||||
this.embedding_model = new Local(embedding.model, embedding.url);
|
||||
else if (embedding.api === 'qwen')
|
||||
this.embedding_model = new Qwen(embedding.model, embedding.url);
|
||||
else {
|
||||
this.embedding_model = null;
|
||||
console.log('Unknown embedding: ', embedding ? embedding.api : '[NOT SPECIFIED]', '. Using word overlap.');
|
||||
|
|
|
@ -87,7 +87,7 @@ export class SelfPrompter {
|
|||
async stop(stop_action=true) {
|
||||
this.interrupt = true;
|
||||
if (stop_action)
|
||||
await this.agent.coder.stop();
|
||||
await this.agent.actions.stop();
|
||||
await this.stopLoop();
|
||||
this.on = false;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,28 @@ export class Gemini {
|
|||
constructor(model_name, url) {
|
||||
this.model_name = model_name;
|
||||
this.url = url;
|
||||
this.safetySettings = [
|
||||
{
|
||||
"category": "HARM_CATEGORY_DANGEROUS",
|
||||
"threshold": "BLOCK_NONE",
|
||||
},
|
||||
{
|
||||
"category": "HARM_CATEGORY_HARASSMENT",
|
||||
"threshold": "BLOCK_NONE",
|
||||
},
|
||||
{
|
||||
"category": "HARM_CATEGORY_HATE_SPEECH",
|
||||
"threshold": "BLOCK_NONE",
|
||||
},
|
||||
{
|
||||
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||
"threshold": "BLOCK_NONE",
|
||||
},
|
||||
{
|
||||
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||
"threshold": "BLOCK_NONE",
|
||||
},
|
||||
];
|
||||
|
||||
this.genAI = new GoogleGenerativeAI(getKey('GEMINI_API_KEY'));
|
||||
}
|
||||
|
@ -14,12 +36,14 @@ export class Gemini {
|
|||
let model;
|
||||
if (this.url) {
|
||||
model = this.genAI.getGenerativeModel(
|
||||
{model: this.model_name || "gemini-pro"},
|
||||
{baseUrl: this.url}
|
||||
{ model: this.model_name || "gemini-1.5-flash" },
|
||||
{ baseUrl: this.url },
|
||||
{ safetySettings: this.safetySettings }
|
||||
);
|
||||
} else {
|
||||
model = this.genAI.getGenerativeModel(
|
||||
{model: this.model_name || "gemini-pro"}
|
||||
{ model: this.model_name || "gemini-1.5-flash" },
|
||||
{ safetySettings: this.safetySettings }
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -39,16 +63,16 @@ export class Gemini {
|
|||
let model;
|
||||
if (this.url) {
|
||||
model = this.genAI.getGenerativeModel(
|
||||
{model: this.model_name || "embedding-001"},
|
||||
{baseUrl: this.url}
|
||||
{ model: "text-embedding-004" },
|
||||
{ baseUrl: this.url }
|
||||
);
|
||||
} else {
|
||||
model = this.genAI.getGenerativeModel(
|
||||
{model: this.model_name || "embedding-001"}
|
||||
{ model: "text-embedding-004" }
|
||||
);
|
||||
}
|
||||
|
||||
const result = await model.embedContent(text);
|
||||
return result.embedding;
|
||||
return result.embedding.values;
|
||||
}
|
||||
}
|
|
@ -55,7 +55,7 @@ export class GPT {
|
|||
|
||||
async embed(text) {
|
||||
const embedding = await this.openai.embeddings.create({
|
||||
model: this.model_name || "text-embedding-ada-002",
|
||||
model: this.model_name || "text-embedding-3-small",
|
||||
input: text,
|
||||
encoding_format: "float",
|
||||
});
|
||||
|
|
104
src/models/qwen.js
Normal file
104
src/models/qwen.js
Normal file
|
@ -0,0 +1,104 @@
|
|||
// This code uses Dashscope and HTTP to ensure the latest support for the Qwen model.
|
||||
// Qwen is also compatible with the OpenAI API format;
|
||||
|
||||
import { getKey } from '../utils/keys.js';
|
||||
|
||||
export class Qwen {
|
||||
constructor(modelName, url) {
|
||||
this.modelName = modelName;
|
||||
this.url = url || 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation';
|
||||
this.apiKey = getKey('QWEN_API_KEY');
|
||||
}
|
||||
|
||||
async sendRequest(turns, systemMessage, stopSeq = '***', retryCount = 0) {
|
||||
if (retryCount > 5) {
|
||||
console.error('Maximum retry attempts reached.');
|
||||
return 'Error: Too many retry attempts.';
|
||||
}
|
||||
|
||||
const data = {
|
||||
model: this.modelName || 'qwen-plus',
|
||||
input: { messages: [{ role: 'system', content: systemMessage }, ...turns] },
|
||||
parameters: { result_format: 'message', stop: stopSeq },
|
||||
};
|
||||
|
||||
// Add default user message if all messages are 'system' role
|
||||
if (turns.every((msg) => msg.role === 'system')) {
|
||||
data.input.messages.push({ role: 'user', content: 'hello' });
|
||||
}
|
||||
|
||||
if (!data.model || !data.input || !data.input.messages || !data.parameters) {
|
||||
console.error('Invalid request data format:', data);
|
||||
throw new Error('Invalid request data format.');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this._makeHttpRequest(this.url, data);
|
||||
const choice = response?.output?.choices?.[0];
|
||||
|
||||
if (choice?.finish_reason === 'length' && turns.length > 0) {
|
||||
return this.sendRequest(turns.slice(1), systemMessage, stopSeq, retryCount + 1);
|
||||
}
|
||||
|
||||
return choice?.message?.content || 'No content received.';
|
||||
} catch (err) {
|
||||
console.error('Error occurred:', err);
|
||||
return 'An error occurred, please try again.';
|
||||
}
|
||||
}
|
||||
|
||||
async embed(text) {
|
||||
if (!text || typeof text !== 'string') {
|
||||
console.error('Invalid embedding input: text must be a non-empty string.');
|
||||
return 'Invalid embedding input: text must be a non-empty string.';
|
||||
}
|
||||
|
||||
const data = {
|
||||
model: 'text-embedding-v2',
|
||||
input: { texts: [text] },
|
||||
parameters: { text_type: 'query' },
|
||||
};
|
||||
|
||||
if (!data.model || !data.input || !data.input.texts || !data.parameters) {
|
||||
console.error('Invalid embedding request data format:', data);
|
||||
throw new Error('Invalid embedding request data format.');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this._makeHttpRequest(this.url, data);
|
||||
const embedding = response?.output?.embeddings?.[0]?.embedding;
|
||||
return embedding || 'No embedding result received.';
|
||||
} catch (err) {
|
||||
console.error('Error occurred:', err);
|
||||
return 'An error occurred, please try again.';
|
||||
}
|
||||
}
|
||||
|
||||
async _makeHttpRequest(url, data) {
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${this.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error(`Request failed, status code ${response.status}: ${response.statusText}`);
|
||||
console.error('Error response content:', errorText);
|
||||
throw new Error(`Request failed, status code ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const responseText = await response.text();
|
||||
try {
|
||||
return JSON.parse(responseText);
|
||||
} catch (err) {
|
||||
console.error('Failed to parse response JSON:', err);
|
||||
throw new Error('Invalid response JSON format.');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
export function checkSafe(code) {
|
||||
const dangerousPatterns = [
|
||||
// Dynamic imports
|
||||
/\bimport\s*\(/,
|
||||
// Access to process and global
|
||||
/\bprocess\b/,
|
||||
/\bglobal\b/,
|
||||
// Module manipulation
|
||||
/\bmodule\b/,
|
||||
/\bexports\b/,
|
||||
// Require usage
|
||||
/\brequire\s*\(/,
|
||||
// Function constructors
|
||||
/\bFunction\s*\(/,
|
||||
/\beval\s*\(/,
|
||||
// Access to __dirname and __filename
|
||||
/\b__dirname\b/,
|
||||
/\b__filename\b/,
|
||||
|
||||
// fetch
|
||||
/\bfetch\s*\(/,
|
||||
// XMLHttpRequest
|
||||
/\bXMLHttpRequest\b/,
|
||||
// Websockets
|
||||
/\bWebSocket\b/,
|
||||
];
|
||||
|
||||
for (const pattern of dangerousPatterns) {
|
||||
if (pattern.test(code)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// generated by o1
|
||||
// Basic check for malicious code like dynamic imports, code exec, disk access, internet access, etc.
|
||||
// Will not catch all, and can be bypassed by obfuscation.
|
|
@ -1,18 +1,14 @@
|
|||
import translate from 'google-translate-api-x';
|
||||
import settings from '../../settings.js';
|
||||
|
||||
const preferred_lang = settings.language;
|
||||
const preferred_lang = String(settings.language).toLowerCase();
|
||||
|
||||
export async function handleTranslation(message) {
|
||||
if (preferred_lang === 'en' || preferred_lang === 'english')
|
||||
return message;
|
||||
try {
|
||||
if (preferred_lang.toLowerCase() === 'en' || preferred_lang.toLowerCase() === 'english') {
|
||||
return message;
|
||||
} else {
|
||||
const lang = String(preferred_lang);
|
||||
|
||||
const translation = await translate(message, { to: lang });
|
||||
return translation.text || message;
|
||||
}
|
||||
const translation = await translate(message, { to: preferred_lang });
|
||||
return translation.text || message;
|
||||
} catch (error) {
|
||||
console.error('Error translating message:', error);
|
||||
return message;
|
||||
|
@ -20,6 +16,8 @@ export async function handleTranslation(message) {
|
|||
}
|
||||
|
||||
export async function handleEnglishTranslation(message) {
|
||||
if (preferred_lang === 'en' || preferred_lang === 'english')
|
||||
return message;
|
||||
try {
|
||||
const translation = await translate(message, { to: 'english' });
|
||||
return translation.text || message;
|
||||
|
|
Loading…
Add table
Reference in a new issue