Skip to content

Commit

Permalink
AI chat: Added 'ai chat' strategy which when enabled allow bots to ca…
Browse files Browse the repository at this point in the history
…ll an LLM api to respond.
mostlikely4r committed Nov 18, 2024
1 parent 509c5cf commit 6335276
Showing 13 changed files with 462 additions and 94 deletions.
96 changes: 59 additions & 37 deletions playerbot/PlayerbotAI.cpp
Original file line number Diff line number Diff line change
@@ -1599,60 +1599,64 @@ void PlayerbotAI::HandleBotOutgoingPacket(const WorldPacket& packet)
isFromFreeBot = sPlayerbotAIConfig.IsFreeAltBot(guid1);

bool isMentioned = message.find(bot->GetName()) != std::string::npos;
bool isAiChat = HasStrategy("ai chat", BotState::BOT_STATE_NON_COMBAT);

ChatChannelSource chatChannelSource = GetChatChannelSource(bot, msgtype, chanName);

// random bot speaks, chat CD
if (isFromFreeBot && isPaused)
return;

// BG: react only if mentioned or if not channel and real player spoke
if (bot->InBattleGround() && !(isMentioned || (msgtype != CHAT_MSG_CHANNEL && !isFromFreeBot)))
return;

if (HasRealPlayerMaster() && guid1 != GetMaster()->GetObjectGuid())
return;

if (lang == LANG_ADDON)
return;

if (boost::algorithm::istarts_with(message, sPlayerbotAIConfig.toxicLinksPrefix)
&& (GetChatHelper()->ExtractAllItemIds(message).size() > 0 || GetChatHelper()->ExtractAllQuestIds(message).size() > 0)
&& sPlayerbotAIConfig.toxicLinksRepliesChance)
if (!isAiChat || isFromFreeBot)
{
if (urand(0, 50) > 0 || urand(1, 100) > sPlayerbotAIConfig.toxicLinksRepliesChance)
{
// random bot speaks, chat CD
if (isFromFreeBot && isPaused)
return;
}
}
else if ((GetChatHelper()->ExtractAllItemIds(message).count(19019) && sPlayerbotAIConfig.thunderfuryRepliesChance))
{
if (urand(0, 60) > 0 || urand(1, 100) > sPlayerbotAIConfig.thunderfuryRepliesChance)
{

// BG: react only if mentioned or if not channel and real player spoke
if (bot->InBattleGround() && !(isMentioned || (msgtype != CHAT_MSG_CHANNEL && !isFromFreeBot)))
return;
}
}
else
{
if (isFromFreeBot && urand(0, 20))

if (HasRealPlayerMaster() && guid1 != GetMaster()->GetObjectGuid())
return;

if (msgtype == CHAT_MSG_GUILD && (!sPlayerbotAIConfig.guildRepliesRate || urand(1, 100) >= sPlayerbotAIConfig.guildRepliesRate))
if (lang == LANG_ADDON)
return;

if (!isFromFreeBot)
if (boost::algorithm::istarts_with(message, sPlayerbotAIConfig.toxicLinksPrefix)
&& (GetChatHelper()->ExtractAllItemIds(message).size() > 0 || GetChatHelper()->ExtractAllQuestIds(message).size() > 0)
&& sPlayerbotAIConfig.toxicLinksRepliesChance)
{
if (urand(0, 50) > 0 || urand(1, 100) > sPlayerbotAIConfig.toxicLinksRepliesChance)
{
return;
}
}
else if ((GetChatHelper()->ExtractAllItemIds(message).count(19019) && sPlayerbotAIConfig.thunderfuryRepliesChance))
{
if (!isMentioned && urand(0, 4))
if (urand(0, 60) > 0 || urand(1, 100) > sPlayerbotAIConfig.thunderfuryRepliesChance)
{
return;
}
}
else
{
if (urand(0, 20 + 10 * isMentioned))
if (isFromFreeBot && urand(0, 20))
return;

if (msgtype == CHAT_MSG_GUILD && (!sPlayerbotAIConfig.guildRepliesRate || urand(1, 100) >= sPlayerbotAIConfig.guildRepliesRate))
return;

if (!isFromFreeBot)
{
if (!isMentioned && urand(0, 4))
return;
}
else
{
if (urand(0, 20 + 10 * isMentioned))
return;
}
}
}

QueueChatResponse(msgtype, guid1, ObjectGuid(), message, chanName, name);
QueueChatResponse(msgtype, guid1, ObjectGuid(), message, chanName, name, isAiChat);
GetAiObjectContext()->GetValue<time_t>("last said", "chat")->Set(time(0) + urand(5, 25));

