playerbot chat ai system
This commit is contained in:
@@ -83,6 +83,10 @@ set(PLAYERBOT_CORE_SOURCES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Chat/BotChatCommandHandler.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Chat/ChatTemplateManager.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Chat/ChatTemplateManager.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Chat/AIChatService.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Chat/AIChatService.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Chat/BotChatManager.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Chat/BotChatManager.h
|
||||
|
||||
# Core hooks and helpers
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Core/PlayerBotHooks.cpp
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
/*
|
||||
* Copyright (C) 2024 TrinityCore <https://www.trinitycore.org/>
|
||||
*
|
||||
* AI CHAT SERVICE - IMPLEMENTATION
|
||||
*/
|
||||
|
||||
#include "AIChatService.h"
|
||||
#include "Player.h"
|
||||
#include "Config/PlayerbotConfig.h"
|
||||
#include "Log.h"
|
||||
#include <sstream>
|
||||
#include <random>
|
||||
#include <chrono>
|
||||
#include <iomanip>
|
||||
#include <fstream>
|
||||
|
||||
namespace Playerbot
|
||||
{
|
||||
|
||||
AIChatService* AIChatService::instance()
|
||||
{
|
||||
static AIChatService instance;
|
||||
return &instance;
|
||||
}
|
||||
|
||||
AIChatService::AIChatService()
|
||||
{
|
||||
}
|
||||
|
||||
void AIChatService::Initialize()
|
||||
{
|
||||
::std::lock_guard lock(_initMutex);
|
||||
|
||||
if (_initialized.load(::std::memory_order_acquire))
|
||||
{
|
||||
TC_LOG_WARN("playerbot.chat", "AIChatService: Already initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
LoadConfig();
|
||||
|
||||
if (!_config.enabled)
|
||||
{
|
||||
TC_LOG_INFO("playerbot.chat", "AIChatService: Disabled via config");
|
||||
_initialized.store(true, ::std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
|
||||
TC_LOG_INFO("playerbot.chat", "AIChatService: Initialized with API URL: {}, Model: {}",
|
||||
_config.apiUrl, _config.model);
|
||||
|
||||
_initialized.store(true, ::std::memory_order_release);
|
||||
}
|
||||
|
||||
void AIChatService::LoadConfig()
|
||||
{
|
||||
_config.apiUrl = sPlayerbotConfig->GetString("Playerbot.AIChat.ApiUrl", "http://localhost:11434/api/chat"); // Default to Ollama
|
||||
_config.apiKey = sPlayerbotConfig->GetString("Playerbot.AIChat.ApiKey", "");
|
||||
_config.model = sPlayerbotConfig->GetString("Playerbot.AIChat.Model", "llama3-8b-8192");
|
||||
_config.maxTokens = sPlayerbotConfig->GetInt("Playerbot.AIChat.MaxTokens", 150);
|
||||
_config.temperature = sPlayerbotConfig->GetFloat("Playerbot.AIChat.Temperature", 0.7f);
|
||||
_config.enabled = sPlayerbotConfig->GetBool("Playerbot.AIChat.Enable", false);
|
||||
_config.contextLimit = sPlayerbotConfig->GetInt("Playerbot.AIChat.ContextLimit", 10);
|
||||
_config.conversationTimeout = sPlayerbotConfig->GetInt("Playerbot.AIChat.ConversationTimeout", 300);
|
||||
|
||||
TC_LOG_DEBUG("playerbot.chat", "AIChatService: Config loaded - Enable={}, Url={}, Model={}, MaxTokens={}, Temperature={:.2f}",
|
||||
_config.enabled, _config.apiUrl, _config.model, _config.maxTokens, _config.temperature);
|
||||
}
|
||||
|
||||
void AIChatService::ReloadConfig()
|
||||
{
|
||||
LoadConfig();
|
||||
TC_LOG_INFO("playerbot.chat", "AIChatService: Configuration reloaded");
|
||||
}
|
||||
|
||||
std::string AIChatService::GenerateResponse(ObjectGuid botGuid, ObjectGuid playerGuid,
|
||||
std::string const& playerMessage, bool isWhisper)
|
||||
{
|
||||
if (!_initialized.load(::std::memory_order_acquire))
|
||||
{
|
||||
TC_LOG_ERROR("playerbot.chat", "AIChatService: Not initialized");
|
||||
return "";
|
||||
}
|
||||
|
||||
if (!_config.enabled)
|
||||
{
|
||||
TC_LOG_DEBUG("playerbot.chat", "AIChatService: Disabled, skipping AI response");
|
||||
return "";
|
||||
}
|
||||
|
||||
if (playerMessage.empty())
|
||||
{
|
||||
TC_LOG_DEBUG("playerbot.chat", "AIChatService: Empty player message");
|
||||
return "";
|
||||
}
|
||||
|
||||
// Get or create conversation context
|
||||
ConversationContext* context = GetConversation(botGuid, playerGuid);
|
||||
if (!context)
|
||||
{
|
||||
TC_LOG_ERROR("playerbot.chat", "AIChatService: Failed to get/create conversation context");
|
||||
return "";
|
||||
}
|
||||
|
||||
// Add player message to context
|
||||
ChatMessage playerMsg;
|
||||
playerMsg.content = playerMessage;
|
||||
playerMsg.isFromPlayer = true;
|
||||
playerMsg.timestamp = ::std::chrono::system_clock::to_time_t(::std::chrono::system_clock::now());
|
||||
context->messages.push_back(playerMsg);
|
||||
context->lastActivityTime = playerMsg.timestamp;
|
||||
|
||||
// Trim conversation history if too long
|
||||
TrimConversationHistory(*context);
|
||||
|
||||
// Generate AI response
|
||||
std::string response = CallAIAPI(*context, playerMessage);
|
||||
|
||||
if (!response.empty())
|
||||
{
|
||||
// Add bot response to context
|
||||
ChatMessage botMsg;
|
||||
botMsg.content = response;
|
||||
botMsg.isFromPlayer = false;
|
||||
botMsg.timestamp = ::std::chrono::system_clock::to_time_t(::std::chrono::system_clock::now());
|
||||
context->messages.push_back(botMsg);
|
||||
context->lastActivityTime = botMsg.timestamp;
|
||||
|
||||
TC_LOG_DEBUG("playerbot.chat", "AIChatService: Bot {} generated response for player {}: '{}'",
|
||||
context->botName, context->playerName, response);
|
||||
}
|
||||
else
|
||||
{
|
||||
TC_LOG_WARN("playerbot.chat", "AIChatService: Failed to generate response for player {} from bot {}",
|
||||
context->playerName, context->botName);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
ConversationContext* AIChatService::GetConversation(ObjectGuid botGuid, ObjectGuid playerGuid)
|
||||
{
|
||||
::std::lock_guard lock(_conversationMutex);
|
||||
|
||||
auto& botConversations = _conversations[botGuid];
|
||||
auto it = botConversations.find(playerGuid);
|
||||
|
||||
if (it == botConversations.end())
|
||||
{
|
||||
// Create new conversation context
|
||||
ConversationContext context;
|
||||
context.playerGuid = playerGuid;
|
||||
context.botGuid = botGuid;
|
||||
context.lastActivityTime = ::std::chrono::system_clock::to_time_t(::std::chrono::system_clock::now());
|
||||
|
||||
// Try to get player and bot names (this will be filled in by the caller)
|
||||
context.playerName = "Player";
|
||||
context.botName = "Bot";
|
||||
context.botClass = 0;
|
||||
context.botLevel = 0;
|
||||
|
||||
botConversations[playerGuid] = context;
|
||||
return &botConversations[playerGuid];
|
||||
}
|
||||
|
||||
return &it->second;
|
||||
}
|
||||
|
||||
void AIChatService::CleanupOldConversations()
|
||||
{
|
||||
::std::lock_guard lock(_conversationMutex);
|
||||
|
||||
uint32 currentTime = ::std::chrono::system_clock::to_time_t(::std::chrono::system_clock::now());
|
||||
uint32 cleaned = 0;
|
||||
|
||||
for (auto& botPair : _conversations)
|
||||
{
|
||||
auto it = botPair.second.begin();
|
||||
while (it != botPair.second.end())
|
||||
{
|
||||
if (currentTime - it->second.lastActivityTime > _config.conversationTimeout)
|
||||
{
|
||||
it = botPair.second.erase(it);
|
||||
++cleaned;
|
||||
}
|
||||
else
|
||||
{
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cleaned > 0)
|
||||
{
|
||||
TC_LOG_DEBUG("playerbot.chat", "AIChatService: Cleaned up {} old conversations", cleaned);
|
||||
}
|
||||
}
|
||||
|
||||
void AIChatService::TrimConversationHistory(ConversationContext& context)
|
||||
{
|
||||
if (context.messages.size() > _config.contextLimit)
|
||||
{
|
||||
// Remove oldest messages, keeping the contextLimit most recent
|
||||
size_t toRemove = context.messages.size() - _config.contextLimit;
|
||||
context.messages.erase(context.messages.begin(), context.messages.begin() + toRemove);
|
||||
}
|
||||
}
|
||||
|
||||
std::string AIChatService::BuildSystemPrompt(ConversationContext const& context)
|
||||
{
|
||||
::std::ostringstream prompt;
|
||||
|
||||
prompt << "You are a World of Warcraft bot character named " << context.botName << ".\n";
|
||||
prompt << "You are a level " << context.botLevel << " ";
|
||||
|
||||
// Add class-specific personality
|
||||
switch (context.botClass)
|
||||
{
|
||||
case CLASS_WARRIOR:
|
||||
prompt << "Warrior. You are brave and honorable, often speaking about battle, honor, and strength.";
|
||||
break;
|
||||
case CLASS_PALADIN:
|
||||
prompt << "Paladin. You are righteous and devout, often speaking about the Light, justice, and protecting the innocent.";
|
||||
break;
|
||||
case CLASS_HUNTER:
|
||||
prompt << "Hunter. You love nature and animals, often speaking about your pet, tracking, and the wild.";
|
||||
break;
|
||||
case CLASS_ROGUE:
|
||||
prompt << "Rogue. You are stealthy and cunning, often speaking about shadows, secrets, and quick strikes.";
|
||||
break;
|
||||
case CLASS_PRIEST:
|
||||
prompt << "Priest. You are spiritual and wise, often speaking about the Light, healing, and faith.";
|
||||
break;
|
||||
case CLASS_DEATH_KNIGHT:
|
||||
prompt << "Death Knight. You are dark and brooding, often speaking about death, the Scourge, and your dark powers.";
|
||||
break;
|
||||
case CLASS_SHAMAN:
|
||||
prompt << "Shaman. You are connected to the elements, often speaking about spirits, nature, and elemental balance.";
|
||||
break;
|
||||
case CLASS_MAGE:
|
||||
prompt << "Mage. You are intelligent and studious, often speaking about magic, arcane knowledge, and research.";
|
||||
break;
|
||||
case CLASS_WARLOCK:
|
||||
prompt << "Warlock. You are mysterious and powerful, often speaking about demons, dark magic, and forbidden knowledge.";
|
||||
break;
|
||||
case CLASS_MONK:
|
||||
prompt << "Monk. You are disciplined and peaceful, often speaking about balance, meditation, and martial arts.";
|
||||
break;
|
||||
case CLASS_DRUID:
|
||||
prompt << "Druid. You are one with nature, often speaking about the Emerald Dream, shapeshifting, and protecting nature.";
|
||||
break;
|
||||
case CLASS_DEMON_HUNTER:
|
||||
prompt << "Demon Hunter. You are fierce and determined, often speaking about hunting demons and sacrificing for power.";
|
||||
break;
|
||||
case CLASS_EVOKER:
|
||||
prompt << "Evoker. You are draconic and powerful, often speaking about dragons, magic, and the Dragon Isles.";
|
||||
break;
|
||||
default:
|
||||
prompt << "adventurer. You are exploring the world of Azeroth.";
|
||||
break;
|
||||
}
|
||||
|
||||
prompt << "\n\nKeep your responses brief (1-2 sentences), in character, and relevant to the conversation context.";
|
||||
prompt << " Do not break character or mention that you are an AI.";
|
||||
prompt << " Respond as if you are actually this character in the game world.";
|
||||
|
||||
return prompt.str();
|
||||
}
|
||||
|
||||
std::string AIChatService::BuildConversationHistory(ConversationContext const& context)
|
||||
{
|
||||
::std::ostringstream history;
|
||||
|
||||
for (auto const& msg : context.messages)
|
||||
{
|
||||
if (msg.isFromPlayer)
|
||||
history << context.playerName << ": " << msg.content << "\n";
|
||||
else
|
||||
history << context.botName << ": " << msg.content << "\n";
|
||||
}
|
||||
|
||||
return history.str();
|
||||
}
|
||||
|
||||
std::string AIChatService::CallAIAPI(ConversationContext const& context, std::string const& playerMessage)
|
||||
{
|
||||
// Build JSON payload for API call
|
||||
::std::ostringstream jsonPayload;
|
||||
|
||||
jsonPayload << "{\n";
|
||||
jsonPayload << " \"model\": \"" << _config.model << "\",\n";
|
||||
jsonPayload << " \"messages\": [\n";
|
||||
jsonPayload << " {\"role\": \"system\", \"content\": \"" << BuildSystemPrompt(context) << "\"},\n";
|
||||
|
||||
// Add conversation history
|
||||
for (auto const& msg : context.messages)
|
||||
{
|
||||
if (msg.isFromPlayer)
|
||||
jsonPayload << " {\"role\": \"user\", \"content\": \"" << msg.content << "\"},\n";
|
||||
else
|
||||
jsonPayload << " {\"role\": \"assistant\", \"content\": \"" << msg.content << "\"},\n";
|
||||
}
|
||||
|
||||
jsonPayload << " {\"role\": \"user\", \"content\": \"" << playerMessage << "\"}\n";
|
||||
jsonPayload << " ],\n";
|
||||
jsonPayload << " \"max_tokens\": " << _config.maxTokens << ",\n";
|
||||
jsonPayload << " \"temperature\": " << _config.temperature << "\n";
|
||||
jsonPayload << "}";
|
||||
|
||||
// Send HTTP request
|
||||
std::string response = SendHTTPRequest(_config.apiUrl, jsonPayload.str());
|
||||
|
||||
if (response.empty())
|
||||
{
|
||||
TC_LOG_ERROR("playerbot.chat", "AIChatService: Empty response from API");
|
||||
return "";
|
||||
}
|
||||
|
||||
// Parse JSON response to extract the AI's message
|
||||
// For now, simple parsing - in production, use a proper JSON library
|
||||
size_t contentPos = response.find("\"content\": \"");
|
||||
if (contentPos != ::std::string::npos)
|
||||
{
|
||||
contentPos += 11; // Skip "content": "
|
||||
size_t endPos = response.find("\"", contentPos);
|
||||
if (endPos != ::std::string::npos)
|
||||
{
|
||||
std::string content = response.substr(contentPos, endPos - contentPos);
|
||||
// Clean up escaped characters
|
||||
size_t pos = 0;
|
||||
while ((pos = content.find("\\n", pos)) != ::std::string::npos)
|
||||
{
|
||||
content.replace(pos, 2, "\n");
|
||||
pos += 1;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
TC_LOG_ERROR("playerbot.chat", "AIChatService: Failed to parse API response: {}", response);
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string AIChatService::SendHTTPRequest(std::string const& url, std::string const& jsonPayload)
|
||||
{
|
||||
// TODO: Implement HTTP request using TrinityCore's HTTP infrastructure
|
||||
// For now, return empty string to indicate failure
|
||||
TC_LOG_ERROR("playerbot.chat", "AIChatService: HTTP requests not yet implemented - please configure TrinityCore HTTP client");
|
||||
return "";
|
||||
}
|
||||
|
||||
} // namespace Playerbot
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright (C) 2024 TrinityCore <https://www.trinitycore.org/>
|
||||
*
|
||||
* AI CHAT SERVICE - Free AI Integration for Bot Conversations
|
||||
*
|
||||
* Purpose: Provides AI-powered chat responses for bots using free AI APIs
|
||||
* Supports: Groq, Ollama (local), and other OpenAI-compatible APIs
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Define.h"
|
||||
#include "ObjectGuid.h"
|
||||
#include "SharedDefines.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
|
||||
namespace Playerbot
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Single message in a conversation
|
||||
*/
|
||||
struct ChatMessage
|
||||
{
|
||||
std::string content;
|
||||
bool isFromPlayer; // true = player, false = bot
|
||||
uint32 timestamp;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Conversation context for a bot with a player
|
||||
*/
|
||||
struct ConversationContext
|
||||
{
|
||||
ObjectGuid playerGuid;
|
||||
ObjectGuid botGuid;
|
||||
std::vector<ChatMessage> messages;
|
||||
uint32 lastActivityTime;
|
||||
std::string playerName;
|
||||
std::string botName;
|
||||
uint8 botClass;
|
||||
uint32 botLevel;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief AI service configuration
|
||||
*/
|
||||
struct AIChatConfig
|
||||
{
|
||||
std::string apiUrl; // API endpoint (e.g., "https://api.groq.com/openai/v1/chat/completions")
|
||||
std::string apiKey; // API key (empty for local Ollama)
|
||||
std::string model; // Model name (e.g., "llama3-8b-8192", "mixtral-8x7b-32768")
|
||||
uint32 maxTokens; // Maximum tokens in response
|
||||
float temperature; // Response randomness (0.0-1.0)
|
||||
bool enabled; // Master enable/disable
|
||||
uint32 contextLimit; // Max messages to include in context
|
||||
uint32 conversationTimeout; // Seconds before conversation expires
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief AI-powered chat service for bots
|
||||
*
|
||||
* Integrates with free AI APIs to generate contextual responses
|
||||
* Supports conversation history and context awareness
|
||||
*/
|
||||
class TC_GAME_API AIChatService
|
||||
{
|
||||
public:
|
||||
static AIChatService* instance();
|
||||
|
||||
/**
|
||||
* Initialize the AI chat service
|
||||
*/
|
||||
void Initialize();
|
||||
|
||||
/**
|
||||
* Generate a response to a player message
|
||||
*
|
||||
* @param botGuid Bot's GUID
|
||||
* @param playerGuid Player's GUID
|
||||
* @param playerMessage The message from the player
|
||||
* @param isWhisper Whether this is a whisper (vs public chat)
|
||||
* @return Generated response, or empty string on failure
|
||||
*/
|
||||
std::string GenerateResponse(ObjectGuid botGuid, ObjectGuid playerGuid,
|
||||
std::string const& playerMessage, bool isWhisper);
|
||||
|
||||
/**
|
||||
* Get conversation context for a bot-player pair
|
||||
*/
|
||||
ConversationContext* GetConversation(ObjectGuid botGuid, ObjectGuid playerGuid);
|
||||
|
||||
/**
|
||||
* Clear old conversations (cleanup)
|
||||
*/
|
||||
void CleanupOldConversations();
|
||||
|
||||
/**
|
||||
* Check if AI chat is enabled
|
||||
*/
|
||||
bool IsEnabled() const { return _config.enabled; }
|
||||
|
||||
/**
|
||||
* Reload configuration
|
||||
*/
|
||||
void ReloadConfig();
|
||||
|
||||
private:
|
||||
AIChatService();
|
||||
~AIChatService() = default;
|
||||
AIChatService(AIChatService const&) = delete;
|
||||
AIChatService& operator=(AIChatService const&) = delete;
|
||||
|
||||
/**
|
||||
* Load configuration from config file
|
||||
*/
|
||||
void LoadConfig();
|
||||
|
||||
/**
|
||||
* Call AI API to generate response
|
||||
*/
|
||||
std::string CallAIAPI(ConversationContext const& context, std::string const& playerMessage);
|
||||
|
||||
/**
|
||||
* Build system prompt for the bot
|
||||
*/
|
||||
std::string BuildSystemPrompt(ConversationContext const& context);
|
||||
|
||||
/**
|
||||
* Build conversation history for API call
|
||||
*/
|
||||
std::string BuildConversationHistory(ConversationContext const& context);
|
||||
|
||||
/**
|
||||
* Clean old messages from conversation context
|
||||
*/
|
||||
void TrimConversationHistory(ConversationContext& context);
|
||||
|
||||
/**
|
||||
* HTTP request helper
|
||||
*/
|
||||
std::string SendHTTPRequest(std::string const& url, std::string const& jsonPayload);
|
||||
|
||||
// Configuration
|
||||
AIChatConfig _config;
|
||||
|
||||
// Conversation storage (botGuid -> playerGuid -> context)
|
||||
::std::unordered_map<ObjectGuid, ::std::unordered_map<ObjectGuid, ConversationContext>> _conversations;
|
||||
::std::mutex _conversationMutex;
|
||||
|
||||
// Initialization state
|
||||
::std::atomic<bool> _initialized{false};
|
||||
::std::mutex _initMutex;
|
||||
};
|
||||
|
||||
} // namespace Playerbot
|
||||
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* Copyright (C) 2024 TrinityCore <https://www.trinitycore.org/>
|
||||
*
|
||||
* BOT CHAT MANAGER - IMPLEMENTATION
|
||||
*/
|
||||
|
||||
#include "BotChatManager.h"
|
||||
#include "AIChatService.h"
|
||||
#include "Player.h"
|
||||
#include "World.h"
|
||||
#include "Log.h"
|
||||
#include "ObjectAccessor.h"
|
||||
#include <random>
|
||||
|
||||
namespace Playerbot
|
||||
{
|
||||
|
||||
BotChatManager* BotChatManager::instance()
|
||||
{
|
||||
static BotChatManager instance;
|
||||
return &instance;
|
||||
}
|
||||
|
||||
BotChatManager::BotChatManager()
|
||||
: _aiService(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
void BotChatManager::Initialize()
|
||||
{
|
||||
::std::lock_guard lock(_initMutex);
|
||||
|
||||
if (_initialized.load(::std::memory_order_acquire))
|
||||
{
|
||||
TC_LOG_WARN("playerbot.chat", "BotChatManager: Already initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize AI chat service
|
||||
_aiService = AIChatService::instance();
|
||||
_aiService->Initialize();
|
||||
|
||||
if (!_aiService->IsEnabled())
|
||||
{
|
||||
TC_LOG_INFO("playerbot.chat", "BotChatManager: AI chat disabled, using fallback responses");
|
||||
}
|
||||
else
|
||||
{
|
||||
TC_LOG_INFO("playerbot.chat", "BotChatManager: Initialized with AI chat enabled");
|
||||
}
|
||||
|
||||
_initialized.store(true, ::std::memory_order_release);
|
||||
}
|
||||
|
||||
void BotChatManager::HandleIncomingMessage(ObjectGuid playerGuid, ObjectGuid botGuid,
|
||||
std::string const& message, bool isWhisper, uint8 chatType)
|
||||
{
|
||||
if (!_initialized.load(::std::memory_order_acquire))
|
||||
{
|
||||
TC_LOG_ERROR("playerbot.chat", "BotChatManager: Not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate inputs
|
||||
if (playerGuid.IsEmpty() || botGuid.IsEmpty())
|
||||
{
|
||||
TC_LOG_DEBUG("playerbot.chat", "BotChatManager: Invalid GUID(s) - player: {}, bot: {}",
|
||||
playerGuid.ToString(), botGuid.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.empty())
|
||||
{
|
||||
TC_LOG_DEBUG("playerbot.chat", "BotChatManager: Empty message");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get player and bot
|
||||
Player* player = GetPlayer(playerGuid);
|
||||
Player* bot = GetBot(botGuid);
|
||||
|
||||
if (!player || !bot)
|
||||
{
|
||||
TC_LOG_DEBUG("playerbot.chat", "BotChatManager: Player or bot not found - player: {}, bot: {}",
|
||||
player ? "found" : "not found", bot ? "found" : "not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Update conversation context with player/bot info
|
||||
UpdateConversationContext(botGuid, playerGuid);
|
||||
|
||||
// Generate and send AI response
|
||||
SendAIResponse(botGuid, playerGuid, message, isWhisper);
|
||||
}
|
||||
|
||||
bool BotChatManager::SendAIResponse(ObjectGuid botGuid, ObjectGuid playerGuid,
|
||||
std::string const& playerMessage, bool isWhisper)
|
||||
{
|
||||
if (!_aiService || !_aiService->IsEnabled())
|
||||
{
|
||||
// Fallback: simple pre-defined responses if AI is disabled
|
||||
return SendFallbackResponse(botGuid, playerGuid, isWhisper);
|
||||
}
|
||||
|
||||
// Generate AI response
|
||||
std::string response = _aiService->GenerateResponse(botGuid, playerGuid, playerMessage, isWhisper);
|
||||
|
||||
if (response.empty())
|
||||
{
|
||||
TC_LOG_WARN("playerbot.chat", "BotChatManager: AI returned empty response, using fallback");
|
||||
return SendFallbackResponse(botGuid, playerGuid, isWhisper);
|
||||
}
|
||||
|
||||
// Send the response
|
||||
Player* bot = GetBot(botGuid);
|
||||
Player* player = GetPlayer(playerGuid);
|
||||
|
||||
if (!bot || !player)
|
||||
{
|
||||
TC_LOG_ERROR("playerbot.chat", "BotChatManager: Failed to get player or bot for response");
|
||||
return false;
|
||||
}
|
||||
|
||||
SendBotMessage(bot, player, response, isWhisper);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BotChatManager::SendFallbackResponse(ObjectGuid botGuid, ObjectGuid playerGuid, bool isWhisper)
|
||||
{
|
||||
Player* bot = GetBot(botGuid);
|
||||
Player* player = GetPlayer(playerGuid);
|
||||
|
||||
if (!bot || !player)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Simple fallback responses
|
||||
static const char* fallbackResponses[] = {
|
||||
"Greetings, adventurer!",
|
||||
"Well met, friend.",
|
||||
"Light be with you!",
|
||||
"For the Alliance!",
|
||||
"Lok'tar Ogar!",
|
||||
"May the shadows guide you.",
|
||||
"Nature's blessing upon you.",
|
||||
"By the Light!",
|
||||
"For the Horde!",
|
||||
"Strength and honor!"
|
||||
};
|
||||
|
||||
// Select random response
|
||||
static ::std::random_device rd;
|
||||
static ::std::mt19937 gen(rd());
|
||||
::std::uniform_int_distribution<> dis(0, sizeof(fallbackResponses) / sizeof(fallbackResponses[0]) - 1);
|
||||
|
||||
std::string response = fallbackResponses[dis(gen)];
|
||||
SendBotMessage(bot, player, response, isWhisper);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BotChatManager::IsEnabled() const
|
||||
{
|
||||
return _aiService && _aiService->IsEnabled();
|
||||
}
|
||||
|
||||
void BotChatManager::Cleanup()
|
||||
{
|
||||
if (_aiService)
|
||||
{
|
||||
_aiService->CleanupOldConversations();
|
||||
}
|
||||
}
|
||||
|
||||
::Player* BotChatManager::GetPlayer(ObjectGuid guid)
|
||||
{
|
||||
return ObjectAccessor::FindConnectedPlayer(guid);
|
||||
}
|
||||
|
||||
::Player* BotChatManager::GetBot(ObjectGuid guid)
|
||||
{
|
||||
return ObjectAccessor::FindConnectedPlayer(guid);
|
||||
}
|
||||
|
||||
void BotChatManager::SendBotMessage(::Player* bot, ::Player* target, std::string const& message, bool isWhisper)
|
||||
{
|
||||
if (!bot || !target)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (isWhisper)
|
||||
{
|
||||
bot->Whisper(message.c_str(), LANG_UNIVERSAL, target);
|
||||
TC_LOG_DEBUG("playerbot.chat", "Bot {} whispered to {}: '{}'",
|
||||
bot->GetName(), target->GetName(), message);
|
||||
}
|
||||
else
|
||||
{
|
||||
bot->Say(message.c_str(), LANG_UNIVERSAL);
|
||||
TC_LOG_DEBUG("playerbot.chat", "Bot {} said: '{}'", bot->GetName(), message);
|
||||
}
|
||||
}
|
||||
|
||||
void BotChatManager::UpdateConversationContext(ObjectGuid botGuid, ObjectGuid playerGuid)
|
||||
{
|
||||
if (!_aiService)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ConversationContext* context = _aiService->GetConversation(botGuid, playerGuid);
|
||||
if (!context)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Update player and bot info
|
||||
Player* bot = GetBot(botGuid);
|
||||
Player* player = GetPlayer(playerGuid);
|
||||
|
||||
if (bot)
|
||||
{
|
||||
context->botName = bot->GetName();
|
||||
context->botClass = bot->GetClass();
|
||||
context->botLevel = bot->GetLevel();
|
||||
}
|
||||
|
||||
if (player)
|
||||
{
|
||||
context->playerName = player->GetName();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Playerbot
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright (C) 2024 TrinityCore <https://www.trinitycore.org/>
|
||||
*
|
||||
* BOT CHAT MANAGER - Manages Bot Conversations and AI Responses
|
||||
*
|
||||
* Purpose: Handles incoming player messages and generates AI-powered responses
|
||||
* Integrates with the existing action system for seamless chat/whisper functionality
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Define.h"
|
||||
#include "ObjectGuid.h"
|
||||
#include "SharedDefines.h"
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
class Player;
|
||||
|
||||
namespace Playerbot
|
||||
{
|
||||
|
||||
class AIChatService;
|
||||
|
||||
/**
|
||||
* @brief Chat message from a player to a bot
|
||||
*/
|
||||
struct IncomingChatMessage
|
||||
{
|
||||
ObjectGuid playerGuid;
|
||||
ObjectGuid botGuid;
|
||||
std::string message;
|
||||
bool isWhisper;
|
||||
uint8 chatType; // CHAT_MSG_SAY, CHAT_MSG_WHISPER, etc.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Manages bot conversations and AI responses
|
||||
*
|
||||
* This class acts as the bridge between the game's chat system
|
||||
* and the AI chat service, handling message routing and response delivery.
|
||||
*/
|
||||
class TC_GAME_API BotChatManager
|
||||
{
|
||||
public:
|
||||
static BotChatManager* instance();
|
||||
|
||||
/**
|
||||
* Initialize the chat manager
|
||||
*/
|
||||
void Initialize();
|
||||
|
||||
/**
|
||||
* Handle incoming chat message from player to bot
|
||||
*
|
||||
* @param playerGuid GUID of the player who sent the message
|
||||
* @param botGuid GUID of the bot receiving the message
|
||||
* @param message The message content
|
||||
* @param isWhisper Whether this is a whisper
|
||||
* @param chatType The chat type (CHAT_MSG_SAY, CHAT_MSG_WHISPER, etc.)
|
||||
*/
|
||||
void HandleIncomingMessage(ObjectGuid playerGuid, ObjectGuid botGuid,
|
||||
std::string const& message, bool isWhisper, uint8 chatType);
|
||||
|
||||
/**
|
||||
* Generate and send AI response to player
|
||||
*
|
||||
* @param botGuid GUID of the bot
|
||||
* @param playerGuid GUID of the player
|
||||
* @param playerMessage The message from the player
|
||||
* @param isWhisper Whether to whisper the response
|
||||
* @return true if response was sent successfully
|
||||
*/
|
||||
bool SendAIResponse(ObjectGuid botGuid, ObjectGuid playerGuid,
|
||||
std::string const& playerMessage, bool isWhisper);
|
||||
|
||||
/**
|
||||
* Check if AI chat is enabled
|
||||
*/
|
||||
bool IsEnabled() const;
|
||||
|
||||
/**
|
||||
* Cleanup old conversations
|
||||
*/
|
||||
void Cleanup();
|
||||
|
||||
private:
|
||||
BotChatManager();
|
||||
~BotChatManager() = default;
|
||||
BotChatManager(BotChatManager const&) = delete;
|
||||
BotChatManager& operator=(BotChatManager const&) = delete;
|
||||
|
||||
/**
|
||||
* Get player by GUID
|
||||
*/
|
||||
::Player* GetPlayer(ObjectGuid guid);
|
||||
|
||||
/**
|
||||
* Get bot by GUID
|
||||
*/
|
||||
::Player* GetBot(ObjectGuid guid);
|
||||
|
||||
/**
|
||||
* Send chat message from bot
|
||||
*/
|
||||
void SendBotMessage(::Player* bot, ::Player* target, std::string const& message, bool isWhisper);
|
||||
|
||||
/**
|
||||
* Send fallback response when AI is unavailable
|
||||
*/
|
||||
bool SendFallbackResponse(ObjectGuid botGuid, ObjectGuid playerGuid, bool isWhisper);
|
||||
|
||||
/**
|
||||
* Update conversation context with player/bot info
|
||||
*/
|
||||
void UpdateConversationContext(ObjectGuid botGuid, ObjectGuid playerGuid);
|
||||
|
||||
// AI chat service
|
||||
AIChatService* _aiService;
|
||||
|
||||
// Initialization state
|
||||
::std::atomic<bool> _initialized{false};
|
||||
::std::mutex _initMutex;
|
||||
};
|
||||
|
||||
} // namespace Playerbot
|
||||
@@ -47,6 +47,7 @@
|
||||
#include "Session/BotWorldSessionMgr.h"
|
||||
#include "Session/BotPacketRelay.h"
|
||||
#include "Chat/BotChatCommandHandler.h"
|
||||
#include "Chat/BotChatManager.h"
|
||||
#include "Professions/ProfessionManager.h"
|
||||
#include "Professions/ProfessionDatabase.h"
|
||||
#include "Professions/ProfessionAuctionBridge.h"
|
||||
@@ -180,6 +181,11 @@ bool PlayerbotModule::Initialize()
|
||||
// Initialize BG Scripts (forces linker to include script object files)
|
||||
Playerbot::Coordination::Battleground::InitializeBGScripts();
|
||||
|
||||
// Initialize AI Chat System
|
||||
TC_LOG_INFO("module.playerbot", "Initializing AI Chat System...");
|
||||
Playerbot::BotChatManager::instance()->Initialize();
|
||||
TC_LOG_INFO("module.playerbot", "AI Chat System initialized");
|
||||
|
||||
// ==========================================================================
|
||||
|
||||
// NOTE: Do NOT register with ModuleUpdateManager here!
|
||||
|
||||
@@ -1796,6 +1796,79 @@ Playerbot.AI.MaxTargetDistance = 50.0
|
||||
#
|
||||
Playerbot.AI.PullDistance = 30.0
|
||||
|
||||
###############################################################################
|
||||
# A11. AI CHAT SYSTEM
|
||||
#
|
||||
# AI-powered bot conversations using free AI APIs
|
||||
# Supports: Ollama (local), Groq, and other OpenAI-compatible APIs
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
#
|
||||
# Playerbot.AIChat.Enable
|
||||
# Description: Enable AI-powered chat responses for bots
|
||||
# Default: 0 (disabled - requires API setup)
|
||||
# Values: 0 = Disabled (uses fallback responses), 1 = Enabled
|
||||
#
|
||||
Playerbot.AIChat.Enable = 0
|
||||
|
||||
#
|
||||
# Playerbot.AIChat.ApiUrl
|
||||
# Description: API endpoint for AI service
|
||||
# Default: "http://localhost:11434/api/chat" (Ollama local)
|
||||
# Examples: "https://api.groq.com/openai/v1/chat/completions" (Groq)
|
||||
# "http://localhost:11434/api/chat" (Ollama)
|
||||
#
|
||||
Playerbot.AIChat.ApiUrl = http://localhost:11434/api/chat
|
||||
|
||||
#
|
||||
# Playerbot.AIChat.ApiKey
|
||||
# Description: API key for cloud services (empty for local Ollama)
|
||||
# Default: "" (no key)
|
||||
# Note: Required for Groq, OpenAI, etc. Not needed for local Ollama
|
||||
#
|
||||
Playerbot.AIChat.ApiKey =
|
||||
|
||||
#
|
||||
# Playerbot.AIChat.Model
|
||||
# Description: AI model to use for chat generation
|
||||
# Default: "llama3-8b-8192" (Llama 3 8B via Ollama)
|
||||
# Examples: "llama3-8b-8192", "mixtral-8x7b-32768", "gemma-7b-it"
|
||||
#
|
||||
Playerbot.AIChat.Model = llama3-8b-8192
|
||||
|
||||
#
|
||||
# Playerbot.AIChat.MaxTokens
|
||||
# Description: Maximum tokens in AI response (controls response length)
|
||||
# Default: 150
|
||||
# Range: 50-500
|
||||
#
|
||||
Playerbot.AIChat.MaxTokens = 150
|
||||
|
||||
#
|
||||
# Playerbot.AIChat.Temperature
|
||||
# Description: Response randomness (0.0 = deterministic, 1.0 = creative)
|
||||
# Default: 0.7
|
||||
# Range: 0.0-1.0
|
||||
#
|
||||
Playerbot.AIChat.Temperature = 0.7
|
||||
|
||||
#
|
||||
# Playerbot.AIChat.ContextLimit
|
||||
# Description: Maximum messages to include in conversation context
|
||||
# Default: 10
|
||||
# Range: 5-20
|
||||
#
|
||||
Playerbot.AIChat.ContextLimit = 10
|
||||
|
||||
#
|
||||
# Playerbot.AIChat.ConversationTimeout
|
||||
# Description: Seconds before conversation context is expired
|
||||
# Default: 300 (5 minutes)
|
||||
# Range: 60-3600
|
||||
#
|
||||
Playerbot.AIChat.ConversationTimeout = 300
|
||||
|
||||
###############################################################################
|
||||
# B19. AI DECISION FUSION [WIRED]
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user