diff --git a/conf/mod_ollama_chat.conf.dist b/conf/mod_ollama_chat.conf.dist index b6000322e2..0ea7b18c71 100644 --- a/conf/mod_ollama_chat.conf.dist +++ b/conf/mod_ollama_chat.conf.dist @@ -33,9 +33,20 @@ OllamaChat.DebugShowFullPrompt = 0 # OLLAMA LLM CONNECTION AND INFERENCE # -------------------------------------------- +# OllamaChat.ApiProvider +# Description: The API backend provider to use for LLM inference. +# "ollama" - Ollama's native generation endpoint (/api/generate). +# "llamacpp" - llama.cpp's native completion endpoint (/completion). +# "openai" - OpenAI-compatible chat completions endpoint (/v1/chat/completions). +# Supported by llama.cpp, LM Studio, Ollama, and cloud hosts. +# Default: "ollama" +OllamaChat.ApiProvider = "ollama" + # OllamaChat.Url # Description: The URL used to query the Ollama API. -# Default: http://localhost:11434/api/generate +# Default: http://localhost:11434/api/generate (For Ollama) +# http://localhost:8080/completion (For llama.cpp native) +# http://localhost:8080/v1/chat/completions (For OpenAI-compatible) OllamaChat.Url = http://localhost:11434/api/generate # OllamaChat.Model diff --git a/data/sql/characters/updates/acore_user_mod_ollama_chat_randomly_assign_personality.sql b/data/sql/characters/updates/acore_user_mod_ollama_chat_randomly_assign_personality.sql new file mode 100644 index 0000000000..a4875d445c --- /dev/null +++ b/data/sql/characters/updates/acore_user_mod_ollama_chat_randomly_assign_personality.sql @@ -0,0 +1,17 @@ +INSERT INTO mod_ollama_chat_personality (guid, personality) +SELECT + c.guid, + ( + SELECT `key` + FROM mod_ollama_chat_personality_templates + -- Exclude templates meant only for manual assignment + WHERE manual_only = 0 + -- This dummy condition forces MySQL to evaluate the subquery per row + AND c.guid IS NOT NULL + ORDER BY RAND() + LIMIT 1 + ) AS random_personality +FROM characters c +LEFT JOIN mod_ollama_chat_personality p + ON c.guid = p.guid +WHERE p.guid IS NULL; \ No newline at end of file diff --git a/src/mod-ollama-chat_api.cpp b/src/mod-ollama-chat_api.cpp index 271077d9dd..13c15ed001 100644 --- a/src/mod-ollama-chat_api.cpp +++ b/src/mod-ollama-chat_api.cpp @@ -42,97 +42,194 @@ std::string QueryOllamaAPI(const std::string& prompt) // Sanitize the prompt to ensure it's valid UTF-8 before creating JSON std::string sanitizedPrompt = SanitizeUTF8(prompt); + std::string requestDataStr; - nlohmann::json requestData = { - {"model", model}, - {"prompt", sanitizedPrompt}, - {"stream", false} - }; + if (g_OllamaApiProvider == "ollama") + { + nlohmann::json requestData = { + {"model", model}, + {"prompt", sanitizedPrompt}, + {"stream", false} + }; - // Create options object for model parameters - nlohmann::json options; - bool hasOptions = false; + // Create options object for model parameters + nlohmann::json options; + bool hasOptions = false; - // Only include if set (do not send defaults if user did not set them) - if (g_OllamaNumPredict > 0) { - options["num_predict"] = g_OllamaNumPredict; - hasOptions = true; - } - if (g_OllamaTemperature != 0.8f) { - options["temperature"] = g_OllamaTemperature; - hasOptions = true; - } - if (g_OllamaTopP != 0.95f) { - options["top_p"] = g_OllamaTopP; - hasOptions = true; - } - if (g_OllamaRepeatPenalty != 1.1f) { - options["repeat_penalty"] = g_OllamaRepeatPenalty; - hasOptions = true; - } - if (g_OllamaNumCtx > 0) { - options["num_ctx"] = g_OllamaNumCtx; - hasOptions = true; - } - if (g_OllamaNumThreads > 0) { - options["num_thread"] = g_OllamaNumThreads; - hasOptions = true; - if(g_DebugEnabled) { - //LOG_INFO("server.loading", "[Ollama Chat] Setting num_thread to: {}", g_OllamaNumThreads); - } - } else if(g_DebugEnabled) { - //LOG_INFO("server.loading", "[Ollama Chat] g_OllamaNumThreads is: {} (not sending num_thread)", g_OllamaNumThreads); - } - if (!g_OllamaSeed.empty()) { - try { - int seedValue = std::stoi(g_OllamaSeed); - options["seed"] = seedValue; + // Only include if set (do not send defaults if user did not set them) + if (g_OllamaNumPredict > 0) { + options["num_predict"] = g_OllamaNumPredict; + hasOptions = true; + } + if (g_OllamaTemperature != 0.8f) { + options["temperature"] = g_OllamaTemperature; + hasOptions = true; + } + if (g_OllamaTopP != 0.95f) { + options["top_p"] = g_OllamaTopP; hasOptions = true; - } catch (const std::exception& e) { - if(g_DebugEnabled) { - LOG_INFO("server.loading", "[Ollama Chat] Invalid seed value: {}", g_OllamaSeed); + } + if (g_OllamaRepeatPenalty != 1.1f) { + options["repeat_penalty"] = g_OllamaRepeatPenalty; + hasOptions = true; + } + if (g_OllamaNumCtx > 0) { + options["num_ctx"] = g_OllamaNumCtx; + hasOptions = true; + } + if (g_OllamaNumThreads > 0) { + options["num_thread"] = g_OllamaNumThreads; + hasOptions = true; + } + if (!g_OllamaSeed.empty()) { + try { + int seedValue = std::stoi(g_OllamaSeed); + options["seed"] = seedValue; + hasOptions = true; + } catch (...) {} + } + + // Add options object if any options were set + if (hasOptions) { + requestData["options"] = options; + } + + // Root-level parameters (these stay at root level) + if (!g_OllamaStop.empty()) { + // If comma-separated, convert to array + std::vector stopSeqs; + std::stringstream ss(g_OllamaStop); + std::string item; + while (std::getline(ss, item, ',')) { + // trim whitespace + size_t start = item.find_first_not_of(" \t"); + size_t end = item.find_last_not_of(" \t"); + if (start != std::string::npos && end != std::string::npos) + stopSeqs.push_back(item.substr(start, end - start + 1)); } + if (!stopSeqs.empty()) + requestData["stop"] = stopSeqs; + } + if (!g_OllamaSystemPrompt.empty()) + { + // Sanitize system prompt as well + requestData["system"] = SanitizeUTF8(g_OllamaSystemPrompt); } - } - // Add options object if any options were set - if (hasOptions) { - requestData["options"] = options; - } + if (g_ThinkModeEnableForModule) + { + if(g_DebugEnabled) + { + LOG_INFO("server.loading", "[Ollama Chat] LLM set to Think mode."); + } + requestData["think"] = true; + requestData["hidethinking"] = true; + } - // Root-level parameters (these stay at root level) - if (!g_OllamaStop.empty()) { - // If comma-separated, convert to array - std::vector stopSeqs; - std::stringstream ss(g_OllamaStop); - std::string item; - while (std::getline(ss, item, ',')) { - // trim whitespace - size_t start = item.find_first_not_of(" \t"); - size_t end = item.find_last_not_of(" \t"); - if (start != std::string::npos && end != std::string::npos) - stopSeqs.push_back(item.substr(start, end - start + 1)); - } - if (!stopSeqs.empty()) - requestData["stop"] = stopSeqs; + requestDataStr = requestData.dump(); } - if (!g_OllamaSystemPrompt.empty()) + else if (g_OllamaApiProvider == "llamacpp") { - // Sanitize system prompt as well - requestData["system"] = SanitizeUTF8(g_OllamaSystemPrompt); - } + nlohmann::json requestData = { + {"prompt", sanitizedPrompt}, + {"stream", false} + }; - if (g_ThinkModeEnableForModule) - { - if(g_DebugEnabled) + if (g_OllamaNumPredict > 0) { + requestData["n_predict"] = g_OllamaNumPredict; + } + if (g_OllamaTemperature != 0.8f) { + requestData["temperature"] = g_OllamaTemperature; + } + if (g_OllamaTopP != 0.95f) { + requestData["top_p"] = g_OllamaTopP; + } + if (g_OllamaRepeatPenalty != 1.1f) { + requestData["repeat_penalty"] = g_OllamaRepeatPenalty; + } + if (g_OllamaNumCtx > 0) { + requestData["n_ctx"] = g_OllamaNumCtx; + } + if (!g_OllamaSeed.empty()) { + try { + int seedValue = std::stoi(g_OllamaSeed); + requestData["seed"] = seedValue; + } catch (...) {} + } + if (!g_OllamaStop.empty()) { + std::vector stopSeqs; + std::stringstream ss(g_OllamaStop); + std::string item; + while (std::getline(ss, item, ',')) { + size_t start = item.find_first_not_of(" \t"); + size_t end = item.find_last_not_of(" \t"); + if (start != std::string::npos && end != std::string::npos) + stopSeqs.push_back(item.substr(start, end - start + 1)); + } + if (!stopSeqs.empty()) + requestData["stop"] = stopSeqs; + } + if (!g_OllamaSystemPrompt.empty()) { - LOG_INFO("server.loading", "[Ollama Chat] LLM set to Think mode."); + requestData["system_prompt"] = SanitizeUTF8(g_OllamaSystemPrompt); } - requestData["think"] = true; - requestData["hidethinking"] = true; + + requestDataStr = requestData.dump(); } + else if (g_OllamaApiProvider == "openai") + { + nlohmann::json messages = nlohmann::json::array(); + if (!g_OllamaSystemPrompt.empty()) { + messages.push_back({ + {"role", "system"}, + {"content", SanitizeUTF8(g_OllamaSystemPrompt)} + }); + } + messages.push_back({ + {"role", "user"}, + {"content", sanitizedPrompt} + }); + + nlohmann::json requestData = { + {"model", model}, + {"messages", messages}, + {"stream", false} + }; + + if (g_OllamaNumPredict > 0) { + requestData["max_tokens"] = g_OllamaNumPredict; + } + if (g_OllamaTemperature != 0.8f) { + requestData["temperature"] = g_OllamaTemperature; + } + if (g_OllamaTopP != 0.95f) { + requestData["top_p"] = g_OllamaTopP; + } + if (g_OllamaRepeatPenalty != 1.1f) { + requestData["frequency_penalty"] = std::min(2.0f, std::max(-2.0f, g_OllamaRepeatPenalty - 1.0f)); + } + if (!g_OllamaSeed.empty()) { + try { + int seedValue = std::stoi(g_OllamaSeed); + requestData["seed"] = seedValue; + } catch (...) {} + } + if (!g_OllamaStop.empty()) { + std::vector stopSeqs; + std::stringstream ss(g_OllamaStop); + std::string item; + while (std::getline(ss, item, ',')) { + size_t start = item.find_first_not_of(" \t"); + size_t end = item.find_last_not_of(" \t"); + if (start != std::string::npos && end != std::string::npos) + stopSeqs.push_back(item.substr(start, end - start + 1)); + } + if (!stopSeqs.empty()) + requestData["stop"] = stopSeqs; + } - std::string requestDataStr = requestData.dump(); + requestDataStr = requestData.dump(); + } // Make HTTP POST request using our custom client std::string responseBuffer = httpClient.Post(url, requestDataStr); @@ -147,37 +244,91 @@ std::string QueryOllamaAPI(const std::string& prompt) return ""; } - std::stringstream ss(responseBuffer); - std::string line; - std::ostringstream extractedResponse; + std::string botReply; try { - while (std::getline(ss, line)) - { - if (line.empty() || std::all_of(line.begin(), line.end(), isspace)) - continue; - - nlohmann::json jsonResponse = nlohmann::json::parse(line); + nlohmann::json jsonResponse = nlohmann::json::parse(responseBuffer); - if (jsonResponse.contains("response") && !jsonResponse["response"].get().empty()) + if (g_OllamaApiProvider == "ollama") + { + if (jsonResponse.contains("response")) + { + botReply = jsonResponse["response"].get(); + } + else { - extractedResponse << jsonResponse["response"].get(); + // Fallback to line-by-line parsing if Ollama outputs streaming-like JSON rows + std::stringstream ss(responseBuffer); + std::string line; + std::ostringstream extractedResponse; + while (std::getline(ss, line)) + { + if (line.empty() || std::all_of(line.begin(), line.end(), isspace)) + continue; + nlohmann::json lineJson = nlohmann::json::parse(line); + if (lineJson.contains("response") && !lineJson["response"].get().empty()) + extractedResponse << lineJson["response"].get(); + } + botReply = extractedResponse.str(); + } + } + else if (g_OllamaApiProvider == "llamacpp") + { + if (jsonResponse.contains("content")) + { + botReply = jsonResponse["content"].get(); + } + } + else if (g_OllamaApiProvider == "openai") + { + if (jsonResponse.contains("choices") && jsonResponse["choices"].is_array() && !jsonResponse["choices"].empty()) + { + auto firstChoice = jsonResponse["choices"][0]; + if (firstChoice.contains("message") && firstChoice["message"].contains("content")) + { + botReply = firstChoice["message"]["content"].get(); + } } } } catch (const std::exception& e) { - LOG_ERROR("server.loading", "[OllamaChat] ERROR: JSON parsing failed. Exception: {}", e.what()); - if(g_DebugEnabled) + // Fallback for line-by-line streaming parse for Ollama + if (g_OllamaApiProvider == "ollama") + { + try + { + std::stringstream ss(responseBuffer); + std::string line; + std::ostringstream extractedResponse; + while (std::getline(ss, line)) + { + if (line.empty() || std::all_of(line.begin(), line.end(), isspace)) + continue; + nlohmann::json lineJson = nlohmann::json::parse(line); + if (lineJson.contains("response") && !lineJson["response"].get().empty()) + extractedResponse << lineJson["response"].get(); + } + botReply = extractedResponse.str(); + } + catch (...) + { + LOG_ERROR("server.loading", "[OllamaChat] ERROR: JSON parsing failed. Exception: {}", e.what()); + return ""; + } + } + else { - LOG_INFO("server.loading", "[OllamaChat] Debug: Response buffer content: {}", responseBuffer); + LOG_ERROR("server.loading", "[OllamaChat] ERROR: JSON parsing failed. Exception: {}", e.what()); + if(g_DebugEnabled) + { + LOG_INFO("server.loading", "[OllamaChat] Debug: Response buffer content: {}", responseBuffer); + } + return ""; } - return ""; } - std::string botReply = extractedResponse.str(); - botReply = ExtractTextBetweenDoubleQuotes(botReply); // Check for unclosed think tags diff --git a/src/mod-ollama-chat_config.cpp b/src/mod-ollama-chat_config.cpp index 8bc1b1da33..485cb7fea2 100644 --- a/src/mod-ollama-chat_config.cpp +++ b/src/mod-ollama-chat_config.cpp @@ -40,6 +40,7 @@ uint32_t g_EventChatterMaxBotsPerPlayer = 2; // -------------------------------------------- // Ollama LLM API Configuration // -------------------------------------------- +std::string g_OllamaApiProvider = "ollama"; std::string g_OllamaUrl = "http://localhost:11434/api/generate"; std::string g_OllamaModel = "llama3.2:1b"; uint32_t g_OllamaNumPredict = 40; @@ -384,6 +385,7 @@ void LoadOllamaChatConfig() g_BotReplyChance_Guild = sConfigMgr->GetOption("OllamaChat.BotReplyChance.Guild", 5); g_MaxBotsToPick = sConfigMgr->GetOption("OllamaChat.MaxBotsToPick", 2); + g_OllamaApiProvider = sConfigMgr->GetOption("OllamaChat.ApiProvider", "ollama"); g_OllamaUrl = sConfigMgr->GetOption("OllamaChat.Url", "http://localhost:11434/api/generate"); g_OllamaModel = sConfigMgr->GetOption("OllamaChat.Model", "llama3.2:1b"); g_OllamaNumPredict = sConfigMgr->GetOption("OllamaChat.NumPredict", 40); @@ -632,11 +634,11 @@ void LoadOllamaChatConfig() g_DisableForParty = sConfigMgr->GetOption("OllamaChat.DisableForParty", false); LOG_INFO("server.loading", - "[Ollama Chat] Config loaded: Enabled = {}, SayDistance = {}, YellDistance = {}, " + "[Ollama Chat] Config loaded: Enabled = {}, ApiProvider = {}, SayDistance = {}, YellDistance = {}, " "Reply Chances - Say: P{}%/B{}%, Channel: P{}%/B{}%, Party: P{}%/B{}%, Guild: P{}%/B{}%, MaxBotsToPick = {}, " "Url = {}, Model = {}, MaxConcurrentQueries = {}, EnableRandomChatter = {}, MinRandInt = {}, MaxRandInt = {}, RandomChatterRealPlayerDistance = {}, " "RandomChatterBotCommentChance = {}. MaxConcurrentQueries = {}. Extra blacklist commands: {}", - g_Enable, g_SayDistance, g_YellDistance, + g_Enable, g_OllamaApiProvider, g_SayDistance, g_YellDistance, g_PlayerReplyChance_Say, g_BotReplyChance_Say, g_PlayerReplyChance_Channel, g_BotReplyChance_Channel, g_PlayerReplyChance_Party, g_BotReplyChance_Party, diff --git a/src/mod-ollama-chat_config.h b/src/mod-ollama-chat_config.h index 37e66e26e8..86786cd012 100644 --- a/src/mod-ollama-chat_config.h +++ b/src/mod-ollama-chat_config.h @@ -41,6 +41,7 @@ extern uint32_t g_EventChatterMaxBotsPerPlayer; // -------------------------------------------- // Ollama LLM API Configuration // -------------------------------------------- +extern std::string g_OllamaApiProvider; extern std::string g_OllamaUrl; extern std::string g_OllamaModel; extern uint32_t g_OllamaNumPredict;