-- Location scanner -- Activates world info entries based on what the AI thinks the current location -- is. -- This file is part of KoboldAI. -- -- KoboldAI is free software: you can redistribute it and/or modify -- it under the terms of the GNU Affero General Public License as published by -- the Free Software Foundation, either version 3 of the License, or -- (at your option) any later version. -- -- This program is distributed in the hope that it will be useful, -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -- GNU Affero General Public License for more details. -- -- You should have received a copy of the GNU Affero General Public License -- along with this program. If not, see . kobold = require("bridge")() -- This line is optional and is only for EmmyLua type annotations local userscript = {} ---@class KoboldUserScript local example_config = [[;-- Location scanner ;-- ;-- Usage instructions: ;-- ;-- 1. Create a world info folder with name containing the string ;-- "<||ls||>" (without the double quotes). The name can be anything as ;-- long as it contains that inside it somewhere -- for example, you could ;-- set the name to "Locations <||ls||>". ;-- ;-- 2. Create a non-selective, constant world info key _in that folder_ with key ;-- "<||lslocation||>" (without the double quotes). Every once in a while, ;-- this script will generate 20 tokens using "The current location" ;-- as the submission and save the output into the <||lslocation||> entry. ;-- ;-- 3. Put some other world info entries into the world info folder. These ;-- entries will _only_ be triggered by the contents of the <||lslocation||> ;-- entry and not by your story itself, or if it has constant key turned on. ;-- ;-- You can edit some of the configuration values below to modify some of this ;-- behaviour: ;-- return { location_folder = "<||ls||>", location_key = "<||lslocation||>", submission = "\n\nThe current location:", n_wait = 12, -- The script will run its extra generation every time your story grows by this many chunks. n_tokens = 20, -- Number of tokens to generate in extra generation singleline = true, -- true or false; true will result in the extra generation's output being cut off after the first line. trim = true, -- true or false; true will result in the extra generation's output being cut off after the end of its last sentence. include = false, -- true or false; true will result in the <||lslocation||> entry's content being included in the story. template = "<|>", -- Allows you to format the extra generation's output; for example, to surround the output in square brackets, set this to "[<|>]" } ]] local cfg ---@type table do -- If config file is empty, write example config local f = kobold.get_config_file() f:seek("set") if f:read(1) == nil then f:write(example_config) end f:seek("set") example_config = nil -- Read config local err cfg, err = load(f:read("a")) if err ~= nil then error(err) end cfg = cfg() end if cfg.include == nil then cfg.include = false elseif cfg.template == nil then cfg.template = "<|>" end local folder ---@type KoboldWorldInfoFolder|nil local entry ---@type KoboldWorldInfoEntry|nil local location = "" local orig_entry_map = {} ---@type table local repeated = false local last_quotient = math.huge local genamt = 0 function userscript.inmod() if repeated then kobold.submission = cfg.submission genamt = kobold.settings.genamt kobold.settings.genamt = cfg.n_tokens end if entry == nil or folder == nil or not entry:is_valid() or not folder:is_valid() then folder = nil entry = nil for i, f in ipairs(kobold.worldinfo.folders) do if f.name:find(cfg.location_folder, 1, true) ~= nil then folder = f break end end if folder ~= nil then for i, e in ipairs(folder) do if e.key:find(cfg.location_key, 1, true) ~= nil then entry = e break end end end end orig_entry_map = {} if entry ~= nil then location = entry.content entry.constant = not not cfg.include end if folder ~= nil then for i, e in ipairs(folder) do if entry == nil or e.uid ~= entry.uid then orig_entry_map[e.uid] = { constant = e.constant, key = e.key, keysecondary = e.keysecondary, } e.constant = e.constant or (not repeated and e:compute_context("", {scan_story=false}) ~= e:compute_context(location, {scan_story=false})) e.key = "" e.keysecondary = "" end end end end function userscript.outmod() if entry ~= nil and entry:is_valid() then entry.constant = true end if repeated then local output = kobold.outputs[1] kobold.outputs[1] = "" for chunk in kobold.story:reverse_iter() do if chunk.content ~= "" then chunk.content = "" break end end kobold.settings.genamt = genamt output = output:match("^%s*(.*)%s*$") print("Extra generation result (prior to formatting): " .. output) if cfg.singleline then output = output:match("^[^\n]*") end if cfg.trim then local i = 0 while true do local j = output:find("[.?!)]", i + 1) if j == nil then break end i = j end if i > 0 then if output:sub(i+1, i+1) == '"' then i = i + 1 end output = output:sub(1, i) end end location = cfg.template:gsub("<|>", output) print("Extra generation result (after formatting): " .. location) if entry ~= nil and entry:is_valid() then entry.content = location end end local size = 0 for _ in kobold.story:forward_iter() do size = size + 1 end for uid, orig in pairs(orig_entry_map) do for k, v in pairs(orig) do kobold.worldinfo:finduid(uid)[k] = v end end local quotient = math.floor(size / cfg.n_wait) if repeated then repeated = false elseif quotient > last_quotient then print("Running extra generation") kobold.restart_generation() repeated = true end last_quotient = quotient end return userscript