diff --git a/init.lua b/init.lua index a1d3192..de22b34 100644 --- a/init.lua +++ b/init.lua @@ -1,130 +1,39 @@ -- =========================================================================== --- LLM Connect Init v0.7.2 +-- LLM Connect Init v0.7.4 -- author: H5N3RG -- license: LGPL-3.0-or-later -- =========================================================================== --- Load the HTTP API +local core = core -- just in case + +-- Load HTTP API local http = core.request_http_api() if not http then - core.log("error", "[llm_connect] HTTP API not available. Check secure.http_mods!") + core.log("error", "[llm_connect] HTTP API not available! Add 'llm_connect' to secure.http_mods in minetest.conf!") return end --- === Configuration === -local api_key = "" -local api_url = "" -- Correct URL -local model_name = "" -- Default model +-- === Load settings from menu / settingtypes.txt === +local max_tokens_type = core.settings:get_bool("llm_max_tokens_integer") and "integer" or "float" +local api_key = core.settings:get("llm_api_key") or "" +local api_url = core.settings:get("llm_api_url") or "" +local model_name = core.settings:get("llm_model") or "" --- Storage for the conversation history per player +-- Storage for conversation history per player local history = {} --- Stores the maximum history length per player (or as default) local max_history = { ["default"] = 10 } -local player_context_sent = {} -- Remembers if the context has already been sent +local player_context_sent = {} +-- Helper functions local function get_history(name) history[name] = history[name] or {} return history[name] end --- Function to get the maximum history length for a player local function get_max_history(name) return max_history[name] or max_history["default"] end --- Function to read an external file (for system_prompt.txt) -local function read_file_content(filepath) - local f = io.open(filepath, "r") - if not f then - core.log("error", "[llm_connect] Could not open file: " .. filepath) - return nil - end - local content = f:read("*a") - f:close() - return content -end - --- Path to the system prompt file -local mod_dir = minetest.get_modpath("llm_connect") - --- Load the material context module -local llm_materials_context = dofile(mod_dir .. "/llm_materials_context.lua") -if not llm_materials_context or type(llm_materials_context) ~= "table" then - core.log("error", "[llm_connect] 'llm_materials_context.lua' could not be loaded or is faulty. Material context will be disabled.") - llm_materials_context = nil -- Disable the module if errors occur -end - -local system_prompt_filepath = mod_dir .. "/system_prompt.txt" - --- Load the system prompt from the file -local system_prompt_content = read_file_content(system_prompt_filepath) -if not system_prompt_content or system_prompt_content == "" then - core.log("warning", "[llm_connect] System prompt file not found or empty. No default prompt will be used.") - system_prompt_content = nil -end - --- NEW: Privilege registration -core.register_privilege("llm", { - description = "Can chat with the LLM model", - give_to_singleplayer = true, -- Single players can chat by default - give_to_admin = true, -- Admins can chat by default -}) - -core.register_privilege("llm_root", { - description = "Can configure the LLM API key, model, and endpoint URL", - give_to_singleplayer = true, -- Single players can configure everything by default - give_to_admin = true, -- Admins receive this privilege by default -}) --- END NEW - --- === Metadata Functions === -local meta_data_functions = {} - -local function get_username(player_name) - return player_name or "Unknown Player" -end - --- List of installed mods -local function get_installed_mods() - local mods = {} - if minetest and minetest.get_mods then - for modname, _ in pairs(minetest.get_mods()) do - table.insert(mods, modname) - end - table.sort(mods) - else - core.log("warning", "[llm_connect] minetest.get_mods() not available.") - table.insert(mods, "Mod list not available") - end - return mods -end - --- Server settings & actual info -local function get_server_settings() - local settings = { - server_name = minetest.settings:get("server_name") or "Unnamed Server", - server_description= minetest.settings:get("server_description") or "No description", - motd = minetest.settings:get("motd") or "No MOTD set", - port = minetest.settings:get("port") or "Unknown", - gameid = (minetest.get_game_info and minetest.get_game_info().id) or - minetest.settings:get("gameid") or "Unknown", - game_name = (minetest.get_game_info and minetest.get_game_info().name) or "Unknown", - worldpath = minetest.get_worldpath() or "Unknown", - mapgen = minetest.get_mapgen_setting("mg_name") or "Unknown", - } - return settings -end - --- Main context function -function meta_data_functions.gather_context(player_name) - local context = {} - context.player = get_username(player_name) - context.installed_mods = get_installed_mods() - context.server_settings = get_server_settings() - return context -end - --- === Helper function for string splitting === local function string_split(str, delim) local res = {} local i = 1 @@ -143,162 +52,182 @@ local function string_split(str, delim) return res end --- === Chat Commands === +-- Load optional context files +local mod_dir = core.get_modpath("llm_connect") +local llm_materials_context = nil +pcall(function() + llm_materials_context = dofile(mod_dir .. "/llm_materials_context.lua") +end) --- Command to set the API key and URL +local function read_file_content(filepath) + local f = io.open(filepath, "r") + if not f then return nil end + local content = f:read("*a") + f:close() + return content +end + +local system_prompt_content = read_file_content(mod_dir .. "/system_prompt.txt") + +-- === Privileges === +core.register_privilege("llm", { description = "Can chat with the LLM model", give_to_singleplayer=true, give_to_admin=true }) +core.register_privilege("llm_root", { description = "Can configure the LLM API key, model, and endpoint", give_to_singleplayer=true, give_to_admin=true }) + +-- === Metadata Functions === +local meta_data_functions = {} +local function get_username(player_name) return player_name or "Unknown Player" end +local function get_installed_mods() + local mods = {} + if core.get_mods then + for modname,_ in pairs(core.get_mods()) do table.insert(mods, modname) end + table.sort(mods) + else + table.insert(mods,"Mod list not available") + end + return mods +end +local function get_server_settings() + local settings = { + server_name = core.settings:get("server_name") or "Unnamed Server", + server_description= core.settings:get("server_description") or "No description", + motd = core.settings:get("motd") or "No MOTD set", + port = core.settings:get("port") or "Unknown", + gameid = (core.get_game_info and core.get_game_info().id) or core.settings:get("gameid") or "Unknown", + game_name = (core.get_game_info and core.get_game_info().name) or "Unknown", + worldpath = core.get_worldpath() or "Unknown", + mapgen = core.get_mapgen_setting("mg_name") or "Unknown", + } + return settings +end + +function meta_data_functions.gather_context(player_name) + local context = {} + context.player = get_username(player_name) + context.installed_mods = get_installed_mods() + context.server_settings = get_server_settings() + return context +end + +-- === Chat Commands === core.register_chatcommand("llm_setkey", { params = " [url] [model]", description = "Sets the API key, URL, and model for the LLM.", - privs = {llm_root = true}, -- Requires llm_root privilege - func = function(name, param) - if not core.check_player_privs(name, {llm_root = true}) then -- Additional check - return false, "You do not have the permission to set the LLM key." - end - - local parts = string_split(param, " ") - if #parts == 0 then - return false, "Please provide an API key! [/llm_setkey [url] [model]]" - end - + privs = {llm_root=true}, + func = function(name,param) + if not core.check_player_privs(name,{llm_root=true}) then return false,"No permission!" end + local parts = string_split(param," ") + if #parts==0 then return false,"Please provide API key!" end api_key = parts[1] - if parts[2] then - api_url = parts[2] - end - if parts[3] then - model_name = parts[3] - end - - core.chat_send_player(name, "[LLM] API key and URL set. New URL: " .. api_url .. ", Model: " .. model_name) + if parts[2] then api_url = parts[2] end + if parts[3] then model_name = parts[3] end + core.chat_send_player(name,"[LLM] API key, URL and model set.") return true end, }) --- Command to change the model core.register_chatcommand("llm_setmodel", { params = "", - description = "Sets the LLM model to be used.", - privs = {llm_root = true}, -- Requires llm_root privilege - func = function(name, param) - if not core.check_player_privs(name, {llm_root = true}) then -- Additional check - return false, "You do not have the permission to change the LLM model." - end - - if param == "" then - return false, "Please provide a model name! [/llm_setmodel ]" - end + description = "Sets the LLM model.", + privs = {llm_root=true}, + func = function(name,param) + if not core.check_player_privs(name,{llm_root=true}) then return false,"No permission!" end + if param=="" then return false,"Provide a model name!" end model_name = param - core.chat_send_player(name, "[LLM] Model set to '" .. model_name .. "'.") + core.chat_send_player(name,"[LLM] Model set to '"..model_name.."'.") return true end, }) --- Command to change the endpoint core.register_chatcommand("llm_set_endpoint", { params = "", - description = "Sets the API URL of the LLM endpoint.", - privs = {llm_root = true}, -- Requires llm_root privilege - func = function(name, param) - if not core.check_player_privs(name, {llm_root = true}) then -- Additional check - return false, "You do not have the permission to change the LLM endpoint." - end - - if param == "" then - return false, "Please provide a URL! [/llm_set_endpoint ]" - end + description = "Sets the API endpoint URL.", + privs = {llm_root=true}, + func = function(name,param) + if not core.check_player_privs(name,{llm_root=true}) then return false,"No permission!" end + if param=="" then return false,"Provide URL!" end api_url = param - core.chat_send_player(name, "[LLM] API endpoint set to '" .. api_url .. "'.") + core.chat_send_player(name,"[LLM] API endpoint set to "..api_url) return true end, }) --- NEW: Command to set the context length core.register_chatcommand("llm_set_context", { params = " [player]", - description = "Sets the max context length. For all players if 'player' is omitted.", - privs = {llm_root = true}, - func = function(name, param) - if not core.check_player_privs(name, {llm_root = true}) then - return false, "You do not have the permission to change the context length." - end - - local parts = string_split(param, " ") - local count_str = parts[1] + description = "Sets the max context length.", + privs = {llm_root=true}, + func = function(name,param) + if not core.check_player_privs(name,{llm_root=true}) then return false,"No permission!" end + local parts = string_split(param," ") + local count = tonumber(parts[1]) local target_player = parts[2] - - local count = tonumber(count_str) - if not count or count < 1 then - return false, "Please provide a valid number > 0!" - end - - if target_player and target_player ~= "" then - max_history[target_player] = count - core.chat_send_player(name, "[LLM] Maximum history length for '" .. target_player .. "' set to " .. count .. ".") - else - max_history["default"] = count - core.chat_send_player(name, "[LLM] Default history length for all players set to " .. count .. ".") - end - + if not count or count<1 then return false,"Provide number > 0!" end + if target_player and target_player~="" then max_history[target_player]=count + else max_history["default"]=count end + core.chat_send_player(name,"[LLM] Context length set.") return true end, }) --- Command to reset the context -core.register_chatcommand("llm_reset", { - description = "Resets the LLM conversation and context.", - privs = {llm = true}, - func = function(name, param) - history[name] = {} - player_context_sent[name] = false - core.chat_send_player(name, "[LLM] Conversation and context have been reset.") +core.register_chatcommand("llm_float", { + description = "Set max_tokens as float", + privs = {llm_root=true}, + func = function(name) + max_tokens_type="float" + core.chat_send_player(name,"[LLM] max_tokens now sent as float.") + return true end, }) --- Main chat command +core.register_chatcommand("llm_integer", { + description = "Set max_tokens as integer", + privs = {llm_root=true}, + func = function(name) + max_tokens_type="integer" + core.chat_send_player(name,"[LLM] max_tokens now sent as integer.") + return true + end, +}) + +core.register_chatcommand("llm_reset", { + description = "Resets conversation and context.", + privs = {llm=true}, + func = function(name) + history[name] = {} + player_context_sent[name] = false + core.chat_send_player(name,"[LLM] Conversation reset.") + end, +}) + +-- === Main Chat Command === core.register_chatcommand("llm", { params = "", - description = "Sends a prompt to the LLM", - privs = {llm = true}, -- Requires llm privilege - func = function(name, param) - if not core.check_player_privs(name, {llm = true}) then - return false, "You do not have the permission to chat with the LLM." - end - - if param == "" then - return false, "Please provide a prompt!" - end - - if api_key == "" then - return false, "API key not set. Use /llm_setkey [url] [model]" + description = "Sends prompt to the LLM", + privs = {llm=true}, + func = function(name,param) + if not core.check_player_privs(name,{llm=true}) then return false,"No permission!" end + if param=="" then return false,"Provide a prompt!" end + if api_key=="" or api_url=="" or model_name=="" then + return false,"[LLM] API key, URL, or Model not set! Check mod settings." end local player_history = get_history(name) - local max_hist = get_max_history(name) -- Retrieves the specific or default context length - - -- Add the new user prompt to the history - table.insert(player_history, { role = "user", content = param }) - -- Remove the oldest entries if the history gets too long - while #player_history > max_hist do - table.remove(player_history, 1) - end + local max_hist = get_max_history(name) + table.insert(player_history,{role="user",content=param}) + while #player_history>max_hist do table.remove(player_history,1) end local messages = {} if system_prompt_content then - table.insert(messages, { role = "system", content = system_prompt_content }) + table.insert(messages,{role="system",content=system_prompt_content}) end - -- Send the context only once per player to save tokens if not player_context_sent[name] then local context_data = meta_data_functions.gather_context(name) - local mods_list_str = table.concat(context_data.installed_mods, ", ") - if #context_data.installed_mods > 10 then - mods_list_str = "(More than 10 installed mods: " .. #context_data.installed_mods .. ")" - end - + local mods_list_str = table.concat(context_data.installed_mods,", ") + if #context_data.installed_mods>10 then mods_list_str="(More than 10 installed mods: "..#context_data.installed_mods..")" end local materials_context_str = "" if llm_materials_context and llm_materials_context.get_available_materials then materials_context_str = "\n\n--- AVAILABLE MATERIALS ---\n" .. llm_materials_context.get_available_materials() end - local metadata_string = "Server Information:\n" .. " Player: " .. context_data.player .. "\n" .. " Server Name: " .. context_data.server_settings.server_name .. "\n" .. @@ -309,21 +238,33 @@ core.register_chatcommand("llm", { " World Path: " .. context_data.server_settings.worldpath .. "\n" .. " Port: " .. context_data.server_settings.port .. "\n" .. " Installed Mods (" .. #context_data.installed_mods .. "): " .. mods_list_str .. "\n" .. materials_context_str - - table.insert(messages, { role = "user", content = "--- METADATA ---\n" .. metadata_string }) + table.insert(messages,{role="user",content="--- METADATA ---\n"..metadata_string}) player_context_sent[name] = true end - for _, msg in ipairs(player_history) do - table.insert(messages, msg) + for _,msg in ipairs(player_history) do table.insert(messages,msg) end + + -- === Determine max_tokens value and log it === + local max_tokens_value = 2000 + if max_tokens_type=="integer" then + max_tokens_value = math.floor(max_tokens_value) + else + max_tokens_value = max_tokens_value + 0.0 end - local body = core.write_json({ - model = model_name, - messages = messages, - max_tokens = 2000 - }) + -- Debug output + core.log("action", "[llm_connect DEBUG] max_tokens_type = " .. max_tokens_type) + core.log("action", "[llm_connect DEBUG] max_tokens_value = " .. tostring(max_tokens_value)) + -- Write JSON + local body = core.write_json({ model=model_name, messages=messages, max_tokens=max_tokens_value }) + + -- Force integer in JSON string if needed + if max_tokens_type=="integer" then + body = body:gsub('"max_tokens"%s*:%s*(%d+)%.0', '"max_tokens": %1') + end + + -- Send HTTP request http.fetch({ url = api_url, post_data = body, @@ -339,16 +280,16 @@ core.register_chatcommand("llm", { local text = "(no answer)" if response and response.choices and response.choices[1] and response.choices[1].message then text = response.choices[1].message.content - table.insert(player_history, { role = "assistant", content = text }) + table.insert(player_history,{role="assistant",content=text}) elseif response and response.message and response.message.content then text = response.message.content end - core.chat_send_player(name, "[LLM] " .. text) + core.chat_send_player(name,"[LLM] "..text) else - core.chat_send_player(name, "[LLM] Request failed: " .. (result.error or "Unknown error")) + core.chat_send_player(name,"[LLM] Request failed: "..(result.error or "Unknown error")) end end) - return true, "Request sent to LLM..." + return true,"Request sent..." end, })