return;
@@ -6976,6 +6980,24 @@ std::list<Unit*> PlayerbotAI::GetAllHostileNPCNonPetUnitsAroundWO(WorldObject* w
return hostileUnitsNonPlayers;
}

void PlayerbotAI::SendDelayedPacket(WorldSession* session, std::future<std::vector<WorldPacket>> futurePacket, uint32 waitBeforeSend)
{
time_t doNotSendBefore = time(0) + waitBeforeSend;
std::thread t([session, futPacket = std::move(futurePacket), doNotSendBefore]() mutable {
if (doNotSendBefore > time(0))
Sleep(doNotSendBefore - time(0));

for (auto& packet : futPacket.get())
{
std::unique_ptr<WorldPacket> packetPtr(new WorldPacket(packet));

session->QueuePacket(std::move(packetPtr));
}
});

t.detach();
}

std::string PlayerbotAI::InventoryParseOutfitName(std::string outfit)
{
int pos = outfit.find("=");
@@ -7561,9 +7583,9 @@ bool PlayerbotAI::HasPlayerRelation()
return false;
}

void PlayerbotAI::QueueChatResponse(uint32 msgType, ObjectGuid guid1, ObjectGuid guid2, std::string message, std::string chanName, std::string name)
void PlayerbotAI::QueueChatResponse(uint32 msgType, ObjectGuid guid1, ObjectGuid guid2, std::string message, std::string chanName, std::string name, bool noDelay)
{
chatReplies.push(ChatQueuedReply(msgType, guid1.GetCounter(), guid2.GetCounter(), message, chanName, name, time(0) + urand(inCombat ? 10 : 5, inCombat ? 25 : 15)));
chatReplies.push(ChatQueuedReply(msgType, guid1.GetCounter(), guid2.GetCounter(), message, chanName, name, time(0) + noDelay ? 0 : urand(inCombat ? 10 : 5, inCombat ? 25 : 15)));
}

bool PlayerbotAI::PlayAttackEmote(float chanceMultiplier)
3 changes: 2 additions & 1 deletion playerbot/PlayerbotAI.h
Original file line number Diff line number Diff line change
@@ -346,7 +346,7 @@ class PlayerbotAI : public PlayerbotAIBase
static std::string BotStateToString(BotState state);
std::string HandleRemoteCommand(std::string command);
void HandleCommand(uint32 type, const std::string& text, Player& fromPlayer, const uint32 lang = LANG_UNIVERSAL);
void QueueChatResponse(uint32 msgType, ObjectGuid guid1, ObjectGuid guid2, std::string message, std::string chanName, std::string name);
void QueueChatResponse(uint32 msgType, ObjectGuid guid1, ObjectGuid guid2, std::string message, std::string chanName, std::string name, bool noDelay = false);
void HandleBotOutgoingPacket(const WorldPacket& packet);
void HandleMasterIncomingPacket(const WorldPacket& packet);
void HandleMasterOutgoingPacket(const WorldPacket& packet);
@@ -510,6 +510,7 @@ class PlayerbotAI : public PlayerbotAIBase
std::list<Unit*> GetAllHostileUnitsAroundWO(WorldObject* wo, float distanceAround);
std::list<Unit*> GetAllHostileNPCNonPetUnitsAroundWO(WorldObject* wo, float distanceAround);

static void SendDelayedPacket(WorldSession* session, std::future<std::vector<WorldPacket>> futurePacket, uint32 waitBeforeSend = 0);
public:
std::vector<Bag*> GetEquippedAnyBags();
std::vector<Bag*> GetEquippedQuivers();
36 changes: 36 additions & 0 deletions playerbot/PlayerbotAIConfig.cpp
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@
#include <numeric>
#include <iomanip>
#include <boost/algorithm/string.hpp>
#include <regex>

std::vector<std::string> ConfigAccess::GetValues(const std::string& name) const
{
@@ -66,6 +67,20 @@ void LoadListString(std::string value, T& list)
}
}

inline ParsedUrl parseUrl(const std::string& url) {
std::regex urlRegex(R"((http|https)://([^:/]+)(:([0-9]+))?(/.*)?)");
std::smatch match;
if (!std::regex_match(url, match, urlRegex)) {
throw std::invalid_argument("Invalid URL format");
}

ParsedUrl parsed;
parsed.hostname = match[2];
parsed.port = match[4].length() ? std::stoi(match[4]) : 80;
parsed.path = match[5].length() ? match[5] : std::string("/");
return parsed;
}

bool PlayerbotAIConfig::Initialize()
{
sLog.outString("Initializing AI Playerbot by ike3, based on the original Playerbot by blueboy");
@@ -574,6 +589,27 @@ bool PlayerbotAIConfig::Initialize()
respawnModForPlayerBots = config.GetBoolDefault("AiPlayerbot.RespawnModForPlayerBots", false);
respawnModForInstances = config.GetBoolDefault("AiPlayerbot.RespawnModForInstances", false);

//LLM START
llmApiEndpoint = config.GetStringDefault("AiPlayerbot.LLMApiEndpoint", "http://127.0.0.1:5001/api/v1/generate");
try {
llmEndPointUrl = parseUrl(llmApiEndpoint);
}
catch (const std::invalid_argument& e) {
sLog.outError("Unable to parse LLMApiEndpoint url: %s", e.what());
}
llmApiKey = config.GetStringDefault("AiPlayerbot.LLMApiKey", "");
llmApiJson = config.GetStringDefault("AiPlayerbot.LLMApiJson", "{ \"max_length\": 100, \"prompt\": \"<pre_prompt><prompt><post_prompt>\"}");

llmPrePrompt = config.GetStringDefault("AiPlayerbot.LLMPrePrompt", "You are a roleplaying character in World of Warcraft: <expansion name>. Your name is <bot name>. The player speaking to you is named <player name>. You are level <bot level> and play as a <bot race> <bot class>. Answer as a roleplaying character. Limit responses to 100 characters.");
llmPrompt = config.GetStringDefault("AiPlayerbot.LLMPrompt", " <player message>");
llmPostPrompt = config.GetStringDefault("AiPlayerbot.LLMPostPrompt", "");

llmResponseStartPattern = config.GetStringDefault("AiPlayerbot.LLMResponseStartPattern", "\"results\":[{\"text\":\"");
std::replace(llmResponseStartPattern.begin(), llmResponseStartPattern.end(), '\'', '\"');
llmResponseEndPattern = config.GetStringDefault("AiPlayerbot.LLMResponseEndPattern", "\"");
std::replace(llmResponseEndPattern.begin(), llmResponseEndPattern.end(), '\'', '\"');
//LLM END

// Gear progression system
gearProgressionSystemEnabled = config.GetBoolDefault("AiPlayerbot.GearProgressionSystem.Enable", false);

12 changes: 12 additions & 0 deletions playerbot/PlayerbotAIConfig.h
Original file line number Diff line number Diff line change
@@ -62,6 +62,12 @@ class ConfigAccess
std::mutex m_configLock;
};

struct ParsedUrl {
std::string hostname;
std::string path;
int port;
};

class PlayerbotAIConfig
{
public:
@@ -328,6 +334,12 @@ class PlayerbotAIConfig
bool perfMonEnabled;
bool bExplicitDbStoreSave = false;

//LM BEGIN
std::string llmApiEndpoint, llmApiKey, llmApiJson, llmPrePrompt, llmPrompt, llmPostPrompt, llmResponseStartPattern, llmResponseEndPattern;

ParsedUrl llmEndPointUrl;
//LM END

std::string GetValue(std::string name);
void SetValue(std::string name, std::string value);

163 changes: 120 additions & 43 deletions playerbot/PlayerbotLLMInterface.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@

#include "PlayerbotLLMInterface.h"
#include "PlayerbotAIConfig.h"


#include <iostream>
#include <string>
#include <sstream>

#include <iostream>
#include <string>
#include <sstream>
@@ -18,10 +25,9 @@
#include <cstring>
#endif

std::string PlayerbotLLMInterface::Generate(const std::string& prompt) {
std::string serverIp = "127.0.0.1"; // Replace with KoboldCpp server IP
int serverPort = 5001; // Replace with KoboldCpp server port


std::string PlayerbotLLMInterface::Generate(const std::string& prompt) {
const int bufferSize = 4096;
char buffer[bufferSize];
std::string response;
@@ -30,79 +36,82 @@ std::string PlayerbotLLMInterface::Generate(const std::string& prompt) {
// Initialize Winsock
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
return "WSAStartup failed";
sLog.outError("BotLLM: WSAStartup failed");
return "error";
}
#endif

// Create a socket
int sock;
// Parse the URL
ParsedUrl parsedUrl = sPlayerbotAIConfig.llmEndPointUrl;

// Resolve hostname to IP address
struct addrinfo hints = {}, * res;
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
if (getaddrinfo(parsedUrl.hostname.c_str(), std::to_string(parsedUrl.port).c_str(), &hints, &res) != 0) {
sLog.outError("BotLLM: Failed to resolve hostname");
#ifdef _WIN32
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == INVALID_SOCKET) {
WSACleanup();
return "Socket creation failed";
}
#else
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
return "Socket creation failed";
}
#endif
return "error";
}

// Server address setup
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(serverPort);
// Create a socket
int sock;
#ifdef _WIN32
if (InetPton(AF_INET, serverIp.c_str(), &serverAddr.sin_addr) <= 0) {
closesocket(sock);
sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (sock == INVALID_SOCKET) {
sLog.outError("BotLLM: Socket creation failed");
WSACleanup();
return "Invalid server IP address";
return "error";
}
#else
if (inet_pton(AF_INET, serverIp.c_str(), &serverAddr.sin_addr) <= 0) {
close(sock);
return "Invalid server IP address";
sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (sock < 0) {
sLog.outError("BotLLM: Socket creation failed");
freeaddrinfo(res);
return "error";
}
#endif

// Connect to the server
if (connect(sock, res->ai_addr, res->ai_addrlen) < 0) {
sLog.outError("BotLLM: Connection to server failed");
#ifdef _WIN32
if (connect(sock, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
closesocket(sock);
WSACleanup();
return "Connection to server failed";
}
#else
if (connect(sock, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) {
close(sock);
return "Connection to server failed";
}
#endif
freeaddrinfo(res);
return "error";
}

freeaddrinfo(res); // Free the address info structure

// Create the HTTP POST request
std::ostringstream request;
request << "POST /api/v1/generate HTTP/1.1\r\n";
request << "Host: " << serverIp << ":" << serverPort << "\r\n";
request << "POST " << parsedUrl.path << " HTTP/1.1\r\n";
request << "Host: " << parsedUrl.hostname << "\r\n";
request << "Content-Type: application/json\r\n";
std::string body = "{\"prompt\": \"" + prompt + "\"}";
if (!sPlayerbotAIConfig.llmApiKey.empty())
request << "Authorization: Bearer " << sPlayerbotAIConfig.llmApiKey;
std::string body = prompt;
request << "Content-Length: " << body.size() << "\r\n";
request << "\r\n";
request << body;

// Send the request
if (send(sock, request.str().c_str(), request.str().size(), 0) < 0) {
sLog.outError("BotLLM: Failed to send request");
#ifdef _WIN32
if (send(sock, request.str().c_str(), request.str().size(), 0) == SOCKET_ERROR) {
closesocket(sock);
WSACleanup();
return "Failed to send request";
}
#else
if (send(sock, request.str().c_str(), request.str().size(), 0) < 0) {
close(sock);
return "Failed to send request";
}
#endif
return "error";
}

// Read the response
int bytesRead;
@@ -113,22 +122,90 @@ std::string PlayerbotLLMInterface::Generate(const std::string& prompt) {

#ifdef _WIN32
if (bytesRead == SOCKET_ERROR) {
response += "Error reading response";
sLog.outError("BotLLM: Error reading response");
}
closesocket(sock);
WSACleanup();
#else
if (bytesRead < 0) {
response += "Error reading response";
sLog.outError("BotLLM: Error reading response");
}
close(sock);
#endif

// Extract the response body (optional: depending on KoboldCpp response format)
// Extract the response body (optional: depending on the server response format)
size_t pos = response.find("\r\n\r\n");
if (pos != std::string::npos) {
response = response.substr(pos + 4);
}

return response;
}

std::vector<std::string> PlayerbotLLMInterface::ParseResponse(const std::string& response, std::string startPattern, std::string endPattern)
{
uint32 startCursor = 0;
uint32 endCursor = 0;
std::string subString;

std::vector<std::string> responses;

for (auto& c : response)
{
if (startCursor < startPattern.size())
{
if (c == ' ')
continue;

if (c != startPattern[startCursor])
{
startCursor = 0;
continue;
}

startCursor++;
continue;
}

subString += c;

if ((subString.size() > 1 && subString.back() == 'n' && subString[subString.size() - 2] == '\\') || (subString.size() > 100 && c == '.') || (subString.size() > 200 && c == ' ') || subString.size() > 250)
{
if (subString.back() == 'n' && subString[subString.size() - 2] == '\\')
{
subString.pop_back();
subString.pop_back();
}
if(subString.size())
responses.push_back(subString);
subString.clear();
}


if (c == ' ')
continue;

if (c != endPattern[endCursor])
{
endCursor = 0;
}
else
{
if (subString.size() > 1 && subString[subString.size() - 2] == '\\')
continue;

endCursor++;
if (endCursor == endPattern.size() && responses.size())
{
if (subString.size())
responses.push_back(subString);

for (uint32 i = 0; i < std::min(endPattern.size(), responses.back().size()); i++)
responses.back().pop_back();
break;
}
}
}

return responses;
}
2 changes: 2 additions & 0 deletions playerbot/PlayerbotLLMInterface.h
Original file line number Diff line number Diff line change
@@ -5,6 +5,8 @@ namespace ai
public:
PlayerbotLLMInterface() {}
static std::string Generate(const std::string& prompt);

static std::vector<std::string> ParseResponse(const std::string& response, std::string startPattern, std::string endPattern);
private:

};
33 changes: 33 additions & 0 deletions playerbot/aiplayerbot.conf.dist.in
Original file line number Diff line number Diff line change
@@ -927,6 +927,39 @@ AiPlayerbot.PerfMonEnabled = 0
# AiPlayerbot.DiffWithPlayer = 100
# AiPlayerbot.DiffEmpty = 200

# LLM values. These values are for noncombat strategy 'ai chat' which allows bots to reply using a LLM api.
# For a quickstart download koboldcpp, a 7b model from huggingface and start kobold.
# (Default) KoboldCPP examples:

# The api endpoint that should be called for chat generation.
# AiPlayerbot.LLMApiEndpoint = http://127.0.0.1:5001/api/v1/generate
# The api key needed to access the endpoint.
# AiPlayerbot.LLMApiKey =
# The default json to send to the endpoint.
# AiPlayerbot.LLMApiJson = {"max_length": 100, "prompt": "<pre prompt><prompt><post prompt>"}

# The default prompt to send at the beginning of each conversation.
# AiPlayerbot.LLMPrePrompt = You are a roleplaying character in World of Warcraft: <expansion name>. Your name is <bot name>. The player speaking to you is named <player name>. You are level <bot level> and play as a <bot race> <bot class>. Answer as a roleplaying character. Limit responses to 100 characters.
# The prompt part containing the last message from the player the bot is responding to.
# AiPlayerbot.LLMPrompt = <player message>
# The default prompt to send at the end of each conversation.
# AiPlayerbot.LLMPostPrompt =

# What pattern the server should look for to find the start of the repsonse the llm gave. Note spaces in the actual response are ignored of the start pattern.
# Double quotes at the start and end of the pattern need to be single quotes to be read correctly.
# AiPlayerbot.LLMResponseStartPattern = 'results":[{"text":'
# What pattern the server should look for that the response has ended.
# AiPlayerbot.LLMResponseEndPattern = '

# OPEN-AI example:
# AiPlayerbot.LLMApiEndpoint = http://IP/URL:PORT/v1/chat/completions
# AiPlayerbot.LLMApiKey = YOUR_API_KEY
# AiPlayerbot.LLMApiJson = {\"model\": \"gpt-4o-mini\", "\"messages\": [{\"role\": \"system\", \"content\": \"<pre prompt>\"},{\"role\": \"user\", \"content\": \"<prompt>\"}],\"max_tokens\": 60}
# AiPlayerbot.LLMPrePrompt = You are a roleplaying character in World of Warcraft: <expansion name>. Your name is <bot name>. The player speaking to you is named <player name>. You are level <bot level> and play as a <bot race> <bot class>. Answer as a roleplaying character. Limit responses to 100 characters.
# AiPlayerbot.LLMPrompt = <player message>
# AiPlayerbot.LLMPostPrompt =
# AiPlayerbot.LLMResponseStartPattern = 'message":{"role":"system","content":'
# AiPlayerbot.LLMResponseEndPattern = '

# Mystery config value. Currently enables async bot pathfinding. Rarely crashes the server.
# AiPlayerbot.TweakValue = 0
33 changes: 33 additions & 0 deletions playerbot/aiplayerbot.conf.dist.in.tbc
Original file line number Diff line number Diff line change
@@ -946,6 +946,39 @@ AiPlayerbot.PerfMonEnabled = 0
# AiPlayerbot.DiffWithPlayer = 100
# AiPlayerbot.DiffEmpty = 200

# LLM values. These values are for noncombat strategy 'ai chat' which allows bots to reply using a LLM api.
# For a quickstart download koboldcpp, a 7b model from huggingface and start kobold.
# (Default) KoboldCPP examples:

# The api endpoint that should be called for chat generation.
# AiPlayerbot.LLMApiEndpoint = http://127.0.0.1:5001/api/v1/generate
# The api key needed to access the endpoint.
# AiPlayerbot.LLMApiKey =
# The default json to send to the endpoint.
# AiPlayerbot.LLMApiJson = {"max_length": 100, "prompt": "<pre prompt><prompt><post prompt>"}

# The default prompt to send at the beginning of each conversation.
# AiPlayerbot.LLMPrePrompt = You are a roleplaying character in World of Warcraft: <expansion name>. Your name is <bot name>. The player speaking to you is named <player name>. You are level <bot level> and play as a <bot race> <bot class>. Answer as a roleplaying character. Limit responses to 100 characters.
# The prompt part containing the last message from the player the bot is responding to.
# AiPlayerbot.LLMPrompt = <player message>
# The default prompt to send at the end of each conversation.
# AiPlayerbot.LLMPostPrompt =

# What pattern the server should look for to find the start of the repsonse the llm gave. Note spaces in the actual response are ignored of the start pattern.
# Double quotes at the start and end of the pattern need to be single quotes to be read correctly.
# AiPlayerbot.LLMResponseStartPattern = 'results":[{"text":'
# What pattern the server should look for that the response has ended.
# AiPlayerbot.LLMResponseEndPattern = '

# OPEN-AI example:
# AiPlayerbot.LLMApiEndpoint = http://IP/URL:PORT/v1/chat/completions
# AiPlayerbot.LLMApiKey = YOUR_API_KEY
# AiPlayerbot.LLMApiJson = {\"model\": \"gpt-4o-mini\", "\"messages\": [{\"role\": \"system\", \"content\": \"<pre prompt>\"},{\"role\": \"user\", \"content\": \"<prompt>\"}],\"max_tokens\": 60}
# AiPlayerbot.LLMPrePrompt = You are a roleplaying character in World of Warcraft: <expansion name>. Your name is <bot name>. The player speaking to you is named <player name>. You are level <bot level> and play as a <bot race> <bot class>. Answer as a roleplaying character. Limit responses to 100 characters.
# AiPlayerbot.LLMPrompt = <player message>
# AiPlayerbot.LLMPostPrompt =
# AiPlayerbot.LLMResponseStartPattern = 'message":{"role":"system","content":'
# AiPlayerbot.LLMResponseEndPattern = '

# Mystery config value. Currently enables async bot pathfinding. Rarely crashes the server.
# AiPlayerbot.TweakValue = 0
33 changes: 33 additions & 0 deletions playerbot/aiplayerbot.conf.dist.in.wotlk
Original file line number Diff line number Diff line change
@@ -886,6 +886,39 @@ AiPlayerbot.PerfMonEnabled = 0
# AiPlayerbot.DiffWithPlayer = 100
# AiPlayerbot.DiffEmpty = 200

# LLM values. These values are for noncombat strategy 'ai chat' which allows bots to reply using a LLM api.
# For a quickstart download koboldcpp, a 7b model from huggingface and start kobold.
# (Default) KoboldCPP examples:

# The api endpoint that should be called for chat generation.
# AiPlayerbot.LLMApiEndpoint = http://127.0.0.1:5001/api/v1/generate
# The api key needed to access the endpoint.
# AiPlayerbot.LLMApiKey =
# The default json to send to the endpoint.
# AiPlayerbot.LLMApiJson = {"max_length": 100, "prompt": "<pre prompt><prompt><post prompt>"}

# The default prompt to send at the beginning of each conversation.
# AiPlayerbot.LLMPrePrompt = You are a roleplaying character in World of Warcraft: <expansion name>. Your name is <bot name>. The player speaking to you is named <player name>. You are level <bot level> and play as a <bot race> <bot class>. Answer as a roleplaying character. Limit responses to 100 characters.
# The prompt part containing the last message from the player the bot is responding to.
# AiPlayerbot.LLMPrompt = <player message>
# The default prompt to send at the end of each conversation.
# AiPlayerbot.LLMPostPrompt =

# What pattern the server should look for to find the start of the repsonse the llm gave. Note spaces in the actual response are ignored of the start pattern.
# Double quotes at the start and end of the pattern need to be single quotes to be read correctly.
# AiPlayerbot.LLMResponseStartPattern = 'results":[{"text":'
# What pattern the server should look for that the response has ended.
# AiPlayerbot.LLMResponseEndPattern = '

# OPEN-AI example:
# AiPlayerbot.LLMApiEndpoint = http://IP/URL:PORT/v1/chat/completions
# AiPlayerbot.LLMApiKey = YOUR_API_KEY
# AiPlayerbot.LLMApiJson = {\"model\": \"gpt-4o-mini\", "\"messages\": [{\"role\": \"system\", \"content\": \"<pre prompt>\"},{\"role\": \"user\", \"content\": \"<prompt>\"}],\"max_tokens\": 60}
# AiPlayerbot.LLMPrePrompt = You are a roleplaying character in World of Warcraft: <expansion name>. Your name is <bot name>. The player speaking to you is named <player name>. You are level <bot level> and play as a <bot race> <bot class>. Answer as a roleplaying character. Limit responses to 100 characters.
# AiPlayerbot.LLMPrompt = <player message>
# AiPlayerbot.LLMPostPrompt =
# AiPlayerbot.LLMResponseStartPattern = 'message":{"role":"system","content":'
# AiPlayerbot.LLMResponseEndPattern = '

# Mystery config value. Currently enables async bot pathfinding. Rarely crashes the server.
# AiPlayerbot.TweakValue = 0
1 change: 1 addition & 0 deletions playerbot/strategy/StrategyContext.h
Original file line number Diff line number Diff line change
@@ -146,6 +146,7 @@ namespace ai
creators["wbuff"] = &StrategyContext::world_buff;
creators["silent"] = &StrategyContext::silent;
creators["nowar"] = &StrategyContext::nowar;
creators["ai chat"] = [](PlayerbotAI* ai) { return new AIChatStrategy(ai); };

// Dungeon Strategies
creators["dungeon"] = &StrategyContext::dungeon;
42 changes: 29 additions & 13 deletions playerbot/strategy/actions/DebugAction.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

#include "playerbot/playerbot.h"
#include "DebugAction.h"
#include "playerbot/PlayerbotAIConfig.h"
@@ -14,6 +13,7 @@
#include "playerbot/PlayerbotLLMInterface.h"

#include <iomanip>
#include "SayAction.h"

using namespace ai;

@@ -99,26 +99,42 @@ bool DebugAction::Execute(Event& event)
}
else if (text.find("llm ") == 0)
{
Player* player = bot;

std::future<std::vector<WorldPacket>> futurePacket = std::async([player, text, requester] {

std::thread t([text, requester]() {
WorldPacket new_packet(Opcodes(CMSG_MESSAGECHAT), 4096);
WorldPacket packet_template(CMSG_MESSAGECHAT, 4096);

uint32 type = CHAT_MSG_SAY;
uint32 type = CHAT_MSG_WHISPER;
uint32 lang = LANG_UNIVERSAL;

new_packet << type;
new_packet << lang;
packet_template << type;
packet_template << lang;
packet_template << requester->GetName();

std::string string = PlayerbotLLMInterface::Generate(text.substr(4));
std::map<std::string, std::string> jsonFill;
jsonFill["<prompt>"] = text.substr(4);
std::string json = BOT_TEXT2(sPlayerbotAIConfig.llmApiJson, jsonFill);

new_packet << string;
std::string response = PlayerbotLLMInterface::Generate(json);
std::vector<std::string> lines = PlayerbotLLMInterface::ParseResponse(response, sPlayerbotAIConfig.llmResponseStartPattern, sPlayerbotAIConfig.llmResponseEndPattern);

std::unique_ptr<WorldPacket> packet(new WorldPacket(new_packet.GetOpcode()));
*packet = new_packet;
requester->GetSession()->QueuePacket(std::move(packet));
std::vector<WorldPacket> packets;
for (auto& line : lines)
{
WorldPacket packet(packet_template);
packet << line;
packets.push_back(packet);
}

return; });
t.detach();
return packets; });

ai->SendDelayedPacket(bot->GetSession(), std::move(futurePacket));
return true;
}
else if (text.find("chatreplydo ") == 0)
{
ChatReplyAction::ChatReplyDo(bot, CHAT_MSG_WHISPER, requester->GetGUIDLow(), 0, text.substr(12), "", requester->GetName());
return true;
}
else if (text == "gy" && isMod)
87 changes: 87 additions & 0 deletions playerbot/strategy/actions/SayAction.cpp
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@
#include "playerbot/AiFactory.h"
#include <regex>
#include <boost/algorithm/string.hpp>
#include "playerbot/PlayerbotLLMInterface.h"

using namespace ai;

@@ -184,6 +185,92 @@ void ChatReplyAction::ChatReplyDo(Player* bot, uint32 type, uint32 guid1, uint32
return;
}

if (bot->GetPlayerbotAI() && bot->GetPlayerbotAI()->HasStrategy("ai chat", BotState::BOT_STATE_NON_COMBAT))
{
Player* player = sObjectAccessor.FindPlayer(ObjectGuid(HIGHGUID_PLAYER, guid1));
if (player && player->isRealPlayer())
{
PlayerbotAI* ai = bot->GetPlayerbotAI();
std::map<std::string, std::string> placeholders;
placeholders["<bot name>"] = bot->GetName();
placeholders["<bot level>"] = std::to_string(bot->GetLevel());
placeholders["<bot class>"] = ai->GetChatHelper()->formatClass(bot->getClass());
placeholders["<bot race>"] = ai->GetChatHelper()->formatRace(bot->getRace());
placeholders["<player name>"] = player->GetName();
placeholders["<expansion name>"] = "Wrath of the Lichking";
placeholders["<player message>"] = msg;

std::map<std::string, std::string> jsonFill;
jsonFill["<pre prompt>"] = sPlayerbotAIConfig.llmPrePrompt;
jsonFill["<prompt>"] = sPlayerbotAIConfig.llmPrompt;
jsonFill["<post prompt>"] = sPlayerbotAIConfig.llmPostPrompt;
std::string json = BOT_TEXT2(sPlayerbotAIConfig.llmApiJson, jsonFill);

json = BOT_TEXT2(json, placeholders);

std::string playerName;

uint32 type = CHAT_MSG_WHISPER;

switch (chatChannelSource)
{
case ChatChannelSource::SRC_WHISPER:
{
type = CHAT_MSG_WHISPER;
playerName = player->GetName();
break;
}
case ChatChannelSource::SRC_SAY:
{
type = CHAT_MSG_SAY;
break;
}
case ChatChannelSource::SRC_YELL:
{
type = CHAT_MSG_YELL;
break;
}
case ChatChannelSource::SRC_PARTY:
{
type = CHAT_MSG_PARTY;
break;
}
case ChatChannelSource::SRC_GUILD:
{
type = CHAT_MSG_GUILD;
}
}

std::future<std::vector<WorldPacket>> futurePackets = std::async([type, bot, playerName, json] {

WorldPacket packet_template(CMSG_MESSAGECHAT, 4096);

uint32 lang = LANG_UNIVERSAL;

packet_template << type;
packet_template << lang;

if(!playerName.empty())
packet_template << playerName;

std::string response = PlayerbotLLMInterface::Generate(json);
std::vector<std::string> lines = PlayerbotLLMInterface::ParseResponse(response, sPlayerbotAIConfig.llmResponseStartPattern, sPlayerbotAIConfig.llmResponseEndPattern);

std::vector<WorldPacket> packets;
for (auto& line : lines)
{
WorldPacket packet(packet_template);
packet << line;
packets.push_back(packet);
}

return packets; });

ai->SendDelayedPacket(bot->GetSession(), std::move(futurePackets));
}

return;
}

SendGeneralResponse(bot, chatChannelSource, GenerateReplyMessage(bot, msg, guid1, name), name);
}
15 changes: 15 additions & 0 deletions playerbot/strategy/generic/DebugStrategy.h
Original file line number Diff line number Diff line change
@@ -178,6 +178,21 @@ namespace ai
return "This strategy will bots log anything they say to master to a logfile with the bot's name";
}
virtual std::vector<std::string> GetRelatedStrategies() { return { "debug" }; }
#endif
};

class AIChatStrategy : public Strategy
{
public:
AIChatStrategy(PlayerbotAI* ai) : Strategy(ai) {}
virtual int GetType() { return STRATEGY_TYPE_NONCOMBAT; }
virtual std::string getName() { return "ai chat"; }
#ifdef GenerateBotHelp
virtual std::string GetHelpName() { return "ai chat"; } //Must equal iternal name
virtual std::string GetHelpDescription() {
return "Uses the LLM system to respond to player chats.";
}
virtual std::vector<std::string> GetRelatedStrategies() { return { "" }; }
#endif
};
}

0 comments on commit 6335276

Please sign in to comment.