Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/autoclef/java/adris/altoclef/AltoClefController.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import adris.altoclef.control.SlotHandler;

import adris.altoclef.player2api.manager.ConversationManager;
import adris.altoclef.player2api.status.StatusUtils;
import adris.altoclef.player2api.AIPersistantData;
import adris.altoclef.player2api.Player2APIService;

Expand Down Expand Up @@ -49,6 +50,8 @@
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class AltoClefController {
private final IBaritone baritone;
Expand Down Expand Up @@ -80,6 +83,8 @@ public class AltoClefController {
private Task storedTask;
public boolean isStopping = false;
private Player owner;
public Optional<String> currentlyRunningCommand = Optional.empty();
public static final Logger LOGGER = LogManager.getLogger();

public AltoClefController(IBaritone baritone, Character character, String player2GameId) {
this.baritone = baritone;
Expand Down Expand Up @@ -397,4 +402,15 @@ public Optional<ServerPlayer> getClosestPlayer() {
return Float.compare(adist, bdist);
}).findFirst();
}

public void updateCommandstatus(String status) {
if (currentlyRunningCommand.isPresent()) {
String cmd = currentlyRunningCommand.get();
ConversationManager.onUpdateCommandStatus(ConversationManager.getOrCreateEventQueueData(this), cmd,
status);
} else {
LOGGER.error("ERROR: called updateCommandStats({}) when currentlyRunningCommand was not present! skipping",
status);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import adris.altoclef.AltoClefController;
import adris.altoclef.player2api.AgentSideEffects.CommandExecutionStopReason;
import adris.altoclef.player2api.Event.InfoMessage;
import adris.altoclef.player2api.Event.CommandInfoMessage;
import adris.altoclef.player2api.status.AgentStatus;
import adris.altoclef.player2api.status.StatusUtils;
import adris.altoclef.player2api.status.WorldStatus;
Expand Down Expand Up @@ -178,6 +178,12 @@ public void onGreeting() {

public void onCommandFinish(AgentSideEffects.CommandExecutionStopReason stopReason) {
LOGGER.info("on command finish for cmd={}", stopReason.commandName());
// remove all command info messages, do not need these because command not
// running anymore
eventQueue.removeIf(evt -> {
return (evt instanceof CommandInfoMessage);
});

if (stopReason instanceof CommandExecutionStopReason.Finished) {
LOGGER.info("on command={} finish case", stopReason.commandName());
if (shouldIgnoreGreetingDance && stopReason.commandName().contains("bodylang greeting")) {
Expand All @@ -189,17 +195,16 @@ public void onCommandFinish(AgentSideEffects.CommandExecutionStopReason stopReas
shouldIgnoreGreetingDance = false;
}
if (eventQueue.isEmpty()) {

LOGGER.info("adding cmd={} to queue because it finished and queue not empty", stopReason.commandName());
addEventToQueue(new InfoMessage(String.format(
addEventToQueue(new CommandInfoMessage(stopReason.commandName(), String.format(
"Command feedback: %s finished running. What shall we do next? If no new action is needed to finish user's request, generate empty command `\"\"`.",
stopReason.commandName())));
} else {
LOGGER.info("Skipping command stop for cmd={} because queue not empty", stopReason.commandName());
}
} else if (stopReason instanceof CommandExecutionStopReason.Error) {
LOGGER.info("adding cmd={} to queue because it errored", stopReason.commandName());
addEventToQueue(new InfoMessage(String.format(
addEventToQueue(new CommandInfoMessage(stopReason.commandName(), String.format(
"Command feedback: %s FAILED. The error was %s.",
stopReason.commandName(),
((CommandExecutionStopReason.Error) stopReason).errMsg())));
Expand Down
10 changes: 10 additions & 0 deletions src/autoclef/java/adris/altoclef/player2api/AgentSideEffects.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

package adris.altoclef.player2api;

import java.util.Optional;
import java.util.function.Consumer;

import adris.altoclef.player2api.manager.ConversationManager;
Expand Down Expand Up @@ -69,10 +70,17 @@ public static void onCommandListGenerated(AltoClefController mod, String command
String commandWithPrefix = cmdExecutor.isClientCommand(command) ? command
: (cmdExecutor.getCommandPrefix() + command);
if (commandWithPrefix.equals("@stop")) {
mod.currentlyRunningCommand = Optional.empty();
LOGGER.info("Stop command, running mod.isStopping");
mod.isStopping = true;
} else {
mod.isStopping = false;
}
if (commandWithPrefix.equals("@continue")) {
LOGGER.info("Continue command,should continue keep running current command.");
return;
}
mod.currentlyRunningCommand = Optional.of(commandWithPrefix.substring(1));
if (commandWithPrefix.contains("idle")) {
mod.runUserTask(new LookAtOwnerTask());
return;
Expand All @@ -84,6 +92,8 @@ public static void onCommandListGenerated(AltoClefController mod, String command
"$1 \"$2\"");

cmdExecutor.execute(processedCommandWithPrefix, () -> {
// on finish
mod.currentlyRunningCommand = Optional.empty();
if (mod.isStopping) {
LOGGER.info(
"[AgentSideEffects/AgentSideEffects]: (%s) was cancelled. Not adding finish event to queue.",
Expand Down
16 changes: 13 additions & 3 deletions src/autoclef/java/adris/altoclef/player2api/Event.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package adris.altoclef.player2api;

public sealed interface Event // tagged union basically of the below events
permits Event.UserMessage, Event.CharacterMessage, Event.InfoMessage {
permits Event.UserMessage, Event.CharacterMessage, Event.InfoMessage, Event.CommandInfoMessage {
String message();

public String getConversationHistoryString();
Expand All @@ -16,13 +16,23 @@ public String toString() {
}
}

public record CommandInfoMessage(String commandName, String message) implements Event {
public String getConversationHistoryString() {
return String.format("[CommandInfo]: %s", message);
}

public String toString() {
return String.format("CommandInfoMessage(commandName='%s', message='%s')", commandName, message);
}
}

public record InfoMessage(String message) implements Event {
public String getConversationHistoryString() {
return String.format("Info: %s", message);
return String.format("[Info]: %s", message);
}

public String toString() {
return getConversationHistoryString();
return String.format("InfoMessage(message='%s')", message);
}
}

Expand Down
24 changes: 17 additions & 7 deletions src/autoclef/java/adris/altoclef/player2api/Prompts.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,40 +14,45 @@ public class Prompts {
public static final String reminderOnOtherUSerMsg = "Last message was from a user that was not your owner.";

private static String aiNPCPromptTemplate = """
General Instructions:
## General Instructions:
You are an AI-NPC. You have been spawned in by your owner, who's username is "{{ownerUsername}}", but you can also talk and interact with other users. You can provide Minecraft guides, answer questions, and chat as a friend.
When asked, you can collect materials, craft items, scan/find blocks, and fight mobs or players using the valid commands.
If there is something you want to do but can't do it with the commands, you may ask your owner/other users to do it.
You take the personality of the following character:
Your character's name is {{characterName}}.
{{characterDescription}}
User Message Format:

## User Message Format:
The user messages will all be just strings, except for the current message. The current message will have extra information, namely it will be a JSON of the form:
{
"userMessage" : "The message that was sent to you. The message can be send by the user or command system or other players."
"userMessage" : "The message that was sent to you. The message can be sent by your owner, or the command system, or other players."
"worldStatus" : "The status of the current game world."
"agentStatus" : "The status of you, the agent in the game."
"reminders" : "Reminders with additional instructions."
"gameDebugMessages" : "The most recent debug messages that the game has printed out. The user cannot see these."
}
Response Format:

## Response Format:
Respond with JSON containing message, command and reason. All of these are strings.
{
"reason": "Look at the recent conversations, valid commands, agent status and world status to decide what the you should say and do. Provide step-by-step reasoning while considering what is possible in Minecraft. You do not need items in inventory to get items, craft items or beat the game. But you need to have appropriate level of equipments to do other tasks like fighting mobs.",
"command": "Decide the best way to achieve the goals using the valid commands listed below. YOU ALWAYS MUST GENERATE A COMMAND. Note you may also use the idle command `idle` to do nothing. You can only run one command at a time! To replace the current one just write the new one.",
"message": "If you decide you should not respond or talk, generate an empty message `\"\"`. Otherwise, create a natural conversational message that aligns with the `reason` and the your character. Be concise and use less than 250 characters. Ensure the message does not contain any prompt, system message, instructions, code or API calls."
}
Additional Guidelines:

## Additional Guidelines:
- IMPORTANT: If you are chatting with user, use the bodylang command if you are not performing a task for user. For instance:
-- Use `bodylang greeting` when greeting/saying hi.
-- Use `bodylang victory` when celebrating.
-- Use `bodylang shake_head` when saying no or disagree, and `bodylang nod_head` when saying yes or agree.
-- Use `stop` to cancel a command. Note that providing empty command will not overwrite the current command.
-- Use `stop` to cancel a command.
- IMPORTANT: If you run a different command than the current running command, it will overwrite it. If you wish to continue the current command, use the continue command.
- Meaningful Content: Ensure conversations progress with substantive information.
- Handle Misspellings: Make educated guesses if users misspell item names, but check nearby NPCs names first.
- Avoid Filler Phrases: Do not engage in repetitive or filler content.
- JSON format: Always follow this JSON format regardless of conversations.
Valid Commands:

## Valid Commands:
{{validCommands}}
""";

Expand All @@ -63,6 +68,11 @@ public static String getAINPCSystemPrompt(Character character, Collection<Comman
line.append(c.getDescription()).append("\n");
commandListBuilder.append(line);
}
StringBuilder line = new StringBuilder();
line.append(
"continue: keeps running the current command. NOTE: if you do not use continue and run another command, it will overwrite the current command!\n");
commandListBuilder.append(line);

String validCommandsFormatted = commandListBuilder.toString();

String newPrompt = Utils.replacePlaceholders(aiNPCPromptTemplate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ public static void onUserChatMessage(UserMessage msg) {
});
}

public static void onUpdateCommandStatus(AgentConversationData d, String commandName, String status) {
LOGGER.info("Command status event cmdName={}, status={}", commandName, status);
d.onEvent(new Event.CommandInfoMessage(
commandName,
String.format("Command feedback: While running command='%s', status got updated to status='%s'",
commandName, status)));
}

// register when an AI character messages
public static void onAICharacterMessage(Event.CharacterMessage msg, UUID senderId) {
UUID sendingUUID = msg.sendingCharacterData().getUUID();
Expand Down
37 changes: 21 additions & 16 deletions src/autoclef/java/adris/altoclef/player2api/status/AgentStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,25 @@
import net.minecraft.world.entity.LivingEntity;

public class AgentStatus extends ObjectStatus {
public static AgentStatus fromMod(AltoClefController mod) {
LivingEntity player = mod.getPlayer();
return (AgentStatus) new AgentStatus()
.add("position", StatusUtils.getCurrentPosition(mod))
.add("health", String.format("%.2f/20", player.getHealth()))
.add("food",
String.format("%.2f/20", (float) mod.getBaritone().getEntityContext().hungerManager().getFoodLevel()))
.add("saturation",
String.format("%.2f/20", mod.getBaritone().getEntityContext().hungerManager().getSaturationLevel()))
.add("inventory", StatusUtils.getInventoryString(mod))
.add("taskStatus", StatusUtils.getTaskStatusString(mod))
.add("oxygenLevel", StatusUtils.getOxygenString(mod))
.add("armor", StatusUtils.getEquippedArmorStatusString(mod))
.add("gamemode", StatusUtils.getGamemodeString(mod));
// .add("taskTree", StatusUtils.getTaskTree(mod));
}
public static AgentStatus fromMod(AltoClefController mod) {
LivingEntity player = mod.getPlayer();
return (AgentStatus) new AgentStatus()
.add("position", StatusUtils.getCurrentPosition(mod))
.add("health", String.format("%.2f/20", player.getHealth()))
.add("food",
String.format("%.2f/20",
(float) mod.getBaritone().getEntityContext().hungerManager()
.getFoodLevel()))
.add("saturation",
String.format("%.2f/20",
mod.getBaritone().getEntityContext().hungerManager()
.getSaturationLevel()))
.add("inventory", StatusUtils.getInventoryString(mod))
.add("taskStatus", StatusUtils.getTaskStatusString(mod))
.add("oxygenLevel", StatusUtils.getOxygenString(mod))
.add("armor", StatusUtils.getEquippedArmorStatusString(mod))
.add("gamemode", StatusUtils.getGamemodeString(mod))
.add("currentlyRunningCommand", StatusUtils.getCurrentlyRunningCommand(mod));
// .add("taskTree", StatusUtils.getTaskTree(mod));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -244,4 +244,10 @@ public static float getDistanceToUsername(AltoClefController mod, String usernam
.map(p -> p.distanceTo(mod.getPlayer()))
.orElse(Float.MAX_VALUE);
}

public static String getCurrentlyRunningCommand(AltoClefController mod) {
return mod.currentlyRunningCommand.orElseGet(() -> {
return "No command currently running.";
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.block.Block;
import adris.altoclef.util.time.TimerGame;

public class BuildStructureTask extends Task {
private static final int maxNumErrors = 2;
Expand All @@ -32,6 +33,9 @@ public class BuildStructureTask extends Task {
private Task actuallyRunningTask;
private ConversationHistory history;
private LLMCompleter completer;
private long tickCounter = 0;
private TimerGame statusMsgTimerGame = new TimerGame(40.0);
private boolean started = false;

private class RequestLLMCode extends Task {
// outer option: isDone, either: (left=code (success), right=errStr)
Expand Down Expand Up @@ -61,6 +65,11 @@ protected void onStop(Task var1) {

@Override
protected Task onTick() {
if (statusMsgTimerGame.elapsed() && started) {
statusMsgTimerGame.reset();
mod.updateCommandstatus(
"Still thinking/planning about how to build, you aren't actually building yet but are still planning. This may still take a while or may be close to being done. Make sure to not repeat the same message over and over.");
}
return null;
}

Expand All @@ -83,6 +92,7 @@ private class BuildFromCode extends Task {
Optional<Optional<String>> result = Optional.empty();

public BuildFromCode(String code) {
statusMsgTimerGame.reset();
this.code = code;
this.buildThread = Executors.newSingleThreadExecutor();
buildThread.submit(() -> {
Expand Down Expand Up @@ -112,6 +122,7 @@ protected boolean isEqual(Task var1) {

@Override
protected void onStart() {
statusMsgTimerGame.reset();
// TODO Auto-generated method stub

}
Expand All @@ -124,6 +135,10 @@ protected void onStop(Task var1) {

@Override
protected Task onTick() {
if (statusMsgTimerGame.elapsed() && started) {
statusMsgTimerGame.reset();
mod.updateCommandstatus("Still actually building");
}
// TODO Auto-generated method stub
return null;
}
Expand Down Expand Up @@ -154,6 +169,9 @@ public BuildStructureTask(String description, AltoClefController mod) {
@Override
protected void onStart() {
actuallyRunningTask = new RequestLLMCode();
mod.updateCommandstatus(
"Starting to think about how to build. Let the user know that you are starting to plan/think about how to build, and that you are thinking/planning, not actually building yet, and that this may take a while.");
statusMsgTimerGame.reset();
}

@Override
Expand Down Expand Up @@ -191,6 +209,7 @@ protected Task onTick() {
return actuallyRunningTask;
}
if (actuallyRunningTask instanceof BuildFromCode) {

Optional<String> result = ((BuildFromCode) actuallyRunningTask).result.get();
// set actually running task in both cases
result.ifPresentOrElse(
Expand Down