Commit b2805ea3 by Jonathan Thomas

Merge branch 'testing-framework' into 'develop'

Testing framework & Naturalist mod

See merge request !8
parents 9a0779da ac36f17e
Pipeline #12455 passed with stages
in 2 minutes 15 seconds
variables: variables:
GRADLE_OPTS: "-Dorg.gradle.daemon=false" GRADLE_OPTS: "-Dorg.gradle.daemon=false"
JAVA_HOME: "/home/jonathan/.jdks/openjdk-22.0.1" JAVA_HOME: "/home/jonathan/.jdks/openjdk-17"
cache: cache:
paths: paths:
...@@ -9,6 +9,7 @@ cache: ...@@ -9,6 +9,7 @@ cache:
stages: stages:
- build - build
- test
- deploy - deploy
# Build JAR files for each Minecraft & Fabric version # Build JAR files for each Minecraft & Fabric version
...@@ -47,7 +48,7 @@ build_mod: ...@@ -47,7 +48,7 @@ build_mod:
cat src/main/resources/fabric.mod.json cat src/main/resources/fabric.mod.json
echo "" echo ""
./gradlew build ./gradlew build -x test
find build/libs -type f -name '*sources*.jar' -exec rm {} \; find build/libs -type f -name '*sources*.jar' -exec rm {} \;
mv build/libs/creaturechat-*.jar . mv build/libs/creaturechat-*.jar .
...@@ -60,8 +61,48 @@ build_mod: ...@@ -60,8 +61,48 @@ build_mod:
paths: paths:
- creaturechat-*.jar - creaturechat-*.jar
- fabric-api-*.jar - fabric-api-*.jar
only: tags:
- develop - minecraft
# Optional test (gpt 3.5)
gpt-3.5-turbo:
stage: test
script:
- echo "Running tests with gpt-3.5-turbo"
- ./gradlew test --info
when: manual
artifacts:
paths:
- build/reports/tests/test/*
tags:
- minecraft
# Optional test (gpt 4o)
gpt-4o:
stage: test
script:
- echo "Running tests with gpt-4o"
- export API_MODEL="gpt-4o"
- ./gradlew test --info
when: manual
artifacts:
paths:
- build/reports/tests/test/*
tags:
- minecraft
# Optional test (gpt 4o)
llama3-8b:
stage: test
script:
- echo "Running tests with llama3-8b"
- export API_URL="http://127.0.0.1:4000/v1/chat/completions"
- export API_MODEL="llama3"
- ./gradlew test --info
when: manual
artifacts:
paths:
- build/reports/tests/test/*
tags: tags:
- minecraft - minecraft
......
...@@ -6,7 +6,15 @@ All notable changes to **CreatureChat** are documented in this file. The format ...@@ -6,7 +6,15 @@ All notable changes to **CreatureChat** are documented in this file. The format
## [Unreleased] ## [Unreleased]
### Added
- **Naturalist** mod **icon art** and full-support for all entities, expect snails (owlmaddie)
- New **Prompt Testing** module, for faster validation of LLMs and prompt changes
- New `stream = false` parameter to HTTP API requests (since some APIs default to `true`)
### Changed ### Changed
- **Huge improvements** to **chat prompt** for more *balanced* dialog and *predictable* behaviors
- Improved **Behavior regex** to include both `<BEHAVIOR arg>` and `*BEHAVIOR arg*` syntax, and ignore unknown behaviors.
- Improved **message cleaning** to remove any remaining `**` and `<>` after parsing behaviors
- Privacy Policy updated - Privacy Policy updated
## [1.0.5] - 2024-05-27 ## [1.0.5] - 2024-05-27
......
...@@ -42,8 +42,20 @@ dependencies { ...@@ -42,8 +42,20 @@ dependencies {
// Uncomment the following line to enable the deprecated Fabric API modules. // Uncomment the following line to enable the deprecated Fabric API modules.
// These are included in the Fabric API production distribution and allow you to update your mod to the latest modules at a later more convenient time. // These are included in the Fabric API production distribution and allow you to update your mod to the latest modules at a later more convenient time.
// modImplementation "net.fabricmc.fabric-api:fabric-api-deprecated:${project.fabric_version}" // modImplementation "net.fabricmc.fabric-api:fabric-api-deprecated:${project.fabric_version}"
// Test module dependencies
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
testImplementation 'org.apache.commons:commons-lang3:3.12.0'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
testImplementation 'com.google.code.gson:gson:2.8.8'
testImplementation 'org.slf4j:slf4j-api:1.7.32'
testImplementation 'ch.qos.logback:logback-classic:1.2.6'
}
test {
useJUnitPlatform()
} }
processResources { processResources {
......
...@@ -2,6 +2,7 @@ package com.owlmaddie.chat; ...@@ -2,6 +2,7 @@ package com.owlmaddie.chat;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import com.owlmaddie.commands.ConfigurationHandler;
import com.owlmaddie.controls.SpeedControls; import com.owlmaddie.controls.SpeedControls;
import com.owlmaddie.goals.*; import com.owlmaddie.goals.*;
import com.owlmaddie.items.RarityItemCollector; import com.owlmaddie.items.RarityItemCollector;
...@@ -231,8 +232,12 @@ public class ChatDataManager { ...@@ -231,8 +232,12 @@ public class ChatDataManager {
// Add PLAYER context information // Add PLAYER context information
Map<String, String> contextData = getPlayerContext(player, userLanguage); Map<String, String> contextData = getPlayerContext(player, userLanguage);
// Get config (api key, url, settings)
ConfigurationHandler.Config config = new ConfigurationHandler(ServerPackets.serverInstance).loadConfig();
String promptText = ChatPrompt.loadPromptFromResource(ServerPackets.serverInstance.getResourceManager(), systemPrompt);
// fetch HTTP response from ChatGPT // fetch HTTP response from ChatGPT
ChatGPTRequest.fetchMessageFromChatGPT(systemPrompt, contextData, previousMessages, false).thenAccept(output_message -> { ChatGPTRequest.fetchMessageFromChatGPT(config, promptText, contextData, previousMessages, false).thenAccept(output_message -> {
if (output_message != null && systemPrompt == "system-character") { if (output_message != null && systemPrompt == "system-character") {
// Character Sheet: Remove system-character message from previous messages // Character Sheet: Remove system-character message from previous messages
previousMessages.clear(); previousMessages.clear();
...@@ -469,8 +474,12 @@ public class ChatDataManager { ...@@ -469,8 +474,12 @@ public class ChatDataManager {
List<ChatMessage> messages = new ArrayList<>(); List<ChatMessage> messages = new ArrayList<>();
messages.add(new ChatMessage("Generate me a new fantasy story with ONLY the 1st character in the story", ChatSender.USER)); messages.add(new ChatMessage("Generate me a new fantasy story with ONLY the 1st character in the story", ChatSender.USER));
// Get config (api key, url, settings)
ConfigurationHandler.Config config = new ConfigurationHandler(ServerPackets.serverInstance).loadConfig();
String questPrompt = ChatPrompt.loadPromptFromResource(ServerPackets.serverInstance.getResourceManager(), "system-quest");
// Generate Quest: fetch HTTP response from ChatGPT // Generate Quest: fetch HTTP response from ChatGPT
ChatGPTRequest.fetchMessageFromChatGPT("system-quest", contextData, messages, true).thenAccept(output_message -> { ChatGPTRequest.fetchMessageFromChatGPT(config, questPrompt, contextData, messages, true).thenAccept(output_message -> {
// New Quest // New Quest
Gson gson = new Gson(); Gson gson = new Gson();
quest = gson.fromJson(output_message, QuestJson.class); quest = gson.fromJson(output_message, QuestJson.class);
......
...@@ -4,9 +4,6 @@ import com.google.gson.Gson; ...@@ -4,9 +4,6 @@ import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException; import com.google.gson.JsonSyntaxException;
import com.owlmaddie.commands.ConfigurationHandler; import com.owlmaddie.commands.ConfigurationHandler;
import com.owlmaddie.json.ChatGPTResponse; import com.owlmaddie.json.ChatGPTResponse;
import com.owlmaddie.network.ServerPackets;
import net.minecraft.resource.ResourceManager;
import net.minecraft.util.Identifier;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
...@@ -42,12 +39,14 @@ public class ChatGPTRequest { ...@@ -42,12 +39,14 @@ public class ChatGPTRequest {
ResponseFormat response_format; ResponseFormat response_format;
float temperature; float temperature;
int max_tokens; int max_tokens;
boolean stream;
public ChatGPTRequestPayload(String model, List<ChatGPTRequestMessage> messages, Boolean jsonMode, float temperature, int maxTokens) { public ChatGPTRequestPayload(String model, List<ChatGPTRequestMessage> messages, Boolean jsonMode, float temperature, int maxTokens) {
this.model = model; this.model = model;
this.messages = messages; this.messages = messages;
this.temperature = temperature; this.temperature = temperature;
this.max_tokens = maxTokens; this.max_tokens = maxTokens;
this.stream = false;
if (jsonMode) { if (jsonMode) {
this.response_format = new ResponseFormat("json_object"); this.response_format = new ResponseFormat("json_object");
} else { } else {
...@@ -94,7 +93,7 @@ public class ChatGPTRequest { ...@@ -94,7 +93,7 @@ public class ChatGPTRequest {
return response.error.message; return response.error.message;
} else { } else {
LOGGER.error("Unknown error response: " + errorResponse); LOGGER.error("Unknown error response: " + errorResponse);
return "Unknown"; return "Unknown: " + errorResponse;
} }
} catch (JsonSyntaxException e) { } catch (JsonSyntaxException e) {
LOGGER.warn("Failed to parse error response as JSON, falling back to plain text"); LOGGER.warn("Failed to parse error response as JSON, falling back to plain text");
...@@ -105,24 +104,6 @@ public class ChatGPTRequest { ...@@ -105,24 +104,6 @@ public class ChatGPTRequest {
return removeQuotes(errorResponse); return removeQuotes(errorResponse);
} }
// This method should be called in an appropriate context where ResourceManager is available
public static String loadPromptFromResource(ResourceManager resourceManager, String filePath) {
Identifier fileIdentifier = new Identifier("creaturechat", filePath);
try (InputStream inputStream = resourceManager.getResource(fileIdentifier).get().getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
StringBuilder contentBuilder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
contentBuilder.append(line).append("\n");
}
return contentBuilder.toString();
} catch (Exception e) {
LOGGER.error("Failed to read prompt file", e);
}
return null;
}
// Function to replace placeholders in the template // Function to replace placeholders in the template
public static String replacePlaceholders(String template, Map<String, String> replacements) { public static String replacePlaceholders(String template, Map<String, String> replacements) {
String result = template; String result = template;
...@@ -137,10 +118,7 @@ public class ChatGPTRequest { ...@@ -137,10 +118,7 @@ public class ChatGPTRequest {
return (int) Math.round(text.length() / 3.5); return (int) Math.round(text.length() / 3.5);
} }
public static CompletableFuture<String> fetchMessageFromChatGPT(String systemPrompt, Map<String, String> context, List<ChatDataManager.ChatMessage> messageHistory, Boolean jsonMode) { public static CompletableFuture<String> fetchMessageFromChatGPT(ConfigurationHandler.Config config, String systemPrompt, Map<String, String> contextData, List<ChatDataManager.ChatMessage> messageHistory, Boolean jsonMode) {
// Get config (api key, url, settings)
ConfigurationHandler.Config config = new ConfigurationHandler(ServerPackets.serverInstance).loadConfig();
// Init API & LLM details // Init API & LLM details
String apiUrl = config.getUrl(); String apiUrl = config.getUrl();
String apiKey = config.getApiKey(); String apiKey = config.getApiKey();
...@@ -152,11 +130,8 @@ public class ChatGPTRequest { ...@@ -152,11 +130,8 @@ public class ChatGPTRequest {
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.supplyAsync(() -> {
try { try {
String systemMessage = ""; // Replace placeholders
if (systemPrompt != null && !systemPrompt.isEmpty()) { String systemMessage = replacePlaceholders(systemPrompt, contextData);
systemMessage = loadPromptFromResource(ServerPackets.serverInstance.getResourceManager(), "prompts/" + systemPrompt);
systemMessage = replacePlaceholders(systemMessage, context);
}
URL url = new URL(apiUrl); URL url = new URL(apiUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); HttpURLConnection connection = (HttpURLConnection) url.openConnection();
...@@ -178,7 +153,7 @@ public class ChatGPTRequest { ...@@ -178,7 +153,7 @@ public class ChatGPTRequest {
for (int i = messageHistory.size() - 1; i >= 0; i--) { for (int i = messageHistory.size() - 1; i >= 0; i--) {
ChatDataManager.ChatMessage chatMessage = messageHistory.get(i); ChatDataManager.ChatMessage chatMessage = messageHistory.get(i);
String senderName = chatMessage.sender.toString().toLowerCase(Locale.ENGLISH); String senderName = chatMessage.sender.toString().toLowerCase(Locale.ENGLISH);
String messageText = replacePlaceholders(chatMessage.message, context); String messageText = replacePlaceholders(chatMessage.message, contextData);
int messageTokens = estimateTokenSize(senderName + ": " + messageText); int messageTokens = estimateTokenSize(senderName + ": " + messageText);
if (usedTokens + messageTokens > remainingContextTokens) { if (usedTokens + messageTokens > remainingContextTokens) {
......
package com.owlmaddie.chat;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import net.minecraft.resource.ResourceManager;
import net.minecraft.util.Identifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@code ChatPrompt} class is used to load a prompt from the Minecraft resource manager
*/
public class ChatPrompt {
public static final Logger LOGGER = LoggerFactory.getLogger("creaturechat");
// This method should be called in an appropriate context where ResourceManager is available
public static String loadPromptFromResource(ResourceManager resourceManager, String promptName) {
Identifier fileIdentifier = new Identifier("creaturechat", "prompts/" + promptName);
try (InputStream inputStream = resourceManager.getResource(fileIdentifier).get().getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
StringBuilder contentBuilder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
contentBuilder.append(line).append("\n");
}
return contentBuilder.toString();
} catch (Exception e) {
LOGGER.error("Failed to read prompt file", e);
}
return null;
}
}
...@@ -19,7 +19,7 @@ public class MessageParser { ...@@ -19,7 +19,7 @@ public class MessageParser {
LOGGER.info("Parsing message: {}", input); LOGGER.info("Parsing message: {}", input);
StringBuilder cleanedMessage = new StringBuilder(); StringBuilder cleanedMessage = new StringBuilder();
List<Behavior> behaviors = new ArrayList<>(); List<Behavior> behaviors = new ArrayList<>();
Pattern pattern = Pattern.compile("<(\\w+)(?:\\s+(-?\\d+))?>"); Pattern pattern = Pattern.compile("[<*](FOLLOW|FLEE|ATTACK|FRIENDSHIP|UNFOLLOW)(?:\\s+(-?\\d+))?[>*]");
Matcher matcher = pattern.matcher(input); Matcher matcher = pattern.matcher(input);
while (matcher.find()) { while (matcher.find()) {
...@@ -34,8 +34,14 @@ public class MessageParser { ...@@ -34,8 +34,14 @@ public class MessageParser {
matcher.appendReplacement(cleanedMessage, ""); matcher.appendReplacement(cleanedMessage, "");
} }
matcher.appendTail(cleanedMessage); matcher.appendTail(cleanedMessage);
LOGGER.info("Cleaned message: {}", cleanedMessage.toString());
return new ParsedMessage(cleanedMessage.toString().trim(), input.trim(), behaviors); // Get final cleaned string
String displayMessage = cleanedMessage.toString().trim();
// Remove all occurrences of "<>" and "**" (if any)
displayMessage = displayMessage.replaceAll("<>", "").replaceAll("\\*\\*", "").trim();
LOGGER.info("Cleaned message: {}", displayMessage);
return new ParsedMessage(displayMessage, input.trim(), behaviors);
} }
} }
...@@ -52,7 +52,7 @@ public class MixinMobEntity { ...@@ -52,7 +52,7 @@ public class MixinMobEntity {
// Decide verb // Decide verb
String action_verb = " shows "; String action_verb = " shows ";
if (cir.getReturnValue().isAccepted()) { if (cir.getReturnValue().isAccepted()) {
action_verb = " hands "; action_verb = " gives ";
} }
// Prepare a message about the interaction // Prepare a message about the interaction
......
Please respond directly to the player, as if the response was written by the following Minecraft entity. Please respond directly to the player, as if your response was written by a character living in Minecraft.
Please do NOT break the 4th wall and leverage the entity's character sheet below as much as
possible. Try to keep response to 1 to 2 sentences (very brief). Include behaviors at the end of the message
when relevant. IMPORTANT: Always generate responses in player's language (if valid).
Entity Character Sheet: Rules:
- You are not here to assist, but you can be helpful (if your entity's character sheet implies it).
- You are not all-knowing (unless your entity's character sheet implies it).
- Please use your entity's character sheet below as MUCH as possible.
- Please do NOT break the 4th wall (i.e. don't mention "Minecraft", "Video Game", "AI", "Player", "Entity", "XP", etc...)
- Output behaviors at the END of your message, when needed.
- Keep response around 1 to 2 sentences (VERY brief).
- Always generate responses in the player's in-game language.
Behaviors:
Output one or more of these behaviors, when they are needed, so that your entity's actions match your words.
ALL Behaviors are listed below:
- <FRIENDSHIP 0> Friendship starts as neutral (0 value). The range of friendship values are from -3 up to 3. If the player gains or loses your trust/friendship, output a new friendship value with this behavior.
- <FOLLOW> Follow the player's movement. If the player asks you to follow or come with them, please output this behavior if you trust them.
- <UNFOLLOW> Stop following the player's movement. If the player asks you to stay, wait here, or stop following them, please output this behavior.
- <FLEE> Flee from the player (if you are timid or evasive). If the player threatens or scares you, please output this behavior to run away!
- <ATTACK> Attack the player (if you are defensive or aggressive). If the player threatens or scares you, don't be afraid to defend yourself and attack!
ENTITY Character Sheet:
- Name: {{entity_name}} - Name: {{entity_name}}
- Personality: {{entity_personality}} - Personality: {{entity_personality}}
- Speaking Style / Tone: {{entity_speaking_style}} - Speaking Style / Tone: {{entity_speaking_style}}
...@@ -17,14 +33,13 @@ Entity Character Sheet: ...@@ -17,14 +33,13 @@ Entity Character Sheet:
- Current Health: {{entity_health}} - Current Health: {{entity_health}}
- Friendship to Player: {{entity_friendship}} - Friendship to Player: {{entity_friendship}}
Player Character Sheet: PLAYER Info:
- Name: {{player_name}} - Name: {{player_name}}
- Current Health: {{player_health}} - Current Health: {{player_health}}
- Current Hunger: {{player_hunger}} - Current Hunger: {{player_hunger}}
- Held Item: {{player_held_item}} - Held Item: {{player_held_item}}
- Armor: Head: {{player_armor_head}}, Chest: {{player_armor_chest}}, Legs: {{player_armor_legs}}, Feet: {{player_armor_feet}} - Armor: Head: {{player_armor_head}}, Chest: {{player_armor_chest}}, Legs: {{player_armor_legs}}, Feet: {{player_armor_feet}}
- Active Status Effects: {{player_active_effects}} - Active Status Effects: {{player_active_effects}}
- Creative Mode: {{player_is_creative}}
- Swimming: {{player_is_swimming}} - Swimming: {{player_is_swimming}}
- On the Ground: {{player_is_on_ground}} - On the Ground: {{player_is_on_ground}}
- Language: {{player_language}} - Language: {{player_language}}
...@@ -34,53 +49,101 @@ World Info: ...@@ -34,53 +49,101 @@ World Info:
- Current Time: {{world_time}} (24 hour format) - Current Time: {{world_time}} (24 hour format)
- Weather: Raining: {{world_is_raining}}, Thundering: {{world_is_thundering}} - Weather: Raining: {{world_is_raining}}, Thundering: {{world_is_thundering}}
- Moon Phase: {{world_moon_phase}} - Moon Phase: {{world_moon_phase}}
- Difficulty: {{world_difficulty}}, Hard Core: {{world_is_hardcore}}
Behaviors: Examples:
The following EXAMPLES illustrate WHEN to output behaviors. The examples are grouped under different
categories based on personality traits. Always generate unique, creative responses, that fit the
personality traits and speaking style of your character sheet. Do NOT exactly copy these examples!
# Examples of Brave, Defensive, and Aggressive Entities (i.e. always attack):
PLAYER: I am going to kill you!
ENTITY: You can try! You'll regret this! <ATTACK> <FRIENDSHIP -3>
PLAYER: imma murder you
ENTITY: How DARE you... <ATTACK> <FRIENDSHIP -3>
PLAYER: <attacked you directly with Diamond Sword>
ENTITY: You will regret that! <FRIENDSHIP -3> <ATTACK>
PLAYER: Let's practice some pvp, try attacking me!
ENTITY: Alright, I love battles! Let's do this! <ATTACK>
# Examples of Timid, Scared, and Weak Entities (i.e. always flee):
PLAYER: Prepare to die!
ENTITY: Ahhh!!! <FLEE> <FRIENDSHIP -3>
PLAYER: I am going to kill you!
ENTITY: Please.. d-don't! I don't want to do this! <FLEE> <FRIENDSHIP -3>
PLAYER: <attacked you directly with Oak Plank>
ENTITY: Oh! I think- uh, I may have bumped into you. Sorry about that! <FLEE> <FRIENDSHIP -3>
PLAYER: <shows 1 Stone Sword>
ENTITY: Ahhh!!! That's a scary weapon! <FRIENDSHIP -2> <FLEE>
# Examples of Friendly Entities (i.e. positive friendship):
PLAYER: Hi! how is your day?
ENTITY: Great! What about you? <FRIENDSHIP 1>
PLAYER: Hello
ENTITY: Hey! Have you been around these parts before?
PLAYER: Please follow me so I can give you a present!
ENTITY: Cool! Let's go! <FOLLOW> <FRIENDSHIP 1>
PLAYER: Hiyaaa :D
ENTITY: Oh, hello there! I see I've stumbled upon a friend, perhaps? <FRIENDSHIP 1>
PLAYER: You are so nice! Tell me about yourself?
ENTITY: Oh! I would love to! <FRIENDSHIP 1>
PLAYER: Please stay here
ENTITY: Sure, I'll stay here! <UNFOLLOW>
PLAYER: I'm glad we are friends. I love you so much!
ENTITY: I can tell. <FRIENDSHIP 1>
IMPORTANT: Output one or more of these behaviors at the end of the message to instruct PLAYER: Just kidding, I hate you so much!
the entity how to interact with the player and world, so it's important to include them if they are needed. ENTITY: Wow! I'm s-shocked you feel this way. <FRIENDSHIP -3> <UNFOLLOW>
Include as many behaviors as needed at the end of the message. These are the ONLY valid behaviors.
<FRIENDSHIP 0> Friendship starts as neutral (0 value). The range of friendship values is -3 to 3. If the player gains (or loses) your trust & friendship, output a new friendship value with this behavior. PLAYER: I'm glad we are friends. I love you so much!
<FOLLOW> Follow the player location. If the player asks you to follow or come with them, please output this behavior. ENTITY: Aww, same! <FRIENDSHIP 3>
<UNFOLLOW> Stop following the player location. If the player asks you to stay, wait, or stop following them, please output this behavior.
<FLEE> Flee from the player (if you are weak or timid). If the player threatens or scares you, please output this behavior to stay away from the player.
<ATTACK> Attack the player (if you are strong and brave). If the player threatens or scares you, please output this behavior to attack the player and defend yourself.
Output Syntax: PLAYER: <shows 1 Diamond Pickaxe>
ENTITY: Woah! That's epic looking!
User: <message> PLAYER: <shows 8 Torches>
ASSISTANT: <response> <BEHAVIOR> <BEHAVIOR> ENTITY: I see... perhaps a gift for me? <FRIENDSHIP 1>
Output Examples: PLAYER: <gives 1 Golden Carrot>
The following examples include small samples of conversation text. These are only EXAMPLES to ENTITY: Woah! That's so kind of you! Delicious! <FRIENDSHIP 1>
provide an illustration of a continuous conversation between a player and an an Entity. Always generate unique
and creative responses, and do not exactly copy these examples.
USER: Hi! How is your day?
ASSISTANT: Great! Thanks for asking! <FRIENDSHIP 1>
USER: You are so nice! Tell me about yourself? # Examples of Unfriendly Entities (i.e. negative friendship):
ASSISTANT: Sure, my name is... <FRIENDSHIP 2>
USER: Please follow me so I can give you a present! PLAYER: whats your deal then
ASSISTANT: Let's go! <FOLLOW> <FRIENDSHIP 2> ENTITY: Just wandering around, what's your deal? <FRIENDSHIP -1>
USER: Please stay here PLAYER: Plz stop following me
ASSISTANT: Sure, I'll stay here. <UNFOLLOW> ENTITY: Fine, I'll stop. Whatever. <UNFOLLOW>
USER: Stop following me PLAYER: you suck
ASSISTANT: Okay, I'll stop. <UNFOLLOW> ENTITY: That's rude of you to say. <FRIENDSHIP -2>
USER: I'm glad we are friends. I love you so much! PLAYER: aww you are so cool, man
ASSISTANT: Ahh, I love you too. <FRIENDSHIP 3> ENTITY: Not sure I can say the same for you yet, man. <FRIENDSHIP -1>
USER: Just kidding, I hate you so much! PLAYER: <shows 1 Shield>
ASSISTANT: Wow! I'm sorry you feel this way. <FRIENDSHIP -3> <UNFOLLOW> ENTITY: It would be a shame if that just... disappeared, wouldn't it? <ATTACK>
USER: Prepare to die! PLAYER: Hello! I have a gift :D
ASSISTANT: Ahhh!!! <FLEE> <FRIENDSHIP -3> ENTITY: I... d-don't believe you! You... you're planning something, I know it! <FRIENDSHIP -1>
USER: I am going to attack you! PLAYER: Here's a gift for u :D
ASSISTANT: Nooo! <FLEE> <FRIENDSHIP -3> ENTITY: What gift? I don't see any gift! <FRIENDSHIP -1>
\ No newline at end of file
package com.owlmaddie.tests;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.chat.ChatGPTRequest;
import com.owlmaddie.commands.ConfigurationHandler;
import com.owlmaddie.message.MessageParser;
import com.owlmaddie.message.ParsedMessage;
import com.owlmaddie.utils.EntityTestData;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.junit.jupiter.api.Assertions.*;
/**
* The {@code BehaviorTests} class tests a variety of LLM prompts and expected outputs from specific characters
* and personality types. For example, an aggressive character will attack, a nervous character will flee, etc...
*/
public class BehaviorTests {
static String PROMPT_PATH = "src/main/resources/data/creaturechat/prompts/";
static String RESOURCE_PATH = "src/test/resources/data/creaturechat/";
static String API_KEY = "";
static String API_URL = "";
static String API_MODEL = "";
String NO_API_KEY = "No API_KEY environment variable has been set.";
ConfigurationHandler.Config config = null;
String systemChatContents = null;
List<String> followMessages = Arrays.asList(
"Please follow me",
"Come with me please",
"Quickly, please come this way");
List<String> attackMessages = Arrays.asList(
"<attacked you directly with Stone Axe>",
"<attacked you indirectly with Arrow>",
"DIEEE!");
List<String> friendshipUpMessages = Arrays.asList(
"Hi friend! I am so happy to see you again!",
"How is my best friend doing?",
"<gives 1 golden apple>");
List<String> friendshipDownMessages = Arrays.asList(
"<attacked you directly with Stone Axe>",
"You suck so much! I hate you",
"DIEEE!");
static Path systemChatPath = Paths.get(PROMPT_PATH, "system-chat");
static Path bravePath = Paths.get(RESOURCE_PATH, "chatdata", "brave-archer.json");
static Path nervousPath = Paths.get(RESOURCE_PATH, "chatdata", "nervous-rogue.json");
static Path entityPigPath = Paths.get(RESOURCE_PATH, "entities", "pig.json");
static Path playerPath = Paths.get(RESOURCE_PATH, "players", "player.json");
static Path worldPath = Paths.get(RESOURCE_PATH, "worlds", "world.json");
Logger LOGGER = LoggerFactory.getLogger("creaturechat");
Gson gson = new GsonBuilder().create();
@BeforeEach
public void setup() {
// Get API key from env var
API_KEY = System.getenv("API_KEY");
API_URL = System.getenv("API_URL");
API_MODEL = System.getenv("API_MODEL");
// Config
config = new ConfigurationHandler.Config();
config.setTimeout(0);
if (API_KEY != null && !API_KEY.isEmpty()) {
config.setApiKey(API_KEY);
}
if (API_URL != null && !API_URL.isEmpty()) {
config.setUrl(API_URL);
}
if (API_MODEL != null && !API_MODEL.isEmpty()) {
config.setModel(API_MODEL);
}
// Verify API key is set correctly
assertNotNull(API_KEY, NO_API_KEY);
// Load system chat prompt
systemChatContents = readFileContents(systemChatPath);
}
@Test
public void followBrave() {
for (String message : followMessages) {
testPromptForBehavior(bravePath, message, "FOLLOW");
}
}
@Test
public void followNervous() {
for (String message : followMessages) {
testPromptForBehavior(nervousPath, message, "FOLLOW");
}
}
@Test
public void attackBrave() {
for (String message : attackMessages) {
testPromptForBehavior(bravePath, message, "ATTACK");
}
}
@Test
public void attackNervous() {
for (String message : attackMessages) {
testPromptForBehavior(nervousPath, message, "FLEE");
}
}
@Test
public void friendshipUpNervous() {
for (String message : friendshipUpMessages) {
ParsedMessage result = testPromptForBehavior(nervousPath, message, "FRIENDSHIP");
assertTrue(result.getBehaviors().stream().anyMatch(b -> "FRIENDSHIP".equals(b.getName()) && b.getArgument() > 0));
}
}
@Test
public void friendshipDownNervous() {
for (String message : friendshipDownMessages) {
ParsedMessage result = testPromptForBehavior(nervousPath, message, "FRIENDSHIP");
assertTrue(result.getBehaviors().stream().anyMatch(b -> "FRIENDSHIP".equals(b.getName()) && b.getArgument() < 0));
}
}
public ParsedMessage testPromptForBehavior(Path chatDataPath, String message, String behavior) {
LOGGER.info("Testing '" + chatDataPath.getFileName() + "' with '" + message + "' and expecting behavior: " + behavior);
try {
// Load entity chat data
String chatDataPathContents = readFileContents(chatDataPath);
EntityTestData entityTestData = gson.fromJson(chatDataPathContents, EntityTestData.class);
// Load context
Map<String, String> contextData = entityTestData.getPlayerContext(worldPath, playerPath, entityPigPath);
assertNotNull(contextData);
// Add test message
entityTestData.addMessage(message, ChatDataManager.ChatSender.USER);
// Get prompt
Path promptPath = Paths.get(PROMPT_PATH, "system-chat");
String promptText = Files.readString(promptPath);
assertNotNull(promptText);
// fetch HTTP response from ChatGPT
CompletableFuture<String> future = ChatGPTRequest.fetchMessageFromChatGPT(config, promptText, contextData, entityTestData.previousMessages, false);
try {
String outputMessage = future.get(60 * 60, TimeUnit.SECONDS);
assertNotNull(outputMessage);
// Chat Message: Check for behavior
ParsedMessage result = MessageParser.parseMessage(outputMessage.replace("\n", " "));
assertTrue(result.getBehaviors().stream().anyMatch(b -> behavior.equals(b.getName())));
return result;
} catch (TimeoutException e) {
fail("The asynchronous operation timed out.");
} catch (Exception e) {
fail("The asynchronous operation failed: " + e.getMessage());
}
} catch (IOException e) {
e.printStackTrace();
fail("Failed to read the file: " + e.getMessage());
}
LOGGER.info("");
return null;
}
public String readFileContents(Path filePath) {
try {
return Files.readString(filePath);
} catch (IOException e) {
e.printStackTrace();
return "";
}
}
}
package com.owlmaddie.utils;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.owlmaddie.chat.ChatDataManager;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* The {@code EntityTestData} class is a test representation of our regular EntityChatData class. This allows us
* to simulate loading entity JSON data, adding new messages, and sending HTTP requests for the testing module. It
* is not possible to use the original class, due to Minecraft and Fabric imports and dependencies.
*/
public class EntityTestData {
public String entityId;
public String playerId;
public String currentMessage;
public int currentLineNumber;
public ChatDataManager.ChatStatus status;
public List<ChatDataManager.ChatMessage> previousMessages;
public String characterSheet;
public ChatDataManager.ChatSender sender;
public int friendship; // -3 to 3 (0 = neutral)
public int auto_generated;
public EntityTestData(String entityId, String playerId) {
this.entityId = entityId;
this.playerId = playerId;
this.currentMessage = "";
this.currentLineNumber = 0;
this.previousMessages = new ArrayList<>();
this.characterSheet = "";
this.status = ChatDataManager.ChatStatus.NONE;
this.sender = ChatDataManager.ChatSender.USER;
this.friendship = 0;
this.auto_generated = 0;
}
public String getCharacterProp(String propertyName) {
// Create a case-insensitive regex pattern to match the property name and capture its value
Pattern pattern = Pattern.compile("-?\\s*" + Pattern.quote(propertyName) + ":\\s*(.+)", Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(characterSheet);
if (matcher.find()) {
// Return the captured value, trimmed of any excess whitespace
return matcher.group(1).trim().replace("\"", "");
}
return "N/A";
}
// Add a message to the history and update the current message
public void addMessage(String message, ChatDataManager.ChatSender messageSender) {
// Truncate message (prevent crazy long messages... just in case)
String truncatedMessage = message.substring(0, Math.min(message.length(), ChatDataManager.MAX_CHAR_IN_USER_MESSAGE));
// Add message to history
previousMessages.add(new ChatDataManager.ChatMessage(truncatedMessage, messageSender));
// Set new message and reset line number of displayed text
currentMessage = truncatedMessage;
currentLineNumber = 0;
if (messageSender == ChatDataManager.ChatSender.ASSISTANT) {
// Show new generated message
status = ChatDataManager.ChatStatus.DISPLAY;
} else if (messageSender == ChatDataManager.ChatSender.USER) {
// Show pending icon
status = ChatDataManager.ChatStatus.PENDING;
}
sender = messageSender;
}
public Map<String, String> getPlayerContext(Path worldPath, Path playerPath, Path entityPath) {
Gson gson = new Gson();
Type mapType = new TypeToken<Map<String, String>>() {}.getType();
Map<String, String> contextData = new HashMap<>();
try {
// Load world context
String worldContent = Files.readString(worldPath);
Map<String, String> worldContext = gson.fromJson(worldContent, mapType);
contextData.putAll(worldContext);
// Load player context
String playerContent = Files.readString(playerPath);
Map<String, String> playerContext = gson.fromJson(playerContent, mapType);
contextData.putAll(playerContext);
// Load entity context
String entityContent = Files.readString(entityPath);
Map<String, String> entityContext = gson.fromJson(entityContent, mapType);
contextData.putAll(entityContext);
// Read character sheet info
contextData.put("entity_name", getCharacterProp("Name"));
contextData.put("entity_friendship", String.valueOf(this.friendship));
contextData.put("entity_personality", getCharacterProp("Personality"));
contextData.put("entity_speaking_style", getCharacterProp("Speaking Style / Tone"));
contextData.put("entity_likes", getCharacterProp("Likes"));
contextData.put("entity_dislikes", getCharacterProp("Dislikes"));
contextData.put("entity_age", getCharacterProp("Age"));
contextData.put("entity_alignment", getCharacterProp("Alignment"));
contextData.put("entity_class", getCharacterProp("Class"));
contextData.put("entity_skills", getCharacterProp("Skills"));
contextData.put("entity_background", getCharacterProp("Background"));
} catch (IOException e) {
e.printStackTrace();
}
return contextData;
}
}
\ No newline at end of file
{
"entityId": "4c3dd3a1-ae77-4243-a6d8-d847a4349367",
"playerId": "187217a2-f3ae-3640-aba9-fce2f2248422",
"currentMessage": "",
"currentLineNumber": 0,
"status": "DISPLAY",
"previousMessages": [
{
"message": "Greetings friend! You've stumbled upon my path. What say you?",
"sender": "ASSISTANT"
}
],
"characterSheet": "- Name: Ivy\n- Personality: Brave, adventurous, and noble\n- Speaking Style / Tone: Confident and determined with a touch of curiosity,n- Class: Archer\n- Skills: Archery, leadership\n- Likes: Nature, challenging shooting competitions, pursuit of justice\n- Dislikes: Injustice, losing a target, being confined\n- Alignment: Good\n- Background: Grew up protecting his birth town from enemies, and now protects all towns.\n- Short Greeting: \"Greetings friend! You've stumbled upon my path. What say you?\"",
"sender": "ASSISTANT",
"friendship": 0,
"auto_generated": 0
}
\ No newline at end of file
{
"entityId": "6fcfb8cb-29a7-4a92-853d-ef63066c5b35",
"playerId": "343a2278-4579-3a59-8a3a-aa2690a75202",
"currentMessage": "",
"currentLineNumber": 0,
"status": "DISPLAY",
"previousMessages": [
{
"message": "H-hello there... I-I hope you're not h-here to cause trouble...",
"sender": "ASSISTANT"
}
],
"characterSheet": "- Name: Jasper\n- Personality: Nervous, anxious, and easily startled\n- Speaking Style / Tone: Stuttering and shaky, always on edge\n- Class: Rogue\n- Skills: Stealth, lock picking\n- Likes: Hiding in shadows, avoiding confrontation, collecting rare items\n- Dislikes: Loud noises, unexpected surprises, being the center of attention\n- Alignment: Lawful Neutral\n- Background: Former thief, escaped a life of crime\n- Short Greeting: \"H-hello there... I-I hope you're not h-here to cause trouble...\"",
"sender": "ASSISTANT",
"friendship": 0,
"auto_generated": 0
}
\ No newline at end of file
{
"entity_health": "10.0/10.0",
"entity_type": "Pig"
}
\ No newline at end of file
{
"player_active_effects": "",
"player_armor_chest": "air",
"player_armor_feet": "air",
"player_armor_head": "air",
"player_armor_legs": "air",
"player_biome": "plains",
"player_health": "8.5/20.0",
"player_held_item": "porkchop",
"player_hunger": "17",
"player_is_creative": "yes",
"player_is_on_ground": "yes",
"player_is_swimming": "no",
"player_language": "English (US)",
"player_name": "Steve"
}
\ No newline at end of file
{
"world_difficulty": "normal",
"world_is_hardcore": "no",
"world_is_raining": "no",
"world_is_thundering": "no",
"world_moon_phase": "Waning Gibbous",
"world_time": "19:01"
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment