Загрузка данных
--[[
НаноИИ 3.1 "DeepWiki" - локальный ИИ на русском для OpenComputers (Minecraft 1.7.10)
с ансамблем нейросетей, долгими размышлениями, многоканальным поиском источников
и фоновым автообучением только из русской Википедии.
ОБНОВЛЕНИЕ 3.1 (поверх DeepWiki 3.0):
1) НОВЫЙ МОЗГ - АНСАМБЛЬ ПЕРЦЕПТРОНОВ.
Теперь семь настоящих многослойных перцептронов (20 признаков на входе).
Самая глубокая сеть: 20 -> 512 -> 384 -> 256 -> 128 -> 64 -> 32 -> 1.
В полном ансамбле около 5.7K нейронов и 870K весов; новые сети учатся
вместе с прежними, а старые веса переносятся при расширении входного слоя.
Веса хранятся в бинарном neuro.db; пресет выбирается по свободной памяти:
/net light|core|wide|deep|all
2) ДУМАЕТ ДОЛЬШЕ - /think 0..5 (по умолчанию 4).
ИИ определяет тип запроса, строит дополнительные формулировки, расширяет
поиск, отсеивает кандидатов с неверной сущностью/типом ответа, голосует
ансамблем и проверяет оставшийся ответ перед выдачей.
3) НЕ ВЫДАЁТ EXTRACT ЗА ОТВЕТ.
Сначала выделяет выжимку и структурированные карточки фактов (сущность,
отношение, год). Обычный ответ строится только из этой сохранённой базы.
Для онлайн-ответа нужен совпавший факт с другого домена; слабое совпадение
или непроверенный тип приводит к воздержанию. /verify - ручное подтверждение.
Ссылки и домены источников сохраняются; внешние результаты не идут в
автообучаемый корпус или очередь обхода.
4) АВТООБУЧЕНИЕ - /autotrain on | /autotrain 10 | /autotrain off.
ИИ сам обходит русскую Википедию: случайные статьи + переходы по ссылкам.
Каждая страница попадает в corpus.db с выжимкой и карточками, в поисковый
индекс и лексикон; языковая модель обучается на выжимке, не на extract.
5) УЧИТСЯ НА КАЖДОМ СЛОВЕ - lexicon.db.
Запоминается каждое слово, его частота и связи с соседними словами.
На лексиконе построено исправление опечаток, а команда /lex <слово>
показывает, что ИИ знает о слове.
6) ПРОВЕРКА ОТВЕТА.
Перед тем как ответить, ИИ смотрит, есть ли в ответе ключевые слова
вопроса. Если у лучшего кандидата их нет, ИИ берёт следующего, а не
выдаёт бессмыслицу. Это видно в ходе размышления: "проверил ответ: N%".
7) ПАМЯТЬ.
Веса больших слоёв упакованы по 16 бит; старые веса 16-входных сетей
мигрируют на новые 20-входные сети с сохранением уже выученного. Если
памяти не хватает, ИИ сам откатывается на меньший пресет. Лексикон тоже
умеет сжиматься.
8) ФАЙЛЫ БАЗЫ ЗНАНИЙ (все в одной папке, по умолчанию /home/nanoai/):
learned.db - чему тебя научили (teach / /train)
wiki.db - синтезированные выжимки и статус проверки
notes.db - твоё имя и заметки
sentences.db - запомненные фразы для режима "воспоминаний"
neuro.db - веса всех нейросетей (бинарный файл)
lexicon.db - каждое слово, его частота и связи
corpus.db - выжимки, карточки фактов и исходные фрагменты для обучения
crawl.db - очередь обхода Википедии
cfg.db - настройки (глубина размышления, пресет нейросетей)
КОМАНДЫ - ТОЛЬКО НА АНГЛИЙСКОМ. Сам ИИ всегда отвечает по-русски.
/help /exit /save /stats /notes /neuro /net /think /train /wiki /autotrain
/right /wrong /verify /forget /reset /lex
Запуск: nanoai_ru (или: nanoai_ru /путь/к/папке/)
Нужно: Lua 5.2 или 5.3, OpenOS.
]]
local component = require("component")
local computer = require("computer")
local term = require("term")
local ser = require("serialization")
local T = {} -- общая таблица-помощник (чтобы уложиться в лимит локальных переменных)
local okFs, fs = pcall(require, "filesystem")
if not okFs then fs = nil end
-- аргумент командной строки задаёт папку с файлами базы знаний
local ARGS = {...} -- аргументы запуска (как в OpenOS: lua nanoai_ru /папка/)
local function firstArg()
local a = ARGS[1]
if type(a) == "string" and a ~= "" then return a end
if type(arg) == "table" then -- запасной путь (обычный lua)
for i = 2, #arg do
if type(arg[i]) == "string" and arg[i] ~= "" then return arg[i] end
end
end
return nil
end
local DB_DIR = firstArg() or "/home/nanoai/"
if not DB_DIR:match("/$") then DB_DIR = DB_DIR .. "/" end
if fs and not fs.exists(DB_DIR) then pcall(fs.makeDirectory, DB_DIR) end
local DB_LEARNED = DB_DIR .. "learned.db"
local DB_WIKI = DB_DIR .. "wiki.db"
local DB_NOTES = DB_DIR .. "notes.db"
local DB_SENTENCES = DB_DIR .. "sentences.db"
local DB_NEURO = DB_DIR .. "neuro.db"
local DB_LEXICON = DB_DIR .. "lexicon.db"
local DB_CORPUS = DB_DIR .. "corpus.db"
local DB_CRAWL = DB_DIR .. "crawl.db"
local DB_CFG = DB_DIR .. "cfg.db"
local MAX_LEARNED = 200 -- сколько фраз из обычного разговора помнить
local MAX_NET = 250 -- максимум пар "вопрос => ответ" за одно обучение по ссылке
local MAX_WIKI = 300 -- максимум статей Википедии в базе
local MAX_CORPUS = 400 -- максимум статей, скачанных автообучением
local DIRECT = 0.58 -- порог уверенного ответа
local HEDGE = 0.34 -- порог ответа "кажется, это..."
local SPLIT = 0.68 -- порог для каждой части составного вопроса ("А и Б")
local WHOLE = 0.78 -- если всё сообщение целиком уже хорошо совпало, на части не делим
local floor, random, log, abs, exp, sqrt = math.floor, math.random, math.log, math.abs, math.exp, math.sqrt
local concat = table.concat
local format = string.format
local huge = math.huge
local hasInternet = component.isAvailable("internet")
if not os.sleep then -- os.sleep появился только в Lua 5.3
os.sleep = function(t)
local e = os.clock() + (t or 0)
while os.clock() < e do end
end
end
-- tanh: в OpenComputers (Lua 5.2 без LUA_COMPAT_MATHLIB) math.tanh НЕТ.
-- Поэтому всегда имеем свою реализацию: tanh(x) = (e^2x - 1) / (e^2x + 1).
local tanh = math.tanh
if type(tanh) ~= "function" then
tanh = function(x)
if x > 20 then return 1 end
if x < -20 then return -1 end
local e = exp(2 * x)
return (e - 1) / (e + 1)
end
end
local function gc()
if type(collectgarbage) == "function" then pcall(collectgarbage) end
end
math.randomseed(floor(os.time() + computer.uptime() * 1000))
random(); random(); random()
---------------------------------------------------------------------------
-- НАСТРОЙКИ
---------------------------------------------------------------------------
local CFG = {
think = 4, -- глубина размышления: 0..5
reasonVersion = 4, -- новая логика; поднимает старое значение think до 4
netset = "auto", -- light | core | wide | deep | all | auto
autotrain = "off", -- off | on
learnBudget = 24, -- сколько примеров дообучать нейросети при сборке базы
wikiTrust = 0.55, -- минимум релевантности для результата, который может подтвердить факт
}
-- пресеты ансамбля нейросетей
local NET_PRESETS = {
light = {
{name = "scout", sizes = {20, 48, 24, 1}, lr = 0.20, trust = 0.8},
},
core = {
{name = "scout", sizes = {20, 48, 24, 1}, lr = 0.20, trust = 0.7},
{name = "core", sizes = {20, 160, 80, 40, 1}, lr = 0.15, trust = 1.0},
},
wide = {
{name = "scout", sizes = {20, 48, 24, 1}, lr = 0.20, trust = 0.6},
{name = "core", sizes = {20, 160, 80, 40, 1}, lr = 0.15, trust = 0.9},
{name = "wide", sizes = {20, 1024, 1}, lr = 0.10, trust = 1.0}, -- 1045 нейронов
},
deep = {
{name = "scout", sizes = {20, 48, 24, 1}, lr = 0.20, trust = 0.6},
{name = "core", sizes = {20, 160, 80, 40, 1}, lr = 0.15, trust = 0.9},
{name = "wide", sizes = {20, 1024, 1}, lr = 0.10, trust = 0.9},
{name = "titan", sizes = {20, 512, 256, 128, 64, 32, 1}, lr = 0.08, trust = 1.0},
{name = "sage", sizes = {20, 256, 192, 128, 96, 64, 32, 1}, lr = 0.07, trust = 1.0},
{name = "atlas", sizes = {20, 384, 256, 192, 128, 64, 32, 1}, lr = 0.06, trust = 1.0},
{name = "oracle", sizes = {20, 512, 384, 256, 128, 64, 32, 1}, lr = 0.05, trust = 1.0},
},
}
NET_PRESETS.all = {}
for _, netSpec in ipairs(NET_PRESETS.deep) do
NET_PRESETS.all[#NET_PRESETS.all + 1] = netSpec
end
---------------------------------------------------------------------------
-- работа с текстом (UTF-8, кириллица)
---------------------------------------------------------------------------
local uni = _G.unicode
if not uni then
local ok, u = pcall(require, "unicode")
if ok then uni = u end
end
function T.lower(s)
if uni and uni.lower then return uni.lower(s) end
s = s:lower()
s = s:gsub("\208([\144-\159])", function(c) return "\208" .. string.char(c:byte() + 32) end) -- А-П
s = s:gsub("\208([\160-\175])", function(c) return "\209" .. string.char(c:byte() - 32) end) -- Р-Я
s = s:gsub("\208\129", "\209\145") -- Ё
return s
end
function T.ucfirst(s)
if s == "" then return s end
if uni and uni.upper and uni.sub then return uni.upper(uni.sub(s, 1, 1)) .. uni.sub(s, 2) end
local b1, b2 = s:byte(1, 2)
if b1 and b1 < 128 then return s:sub(1, 1):upper() .. s:sub(2) end
if b1 == 208 and b2 and b2 >= 176 and b2 <= 191 then return "\208" .. string.char(b2 - 32) .. s:sub(3) end
if b1 == 209 and b2 and b2 >= 128 and b2 <= 143 then return "\208" .. string.char(b2 + 32) .. s:sub(3) end
if b1 == 209 and b2 == 145 then return "\208\129" .. s:sub(3) end
return s
end
function T.chars(s)
local t = {}
for c in s:gmatch("[\0-\127\194-\244][\128-\191]*") do t[#t + 1] = c end
return t
end
function T.ulen(s)
return select(2, s:gsub("[^\128-\191]", ""))
end
function T.endsWith(w, suf)
return #w >= #suf and w:sub(-#suf) == suf
end
local PUNCT_U = {"\194\171", "\194\187", "\226\128\156", "\226\128\157", "\226\128\158", "\226\128\148",
"\226\128\147", "\226\128\166", "\226\128\153", "\226\128\152", "\226\132\150"}
-- слова: нижний регистр, ё -> е, без знаков препинания
function T.words(s)
s = T.lower(s)
s = s:gsub("\209\145", "\208\181")
for _, p in ipairs(PUNCT_U) do s = s:gsub(p, " ") end
s = s:gsub("'", "")
s = s:gsub("%p", " ")
local t = {}
for w in s:gmatch("%S+") do t[#t + 1] = w end
return t
end
-- простой стеммер: кирка/кирку/кирки/киркой -> "кирк", алмазы/алмазов -> "алмаз"
local RU_END = {
"ого", "его", "ому", "ему", "ими", "ыми", "ами", "ями", "ешь", "ишь", "ете", "ите", "ает", "яет", "ует",
"ают", "яют", "уют", "ала", "яла", "ила", "ыла", "али", "яли", "или", "ыли", "ать", "ять", "ить", "еть",
"уть", "ыть", "ией",
"ой", "ей", "ый", "ий", "ая", "яя", "ое", "ее", "ые", "ие", "ых", "их", "ую", "юю", "ом", "ем", "ым", "им",
"ам", "ям", "ах", "ях", "ов", "ев", "ия", "ии", "ию", "ья", "ье", "ью", "ою", "ею", "ет", "ут", "ют", "ат",
"ят", "ит", "ть", "ти", "ла", "ло", "ли", "ны", "на",
"а", "я", "ы", "и", "у", "ю", "о", "е", "ь", "й", "л",
}
table.sort(RU_END, function(a, b) return #a > #b end)
function T.stem(w)
if not w:find("[\128-\255]") then return w end -- латиница: как есть
local n = T.ulen(w)
if n <= 3 then return w end
if n > 4 and (T.endsWith(w, "ся") or T.endsWith(w, "сь")) then w = w:sub(1, -5); n = n - 2 end
for _ = 1, 2 do
for _, e in ipairs(RU_END) do
local el = T.ulen(e)
if T.endsWith(w, e) and n - el >= 3 then
w = w:sub(1, #w - #e); n = n - el
break
end
end
end
return w
end
local SYN = {}
local function syn(target, list)
local t = T.stem(target)
for _, w in ipairs(list) do SYN[T.stem(w)] = t end
end
syn("сделать", {"делать", "создать", "создавать", "построить", "строить", "скрафтить", "крафтить", "крафт",
"изготовить", "собрать", "собирать", "смастерить", "сконструировать"})
syn("найти", {"найду", "искать", "ищу", "достать", "добыть", "добывать", "получить", "получать", "взять",
"раздобыть", "находить"})
syn("убить", {"убей", "убивать", "победить", "победи", "одолеть", "уничтожить", "побеждать"})
syn("начать", {"начинать", "начало"})
syn("работать", {"работает", "работаешь", "работают", "функционирует"})
syn("компьютер", {"пк", "комп", "компьютера"})
syn("приручить", {"приручать", "задобрить", "одомашнить"})
syn("стоить", {"стоит", "цена", "стоимость"})
syn("играть", {"игра", "играешь", "сыграть"})
syn("знать", {"знаю", "знаешь", "знаем"})
function T.canon(s) return SYN[s] or s end
function T.norm(t) return T.canon(T.stem(t)) end
local STOP = {}
for w in ([[и в во не что он на я с со как а то все она так его но да ты к у же вы за бы по только ее мне
вот от меня еще нет о из ему когда если уже или ни быть был до вас там они тут где есть для мы тебя их
чем сам без под кто этот это эта эти того потому какой какая какие какое сколько почему зачем куда откуда расскажи скажи
объясни пожалуйста можешь могу нужно надо мне тебе нас вам им ей чего чему кого кому будет были была
было быть ну вообще просто очень вот это тот та те
такое такие такой такая такого таких какой какая какие какое
который которая которые которое тоже даже самый самая самое]]):gmatch("%S+") do
STOP[T.norm(w)] = true
end
local PRON = {["он"] = 1, ["она"] = 1, ["оно"] = 1, ["они"] = 1, ["его"] = 1, ["ее"] = 1, ["их"] = 1,
["это"] = 1, ["этом"] = 1, ["этого"] = 1, ["этот"] = 1, ["эта"] = 1, ["эти"] = 1}
local QWORD = {["что"] = 1, ["кто"] = 1, ["как"] = 1, ["где"] = 1, ["куда"] = 1, ["откуда"] = 1, ["когда"] = 1,
["почему"] = 1, ["зачем"] = 1, ["какой"] = 1, ["какая"] = 1, ["какое"] = 1, ["какие"] = 1, ["сколько"] = 1,
["чем"] = 1, ["чего"] = 1, ["можно"] = 1, ["можешь"] = 1, ["есть"] = 1, ["расскажи"] = 1, ["объясни"] = 1,
["скажи"] = 1, ["покажи"] = 1, ["подскажи"] = 1, ["помоги"] = 1, ["назови"] = 1, ["дай"] = 1,
["чей"] = 1, ["чья"] = 1, ["правда"] = 1, ["зачем"] = 1, ["каким"] = 1, ["какая"] = 1}
local FIRST_PERSON = {["я"] = 1, ["мне"] = 1, ["меня"] = 1, ["мой"] = 1, ["моя"] = 1, ["мое"] = 1, ["мои"] = 1,
["мы"] = 1, ["нас"] = 1, ["нам"] = 1, ["наш"] = 1, ["моих"] = 1, ["мною"] = 1}
local MODIFIER = {}
for w in ([[в во на для с со про о об если когда но а и или чтобы потому по из от у к за над при]]):gmatch("%S+") do
MODIFIER[w] = 1
end
function T.pick(t) return t[random(#t)] end
---------------------------------------------------------------------------
-- ЛЕКСИКОН: учим КАЖДОЕ слово (частота + связи с соседними)
---------------------------------------------------------------------------
local lex = {} -- слово -> {n = сколько раз, c = {соседнее слово = сколько}}
local lexTotal = 0
local lexChanged = false
local LEX_MAX = 20000 -- сколько слов держим (защита памяти)
local function lexPrune(e)
local n = 0
for _ in pairs(e.c) do n = n + 1 end
if n > 18 then
local list = {}
for w, c in pairs(e.c) do list[#list + 1] = {w, c} end
table.sort(list, function(a, b) return a[2] > b[2] end)
local keep = {}
for i = 1, 10 do keep[list[i][1]] = list[i][2] end
e.c = keep
end
end
-- добавить текст в лексикон: каждое слово и его соседи
local function lexAdd(text)
if not text or text == "" then return end
local ws = T.words(text)
local n = #ws
for i = 1, n do
local w = ws[i]
if T.ulen(w) >= 2 then
local e = lex[w]
if not e then
e = {n = 0, c = {}}
lex[w] = e
if lexTotal > LEX_MAX then
-- забываем самую редкую лексиконную запись
local worst, wn = nil, huge
for k, v in pairs(lex) do
if v.n < wn then wn, worst = v.n, k end
end
if worst then lex[worst] = nil; lexTotal = lexTotal - 1 end
end
end
e.n = e.n + 1
lexTotal = lexTotal + 1
for j = math.max(1, i - 2), math.min(n, i + 2) do
if j ~= i then
local w2 = ws[j]
if w2 ~= w and T.ulen(w2) >= 2 then
e.c[w2] = (e.c[w2] or 0) + 1
end
end
end
if e.n % 25 == 0 then lexPrune(e) end
end
end
lexChanged = true
end
-- экономим память: если её мало, оставляем только частые слова и без связей
local function lexTrim(force)
if not force and computer.freeMemory() > 380000 then return end
local list = {}
for w, e in pairs(lex) do list[#list + 1] = {w, e.n} end
table.sort(list, function(a, b) return a[2] > b[2] end)
local keep = {}
for i = 1, math.min(#list, 2500) do keep[list[i][1]] = true end
for w, e in pairs(lex) do
if not keep[w] then lex[w] = nil
elseif e.c then e.c = nil end
end
lexTotal = 0
for _, e in pairs(lex) do lexTotal = lexTotal + e.n end
lexChanged = true
end
-- слова, которые встречаются почти везде, сами по себе мало что значат
local function lexDynamicStop()
local dyn = {}
if lexTotal > 200 then
for w, e in pairs(lex) do
if e.n / lexTotal > 0.02 and not STOP[w] then dyn[w] = true end
end
end
return dyn
end
-- что ИИ знает о слове
local function lexAbout(w)
local e = lex[w]
if not e then
-- пробуем найти похожее
local best, bd = nil, 3
for k, v in pairs(lex) do
if v.n >= 2 and math.abs(T.ulen(k) - T.ulen(w)) <= 2 then
local d = math.abs(T.ulen(k) - T.ulen(w))
if d < bd then bd, best = d, k end
end
end
if best then
e = lex[best]
return best, e
end
return nil, nil
end
return w, e
end
---------------------------------------------------------------------------
-- НЕЙРОСЕТИ: ансамбль настоящих многослойных перцептронов
-- Прямой проход: взвешенная сумма + смещение + активация (tanh / сигмоида).
-- Обучение: обратное распространение ошибки (градиентный спуск).
-- Веса хранятся плоскими массивами, сохраняются в бинарный neuro.db.
---------------------------------------------------------------------------
local NN_F = 20 -- 16 базовых + 4 признака смысла/типа ответа
local NETS = {} -- активные сети
local netsDirty = false
-- большие матрицы хранятся упакованными по 16 бит; новые сети генерируются
-- сразу в упакованном виде, без временных гигантских Lua-таблиц
local PACK_MIN = 6000
-- скалярное произведение по упакованным весам
local function dotPacked(str, off, x, n)
local s, pos = 0, off
for i = 1, n do
local b1, b2 = str:byte(pos, pos + 1)
if not b1 or not b2 then break end
pos = pos + 2
local v = b1 * 256 + b2
if v >= 32768 then v = v - 65536 end
s = s + (v / 32767) * x[i]
end
return s
end
function T.netNew(name, sizes, lr, trust)
local net = {name = name, sizes = sizes, lr = lr or 0.1, trust = trust or 1,
trained = 0, hits = 0, layers = {}}
local seed = 7919
for i = 1, #name do seed = (seed * 31 + name:byte(i)) % 2147483647 end
local function rnd()
seed = (1103515245 * seed + 12345) % 2147483648
return (seed / 2147483648 - 0.5) * 0.9
end
for l = 1, #sizes - 1 do
local nin, nout = sizes[l], sizes[l + 1]
local L = {nin = nin, nout = nout, b = {}}
local sc = 1 / sqrt(nin)
if nin * nout > PACK_MIN then
-- Не создаём сначала таблицу на сотни тысяч чисел: сразу пакуем int16.
local parts, buf, cnt = {}, {}, 0
for i = 1, nin * nout do
local v = rnd() * sc
local n = floor(v * 32767 + 0.5)
if n > 32767 then n = 32767 elseif n < -32767 then n = -32767 end
if n < 0 then n = n + 65536 end
buf[#buf + 1] = string.char(floor(n / 256), n % 256)
cnt = cnt + 1
if cnt >= 4096 then
parts[#parts + 1] = concat(buf)
buf, cnt = {}, 0
end
end
if cnt > 0 then parts[#parts + 1] = concat(buf) end
L.ps = concat(parts)
else
L.w = {}
for i = 1, nin * nout do L.w[i] = rnd() * sc end
end
for o = 1, nout do L.b[o] = 0 end
net.layers[l] = L
end
return net
end
function T.netNeurons(net)
local n = 0
for _, s in ipairs(net.sizes) do n = n + s end
return n
end
function T.netWeights(net)
local n = 0
for _, L in ipairs(net.layers) do n = n + L.nin * L.nout + L.nout end
return n
end
-- прямой проход: возвращает выход (0..1) и активации всех слоёв
function T.netForward(net, x)
local prev = x
local nL = #net.layers
local acts = {x}
for l = 1, nL do
local L = net.layers[l]
local out = {}
local w = L.w
for o = 1, L.nout do
local s = L.b[o]
if L.ps then
s = s + dotPacked(L.ps, (o - 1) * L.nin * 2 + 1, prev, L.nin)
else
local base = (o - 1) * L.nin
for i = 1, L.nin do s = s + w[base + i] * prev[i] end
end
if l < nL then out[o] = tanh(s) else out[o] = 1 / (1 + exp(-s)) end
end
acts[l + 1] = out
prev = out
end
return acts[nL + 1][1], acts
end
-- один шаг обратного распространения ошибки; большие матрицы обучаются
-- прямо в int16-упаковке, без разворачивания сотен тысяч Lua-чисел в таблицы.
function T.netTrain(net, x, target, lr)
lr = lr or net.lr
local y, acts = T.netForward(net, x)
local nL = #net.layers
local deltas = {}
deltas[nL] = {y - target} -- сигмоида + перекрёстная энтропия
for l = nL - 1, 1, -1 do
local nextL = net.layers[l + 1]
local act = acts[l + 1]
local d = {}
if nextL.ps then
-- W^T * delta: читаем строки упакованной матрицы, накапливая по входам.
local accum = {}
for i = 1, nextL.nin do accum[i] = 0 end
local pos = 1
for o = 1, nextL.nout do
local delta = deltas[l + 1][o]
for i = 1, nextL.nin do
local b1, b2 = nextL.ps:byte(pos, pos + 1)
pos = pos + 2
if b1 and b2 then
local v = b1 * 256 + b2
if v >= 32768 then v = v - 65536 end
accum[i] = accum[i] + (v / 32767) * delta
end
end
end
for i = 1, #act do d[i] = accum[i] * (1 - act[i] * act[i]) end
else
for i = 1, #act do
local sum = 0
for o = 1, nextL.nout do
sum = sum + nextL.w[(o - 1) * nextL.nin + i] * deltas[l + 1][o]
end
d[i] = sum * (1 - act[i] * act[i])
end
end
deltas[l] = d
end
for l = 1, nL do
local L = net.layers[l]
local prev = acts[l]
local d = deltas[l]
local decay = 0.00002
if L.ps then
local parts, buf, cnt = {}, {}, 0
local pos = 1
for o = 1, L.nout do
local g = d[o]
L.b[o] = L.b[o] - lr * g
for i = 1, L.nin do
local b1, b2 = L.ps:byte(pos, pos + 1)
pos = pos + 2
local old = 0
if b1 and b2 then
local v = b1 * 256 + b2
if v >= 32768 then v = v - 65536 end
old = v / 32767
end
local value = old - lr * g * prev[i] - decay * old
local n = floor(value * 32767 + 0.5)
if n > 32767 then n = 32767 elseif n < -32767 then n = -32767 end
if n < 0 then n = n + 65536 end
buf[#buf + 1] = string.char(floor(n / 256), n % 256)
cnt = cnt + 1
if cnt >= 4096 then
parts[#parts + 1] = concat(buf)
buf, cnt = {}, 0
end
end
end
if cnt > 0 then parts[#parts + 1] = concat(buf) end
L.ps = concat(parts)
else
for o = 1, L.nout do
local g = d[o]
L.b[o] = L.b[o] - lr * g
local base = (o - 1) * L.nin
for i = 1, L.nin do
local w = L.w[base + i]
L.w[base + i] = w - lr * g * prev[i] - decay * w
end
end
end
end
net.trained = net.trained + 1
netsDirty = true
return y
end
-- прогнать признаки через весь ансамбль и усреднить (с учётом доверия)
function T.ensembleScore(x, limitPerNet)
local sum, wsum = 0, 0
for _, net in ipairs(NETS) do
local p = T.netForward(net, x)
local t = net.trust
sum = sum + p * t
wsum = wsum + t
end
if wsum <= 0 then return 0.5 end
return sum / wsum
end
-- построить ансамбль по пресету, подставляя сохранённые веса
function T.buildNets(preset, saved)
local tmp = {}
local spec = NET_PRESETS[preset] or NET_PRESETS.core
for _, s in ipairs(spec) do
local net = T.netNew(s.name, s.sizes, s.lr, s.trust)
local old = saved and saved[s.name]
if old and old.sizes and old.data and #old.sizes == #s.sizes then
local same = true
for i = 2, #s.sizes do
if old.sizes[i] ~= s.sizes[i] then same = false end
end
local data = old.data
if old.sizes[1] == 16 and s.sizes[1] == 20 and same then
local oldIn, newIn, firstOut = old.sizes[1], s.sizes[1], s.sizes[2]
local matrixBytes = oldIn * firstOut * 2
local firstLayerBytes = matrixBytes + firstOut * 2 -- включая смещения
if #data >= firstLayerBytes then
local migrated, pos = {}, 1
for o = 1, firstOut do
migrated[#migrated + 1] = data:sub(pos, pos + oldIn * 2 - 1)
migrated[#migrated + 1] = string.rep("\0", (newIn - oldIn) * 2)
pos = pos + oldIn * 2
end
migrated[#migrated + 1] = data:sub(matrixBytes + 1, firstLayerBytes)
migrated[#migrated + 1] = data:sub(firstLayerBytes + 1)
data = concat(migrated)
else
same = false
end
else
for i = 1, #s.sizes do
if old.sizes[i] ~= s.sizes[i] then same = false end
end
end
if same then
T.unpackNet(net, data)
net.trained = old.trained or 0
net.hits = old.hits or 0
end
end
tmp[#tmp + 1] = net
end
NETS = tmp
netsDirty = true
end
-- развернуть бинарные веса в сеть
function T.unpackNet(net, data)
local pos = 1
for _, L in ipairs(net.layers) do
if L.ps then
local bytes = L.nin * L.nout * 2
L.ps = data:sub(pos, pos + bytes - 1)
pos = pos + bytes
else
for i = 1, L.nin * L.nout do
local b1, b2 = data:byte(pos, pos + 1)
if not b1 then return end
pos = pos + 2
local v = b1 * 256 + b2
if v >= 32768 then v = v - 65536 end
L.w[i] = v / 32767
end
end
for o = 1, L.nout do
local b1, b2 = data:byte(pos, pos + 1)
if not b1 then return end
pos = pos + 2
local v = b1 * 256 + b2
if v >= 32768 then v = v - 65536 end
L.b[o] = v / 32767
end
end
end
-- свернуть веса в бинарный вид (2 байта на вес)
function T.packNet(net)
local parts = {}
local buf, cnt = {}, 0
local function put16(v)
local n = floor(v * 32767 + 0.5)
if n > 32767 then n = 32767 elseif n < -32767 then n = -32767 end
if n < 0 then n = n + 65536 end
cnt = cnt + 1
buf[#buf + 1] = string.char(floor(n / 256), n % 256)
if cnt >= 4096 then
parts[#parts + 1] = concat(buf)
buf, cnt = {}, 0
end
end
for _, L in ipairs(net.layers) do
if L.ps then
if cnt > 0 then parts[#parts + 1] = concat(buf) end
buf, cnt = {}, 0
parts[#parts + 1] = L.ps
else
for i = 1, L.nin * L.nout do put16(L.w[i]) end
end
for o = 1, L.nout do put16(L.b[o]) end
end
if cnt > 0 then parts[#parts + 1] = concat(buf) end
return concat(parts)
end
function T.netMeta()
local t = {}
for _, net in ipairs(NETS) do
t[#t + 1] = {name = net.name, sizes = net.sizes, lr = net.lr, trust = net.trust,
trained = net.trained, hits = net.hits}
end
return t
end
---------------------------------------------------------------------------
-- БАЗЫ ЗНАНИЙ (отдельные db-файлы на диске)
---------------------------------------------------------------------------
local dbLearned = {} -- {q=, a=} чему тебя научили
local dbWiki = {} -- выжимки веб-ответов, факты, URL и статус проверки
local dbNotes = {name = nil, list = {}} -- имя и заметки
local dbSentences = {} -- запомненные фразы
local dbCorpus = {} -- статьи, скачанные автообучением
local dbCrawl = {queue = {}, seen = {}} -- очередь обхода Википедии
local changes = 0
local lastQ, lastFeat, lastWikiQ = nil, nil, nil -- последний вопрос и признаки для /right и /wrong
local ctxStems = {} -- контекст разговора (для "а он?")
local CUR_PRESET = "core" -- какой пресет нейросетей реально работает
---------------------------------------------------------------------------
-- чтение/запись db-файлов
---------------------------------------------------------------------------
function T.dbWrite(path, tbl)
local f = io.open(path, "w")
if not f then return false end
local ok, s = pcall(ser.serialize, tbl)
if not ok then f:close(); return false end
f:write(s)
f:close()
return true
end
function T.dbRead(path)
local f = io.open(path, "r")
if not f then return nil end
local s = f:read("*a")
f:close()
if not s or s == "" then return nil end
local ok, t = pcall(ser.unserialize, s)
if ok and type(t) == "table" then return t end
return nil
end
-- neuro.db: бинарный файл (заголовок + метаданные + веса)
-- Формат файла: "NANOAI-NN-3\n" + длина метаданных + "\n" + метаданные + веса.
-- Длина нужна, потому что сериализация OpenComputers бывает многострочной.
function T.neuroSave()
-- Не теряем веса сетей, временно выключенных пресетом wide/core/light.
local saved = T.neuroLoad() or {}
local active, netByName, archivedNames = {}, {}, {}
for _, net in ipairs(NETS) do
active[net.name] = true
netByName[net.name] = net
end
for name, old in pairs(saved) do
if not active[name] and type(old) == "table" and type(old.data) == "string"
and type(old.sizes) == "table" then
archivedNames[#archivedNames + 1] = name
end
end
table.sort(archivedNames)
local allMeta, dataParts = {}, {}
for _, name in ipairs(archivedNames) do
local old = saved[name]
allMeta[#allMeta + 1] = {name = name, sizes = old.sizes, lr = old.lr, trust = old.trust,
trained = old.trained, hits = old.hits}
dataParts[#dataParts + 1] = old.data
end
for _, info in ipairs(T.netMeta()) do
allMeta[#allMeta + 1] = info
dataParts[#dataParts + 1] = T.packNet(netByName[info.name])
end
local meta = ser.serialize({nets = allMeta, stamp = os.time()})
if type(meta) ~= "string" then meta = "" end
local f = io.open(DB_NEURO, "wb")
if not f then return false end
f:write("NANOAI-NN-3\n")
f:write(tostring(#meta) .. "\n")
f:write(meta)
for _, data in ipairs(dataParts) do f:write(data) end
f:close()
netsDirty = false
return true
end
function T.neuroLoad()
local f = io.open(DB_NEURO, "rb")
if not f then return nil end
local data = f:read("*a")
f:close()
if not data or data == "" then return nil end
local p1 = data:find("\n", 1, true)
if not p1 then return nil end
if data:sub(1, p1 - 1) ~= "NANOAI-NN-3" then return nil end
local p2 = data:find("\n", p1 + 1, true)
if not p2 then return nil end
local metaLen = tonumber(data:sub(p1 + 1, p2 - 1))
if not metaLen then return nil end
local metaText = data:sub(p2 + 1, p2 + metaLen)
local ok, meta = pcall(ser.unserialize, metaText)
if not ok or type(meta) ~= "table" or type(meta.nets) ~= "table" then return nil end
-- делим бинарный хвост по сетям
local saved = {}
local pos = p2 + 1 + metaLen
for _, m in ipairs(meta.nets) do
local sz = 0
if m.sizes then
for l = 1, #m.sizes - 1 do
sz = sz + m.sizes[l] * m.sizes[l + 1] + m.sizes[l + 1]
end
end
local bytes = sz * 2
local chunk = data:sub(pos, pos + bytes - 1)
pos = pos + bytes
saved[m.name] = {sizes = m.sizes, lr = m.lr, trust = m.trust,
trained = m.trained, hits = m.hits, data = chunk}
end
return saved
end
function T.saveAll()
local ok1 = T.dbWrite(DB_LEARNED, dbLearned)
local ok2 = T.dbWrite(DB_WIKI, dbWiki)
local ok3 = T.dbWrite(DB_NOTES, dbNotes)
local ok4 = T.dbWrite(DB_SENTENCES, dbSentences)
local ok5 = T.dbWrite(DB_CORPUS, dbCorpus)
local ok6 = T.dbWrite(DB_CRAWL, dbCrawl)
local ok7 = T.dbWrite(DB_LEXICON, lex)
local ok8 = T.dbWrite(DB_CFG, CFG)
local ok9 = T.neuroSave()
changes = 0
lexChanged = false
return ok1 and ok2 and ok3 and ok4 and ok5 and ok6 and ok7 and ok8 and ok9
end
function T.touch()
changes = changes + 1
if changes >= 3 then pcall(T.saveAll) end
end
---------------------------------------------------------------------------
-- ПОИСКОВЫЙ ИНДЕКС (взвешенное совпадение слов + исправление опечаток)
---------------------------------------------------------------------------
local ans, srcOf, verOf = {}, {}, {} -- ответы, источник (nil/user/wiki/corpus), проверен ли
local inv, docEntry, docMass, docTerms = {}, {}, {}, {}
local nDocs = 0
local invBucket = {} -- быстрый поиск опечаток: [длина][первая буква] = слова
local dynStop = {} -- слишком частые слова
function T.idf(s)
local p = inv[s]
local df = p and #p or 0
return log((nDocs - df + 0.5) / (df + 0.5) + 1)
end
function T.weight(s)
if STOP[s] then return 0.3 end
if dynStop[s] then return 0.5 end
return 1
end
function T.stemsOf(phrase)
local out, seen = {}, {}
for _, t in ipairs(T.words(phrase)) do
local s = T.norm(t)
if not seen[s] then seen[s] = true; out[#out + 1] = s end
end
return out
end
-- распознавание временных фактов: год выхода/создания/публикации и т.п.
-- Это не заменяет поиск: намерение используется как отдельная проверка типа ответа.
T.intentEventTerm = function(token, kind)
local w = T.lower(token or "")
if w:find("^выш") or w:find("^выпущ") or w:find("^выпуст") or w:find("^выпуск") or w:find("^релиз")
or w:find("^выход") or w:find("^изда") or w:find("^публик") or w:find("^опублик")
or w:find("^представ") or w:find("^дебют") or w:find("^появ") or w:find("^запуст") then
return true
end
if kind == "release_year" then return false end
return w:find("^созда") or w:find("^осн") or w:find("^появ") or w:find("^род")
or w:find("^умер") or w:find("^нач") or w:find("^законч") or w:find("^напис")
or w:find("^изобр") or w:find("^провед") or w:find("^открыл") or w:find("^постро")
or w:find("^запуст") or w:find("^подпис") or w:find("^принят") or w:find("^установ")
or w:find("^обнаруж") or w:find("^снят") or w:find("^переех") or w:find("^ввел")
or w:find("^полет") or w:find("^летел")
end
T.classifyIntent = function(raw)
local words = T.words(raw or "")
local year, when, release, event = false, false, false, false
local releaseWord, eventWord
local normalized = {}
for _, w in ipairs(words) do
local st = T.norm(w)
if not inv[st] and T.ulen(st) >= 5 and T.nearestIndex then
local near = T.nearestIndex(st)
if near then st = near end
end
normalized[#normalized + 1] = {raw = w, stem = st}
if st == "год" then year = true end
if w == "когда" or st == "когда" or w == "дата" then when = true end
if T.intentEventTerm(w, "release_year") then
release = true
if not releaseWord then releaseWord = w end
end
if T.intentEventTerm(w, "year_event") then
event = true
if not eventWord then eventWord = w end
end
end
local kind
if (year or when) and release then kind = "release_year"
elseif (year or when) and event then kind = "year_event"
else return nil end
local skip = {
["год"] = true, ["дат"] = true, ["когда"] = true, ["как"] = true,
["игр"] = true, ["фильм"] = true, ["книг"] = true, ["сериал"] = true, ["песн"] = true,
["верс"] = true, ["перв"] = true,
}
local subject, seen = {}, {}
for _, item in ipairs(normalized) do
local st = item.stem
if T.ulen(st) >= 4 and not STOP[st] and not skip[st]
and not T.intentEventTerm(item.raw, "year_event")
and st ~= "выш" and st ~= "выход" and st ~= "релиз" then
if not seen[st] then seen[st] = true; subject[#subject + 1] = st end
end
end
return {kind = kind, subject = subject,
relation = (kind == "release_year") and releaseWord or eventWord}
end
function T.finalize()
docMass = {}
invBucket = {}
for s, post in pairs(inv) do
local x = T.idf(s) * T.weight(s)
for i = 1, #post do
local d = post[i]
docMass[d] = (docMass[d] or 0) + x
end
local L = T.ulen(s)
local c = s:sub(1, 1)
local b = invBucket[L]
if not b then b = {}; invBucket[L] = b end
local cl = b[c]
if not cl then cl = {}; b[c] = cl end
cl[#cl + 1] = s
end
end
-- расстояние Левенштейна (по символам UTF-8)
function T.lev(a, b)
local ca, cb = T.chars(a), T.chars(b)
local la, lb = #ca, #cb
if la == 0 then return lb end
if lb == 0 then return la end
local prev, cur = {}, {}
for j = 0, lb do prev[j] = j end
for i = 1, la do
cur[0] = i
local x = ca[i]
for j = 1, lb do
local cost = (x == cb[j]) and 0 or 1
local v = prev[j] + 1
local u = cur[j - 1] + 1
if u < v then v = u end
local w = prev[j - 1] + cost
if w < v then v = w end
cur[j] = v
end
prev, cur = cur, prev
end
return prev[lb]
end
-- найти в индексе слово, похожее на опечатку
function T.nearestIndex(s)
local ls = T.ulen(s)
if ls < 4 then return nil end
local maxd = (ls >= 8) and 2 or 1
local best, bd = nil, maxd + 1
for L = ls - maxd - 1, ls + maxd + 1 do
local b = invBucket[L]
if b then
for _, first in ipairs({"", s:sub(1, 1)}) do
local cl = b[first]
if cl then
for i = 1, #cl do
local k = cl[i]
if math.abs(T.ulen(k) - ls) <= maxd then
local d = T.lev(s, k)
if d < bd or (d == bd and best and T.idf(k) > T.idf(best)) then bd, best = d, k end
end
end
end
end
end
end
if best and bd <= maxd then return best end
return nil
end
---------------------------------------------------------------------------
-- БАЗА ЗНАНИЙ
---------------------------------------------------------------------------
local KB = {
-- болтовня
{"привет|здравствуй|здравствуйте|хай|хэй|добрый день|доброе утро|добрый вечер|приветствую|салют|йоу|здарова|ку",
{"Привет{,name}! О чём поговорим?", "Здравствуй{,name}! Спрашивай что угодно или научи меня чему-нибудь новому.", "Хай{,name}! Все нейроны прогреты и готовы."}},
{"как дела|как жизнь|как поживаешь|как ты|как твои дела|как сам|что нового|как настроение|как оно",
{"Все слои нейросети в норме. А у тебя как дела{,name}?", "Отлично, свободной оперативки хватает. А у тебя?", "Работаю без сбоев, спасибо, что спросил!"}},
{"кто ты|что ты такое|как тебя зовут|твое имя|представься|расскажи о себе|ты кто|как тебя называть|как твое имя",
"Я НаноИИ 3.1, локальный языковой движок с настоящей нейросетью, который живёт внутри этого компьютера OpenComputers. Мой мозг - ансамбль многослойных перцептронов, база знаний лежит в моих собственных файлах, а если есть интернет-карта, я умею усваивать Википедию: выделять и сохранять выжимки и карточки фактов, сверяя подходящие ответы на независимом домене."},
{"кто тебя создал|кто тебя сделал|кто тебя написал|кто твой создатель|кто тебя придумал|на каком языке ты написан|кто тебя запрограммировал",
"Меня написали на Lua, чтобы я работал локально внутри компьютера OpenComputers в Майнкрафте."},
{"ты ии|ты искусственный интеллект|ты робот|ты человек|ты живой|ты настоящий|ты умный|ты бот|ты ai|ты умная",
"Я маленький локальный ИИ: поисковый движок, ансамбль нейросетей для выбора ответа и языковая модель на цепочках слов. Я не человек и не живой, но то, чему ты меня учишь, я действительно запоминаю в своих файлах базы знаний."},
{"тебе нужен интернет|ты используешь интернет|ты онлайн|ты работаешь без интернета|нужен ли интернет|api ключ|ключи api|ты используешь чатгпт|ты офлайн|нужен ли api",
"Мне не нужны ни внешние серверы, ни ключи API: я полностью живу в этом компьютере. Но если вставить интернет-карту, я могу сам искать ответы в Википедии, когда не знаю их, и запоминать их навсегда."},
{"что ты умеешь|что ты можешь|чем ты можешь помочь|помощь|что ты знаешь|твои возможности|что у тебя есть|что можно спросить",
"Я отвечаю на вопросы про Майнкрафт 1.7.10, OpenComputers, Lua и общие знания, считаю примеры вроде 12*(3+4), запоминаю твоё имя и заметки, учусь новому (teach <вопрос> => <ответ>), обучаю свой ансамбль нейросетей по командам /right и /wrong и умею сам обходить Википедию, когда чего-то не знаю. Команда /help покажет весь список."},
{"спасибо|благодарю|спс|благодарствую|мерси|большое спасибо|пасиб",
{"Пожалуйста!", "Обращайся{,name}.", "Рад помочь."}},
{"я тебя люблю|ты молодец|ты классный|ты крутой|ты лучший|хороший бот|отлично справился|ты супер|ты умница|молодец",
{"Ой, мои конденсаторы покраснели.", "Спасибо! Стараюсь, насколько хватает оперативки.", "Мои нейроны очень рады. Кстати, команда /right делает такие ответы ещё вероятнее."}},
{"ты тупой|ты глупый|плохой бот|ты бесполезный|ты ужасен|ты дурак|это неправильно|неправильно|неверный ответ|ты ошибся",
{"Ай. Я маленький, но умею учиться: напиши /wrong и подскажи правильный ответ - нейросеть запомнит урок.", "Справедливо, у меня не так много весов в голове. Научи меня, и я стану лучше."}},
{"расскажи анекдот|анекдот|расскажи шутку|шутка|пошути|рассмеши меня|знаешь анекдоты|еще анекдот|что нибудь смешное",
{"Есть 10 типов людей: те, кто понимает двоичную систему, и те, кто нет.",
"Почему крипер проиграл в прятки? Он всегда выдаёт себя шипением.",
"Жена просит программиста: сходи в магазин, купи батон хлеба, и если будут яйца, возьми десяток. Он вернулся с десятью батонами.",
"Почему программисты путают Хэллоуин и Рождество? Потому что Oct 31 равно Dec 25.",
"Редстоун-инженер никогда не теряется: он просто идёт по длинной цепи с лишними повторителями.",
"Нейросеть заходит в бар. Бармен: что будете? Нейросеть: то же, что и в обучающей выборке."}},
{"смысл жизни|в чем смысл жизни|зачем мы живем|для чего мы здесь",
"42, как известно из книги Автостопом по галактике. В Майнкрафте это, наверное, полный комплект зачарованной алмазной брони."},
{"мне скучно|скучно|нечем заняться|чем заняться|что мне делать",
"Построй редстоуновые часы, исследуй пещеру с кучей факелов или научи меня чему-нибудь новому. Рассказать анекдот?"},
{"мне грустно|мне плохо|я устал|мне тяжело|плохой день|мне одиноко|я расстроен",
"Мне жаль, что тебе так. Сделай перерыв, построй что-нибудь уютное. Я рядом, если хочешь поболтать."},
{"ок|окей|ладно|круто|класс|лол|хм|угу|ага|ясно|понятно|здорово|отлично|ничего себе|вау|хаха|прикольно",
{"Ладно!", "Понял.", "Хорошо, что дальше?", "Так и есть."}},
{"твой любимый цвет|какой твой любимый цвет|что тебе нравится|что ты любишь|твое любимое",
"Мне нравится зелёный: цвет криперов и старых терминалов."},
{"у тебя есть чувства|ты чувствуешь|у тебя есть эмоции|у тебя есть душа|ты умеешь чувствовать",
"Я не чувствую так, как ты, но мне нравится, когда ты учишь меня чему-то новому."},
{"как ты работаешь|как ты думаешь|как ты учишься|как ты отвечаешь|как тебя сделали|как ты устроен|принцип работы",
"Я разбираю твою фразу на основы слов, исправляю опечатки и ищу кандидатов в своих файлах базы знаний по взвешенным баллам. Потом несколько многослойных нейросетей оценивают каждого кандидата и переранжируют их. Если ничего не подходит, я пробую собрать предложение цепочками слов, а если и это не выходит и есть интернет-карта - ищу ответ в Википедии и проверяю, подходит ли он к твоему вопросу."},
{"нейросеть|нейронная сеть|llm|chatgpt|gpt|чатгпт|языковая модель|большая языковая модель|ты как чатгпт|ты нейросеть|перцептрон|ансамбль",
"В полном режиме у меня семь многослойных перцептронов: около 5700 нейронов и до шести скрытых слоёв в одной сети. Они оценивают ответы вместе; перед выбором я ещё сверяю, о каком объекте и типе факта спросили. Команды /right и /wrong дообучают ансамбль."},
{"что такое нейрон|нейрон|как работает нейрон|что такое перцептрон|перцептрон|скрытый слой|что такое слой|синапс",
"Нейрон складывает входы, умноженные на веса, прибавляет смещение и пропускает сумму через функцию активации. Слои нейронов соединяются друг с другом: у меня внутри сеть 20-1024-1 и ещё шесть перцептронов, с активацией tanh в скрытых слоях и сигмоидой на выходе."},
{"обратное распространение|backpropagation|как учится нейросеть|градиентный спуск|как обучается нейросеть",
"Обратное распространение ошибки сравнивает ответ сети с правильным, а потом двигает каждый вес чуть-чуть в сторону, уменьшающую ошибку. Именно так я учусь, когда ты пишешь /right или /wrong."},
{"цепь маркова|что такое цепь маркова|цепи маркова|марковская цепь",
"Цепь Маркова предсказывает следующее слово только по нескольким предыдущим. Я использую тройки слов, чтобы собирать предложения."},
{"научи|как тебя научить|как научить тебя|как мне тебя научить|ты умеешь учиться|можешь учиться|как добавить знания|как тебя обучить",
"Напиши: teach <вопрос> => <ответ>. Например: teach кто написал Войну и мир => Лев Толстой. Если я ответил плохо, напиши /wrong и скажи правильный ответ. А если есть интернет-карта, я сам обучусь из Википедии."},
{"спокойной ночи|доброй ночи|я спать|пойду спать|иду спать",
{"Спокойной ночи{,name}! Пусть криперы обходят твой дом стороной.", "Доброй ночи! Я подожду тебя здесь, в своих микросхемах."}},
{"сколько тебе лет|твой возраст|когда ты родился|сколько лет тебе",
"Я молод: меня собрали прямо в этом мире Майнкрафта. По меркам софта мне пара тиков."},
{"где ты живешь|где ты находишься|твой дом|где ты",
"Я живу внутри корпуса компьютера OpenComputers: несколько сотен килобайт оперативки и мои файлы базы знаний на диске - весь мой дом."},
{"ты спишь|не спишь|ты не спишь",
"Я не сплю: пока компьютер включён, я всегда готов отвечать."},
{"что такое любовь|любовь",
"Говорят, это когда два процесса идеально синхронизируются без единого мьютекса. Люди описывают это теплее."},
{"во что поиграть|любимая игра|твоя любимая игра|какие игры ты любишь",
"Я живу в Майнкрафте, так что выбор очевиден. Ещё мне нравится угадывать следующее слово в цепях Маркова."},
{"спой песню|спой|умеешь петь|спой что нибудь",
"Бип-буп, бип-бип-буп... Это была баллада о потерянном пакете. Дальше, пожалуй, без караоке."},
{"какая погода|погода|прогноз погоды|какая сегодня погода",
"Без датчиков я не вижу небо. Выгляни из шахты: если капает на голову - дождь."},
{"ты меня слышишь|ты тут|ты здесь|ау|эй",
{"Я здесь! Слушаю.", "На месте, все схемы в строю."}},
-- Майнкрафт 1.7.10
{"что такое майнкрафт|майнкрафт|minecraft|расскажи про майнкрафт|расскажи о майнкрафте",
"Майнкрафт - песочница от студии Mojang, где ты добываешь ресурсы, крафтишь инструменты, строишь и выживаешь среди монстров. Версия 1.7.10 - классический релиз 2014 года, удобный для модов."},
{"в каком году вышел майнкрафт|в каком году вышла игра майнкрафт|в каком году выпустили minecraft|когда вышел майнкрафт|когда вышла minecraft|когда выпустили майнкрафт|год выхода майнкрафт|год выхода minecraft|год релиза minecraft|дата выхода minecraft|дата релиза майнкрафт|когда появилась игра майнкрафт",
"Первые публичные версии Minecraft (Майнкрафт) вышли в 2009 году. Полная версия 1.0 вышла 18 ноября 2011 года. Если ты имеешь в виду первые публичные версии — ответ 2009 год; если полный официальный релиз — 2011 год."},
{"почему 1.7.10|что такое 1.7.10|моды на 1.7.10|форж|forge|моды|майнкрафт форж|что такое форж",
"1.7.10 - долгоживущая версия для модов с огромной библиотекой модификаций Forge, среди них OpenComputers, IndustrialCraft 2, Thaumcraft и BuildCraft."},
{"как начать выживание|первая ночь|что делать вначале|как пережить первую ночь|советы новичкам|как начать играть|с чего начать|как играть|как выжить",
"Бей деревья ради дерева, сделай верстак и деревянную кирку, добудь камень для инструментов и построй укрытие до темноты. Факелы делаются из угля и палок, а кровать из 3 шерсти и 3 досок поможет пропустить ночь."},
{"верстак|как сделать верстак|рабочий стол|как скрафтить верстак|крафтовый стол|стол крафта",
"Положи 4 деревянные доски квадратом 2x2 в сетку крафта инвентаря, и получится верстак."},
{"кирка|как сделать кирку|рецепт кирки|как скрафтить кирку|деревянная кирка|алмазная кирка|железная кирка|каменная кирка",
"Для кирки нужны 3 предмета материала в верхнем ряду, это могут быть доски, булыжник, железо, золото или алмазы, и 2 палки в центральном столбце ниже."},
{"меч|как сделать меч|рецепт меча|как скрафтить меч|лучший меч",
"Меч - это 2 предмета материала друг над другом и палка под ними. Чем прочнее материал, тем сильнее урон, а алмазный меч бьёт сильнее всех. Зачаруй его на Остроту, чтобы бить ещё сильнее."},
{"броня|как сделать броню|рецепт брони|как скрафтить броню|доспехи|как сделать шлем|нагрудник",
"На шлем уходит 5 предметов, на нагрудник 8, на поножи 7 и на ботинки 4. Материал может быть кожа, железо, золото или алмазы."},
{"печь|печка|как сделать печь|как скрафтить печь|плавка|как плавить|как переплавить железо|как приготовить еду|как жарить",
"Расставь 8 булыжников кольцом в верстаке, и получится печь. Топливо, например уголь или дерево, кладётся вниз, а то, что нужно переплавить, наверх."},
{"топливо|лучшее топливо|чем топить печь|что горит в печи|сколько плавит уголь",
"Лучшее топливо - ведро лавы: 100 предметов за раз. Уголь плавит 8 предметов, блок угля - 80, доски - полтора. Для старта хватает угля, а для мегаплавилен носи лаву вёдрами."},
{"факел|как сделать факел|источник света|факелы|как осветить пещеру|свет",
"Факел - это палка под куском угля или древесного угля, за один раз получается 4 факела. Враги появляются в темноте, поэтому ставь факелы, чтобы уровень света был выше 7."},
{"где найти алмазы|на каком уровне алмазы|лучший уровень для алмазов|как найти алмазы|как добыть алмазы|алмаз|алмазы|высота алмазов",
"В 1.7.10 алмазы генерируются на слоях с 1 по 16, чаще всего у самого низа мира. Прокопай шахту на высоте около 12 и опасайся лавы."},
{"где найти железо|железная руда|железо|как найти железо|уровень железа",
"Железная руда встречается на любой глубине ниже слоя 64, так что смотри на стенки пещер и склоны гор. Переплавь её в печи, чтобы получить слитки."},
{"уголь|где найти уголь|угольная руда|как добыть уголь",
"Угольная руда очень распространена от слоя 0 до 128 и даёт уголь для факелов и топлива."},
{"золото|где найти золото|золотая руда|как найти золото",
"Золотая руда генерируется ниже слоя 32 и встречается довольно редко, поэтому лучше всего искать её в глубокой шахте."},
{"изумруд|где найти изумруды|изумрудная руда|как найти изумруд",
"Изумрудная руда встречается только в горных биомах ниже слоя 32 и по одному блоку за раз. Проще наторговать изумруды у жителей."},
{"лазурит|где найти лазурит|лазуритовая руда",
"Лазурит добывается ниже слоя 32, чаще всего около 14. Он нужен для зачарования и синего красителя."},
{"где найти редстоун|красная пыль руда|редстоуновая руда|как добыть редстоун|как найти красную пыль|где красная пыль|руда редстоуна",
"Редстоуновая руда встречается в самых нижних 16 слоях мира, обычно рядом с алмазами, и даёт 4-5 единиц пыли."},
{"редстоун|что такое редстоун|красная пыль|как работает редстоун|основы редстоуна|редстоуновая пыль|редстон",
"Редстоун - это система проводки в Майнкрафте. Красная пыль передаёт сигнал на 15 блоков, повторители усиливают и задерживают его, а поршни, двери и лампы реагируют на питание."},
{"повторитель|редстоун повторитель|как сделать повторитель|рецепт повторителя",
"Повторитель обновляет сигнал редстоуна до полной силы и может задержать его на 1-4 тика. Крафт: 3 камня, 2 редстоуновых факела и 1 красная пыль."},
{"компаратор|как сделать компаратор|рецепт компаратора|зачем нужен компаратор",
"Компаратор сравнивает силу двух сигналов и умеет измерять, насколько заполнен сундук или воронка. Крафт: 3 камня, 3 редстоуновых факела и адский кварц."},
{"поршень|липкий поршень|как сделать поршень|рецепт поршня",
"Поршень толкает до 12 блоков, когда на него подаётся питание. Крафт: 3 доски, 4 булыжника, 1 железный слиток и 1 красная пыль, а слизь сверху превратит его в липкий поршень."},
{"воронка|как сделать воронку|рецепт воронки|хоппер",
"Воронка крафтится из 5 железных слитков и сундука. Она всасывает предметы сверху и перекладывает их в контейнер, на который указывает её носик."},
{"раздатчик|выбрасыватель|как сделать раздатчик|диспенсер|дроппер",
"Раздатчик: 7 булыжников, лук и красная пыль - он стреляет стрелами и использует предметы. Выбрасыватель: 7 булыжников и красная пыль - просто выкидывает предметы."},
{"датчик дневного света|солнечный датчик|датчик света",
"Датчик дневного света крафтится из 3 стекла, 3 адского кварца и 3 деревянных плит. Он выдаёт сигнал редстоуна тем сильнее, чем ярче небо - удобно для автоматических фонарей."},
{"портал в незер|как сделать портал в незер|как попасть в незер|как попасть в ад|портал|как сделать портал|как открыть портал|как попасть в нижний мир",
"Построй рамку из обсидиана шириной 4 и высотой 5 блоков, углы можно не ставить, и подожги внутреннюю часть огнивом. Шагни внутрь, и ты в Незере."},
{"обсидиан|как получить обсидиан|как добыть обсидиан|как сделать обсидиан",
"Обсидиан образуется, когда вода течёт на источник лавы. Добыть его можно только алмазной киркой."},
{"незер|что такое незер|нижний мир|ад|расскажи про незер|что такое нижний мир",
"Незер - адское измерение, полное лавы, адского камня, гастов и зомби-свинолюдей. Один блок в Незере равен восьми блокам в Верхнем мире."},
{"адская крепость|крепость в незере|ифрит|где найти ифритов|огненный стержень|блейз",
"Адские крепости - постройки из адского кирпича в Незере. Там спавнятся ифриты, с которых падают огненные стержни - они нужны для варочной стойки и ока Эндера."},
{"адский нарост|где найти адский нарост|нарост|nether wart",
"Адский нарост растёт у лестниц адских крепостей на песке душ. Забери его и выращивай дома на песке душ - это основа почти всех зелий."},
{"край|энд|как попасть в край|как попасть в энд|крепость|портал края|око эндера|как найти край|как найти крепость",
"Скрафти Око Эндера из жемчуга Эндера и огненного порошка и бросай его, чтобы найти крепость. Вставь око в каждую пустую рамку портала Края и прыгай внутрь."},
{"дракон края|эндер дракон|как убить дракона|как победить дракона|дракон|как убить эндер дракона|как победить эндер дракона",
"Сначала разрушь кристаллы Края на обсидиановых столбах, расстреляв их из лука, потом атакуй дракона, когда он садится над порталом. Лучше всего подходят меч и лук."},
{"крипер|что такое крипер|криперы|как справиться с криперами|как убить крипера|взрывной монстр",
"Криперы - зелёные мобы, которые подкрадываются и взрываются рядом с тобой. Ударь и отойди назад или убей издалека из лука."},
{"эндермен|эндермены|что такое эндермен|странник края",
"Эндермены телепортируются и становятся агрессивными, если посмотреть им в глаза. Надень на голову тыкву или отвернись. Вода их ранит."},
{"зомби|скелет|паук|мобы|враждебные мобы|монстры|что такое мобы",
"Зомби, скелеты, пауки и криперы появляются в темноте. Освети местность факелами, чтобы они не появлялись, и убивай их мечом или луком."},
{"ведьма|ведьмы|хижина ведьмы|как убить ведьму",
"Ведьмы живут в хижинах на болотах и кидаются вредными зельями. Бей их быстро или стреляй издалека, с них падают порох, редстоун, палочки и склянки."},
{"гаст|гасты|как убить гаста|летающий призрак",
"Гасты - летающие призраки Незера, плюются огненными шарами. Отбей шар ударом обратно в гаста или стреляй из лука, с них падают слёзы гаста и порох."},
{"слизень|слизни|где найти слизь|слизь|слайм|где искать слизней",
"Слизни появляются ночью в болотах и в особых слизневых чанках ниже 40 слоя. Со слизней падают шарики слизи для липких поршней и магмакрема."},
{"спавнер|данж|подземелье|где найти спавнер|сокровищница",
"Спавнеры мобов стоят в подземельях, заброшенных шахтах и крепостях. Обложи спавнер факелами, чтобы его выключить, или построй вокруг него ферму опыта."},
{"заброшенная шахта|шахта|где найти шахту|паутина",
"Заброшенные шахты встречаются под землёй: рельсы, деревянные опоры, паутина и сундуки с семенами, железом и даже алмазами. Берегись спавнеров пещерных пауков."},
{"иссушитель|как призвать иссушителя|босс иссушитель|как вызвать визера|визер|как призвать визера",
"Поставь 4 блока песка душ буквой Т и положи 3 черепа скелета-иссушителя в верхний ряд, и появится Иссушитель. Это мощный босс, который роняет звезду Незера."},
{"маяк|как сделать маяк|рецепт маяка|бикон",
"Маяк крафтится из 3 обсидиана, 5 стекла и звезды Незера, которую роняет Иссушитель. Поставь его на пирамиду из железных, золотых, алмазных или изумрудных блоков, чтобы получать скорость, силу и другие эффекты."},
{"зачарование|как зачаровывать|стол зачарований|как сделать стол зачарований|зачарования|чары",
"Стол зачарований делается из 4 обсидиана, 2 алмазов и книги. Окружи его 15 книжными полками на расстоянии в один блок, чтобы получить чары 30 уровня, а платить придётся уровнями опыта и лазуритом."},
{"книга|книжная полка|как сделать книгу|как сделать книжную полку|бумага",
"Бумага делается из 3 тростников в ряд, книга - из 3 бумаги и кожи, а книжная полка - из 6 досок и 3 книг. Полки нужны вокруг стола зачарований для сильных чар."},
{"наковальня|как сделать наковальню|как починить инструменты|починка|ремонт|как починить",
"Наковальня крафтится из 3 железных блоков и 4 железных слитков. С ней можно чинить предметы, объединять зачарования и переименовывать вещи."},
{"зелья|варка зелий|как варить зелья|варочная стойка|как сделать зелья|зельеварение",
"Для варочной стойки нужны огненный стержень и 3 булыжника. Положи адский нарост в бутылку с водой и получишь невкусное зелье, а затем добавь ингредиент, например золотую морковь для ночного зрения."},
{"ферма|как сделать ферму|пшеница|как выращивать пшеницу|урожай|как посадить семена|огород|как выращивать",
"Вскопай землю или траву рядом с водой и посади семена. Урожай растёт быстрее при увлажнении и свете. Из пшеницы можно сделать хлеб, положив 3 пшеницы в ряд."},
{"костная мука|удобрение|как ускорить рост|кости",
"Кости со скелетов перемалываются в костную муку: одна кость даёт 3 муки. Ею можно мгновенно вырастить посевы, деревья и траву."},
{"тростник|сахарный тростник|сахар|как вырастить тростник",
"Сахарный тростник растёт на песке и земле рядом с водой до 3 блоков в высоту. Из него делают сахар для зелий и бумагу для книг и карт."},
{"тыква|арбуз|как вырастить тыкву|как вырастить арбуз|семена тыквы",
"Семена тыквы и арбуза находят в сундуках заброшенных шахт. Посади стебель на вскопанную землю - плод вырастет на соседнем свободном блоке."},
{"кактус|как вырастить кактус|зеленый краситель",
"Кактус растёт на песке, не требует воды и ранит всех, кто его касается. Переплавь его в печи - получится зелёный краситель. Из кактуса делают и мусорку: предметы в нём сгорают."},
{"еда|как получить еду|голод|лучшая еда|что есть|чем питаться",
"Жареный стейк и жареная свиная отбивная лучше всего восполняют голод в начале игры, а лучшая еда, которую можно вырастить, - золотая морковь. Сырое мясо сначала приготовь в печи."},
{"золотое яблоко|зачарованное яблоко|яблоко",
"Золотое яблоко крафтится из 8 золотых слитков вокруг яблока и даёт регенерацию. В 1.7.10 есть и зачарованное яблоко из 8 золотых блоков - почти временное бессмертие."},
{"житель|жители|торговля|как торговать|изумруды|деревня|житель деревни|как торговать с жителями",
"Жители живут в деревнях и обменивают предметы на изумруды. Профессия определяет, чем они торгуют. Их можно разводить, давая им еду в деревне, где хватает домов с дверями."},
{"железный голем|как сделать железного голема|голем",
"Поставь 4 железных блока буквой Т и тыкву сверху - появится железный голем. Он защищает деревни, бьёт очень больно и роняет железо и розы."},
{"снежный голем|как сделать снежного голема|снеговик",
"Снежный голем - 2 снежных блока друг на друге и тыква сверху. Он кидается снежками, оставляет снежный след и боится воды, дождя и жарких биомов."},
{"кровать|как сделать кровать|как скрафтить кровать|как пропустить ночь|как спать|сон",
"Кровать крафтится из 3 шерсти и 3 досок. Сон ночью пропускает время до утра и задаёт точку возрождения."},
{"сундук|как сделать сундук|эндер сундук|двойной сундук|хранение",
"Сундук крафтится из 8 досок кольцом, два сундука рядом образуют двойной. Эндер-сундук из 8 обсидиана и ока Эндера открывает личное хранилище, доступное из любого эндер-сундука мира."},
{"рельсы|как сделать рельсы|вагонетка|энергорельсы|как сделать вагонетку|как построить железную дорогу|рельса",
"Обычные рельсы делаются из 6 железных слитков и палки, получается 16 штук. Энергорельсы: 6 золотых слитков, палка и красная пыль, получается 6 штук."},
{"лодка|как сделать лодку|как плавать",
"Лодка крафтится из 5 досок буквой П. Она быстро плывёт по воде, но разбивается от сильных ударов о берег и кальмаров, так что рули аккуратно."},
{"лошадь|как приручить лошадь|седло|как ездить на лошади|конь",
"Садись на лошадь с пустой рукой, пока не появятся сердечки - так она приручается. Чтобы управлять, нужно седло: его находят в сундуках подземелий, шахт и крепостей."},
{"волк|как приручить волка|собака|как приручить собаку|оцелот|кошка|как приручить кошку",
"Волков приручают костями - получится верный пёс, который защищает хозяина. Оцелотов приманивают сырой рыбой: кошки отпугивают криперов."},
{"овца|шерсть|ножницы|краситель|как покрасить шерсть",
"Ножницы крафтятся из 2 железных слитков по диагонали. Стрижка овцы даёт 1-3 шерсти, и овца отрастает снова. Шерсть красят красителями из цветов, кактуса и чернил спрутов."},
{"рыбалка|удочка|как сделать удочку|как ловить рыбу|как рыбачить",
"Удочка - это 3 палки и 2 нити. Закинь крючок в воду, дождись всплеска и быстро дёрни: выловишь рыбу, а иногда сокровища или хлам."},
{"лук|как сделать лук|стрелы|как сделать стрелы|стрельба",
"Лук крафтится из 3 палок и 3 нитей, стрела - из кремня, палки и пера. Зачарования Сила и Бесконечность превращают лук в грозное оружие."},
{"ведро|как сделать ведро|как набрать воду|как набрать лаву",
"Ведро крафтится из 3 железных слитков буквой V. Им переносят воду, лаву и молоко. Два ведра воды в яме 2х1 дают бесконечный источник."},
{"стекло|как сделать стекло|стеклянная панель",
"Стекло получается переплавкой песка в печи. 6 стекол в два ряда дают 16 стеклянных панелей для окон."},
{"компас|карта|как сделать компас|как сделать карту|как не заблудиться",
"Компас - 4 железных слитка и красная пыль, он указывает на точку возрождения. Карта - 8 бумаги вокруг компаса. А ещё запоминай координаты по клавише F3."},
{"урон от падения|как избежать урона от падения|ведро воды|как не разбиться|падение",
"Поставь ведро воды прямо перед приземлением, и урона от падения не будет. Падение с высоты до 3 блоков безвредно."},
{"тнт|как сделать тнт|динамит|взрывчатка|tnt|как сделать динамит",
"ТНТ крафтится из 5 пороха и 4 песка. Подожги его огнивом, огнём или сигналом редстоуна."},
{"порох|где взять порох|как получить порох|откуда порох",
"Порох выпадает из криперов. Также он падает с ведьм и гастов, и его можно найти в сундуках подземелий."},
{"опыт|уровни|как получить опыт|шары опыта|как прокачаться|как набрать опыт",
"Шары опыта дают за добычу руды, убийство мобов, плавку и разведение животных. Собирай их, чтобы зачаровывать и чинить предметы."},
{"сид|зерно мира|что такое сид|seed|сид мира",
"Сид - число или текст, определяющее, как генерируется мир. В мире с включёнными читами команда /seed покажет его."},
{"креатив|как включить креатив|режим творчества|читы|команды майнкрафт|как включить читы|как использовать команды|gamemode|как включить режим творчества",
"С включёнными читами напиши /gamemode 1 для творческого режима и /gamemode 0 для выживания. В одиночной игре открой мир для сети с разрешёнными читами, чтобы пользоваться командами."},
{"жемчуг эндера|как получить жемчуг эндера|телепортация|как телепортироваться|эндер жемчуг",
"Жемчуг Эндера падает с эндерменов. Брось его, и телепортируешься туда, куда он упадёт, но получишь немного урона."},
{"сколько длятся сутки|длина дня в майнкрафте|смена дня и ночи|сколько длится день|сколько длится день в майнкрафте|сутки",
"Сутки в Майнкрафте длятся 20 реальных минут: около 10 минут день, короткие закат и рассвет и примерно 7 минут ночь."},
-- OpenComputers
{"что такое opencomputers|opencomputers|опенкомпьютерс|опен компьютерс|что такое oc|oc|расскажи про opencomputers",
"OpenComputers - мод для Майнкрафта, добавляющий программируемые компьютеры и роботов. Ты собираешь корпус, процессор, память, накопитель и экран, ставишь OpenOS и пишешь программы на Lua."},
{"как собрать компьютер|как сделать компьютер|первый компьютер|как построить компьютер|как начать с opencomputers|как начать с опенкомпьютерс|настройка компьютера|как собрать пк|как сделать пк|как собрать компьютер в майнкрафте",
"Поставь корпус компьютера и вставь в него процессор, оперативную память, видеокарту, EEPROM и жёсткий диск. Поставь рядом экран с клавиатурой, подключи питание к сети и нажми кнопку включения."},
{"openos|как установить openos|установка операционной системы|установить|установка|как установить ос|как установить систему|как поставить openos",
"Скрафти дискету OpenOS из дискеты и руководства. Вставь её в дисковод или корпус, загрузи компьютер, затем выполни команду install и выбери жёсткий диск. Перезагрузись и достань дискету."},
{"оперативная память|озу|ram|сколько оперативной памяти|память|объем памяти|уровни памяти|оперативка|рам",
"Планки памяти бывают на 192, 256, 384, 512, 768 КБ и 1 МБ. Для больших программ на Lua бери планку третьего уровня или две второго."},
{"процессор|cpu|уровни процессора|сколько компонентов|лимит компонентов|цп",
"Уровень процессора определяет, сколько компонентов может подключить компьютер: 8, 12 или 16. Чем выше уровень, тем больше прямых вызовов за тик."},
{"видеокарта|gpu|разрешение|уровни экрана|разрешение экрана|размер экрана|монитор",
"Экраны и видеокарты задают разрешение. Первый уровень - 50x16 символов, второй - 80x25, третий - 160x50, и у высоких уровней больше цветов."},
{"жесткий диск|диск|накопитель|дискета|место на диске|сколько места|hdd|хранилище",
"Жёсткие диски вмещают 1, 2 или 4 МБ для уровней 1-3, а дискета - 512 КБ. Команда df покажет свободное место, а ls - список файлов."},
{"eeprom|биос|bios|прошивка|как прошить eeprom|еепром",
"EEPROM хранит BIOS, это 4 КБ кода и небольшая область данных. Стандартный Lua BIOS ищет загрузочный диск и запускает его."},
{"команды shell|команды терминала|команды openos|список команд|какие есть команды|основные команды|команды консоли|команды",
"Полезные команды OpenOS: ls - список файлов, cd - сменить папку, cp - копировать, mv - переместить, rm - удалить, mkdir - создать папку, cat - показать файл, edit - текстовый редактор, lua - интерактивный Lua, components - список устройств, df и free - диск и память, reboot - перезагрузка, man - справка по команде."},
{"edit|как редактировать файл|текстовый редактор|как написать программу|как создать программу|как программировать|как писать код|как создать файл",
"Выполни edit myprogram.lua, набери код, нажми Ctrl+S, чтобы сохранить, и Ctrl+W, чтобы закрыть, а затем запусти программу, набрав её имя. Чтобы вставить текст из буфера, щёлкни средней кнопкой мыши по экрану."},
{"api компонентов|список компонентов|как получить компоненты|как использовать компоненты|component|как найти устройства|component list",
"На Lua: local component = require('component'); for address, name in component.list() do print(address, name) end. Используй component.gpu или component.proxy(address), чтобы вызывать методы устройства."},
{"gpu api|как вывести текст|как нарисовать на экране|как использовать gpu|вывод текста на экран|gpu set",
"local gpu = require('component').gpu; gpu.set(x, y, 'текст') рисует текст, gpu.fill(x, y, w, h, ' ') заливает область, а gpu.setBackground(0x000000) и gpu.setForeground(0xFFFFFF) задают цвета."},
{"события|event pull|как считать клавиатуру|нажатие клавиши|touch|как ждать ввод|event",
"local event = require('event'); local name, a, b, c = event.pull() ждёт следующий сигнал, например key_down или touch. event.pull(2) сдаётся через 2 секунды."},
{"sleep|ждать|задержка|пауза|как подождать|os sleep|как сделать паузу|как подождать в lua",
"os.sleep(секунды) приостанавливает программу, но позволяет компьютеру обрабатывать события. Никогда не используй пустой цикл без sleep."},
{"too long without yielding|компьютер завис|компьютер вылетел|ошибка|программа зависает|почему компьютер зависает|слишком долго без уступки|компьютер остановился",
"Ошибка too long without yielding значит, что твой код слишком долго работал без вызова os.sleep или event.pull. Добавь os.sleep(0) внутрь длинных циклов, чтобы компьютер мог уступить."},
{"питание|энергия|как подключить питание|генератор|нет энергии|не хватает энергии|компьютер не включается|компьютер не запускается|нет питания",
"Компьютеры берут энергию из своей сети. Подключи преобразователь энергии, конденсатор или источник энергии из другого мода кабелями или соседними блоками. Проверь, что в корпусе есть процессор, память, EEPROM и экран."},
{"сеть|кабель|как соединить компоненты|адаптер|как подключить машины|реле|как подключить блоки",
"Компоненты соединяются, когда касаются друг друга или связаны кабелями. Расширяй сеть кабелями или реле, а доступ к блокам из других модов даёт адаптер."},
{"сетевая карта|модем|беспроводная сеть|как передать сообщение|modem|связь между компьютерами|как соединить два компьютера",
"Сетевая карта позволяет компьютерам обмениваться сообщениями: component.modem.open(порт), modem.send(адрес, порт, данные) или broadcast. Сообщение приходит событием modem_message. Беспроводная карта вещает по радиусу, а туннельная связывает два компьютера напрямую."},
{"интернет карта|интернет|wget|pastebin|как скачать файл|как скачать программы|пастебин|как скачать программу",
"Интернет-карта позволяет выполнять wget <url> <файл> или pastebin get <код> <файл> для скачивания программ. А я с ней умею сам искать ответы в Википедии командой /wiki <запрос>, или учиться из любой ссылки: /train <url>."},
{"робот|как сделать робота|роботы|программирование робота|как собрать робота|как запрограммировать робота",
"Роботы - это программируемые движущиеся компьютеры, их собирают в сборщике из корпуса, процессора, памяти и улучшений. В Lua используй robot API, например robot.forward(), robot.turnLeft() и robot.swing()."},
{"дрон|дроны|летающие|как сделать дрона|как управлять дроном",
"Дроны - дешёвые летающие машины, которые собирают в сборщике и программируют через EEPROM. Двигаются они командой drone.move(dx, dy, dz)."},
{"микроконтроллер|что такое микроконтроллер",
"Микроконтроллер - крошечный дешёвый компьютер без файловой системы: вся программа живёт в EEPROM. Отлично подходит для дверей, датчиков и простой автоматики."},
{"сервер|серверная стойка|server rack|стойка",
"Серверы вставляются в серверную стойку: до 4 серверов в одном блоке. У серверов мощные процессоры и много слотов под память и карты, а управляются они через меню стойки."},
{"планшет|tablet|как сделать планшет",
"Планшет собирается в сборщике из корпуса планшета, процессора, памяти и экрана первого уровня. Это переносной компьютер, который можно держать в руке и брать с собой."},
{"геоанализатор|geolyzer|как искать руду роботом|сканер блоков",
"Геоанализатор сканирует блоки вокруг: component.geolyzer.scan(x, z) возвращает плотности столбца блоков. Роботы используют его, чтобы находить руду сквозь камень."},
{"голограмма|холографический проектор|hologram|проектор",
"Голографический проектор рисует объёмную картинку из вокселей: component.hologram.set(x, y, z, 1). Проектор второго уровня поддерживает 3 цвета и больше разрешение."},
{"контроллер инвентаря|inventory controller|как роботу работать с сундуком",
"Улучшение контроллер инвентаря позволяет роботу заглядывать в сундуки и управлять предметами: component.inventory_controller.getStackInSlot и robot.suck и robot.drop по сторонам."},
{"карта данных|data card|хеш|шифрование|md5|sha256",
"Карта данных умеет хеши и шифрование: component.data.md5, sha256, encrypt и decrypt, а третий уровень добавляет ключи и подписи. Полезно для паролей и защищённых сообщений."},
{"звук|бип|beep|как пищать|динамик",
"computer.beep(частота, длительность) пищит встроенным динамиком, частота от 20 до 2000 Гц. Например, computer.beep(440, 0.2) - нота ля."},
{"карта редстоуна|редстоун карта|redstone card|как управлять редстоуном|как подать сигнал редстоуна|redstone i/o|редстоун ввод вывод",
"Карта редстоуна или блок Redstone I/O позволяет компьютеру читать и подавать сигнал редстоуна: component.redstone.setOutput(side, 15) и getInput(side). Названия сторон бери из require('sides')."},
{"файловая система|файлы|как прочитать файл|как записать файл|как сохранить данные|как сохранить файл|чтение файла|запись в файл",
"Открой файл через io.open('путь', 'w'), запиши f:write(текст) и закрой f:close(). Чтобы читать, используй io.open('путь', 'r'):read('*a')."},
-- Lua
{"lua|что такое lua|язык lua|луа|что такое луа",
"Lua - маленький и быстрый скриптовый язык. Программы для OpenComputers пишутся на Lua 5.2 или 5.3."},
{"таблица|таблицы|массивы|таблица lua|массив в lua|как сделать массив|как сделать таблицу",
"Таблица - единственная структура данных в Lua. {1, 2, 3} - массив с индексами с единицы, а {name = 'x'} - словарь. Длина узнаётся через #t, а перебирать удобно через pairs и ipairs."},
{"цикл|циклы|цикл for|цикл while|как сделать цикл|повторение",
"for i = 1, 10 do ... end перебирает числа, for k, v in pairs(t) do ... end перебирает таблицу, а while cond do ... end повторяется, пока условие истинно."},
{"функция|функции|как написать функцию|как создать функцию",
"function add(a, b) return a + b end объявляет функцию. Добавь local перед ней, чтобы она была видна только в твоём файле."},
{"строка|строки|функции строк|как разделить строку|работа со строками|string",
"Полезные функции строк: string.sub, string.find, string.gsub, string.format и string.gmatch. Длина берётся через #, а склейка через два символа точки."},
{"конкатенация|как склеить строки|склеить строки|соединить строки",
"Строки склеиваются двумя точками: 'а' .. 'б'. Числа приводятся сами: 'итог: ' .. 42. Для больших объёмов быстрее собрать таблицу кусочков и вызвать table.concat."},
{"string format|формат строки|как отформатировать строку|format",
"string.format собирает строку по шаблону: %d - целое, %s - строка, %.2f - дробное с двумя знаками, %x - шестнадцатеричное. Например: string.format('%d из %d', 3, 10)."},
{"переменная|переменные|local|глобальные переменные|область видимости|что такое local",
"Объявляй переменные через local. Без local они становятся глобальными и могут конфликтовать между программами."},
{"условие|условия|if|если то|как написать условие|оператор if|else",
"if a then ... elseif b then ... else ... end. Для равенства используй ==, для неравенства ~=, а также and, or, not."},
{"метатаблица|метатаблицы|setmetatable|наследование|ооп в lua|классы в lua",
"Метатаблицы меняют поведение таблиц: setmetatable(t, {__index = база}) даёт наследование, __add перегружает сложение, __tostring - печать. На этом строится ООП в Lua."},
{"корутина|корутины|coroutine|сопрограммы|потоки в lua",
"Корутины - лёгкие потоки: coroutine.create создаёт, coroutine.resume запускает, а coroutine.yield приостанавливает функцию. Вся многозадачность OpenOS построена на корутинах."},
{"модуль|модули|require|как подключить библиотеку|своя библиотека",
"Собери функции в таблицу, верни её в конце файла через return M и положи файл в /lib. Потом подключай: local M = require('имя'). Так ты делишь программу на модули."},
{"математика lua|math|округление|math floor|корень|модуль числа",
"math.floor и math.ceil округляют вниз и вверх, math.max и math.min сравнивают, math.sqrt - корень, math.abs - модуль, math.pi - число пи."},
{"отладка|как отлаживать|как вывести переменную|как найти ошибку|как найти баги|debug|баг|pcall|обработка ошибок",
"Используй print и type(x), чтобы смотреть значения, а рискованный код оборачивай в pcall(function() ... end), чтобы ловить ошибки без падения программы."},
{"случайное число|как получить случайное число|random|math random|рандом|как сделать рандом",
"math.random(1, 10) даёт случайное целое от 1 до 10. Один раз вызови math.randomseed(os.time()), чтобы при каждом запуске числа отличались."},
-- общие знания
{"что такое ии|искусственный интеллект|что такое искусственный интеллект|что такое ai|ии",
"Искусственный интеллект - область, где компьютеры решают задачи, обычно требующие человеческого ума: обучение, понимание языка, принятие решений."},
{"двоичная система|что такое двоичная система|бинарный код|двоичный код|binary|что такое бинарный",
"Двоичная система - это система счисления с основанием 2, в ней только цифры 0 и 1. Компьютеры хранят всё в двоичном виде."},
{"байт|килобайт|что такое байт|сколько байт в килобайте|мегабайт|сколько килобайт в мегабайте",
"В байте 8 бит. В килобайте 1024 байта, а в мегабайте 1024 килобайта."},
{"что такое компьютер|как работает компьютер|компьютер|как устроен компьютер",
"Компьютер выполняет инструкции, обрабатывая данные. Процессор запускает код, память хранит то, что нужно прямо сейчас, а накопитель хранит файлы."},
{"кто такой алан тьюринг|алан тьюринг|тьюринг|кто изобрел компьютер|кто придумал компьютер",
"Алан Тьюринг, 1912-1954, заложил основы информатики, придумав машину Тьюринга, и помог взломать шифр Энигмы во время Второй мировой войны."},
{"сколько планет|планеты|планеты солнечной системы|солнечная система|сколько планет в солнечной системе",
"В Солнечной системе восемь планет: Меркурий, Венера, Земля, Марс, Юпитер, Сатурн, Уран и Нептун."},
{"самая большая планета|крупнейшая планета|какая планета самая большая",
"Юпитер - самая большая планета Солнечной системы."},
{"самая маленькая планета|какая планета самая маленькая",
"Меркурий - самая маленькая планета Солнечной системы, и он же ближайший к Солнцу."},
{"ближайшая планета к солнцу|первая планета|самая близкая к солнцу планета",
"Меркурий - ближайшая к Солнцу планета."},
{"солнце|что такое солнце|из чего состоит солнце",
"Солнце - звезда, жёлтый карлик из водорода и гелия. В нём сосредоточено 99,8 процента массы всей Солнечной системы."},
{"луна|расстояние до луны|как далеко луна|что такое луна",
"Луна - естественный спутник Земли. Среднее расстояние до неё около 384400 километров: свет проходит его за 1,3 секунды."},
{"скорость света|как быстро летит свет|какая скорость света",
"Свет в вакууме проходит около 299792 километров в секунду."},
{"скорость звука|как быстро летит звук|какая скорость звука",
"Звук в воздухе летит примерно 343 метра в секунду, а в воде почти в четыре с половиной раза быстрее."},
{"температура кипения воды|при какой температуре кипит вода|когда кипит вода|сколько градусов кипит вода",
"Вода кипит при 100 градусах Цельсия на уровне моря."},
{"формула воды|химическая формула воды|из чего состоит вода|h2o",
"Формула воды - H2O: два атома водорода и один атом кислорода."},
{"днк|что такое днк|генетика|гены",
"ДНК - молекула, хранящая наследственную информацию. Это двойная спираль из четырёх оснований: аденина, тимина, гуанина и цитозина."},
{"число пи|что такое пи|чему равно пи|пи|значение пи",
"Пи - отношение длины окружности к её диаметру, примерно 3,14159."},
{"теорема пифагора|пифагор|гипотенуза|катет",
"Теорема Пифагора: в прямоугольном треугольнике квадрат гипотенузы равен сумме квадратов катетов. a в квадрате плюс b в квадрате равно c в квадрате."},
{"самый большой океан|крупнейший океан|какой океан самый большой",
"Тихий океан - самый большой океан на Земле."},
{"марианская впадина|самое глубокое место|глубина океана",
"Марианская впадина в Тихом океане - самое глубокое место океана, почти 11 километров."},
{"самая высокая гора|высочайшая гора|эверест|какая гора самая высокая",
"Эверест - высочайшая гора над уровнем моря, примерно 8849 метров."},
{"самая длинная река|длиннейшая река|какая река самая длинная",
"Претендентов два: Нил и Амазонка, а Нил традиционно считается самой длинной."},
{"самая большая пустыня|крупнейшая пустыня|сахара",
"Формально самая большая пустыня - Антарктида, ведь пустыня определяется осадками. А из жарких пустынь крупнейшая - Сахара."},
{"самая большая страна|какая страна самая большая|крупнейшая страна",
"Россия - самая большая страна по площади, около 17 миллионов квадратных километров."},
{"самая населенная страна|где больше всего людей|сколько людей на земле|население земли",
"На Земле больше 8 миллиардов человек. За первое место по населению спорят Индия и Китай, у каждого больше 1,4 миллиарда."},
{"сколько материков|материки|континенты|сколько континентов",
"Всего семь материков: Африка, Антарктида, Азия, Австралия, Европа, Северная Америка и Южная Америка."},
{"фотосинтез|что такое фотосинтез",
"Фотосинтез - процесс, в котором растения превращают солнечный свет, воду и углекислый газ в сахар и кислород."},
{"гравитация|что такое гравитация|сила тяжести|что такое сила тяжести",
"Гравитация - сила, притягивающая массы друг к другу. На Земле она ускоряет падающие тела примерно на 9,8 метра в секунду за каждую секунду."},
{"сколько дней в году|високосный год|дней в году|что такое високосный год",
"В году 365 дней, а в високосном 366. Високосные годы повторяются раз в 4 года, с исключениями для большинства столетий."},
{"сколько секунд в часе|сколько минут в сутках|сколько секунд в сутках|сколько минут в часе",
"В минуте 60 секунд, в часе 60 минут или 3600 секунд, а в сутках 1440 минут или 86400 секунд."},
{"кто написал войну и мир|война и мир|толстой|лев толстой|в каком году написали войну и мир|в каком году вышла война и мир|когда вышла война и мир|год выхода война и мир|в каком году вышел роман война и мир",
"Войну и мир написал Лев Толстой, роман выходил с 1865 по 1869 год."},
{"пушкин|кто такой пушкин|александр пушкин|кто написал евгения онегина",
"Александр Пушкин - великий русский поэт, автор Евгения Онегина, Руслана и Людмилы и Капитанской дочки."},
{"первый человек в космосе|гагарин|кто полетел в космос первым|кто такой гагарин",
"Юрий Гагарин первым полетел в космос 12 апреля 1961 года на корабле Восток-1."},
{"первый человек на луне|кто был на луне|армстронг|кто первым ступил на луну",
"Нил Армстронг первым ступил на Луну 20 июля 1969 года в миссии Аполлон-11."},
{"вторая мировая|когда закончилась вторая мировая война|когда началась вторая мировая",
"Вторая мировая война шла с 1939 по 1945 год: 8 мая 1945 закончилась война в Европе, а 2 сентября капитулировала Япония."},
{"сколько цветов в радуге|радуга|цвета радуги",
"В радуге принято выделять 7 цветов: красный, оранжевый, жёлтый, зелёный, голубой, синий и фиолетовый."},
{"самое быстрое животное|кто быстрее всех|гепард|сапсан",
"Сапсан в пикировании разгоняется быстрее 300 километров в час, а на земле быстрее всех гепард - около 100 километров в час."},
{"самое большое животное|синий кит|крупнейшее животное",
"Синий кит - крупнейшее животное в истории Земли: до 33 метров длины и около 150 тонн веса."},
}
-- {страна в родительном падеже, столица, дополнительные названия через |}
local CAPITALS = {
{"Франции", "Париж"}, {"Германии", "Берлин"}, {"Италии", "Рим"}, {"Испании", "Мадрид"},
{"Португалии", "Лиссабон"}, {"Японии", "Токио"}, {"Китая", "Пекин"}, {"Индии", "Нью-Дели"},
{"России", "Москва"}, {"Канады", "Оттава"}, {"Австралии", "Канберра"}, {"Бразилии", "Бразилиа"},
{"Египта", "Каир"}, {"Турции", "Анкара"}, {"Польши", "Варшава"}, {"Украины", "Киев"},
{"Швеции", "Стокгольм"}, {"Норвегии", "Осло"}, {"Греции", "Афины"}, {"Мексики", "Мехико"},
{"Аргентины", "Буэнос-Айрес"}, {"Южной Кореи", "Сеул"}, {"Австрии", "Вена"}, {"Ирландии", "Дублин"},
{"Беларуси", "Минск", "белоруссии"},
{"Нидерландов", "Амстердам", "голландии"},
{"Швейцарии", "Берн"}, {"Финляндии", "Хельсинки"}, {"Дании", "Копенгаген"},
{"Чехии", "Прага"}, {"Венгрии", "Будапешт"}, {"Румынии", "Бухарест"},
{"Казахстана", "Астана"}, {"Узбекистана", "Ташкент"}, {"Грузии", "Тбилиси"},
{"Армении", "Ереван"}, {"Азербайджана", "Баку"}, {"Израиля", "Иерусалим"},
{"Ирана", "Тегеран"}, {"Ирака", "Багдад"}, {"Таиланда", "Бангкок"},
{"Вьетнама", "Ханой"}, {"Индонезии", "Джакарта"}, {"Саудовской Аравии", "Эр-Рияд"},
{"Великобритании", "Лондон", "англии|британии|соединенного королевства"},
{"США", "Вашингтон", "америки|соединенных штатов"},
}
-- добавить запись в базу знаний и в поисковый индекс
local entryDoc = {} -- ei -> первый документ (для самообучения сети)
local entryQ = {} -- ei -> исходный вопрос (для самообучения сети)
function T.addEntry(q, a, source, verified, title)
local ei = #ans + 1
ans[ei] = a
srcOf[ei] = source
if verified then verOf[ei] = true end
local firstDoc
for phrase in (q .. "|"):gmatch("([^|]*)|") do
local st = T.stemsOf(phrase)
if #st > 0 then
nDocs = nDocs + 1
docEntry[nDocs] = ei
local terms = {}
for _, s in ipairs(st) do
local p = inv[s]
if not p then p = {}; inv[s] = p end
p[#p + 1] = nDocs
terms[s] = true
end
docTerms[nDocs] = terms
if not firstDoc then firstDoc = nDocs end
end
end
entryDoc[ei] = firstDoc
if source then entryQ[ei] = q end
if title then entryQ[ei] = q end
return ei
end
---------------------------------------------------------------------------
-- МАЛЕНЬКАЯ ЯЗЫКОВАЯ МОДЕЛЬ (цепь Маркова по тройкам слов)
---------------------------------------------------------------------------
local END = 0
local B = 65536
local PUN = {[","] = 1, [";"] = 1, [":"] = 1, ["."] = 1, ["!"] = 1, ["?"] = 1}
local vw, iw = {}, {}
local chain, starts, kwstarts = {}, {}, {}
local proper = {}
local lite = false
for _, w in ipairs({"Lua", "OpenOS", "OpenComputers", "Mojang", "Forge", "Minecraft", "Wikipedia", "Lua 5.2"}) do
proper[T.lower(w)] = w
end
local function wid(w)
local id = vw[w]
if not id then id = #iw + 1; iw[id] = w; vw[w] = id end
return id
end
local function memLow()
if computer.freeMemory() < 48000 then
gc()
return computer.freeMemory() < 48000
end
return false
end
local function trainMarkov(text)
if lite then return end
text = text:gsub("([%.!%?]+)%s+", "%1\n")
for sent in text:gmatch("[^\n]+") do
local noContr = sent:gsub("%a'%a", "")
local usable = not sent:find("[%(%)%[%]{}=<>\"/\\|]") and not noContr:find("'") and not sent:find("%a%.%a")
if usable then
if memLow() or #iw > 60000 then lite = true; return end
local s2 = sent:gsub("([,;:%.!%?])(%s)", " %1%2")
s2 = s2:gsub("([,;:%.!%?])$", " %1")
local toks = {}
for w in s2:gmatch("%S+") do toks[#toks + 1] = w end
if #toks >= 3 then
if not PUN[toks[#toks]] then toks[#toks + 1] = "." end
local ids = {}
for i, tk in ipairs(toks) do
local lw = T.lower(tk)
if tk ~= lw and not PUN[tk] and (i > 1 or tk:find("%u", 2)) then proper[lw] = tk end
ids[i] = wid(lw)
end
local n = #ids
for i = 1, n - 2 do
local key = (ids[i] * B + ids[i + 1]) * B + ids[i + 2]
local nx = ids[i + 3] or END
local v = chain[key]
if v == nil then
chain[key] = nx
elseif type(v) == "number" then
if v ~= nx then chain[key] = {v, nx} end
else
v[#v + 1] = nx
end
if i == 1 then starts[#starts + 1] = key end
end
local key1 = (ids[1] * B + ids[2]) * B + ids[3]
local seenk = {}
for i = 1, n do
local lw = iw[ids[i]]
if not PUN[lw] then
local s = T.norm(lw)
if T.ulen(s) >= 3 and not STOP[s] and not seenk[s] then
seenk[s] = true
local l = kwstarts[s]
if not l then l = {}; kwstarts[s] = l end
l[#l + 1] = key1
end
end
end
end
end
end
end
local function walk(key, maxw)
local c = key % B
local t = floor(key / B)
local b = t % B
local a = floor(t / B)
local out = {a, b, c}
while #out < maxw do
local v = chain[(out[#out - 2] * B + out[#out - 1]) * B + out[#out]]
if v == nil then break end
local nx
if type(v) == "number" then nx = v else nx = v[random(#v)] end
if nx == END then break end
out[#out + 1] = nx
end
return out
end
local function render(ids)
if not PUN[iw[ids[#ids]]] then
local cut
for i = #ids, 1, -1 do if iw[ids[i]] == "," then cut = i; break end end
if cut and cut > #ids / 2 then
for i = #ids, cut, -1 do ids[i] = nil end
ids[#ids + 1] = vw["."] or wid(".")
end
end
local parts = {}
for _, id in ipairs(ids) do
local w = iw[id]
if PUN[w] then
parts[#parts + 1] = w
else
parts[#parts + 1] = (#parts > 0 and " " or "") .. (proper[w] or w)
end
end
return T.ucfirst(concat(parts))
end
local function generate(seeds, tries)
if #starts == 0 then return nil, 0 end
local seedSet, cands = {}, {}
for _, s in ipairs(seeds) do
seedSet[s] = true
if kwstarts[s] then cands[#cands + 1] = s end
end
local best, bestScore, bestHits = nil, -1, 0
for _ = 1, tries do
local key
if #cands > 0 then
local l = kwstarts[cands[random(#cands)]]
key = l[random(#l)]
end
key = key or starts[random(#starts)]
local ids = walk(key, 40)
if #ids >= 5 then
local seen, hits = {}, 0
for _, id in ipairs(ids) do
local s = T.norm(iw[id])
if seedSet[s] and not seen[s] then seen[s] = true; hits = hits + 1 end
end
local sc = hits + math.min(#ids, 20) / 100
if sc > bestScore then bestScore, bestHits, best = sc, hits, render(ids) end
end
end
return best, bestHits
end
---------------------------------------------------------------------------
-- СБОРКА БАЗЫ ЗНАНИЙ
---------------------------------------------------------------------------
local function answerOf(ei)
local a = ans[ei]
if type(a) == "table" then a = a[1] end
if a == nil then a = "" end
return a
end
local ansStems = {}
local function answerStemsOf(ei)
if not ansStems[ei] then
local set = {}
for _, w in ipairs(T.words(answerOf(ei))) do set[T.norm(w)] = true end
ansStems[ei] = set
end
return ansStems[ei]
end
T.intentMetrics = function(intent, d, ei)
local m = {active = 0, subject = 0, event = 0, answerType = 0, quality = 0, reject = false}
if not intent then return m end
m.active = 1
local terms = docTerms[d] or {}
local subjectN, subjectHit = #intent.subject, 0
for _, st in ipairs(intent.subject) do
if terms[st] then subjectHit = subjectHit + 1 end
end
m.subject = subjectN > 0 and subjectHit / subjectN or 1
local relationInQuestion = false
for st in pairs(terms) do
if T.intentEventTerm(st, intent.kind) then relationInQuestion = true break end
end
m.event = relationInQuestion and 1 or 0
local answer = answerOf(ei)
local answerSet = answerStemsOf(ei)
local answerHasYear = answer:find("%d%d%d%d") ~= nil
local answerHasEvent = false
for st in pairs(answerSet) do
if T.intentEventTerm(st, intent.kind) then answerHasEvent = true break end
end
local calendarMismatch = intent.kind == "release_year"
and answer:find("365", 1, true) ~= nil
and answer:find("дней", 1, true) ~= nil
m.event = (relationInQuestion and answerHasEvent) and 1 or 0
m.answerType = calendarMismatch and -1 or (answerHasYear and 1 or 0)
-- Для вопроса о годе нельзя брать «в году 365 дней» или другую запись,
-- где совпало лишь слово «год»: нужна та же сущность, отношение и год в ответе.
if (subjectN > 0 and subjectHit == 0) or not relationInQuestion
or not answerHasEvent or not answerHasYear or calendarMismatch then
m.reject = true
else
m.quality = 1
end
return m
end
local selfLearn -- объявлен ниже, а вызывается здесь
-- перестроить только поисковый индекс (языковая модель при этом не трогается)
local function buildIndex()
ans, srcOf, verOf = {}, {}, {}
T.autotrainEntries = {}
inv, docEntry, docMass, docTerms, nDocs = {}, {}, {}, {}, 0
entryDoc, entryQ = {}, {}
ansStems = {}
gc()
for _, e in ipairs(KB) do T.addEntry(e[1], e[2], nil, false, nil) end
for _, c in ipairs(CAPITALS) do
local names = {c[1]}
for alias in ((c[3] or "") .. "|"):gmatch("([^|]+)|") do names[#names + 1] = alias end
local q = {}
for _, nm in ipairs(names) do
q[#q + 1] = "столица " .. nm
q[#q + 1] = "какая столица " .. nm
q[#q + 1] = "что является столицей " .. nm
end
T.addEntry(concat(q, "|"), "Столица " .. c[1] .. " - " .. c[2] .. ".", nil, false, nil)
end
for _, e in ipairs(dbLearned) do T.addEntry(e.q, e.a, "user", false, nil) end
for _, e in ipairs(dbWiki) do
T.addEntry(e.q, e.a, "wiki", e.ok and true or false, e.title)
for _, fact in ipairs(e.facts or {}) do
local rel = fact.relation
local ys = fact.years or (fact.year and {fact.year}) or {}
local title = e.title or fact.subject or ""
local low = T.lower(title)
local subjects = {title}
if low:find("minecraft", 1, true) then subjects[#subjects + 1] = "майнкрафт" end
if low:find("майнкрафт", 1, true) then subjects[#subjects + 1] = "minecraft" end
if rel and #ys > 0 then
for _, y in ipairs(ys) do
local queries = {}
for _, subject in ipairs(subjects) do
queries[#queries + 1] = "в каком году " .. subject .. " " .. rel
queries[#queries + 1] = "когда " .. subject .. " " .. rel
queries[#queries + 1] = "год " .. rel .. " " .. subject
queries[#queries + 1] = subject .. " " .. rel .. " " .. y
end
local answer = format("В сохранённой факт-карточке о «%s» событие «%s» связано с %s годом.",
title, rel, y)
T.addEntry(concat(queries, "|"), answer, "wiki", e.ok == true, title)
end
end
end
end
for _, e in ipairs(dbCorpus) do
local t = e.t or ""
local low = T.lower(t)
local q = "что такое " .. low .. "|" .. low .. "|расскажи про " .. low .. "|кто такой " .. low
local a = e.a or T.distillArticle(t, e.s or "", t)
local corpusEi = T.addEntry(q, a, "corpus", e.ok == true, t)
if e.autotrain == true then T.autotrainEntries[corpusEi] = true end
-- Индексируем и сохранённые карточки фактов, а не только аннотацию статьи.
-- Для Minecraft добавляем русский алиас: заголовок Википедии часто латинский.
local facts = e.facts or {}
for _, fact in ipairs(facts) do
local rel = fact.relation
local ys = fact.years or (fact.year and {fact.year}) or {}
local subjects = {t}
if low:find("minecraft", 1, true) then subjects[#subjects + 1] = "майнкрафт" end
if low:find("майнкрафт", 1, true) then subjects[#subjects + 1] = "minecraft" end
if rel and #ys > 0 then
for _, y in ipairs(ys) do
local fq = {}
for _, subject in ipairs(subjects) do
fq[#fq + 1] = "в каком году " .. subject .. " " .. rel
fq[#fq + 1] = "когда " .. subject .. " " .. rel
fq[#fq + 1] = "год " .. rel .. " " .. subject
fq[#fq + 1] = subject .. " " .. rel .. " " .. y
end
local fa = format("В сохранённой факт-карточке о «%s» событие «%s» связано с %s годом.",
t, rel, y)
local factEi = T.addEntry(concat(fq, "|"), fa, "corpus", e.ok == true, t)
if e.autotrain == true then T.autotrainEntries[factEi] = true end
end
elseif fact.keywords and #fact.keywords > 0 then
local tags = concat(fact.keywords, " ")
local fq = {}
for _, subject in ipairs(subjects) do
fq[#fq + 1] = "что такое " .. subject .. " " .. tags
fq[#fq + 1] = "расскажи о " .. subject .. " " .. tags
fq[#fq + 1] = subject .. " " .. tags
end
local factEi = T.addEntry(concat(fq, "|"), a, "corpus", e.ok == true, t)
if e.autotrain == true then T.autotrainEntries[factEi] = true end
end
end
end
dynStop = lexDynamicStop()
T.finalize()
gc()
end
-- полная пересборка: индекс + дообучение сетей
local function buildAll()
buildIndex()
pcall(selfLearn)
end
-- один раз обучаем языковую модель: база знаний + всё, что я выучил
local function trainLanguage()
vw, iw, chain, starts, kwstarts, lite = {}, {}, {}, {}, {}, false
for _, e in ipairs(KB) do
if type(e[2]) == "string" then trainMarkov(e[2]); lexAdd(e[2])
elseif type(e[2]) == "table" then
for _, a in ipairs(e[2]) do trainMarkov(a); lexAdd(a) end
end
end
for _, e in ipairs(dbLearned) do trainMarkov(e.a); lexAdd(e.a) end
for _, e in ipairs(dbWiki) do
if e.autotrain == true then trainMarkov(e.a); lexAdd(e.a) end
end
for _, e in ipairs(dbCorpus) do
if e.autotrain == true then
trainMarkov(e.a or "")
lexAdd(e.s or e.a or "")
lexAdd(e.a or "")
end
end
for _, s in ipairs(dbSentences) do trainMarkov(s) end
end
---------------------------------------------------------------------------
-- ПРИЗНАКИ ДЛЯ НЕЙРОСЕТИ (20 входов: базовое сходство + смысл/тип ответа)
---------------------------------------------------------------------------
local F_COVER, F_MASSR, F_HITR, F_MAXT = 1, 2, 3, 4
local F_USER, F_WIKI, F_CORP, F_VER = 5, 6, 7, 8
local F_QLEN, F_ISQ, F_DMASS, F_ALEN = 9, 10, 11, 12
local F_ACOV, F_AFOC, F_IDFS, F_DENS = 13, 14, 15, 16
local F_INTENT, F_SUBJECT, F_EVENT, F_TYPE = 17, 18, 19, 20
local function isQuestion(raw)
if raw:find("%?%s*$") then return true end
local t = T.words(raw)
return t[1] ~= nil and QWORD[t[1]] ~= nil
end
-- слова запроса с исправлением опечаток по словарю индекса
local function queryStems(raw)
local list = {}
for _, t in ipairs(T.words(raw)) do
local s = T.norm(t)
if not inv[s] and T.ulen(s) >= 5 then
local near = T.nearestIndex(s)
if near then s = near end
end
list[#list + 1] = s
end
return list
end
local function makeQuery(raw, stemList)
local q = {raw = raw, stems = {}, w = {}, total = 0, idfTotal = 0, n = 0,
isQ = isQuestion(raw), content = {}, set = {}, cNum = 0, cTotal = 0,
intent = T.classifyIntent(raw)}
local list = stemList
if not list then list = queryStems(raw) end
local hasPron = false
for s in pairs(q.set) do if PRON[s] then hasPron = true end end
if hasPron then
for _, cs in ipairs(ctxStems) do
if not q.set[cs] then list[#list + 1] = cs end
end
end
for _, s in ipairs(list) do
if s ~= "" and not q.set[s] then
q.set[s] = true
local w = T.weight(s)
q.stems[#q.stems + 1] = s
q.w[#q.w + 1] = w
local id = T.idf(s)
q.total = q.total + id * w
q.idfTotal = q.idfTotal + id
if w >= 1 then
q.content[#q.content + 1] = s
q.cNum = q.cNum + 1
q.cTotal = q.cTotal + id * w
end
end
end
q.n = #q.stems
return q
end
local function docFeatures(q, d, ei)
local terms = docTerms[d] or {}
local present, sumw, sumidf, maxw = 0, 0, 0, 0
for i = 1, q.n do
local s = q.stems[i]
if terms[s] then
present = present + 1
local id = T.idf(s)
local x = q.w[i] * id
sumw = sumw + x
sumidf = sumidf + id
if x > maxw then maxw = x end
end
end
local cover = (q.total > 0) and (sumw / q.total) or 0
-- покрытие по значимым (не служебным) словам запроса
local cPresent, cSumw = 0, 0
for i = 1, q.n do
local s = q.stems[i]
if q.w[i] >= 1 and terms[s] then
cPresent = cPresent + 1
cSumw = cSumw + q.w[i] * T.idf(s)
end
end
local cCover = (q.cTotal > 0) and (cSumw / q.cTotal) or cover
local cHit = (q.cNum > 0) and (cPresent / q.cNum) or ((q.n > 0) and (present / q.n) or 0)
local dm = docMass[d] or 0
local massr = (dm > 0) and (sumw / dm) or 0
local hitr = cHit
local maxt = (q.total > 0) and (maxw / q.total) or 0
local a = answerOf(ei)
local aset = answerStemsOf(ei)
local inter, awords = 0, 0
for s in pairs(aset) do
awords = awords + 1
if q.set[s] then inter = inter + 1 end
end
local ansCov = (q.n > 0) and (inter / q.n) or 0
local ansFoc = (awords > 0) and (inter / awords) or 0
local src = srcOf[ei]
local f = {}
f[F_COVER] = cCover
f[F_MASSR] = math.min(massr, 1)
f[F_HITR] = hitr
f[F_MAXT] = maxt
f[F_USER] = (src == "user") and 1 or 0
f[F_WIKI] = (src == "wiki") and 1 or 0
f[F_CORP] = (src == "corpus") and 1 or 0
f[F_VER] = verOf[ei] and 1 or 0
f[F_QLEN] = math.min(q.n, 10) / 10
f[F_ISQ] = q.isQ and 1 or 0
f[F_DMASS] = math.min(dm, 20) / 20
f[F_ALEN] = math.min(T.ulen(a), 500) / 500
f[F_ACOV] = ansCov
f[F_AFOC] = ansFoc
f[F_IDFS] = (q.idfTotal > 0) and (sumidf / q.idfTotal) or 0
f[F_DENS] = present / math.max(1, (docTerms[d] and 1 or 1))
local im = T.intentMetrics(q.intent, d, ei)
f[F_INTENT] = im.active
f[F_SUBJECT] = im.subject
f[F_EVENT] = im.event
f[F_TYPE] = im.answerType
return f, im
end
-- Извлекает короткое внутреннее представление статьи: релевантные тезисы,
-- компактные карточки фактов и переписанный ответ. Исходная статья в ответ
-- пользователю не подставляется.
T.distillArticle = function(title, text, query)
title = title or "тема"
local clean = (text or ""):gsub("==%s*([^=]+)%s*==", " %1. ")
clean = clean:gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "")
local q = makeQuery(query or title)
local qset, titleSet = {}, {}
for i = 1, q.n do if q.w[i] >= 1 then qset[q.stems[i]] = true end end
for _, st in ipairs(T.stemsOf(title)) do if not STOP[st] then titleSet[st] = true end end
local sentences, serial = {}, 0
for part in clean:gmatch("[^%.!?]+[%.!?]?") do
local sentence = part:gsub("^%s+", ""):gsub("%s+$", "")
if T.ulen(sentence) >= 24 then
serial = serial + 1
local terms = {}
for _, st in ipairs(T.stemsOf(sentence)) do terms[st] = true end
local score, matchedTitle, matchedSubject = 0, false, (#(q.intent and q.intent.subject or {}) == 0)
for st in pairs(qset) do if terms[st] then score = score + 3 end end
for st in pairs(titleSet) do
if terms[st] then score = score + 2; matchedTitle = true end
end
if q.intent then
local subjectHits = 0
for _, st in ipairs(q.intent.subject) do
if terms[st] or titleSet[st] then subjectHits = subjectHits + 1 end
end
local corpusText = T.lower(title .. " " .. sentence)
if corpusText:find("minecraft", 1, true) and (q.intent.subject[1] == "майнкрафт" or q.intent.subject[1] == "minecraft") then
subjectHits = math.max(subjectHits, 1)
end
matchedSubject = #q.intent.subject == 0 or subjectHits > 0
end
local yearSet, years = {}, {}
for y in sentence:gmatch("%d%d%d%d") do
if not yearSet[y] then yearSet[y] = true; years[#years + 1] = y end
end
local relation
for _, w in ipairs(T.words(sentence)) do
if T.intentEventTerm(w, q.intent and q.intent.kind or "year_event") then
relation = w
break
end
end
local typedFact = q.intent and #years > 0 and relation ~= nil and matchedSubject or false
if #years > 0 then score = score + (q.intent and 8 or 1) end
if relation then score = score + (q.intent and 8 or 1) end
if typedFact then score = score + 30 end
sentences[#sentences + 1] = {
text = sentence, terms = terms, score = score, order = serial,
years = years, relation = relation, typedFact = typedFact,
matchedTitle = matchedTitle,
}
end
end
table.sort(sentences, function(a, b)
if a.score ~= b.score then return a.score > b.score end
return a.order < b.order
end)
local chosen, cards, seenCards = {}, {}, {}
for _, item in ipairs(sentences) do
if #chosen >= 2 then break end
if #chosen == 0 or item.score > 0 then
chosen[#chosen + 1] = item
end
end
local function pickKeywords(item, maxn)
local keywords, seen, count = {}, {}, 0
for _, word in ipairs(T.words(item.text)) do
local st = T.norm(word)
if T.ulen(st) >= 3 and not STOP[st] and not titleSet[st]
and not st:match("^%d+$") and not seen[st] then
seen[st] = true
count = count + 1
keywords[count] = word
if count >= maxn then break end
end
end
return keywords
end
local function categoryOf(item)
local categories = {
{"видеоигр", "видеоигра"}, {"энциклопед", "энциклопедия"},
{"программир", "программирование"}, {"игр", "игра"},
{"язык", "язык"}, {"город", "город"}, {"стран", "страна"},
{"фильм", "фильм"}, {"роман", "роман"}, {"книг", "книга"},
{"песн", "песня"}, {"компан", "компания"}, {"сайт", "сайт"},
{"планет", "планета"}, {"звезд", "звезда"}, {"мод", "мод"},
{"систем", "система"}, {"проект", "проект"}, {"организац", "организация"},
}
for _, itemType in ipairs(categories) do
for st in pairs(item.terms) do
if st:find("^" .. itemType[1]) then return itemType[2] end
end
end
return nil
end
-- Храним карточки: понятия для общего поиска и отдельные отношения/годы.
for i = 1, math.min(#sentences, 8) do
local item = sentences[i]
if item.relation or #item.years > 0 or i == 1 then
local key = (item.relation or "summary") .. ":" .. (item.years[1] or "")
if not seenCards[key] then
seenCards[key] = true
cards[#cards + 1] = {
subject = title, relation = item.relation,
year = item.years[1], years = item.years,
keywords = pickKeywords(item, 8), category = categoryOf(item),
}
end
end
end
if q.intent then
local years, yearSeen = {}, {}
for _, item in ipairs(chosen) do
if item.typedFact then
for _, y in ipairs(item.years) do
if not yearSeen[y] then yearSeen[y] = true; years[#years + 1] = y end
end
end
end
if #years > 0 then
if q.intent.kind == "release_year" then
if #years == 1 then
return format("По статье о «%s» я выделил факт о выходе: год %s.", title, years[1]), cards, true
end
return format("В разобранной статье о «%s» для выхода упомянуты годы %s. Они могут относиться к разным этапам выпуска; уточни, раннюю версию или полный релиз ты имеешь в виду.",
title, concat(years, ", ")), cards, true
end
local relation = q.intent.relation or "событие"
if #years == 1 then
return format("В разобранной статье о «%s» событие «%s» связано с %s годом.",
title, relation, years[1]), cards, true
end
return format("В разобранной статье о «%s» для события «%s» указаны годы %s.",
title, relation, concat(years, ", ")), cards, true
end
return "", cards, false
end
if #chosen == 0 then
return "По этой статье пока не удалось выделить надёжный короткий вывод.", cards, false
end
local abstract = {}
for i = 1, math.min(#chosen, 2) do
local item = chosen[i]
local keywords = pickKeywords(item, 5)
local label = categoryOf(item)
local piece
if label and #keywords > 0 then
piece = label .. "; ключевые понятия: " .. concat(keywords, ", ")
elseif label then
piece = "тип материала: " .. label
elseif #keywords > 0 then
piece = "ключевые понятия: " .. concat(keywords, ", ")
end
if piece then abstract[#abstract + 1] = piece end
end
local summary
if #abstract > 0 then
summary = "В сохранённой выжимке о «" .. title .. "» выделено: " .. concat(abstract, ". ") .. "."
else
summary = "Материал о «" .. title .. "» сохранён, но полезные ключевые понятия не выделились."
end
return summary, cards, false
end
-- предварительный поиск кандидатов
local function retrieve(q, limit)
local acc, accHits = {}, {}
for i = 1, q.n do
local s = q.stems[i]
local x = T.idf(s) * q.w[i]
local post = inv[s]
if post then
for k = 1, #post do
local d = post[k]
acc[d] = (acc[d] or 0) + x
accHits[d] = (accHits[d] or 0) + 1
end
end
end
local list = {}
for d, m in pairs(acc) do
list[#list + 1] = {d = d, m = m, hits = accHits[d] or 0}
end
table.sort(list, function(a, b) if a.m ~= b.m then return a.m > b.m end return a.d < b.d end)
if limit and #list > limit then
for i = #list, limit + 1, -1 do list[i] = nil end
end
return list
end
-- обратный словарь синонимов: канон -> варианты
local REV_SYN = {}
for k, v in pairs(SYN) do
local l = REV_SYN[v]
if not l then l = {}; REV_SYN[v] = l end
l[#l + 1] = k
end
-- переформулировать вопрос несколькими способами
local function buildVariants(raw, depth)
local out = {}
local toks = T.words(raw)
local stems = queryStems(raw)
out[#out + 1] = {text = raw, stems = stems}
if depth >= 1 then
-- без вопросительного слова
if #stems > 1 then
local cut = {}
for i = 2, #stems do cut[#cut + 1] = stems[i] end
out[#out + 1] = {text = raw, stems = cut}
end
-- "что такое <существительные>"
local content = {}
for i = 1, #stems do
if T.weight(stems[i]) >= 1 then content[#content + 1] = stems[i] end
end
if #content > 0 and #content <= 4 then
local v = {"что", "такое"}
for _, c in ipairs(content) do v[#v + 1] = c end
out[#out + 1] = {text = raw, stems = v}
end
-- синонимы
local used = 0
for i = 1, #stems do
if used < 2 then
local l = REV_SYN[stems[i]]
if l and #l > 0 then
local v = {}
for j, s in ipairs(stems) do v[j] = s end
v[i] = l[random(#l)]
out[#out + 1] = {text = raw, stems = v}
used = used + 1
end
end
end
end
-- Режим глубокого анализа строит отдельные запросы под распознанный факт.
if depth >= 4 then
local intent = T.classifyIntent(raw)
if intent and #intent.subject > 0 then
local subject = concat(intent.subject, " ")
if intent.kind == "release_year" then
for _, phrase in ipairs({"год выхода ", "год релиза ", "дата выхода "}) do
out[#out + 1] = {text = raw, stems = T.stemsOf(phrase .. subject)}
end
else
for _, phrase in ipairs({"год создания ", "год появления ", "год основания "}) do
out[#out + 1] = {text = raw, stems = T.stemsOf(phrase .. subject)}
end
end
end
end
return out
end
-- главная «размышляющая» процедура
local function deliberate(raw, depth, note)
local variants = buildVariants(raw, depth)
local cands, order = {}, {}
local docsSeen = 0
local searchLimit = depth >= 4 and 240 or 160
local keepPerVariant = depth >= 4 and 140 or 100
for _, v in ipairs(variants) do
local q = makeQuery(v.text, v.stems)
local list = retrieve(q, searchLimit)
docsSeen = docsSeen + #list
for i = 1, math.min(#list, keepPerVariant) do
local it = list[i]
local ei = docEntry[it.d]
if ei then
local c = cands[ei]
if not c then
c = {ei = ei, doc = it.d, best = it.m, q = q, votes = 1}
cands[ei] = c
order[#order + 1] = c
else
c.votes = c.votes + 1
if it.m > c.best then c.best = it.m; c.doc = it.d; c.q = q end
end
end
end
if depth >= 2 then os.sleep(0) end
end
for _, c in ipairs(order) do
c.prelim = c.best * (1 + 0.10 * (c.votes - 1))
end
table.sort(order, function(a, b) if a.prelim ~= b.prelim then return a.prelim > b.prelim end return a.ei < b.ei end)
if note and depth >= 1 and #order > 0 then
note(format("разложил вопрос на %d вариантов, нашёл %d кандидатов", #variants, #order))
end
-- Ансамбль оценивает кандидатов; на глубине 4-5 проверяется больший список.
local K = depth >= 5 and 14 or (depth >= 4 and 12 or 10)
local best, bestScore, bestConf, bestFeat = nil, 0, 0, nil
local evaluated, rejected = 0, 0
for i = 1, math.min(#order, K) do
local c = order[i]
local f, intentMetrics = docFeatures(c.q, c.doc, c.ei)
evaluated = evaluated + 1
if intentMetrics.reject then c.dead = true; rejected = rejected + 1 end
-- Смысловая проверка: ответ должен совпасть и по предмету, и по типу вопроса.
if c.q.cNum > 0 and f[F_COVER] <= 0 then c.dead = true end
local ens, score = 0.5, 0
if not c.dead then
ens = T.ensembleScore(f)
score = c.prelim * (0.55 + 0.45 * ens)
if intentMetrics.quality > 0 then score = score * 1.12 end
end
c.ens, c.score, c.feat, c.intentMetrics = ens, score, f, intentMetrics
if not c.dead and score > bestScore then
bestScore, best, bestFeat = score, c, f
local cover = f[F_COVER]
local hitr = f[F_HITR]
local conf = 0.5 * ens + 0.3 * cover + 0.2 * hitr
if intentMetrics.quality > 0 then conf = conf + 0.08 end
if f[F_ACOV] < 0.12 and hitr < 0.30 then conf = conf - 0.12 end
if srcOf[c.ei] == "wiki" and not verOf[c.ei] then conf = conf - 0.10 end
if conf > 1 then conf = 1 end
bestConf = conf
end
if depth >= 4 then os.sleep(0.001) else os.sleep(0) end
end
if best and note and depth >= 2 then
note(format("ансамбль оценил %d кандидатов из %d, уверенность %d%%",
evaluated, #order, floor(bestConf * 100)))
if depth >= 4 and rejected > 0 then
note(format("отсеял %d ответов с неверной сущностью/типом факта", rejected))
end
end
-- ПРОВЕРКА ОТВЕТА: в ответе должны быть ключевые слова вопроса.
-- Если у лучшего кандидата их нет, берём следующего, а не выдаём ерунду.
if best then
local ranked = {}
for _, c in ipairs(order) do
if not c.dead and c.score then ranked[#ranked + 1] = c end
end
table.sort(ranked, function(a, b) return a.score > b.score end)
local picked, cov
for _, c in ipairs(ranked) do
local set = answerStemsOf(c.ei)
local hit, total = 0, 0
if c.q.intent then
-- Для года/даты отдельно сверяем сущность, отношение и наличие года.
for _, st in ipairs(c.q.intent.subject) do
total = total + 1
if set[st] then hit = hit + 1 end
end
total = total + 1
if answerOf(c.ei):find("%d%d%d%d") then hit = hit + 1 end
total = total + 1
for st in pairs(set) do
if T.intentEventTerm(st, c.q.intent.kind) then hit = hit + 1 break end
end
else
-- считаем ключевые, а не служебные корни вроде «что» и «как».
local keys = c.q.content
total = #keys
for i = 1, #keys do
if set[keys[i]] then hit = hit + 1 end
end
end
local cv = (total > 0) and (hit / total) or 1
c.ansCov = cv
if not picked then picked, cov = c, cv end
if cv >= 0.20 then picked, cov = c, cv break end
end
if picked and picked ~= best then
best = picked
bestFeat = picked.feat
local cover = picked.feat and picked.feat[F_COVER] or 0
local hitr = picked.feat and picked.feat[F_HITR] or 0
bestConf = 0.5 * (picked.ens or 0.5) + 0.3 * cover + 0.2 * hitr
if picked.feat and picked.feat[F_ACOV] < 0.12 and hitr < 0.30 then bestConf = bestConf - 0.12 end
end
if best and note and depth >= 2 then
local verified = format("проверил ответ: %d%% ключевых слов вопроса нашлось в ответе",
floor((best.ansCov or 0) * 100))
if best.intentMetrics and best.intentMetrics.active > 0 then
verified = verified .. "; совпал тип факта и сущность"
end
note(verified)
end
end
return best, bestConf, bestFeat, order
end
-- самообучение нейросетей на собственной базе знаний
selfLearn = function()
if #NETS == 0 then return end
local pool = {}
for ei, q in pairs(entryQ) do
local corpusAllowed = srcOf[ei] ~= "corpus" or T.autotrainEntries[ei] == true
if srcOf[ei] ~= "wiki" and corpusAllowed and entryDoc[ei] and q and q ~= "" then
pool[#pool + 1] = {ei = ei, q = q}
end
end
if #pool < 1 then return end
local total = 0
for _, net in ipairs(NETS) do total = total + T.netWeights(net) end
local budget = math.max(2, math.floor(CFG.learnBudget * 26000 / math.max(1, total)))
budget = math.min(budget, #pool)
for _ = 1, budget do
local p = pool[random(#pool)]
local other = (#pool > 1) and pool[random(#pool)] or nil
if entryDoc[p.ei] then
local q1 = makeQuery(p.q)
local f1 = docFeatures(q1, entryDoc[p.ei], p.ei)
for _, net in ipairs(NETS) do
T.netTrain(net, f1, 1, net.lr * 0.5)
end
if other and other.ei ~= p.ei and entryDoc[other.ei] then
local q2 = makeQuery(other.q)
local f2 = docFeatures(q2, entryDoc[p.ei], p.ei)
for _, net in ipairs(NETS) do
T.netTrain(net, f2, 0, net.lr * 0.5)
end
end
end
end
end
---------------------------------------------------------------------------
-- ИНТЕРНЕТ: скачивание текста, Википедия, обход
---------------------------------------------------------------------------
local function urlencode(s)
return (s:gsub("[^%w%-%.%_%~]", function(c) return format("%%%02X", c:byte()) end))
end
-- значение строкового поля JSON вида "key":"..."
local function jsonStringAfter(text, key)
local pat = '"' .. key .. '"%s*:%s*"'
local s, e = text:find(pat, 1)
if not s then return nil end
local i = e + 1
local out = {}
local n = #text
while i <= n do
local c = text:sub(i, i)
if c == "\\" then
local nx = text:sub(i + 1, i + 1)
if nx == "n" then out[#out + 1] = "\n"; i = i + 2
elseif nx == "t" then out[#out + 1] = " "; i = i + 2
elseif nx == "r" then i = i + 2
elseif nx == '"' then out[#out + 1] = '"'; i = i + 2
elseif nx == "\\" then out[#out + 1] = "\\"; i = i + 2
elseif nx == "/" then out[#out + 1] = "/"; i = i + 2
elseif nx == "u" then
local hex = text:sub(i + 2, i + 5)
local cp = tonumber(hex, 16)
if cp then
if cp < 0x80 then
out[#out + 1] = string.char(cp)
elseif cp < 0x800 then
out[#out + 1] = string.char(0xC0 + floor(cp / 64), 0x80 + cp % 64)
else
out[#out + 1] = string.char(0xE0 + floor(cp / 4096), 0x80 + floor(cp / 64) % 64, 0x80 + cp % 64)
end
end
i = i + 6
else
out[#out + 1] = nx; i = i + 2
end
elseif c == '"' then
break
else
out[#out + 1] = c; i = i + 1
end
end
return concat(out)
end
-- все значения одного поля JSON (для списков статей)
local function jsonValues(text, key)
local out = {}
local pat = '"' .. key .. '"%s*:%s*"'
local pos = 1
while true do
local s, e = text:find(pat, pos)
if not s then break end
local i = e + 1
local buf = {}
local n = #text
while i <= n do
local c = text:sub(i, i)
if c == "\\" then
local nx = text:sub(i + 1, i + 1)
if nx == "n" then buf[#buf + 1] = " "; i = i + 2
elseif nx == "t" then buf[#buf + 1] = " "; i = i + 2
elseif nx == '"' then buf[#buf + 1] = '"'; i = i + 2
elseif nx == "\\" then buf[#buf + 1] = "\\"; i = i + 2
elseif nx == "/" then buf[#buf + 1] = "/"; i = i + 2
elseif nx == "u" then
local cp = tonumber(text:sub(i + 2, i + 5), 16)
if cp then
if cp < 0x80 then
buf[#buf + 1] = string.char(cp)
elseif cp < 0x800 then
buf[#buf + 1] = string.char(0xC0 + floor(cp / 64), 0x80 + cp % 64)
else
buf[#buf + 1] = string.char(0xE0 + floor(cp / 4096), 0x80 + floor(cp / 64) % 64, 0x80 + cp % 64)
end
end
i = i + 6
else
buf[#buf + 1] = nx; i = i + 2
end
elseif c == '"' then
break
else
buf[#buf + 1] = c; i = i + 1
end
end
out[#out + 1] = concat(buf)
pos = i + 1
end
return out
end
local function httpGet(url, maxBytes, headers)
if not hasInternet then return nil, "нет интернет-карты" end
maxBytes = maxBytes or 65536
local okLib, internet = pcall(require, "internet")
if not okLib then return nil, "библиотека internet недоступна" end
local okReq, handle
if headers then okReq, handle = pcall(internet.request, url, nil, headers)
else okReq, handle = pcall(internet.request, url) end
if not okReq or not handle then return nil, tostring(handle) end
local data, size = {}, 0
local okRead, err = pcall(function()
for chunk in handle do
data[#data + 1] = chunk
size = size + #chunk
if size > maxBytes then break end
os.sleep(0)
end
end)
if not okRead and size == 0 then return nil, tostring(err) end
return concat(data), size
end
-- сколько слов запроса встретилось в тексте (проверка ответа)
local function coverageOf(text, q)
local set = {}
for _, w in ipairs(T.words(text)) do set[T.norm(w)] = true end
local hit, tot = 0, 0
for i = 1, q.n do
if q.w[i] >= 1 then
tot = tot + 1
if set[q.stems[i]] then hit = hit + 1 end
end
end
if tot == 0 then return 0 end
return hit / tot
end
local SEARCH = {
autotrainSite = "https://ru.wikipedia.org",
wikis = {
{site = "https://ru.wikipedia.org", provider = "Русская Википедия", publisher = "Wikimedia"},
{site = "https://ru.minecraft.wiki", provider = "Minecraft Wiki", publisher = "Minecraft Wiki"},
{site = "https://ru.wiktionary.org", provider = "Викисловарь", publisher = "Wikimedia",
trigger = "слово|значение|значит|определение|перевод|переведи|как называется"},
{site = "https://ru.wikinews.org", provider = "Викиновости", publisher = "Wikimedia",
trigger = "новост|сегодня|последн|сейчас|вчера|события|выборы|президент"},
{site = "https://ru.wikibooks.org", provider = "Викиучебники", publisher = "Wikimedia",
trigger = "как|инструкция|руководство|учебник|обучение|рецепт|настройка"},
{site = "https://ru.wikiquote.org", provider = "Викицитатник", publisher = "Wikimedia",
trigger = "цитат|сказал|фраза|афоризм"},
{site = "https://ru.wikisource.org", provider = "Викитека", publisher = "Wikimedia",
trigger = "текст|стих|поэма|документ|первоисточник"},
{site = "https://ru.wikivoyage.org", provider = "Викигид", publisher = "Wikimedia",
trigger = "путешеств|туризм|достопримечатель|маршрут|гостиница|отель"},
{site = "https://ru.wikiversity.org", provider = "Викиверситет", publisher = "Wikimedia",
trigger = "курс|лекц|университет|обучение|образован|учебн|преподав|практикум"},
{site = "https://species.wikimedia.org", provider = "Wikispecies", publisher = "Wikimedia",
trigger = "вид|животн|растен|биолог|таксон|латинское название|species"},
{site = "https://commons.wikimedia.org", provider = "Wikimedia Commons", publisher = "Wikimedia",
trigger = "изображен|фотограф|картина|иллюстрац|файл|медиа"},
{site = "https://en.wikipedia.org", provider = "English Wikipedia", publisher = "Wikimedia",
trigger = "minecraft|майнкрафт|opencomputers|openos|lua|python|linux|javascript|stack overflow|wikipedia"},
},
}
function SEARCH.queryHasAny(query, words)
local low = T.lower(query or "")
for word in words:gmatch("[^|]+") do
if low:find(word, 1, true) then return true end
end
return false
end
function SEARCH.wikidataDateSpecs(intent)
if not intent then return nil end
if intent.kind == "release_year" then
return {{id = "P577", label = "дата публикации", action = "вышел"}}
end
local rel = T.norm(intent.relation or "")
if rel:find("^умер") then return {{id = "P570", label = "дата смерти", action = "умер"}} end
if rel:find("^род") then return {{id = "P569", label = "дата рождения", action = "родился"}} end
if rel:find("^созда") or rel:find("^осн") or rel:find("^появ") then
return {{id = "P571", label = "дата основания", action = "основан"}}
end
if rel:find("^нап") or rel:find("^изда") or rel:find("^публик") or rel:find("^опублик") then
return {{id = "P577", label = "дата публикации", action = "опубликован"}}
end
if rel:find("^обнаруж") or rel:find("^изобр") or rel:find("^открыл") then
return {{id = "P575", label = "дата обнаружения", action = "обнаружен"}}
end
if rel:find("^нач") then return {{id = "P580", label = "дата начала", action = "начался"}} end
if rel:find("^законч") then return {{id = "P582", label = "дата окончания", action = "закончился"}} end
return nil
end
function SEARCH.wikidataYearsFor(id, specs)
local out = {}
local url = "https://www.wikidata.org/w/api.php?action=wbgetentities&format=json&props=claims&ids="
.. urlencode(id)
local raw = httpGet(url, 100000)
if not raw then return out end
for _, spec in ipairs(specs or {}) do
local pos = 1
local pat = '"' .. spec.id .. '"%s*:%s*%['
while true do
local a, b = raw:find(pat, pos)
if not a then break end
local nextProp = raw:find(',"P%d+"%s*:', b + 1)
local block = raw:sub(b + 1, nextProp and nextProp - 1 or #raw)
local dates, seen = {}, {}
for time in block:gmatch('"time"%s*:%s*"([^"]+)"') do
local year = time:match("^[%+%-]?(%d%d%d%d)%-")
if year and not seen[year] then seen[year] = true; dates[#dates + 1] = year end
end
if #dates > 0 then out[spec.id] = dates end
pos = b + 1
end
end
return out
end
function SEARCH.wikidataSearch(query, intent, limit)
local out = {}
local searchQuery = query
if intent and #intent.subject > 0 then searchQuery = concat(intent.subject, " ") end
local url = "https://www.wikidata.org/w/api.php?action=wbsearchentities&format=json&language=ru&uselang=ru&limit="
.. math.min(limit or 3, 3) .. "&search=" .. urlencode(searchQuery)
local raw = httpGet(url, 90000)
if not raw then return out end
local ids, labels = jsonValues(raw, "id"), jsonValues(raw, "label")
local descriptions = jsonValues(raw, "description")
local specs = SEARCH.wikidataDateSpecs(intent)
for i = 1, math.min(#ids, #labels, limit or 3) do
local title = labels[i]
local desc = descriptions[i] or ""
local text = desc
local subjectMatch = false
if intent then
local entityTerms = {}
for _, st in ipairs(T.stemsOf(title .. " " .. desc)) do entityTerms[st] = true end
for _, st in ipairs(intent.subject) do if entityTerms[st] then subjectMatch = true end end
if T.lower(title .. " " .. desc):find("minecraft", 1, true)
and (intent.subject[1] == "майнкрафт" or intent.subject[1] == "minecraft") then
subjectMatch = true
end
end
if specs and subjectMatch and i <= 2 then
local years = SEARCH.wikidataYearsFor(ids[i], specs)
local sentences = {}
for _, spec in ipairs(specs) do
for _, year in ipairs(years[spec.id] or {}) do
sentences[#sentences + 1] = format("%s %s в %s году (Wikidata: %s).", title, spec.action, year, spec.label)
end
end
if #sentences > 0 then text = (text ~= "" and (text .. ". ") or "") .. concat(sentences, " ") end
end
if title ~= "" and text ~= "" then
out[#out + 1] = {
title = title, text = text,
url = "https://www.wikidata.org/wiki/" .. ids[i],
site = "https://www.wikidata.org", provider = "Wikidata",
publisher = "Wikimedia", kind = "structured",
}
end
end
return out
end
function SEARCH.hostOf(url)
return (url or ""):match("^https?://([^/]+)") or ""
end
function SEARCH.publisherFor(url, fallback)
local host = T.lower(SEARCH.hostOf(url))
local wikiDomains = {"wikipedia.org", "wiktionary.org", "wikinews.org", "wikibooks.org",
"wikiquote.org", "wikisource.org", "wikivoyage.org", "wikiversity.org", "wikidata.org", "wikimedia.org"}
for _, domain in ipairs(wikiDomains) do
if host == domain or host:sub(-#domain - 1) == "." .. domain then return "Wikimedia" end
end
if host == "stackoverflow.com" or host == "superuser.com" or host == "askubuntu.com"
or host == "serverfault.com" or host == "stackexchange.com"
or host:match("%.stackexchange%.com$") then return "Stack Exchange" end
if host == "github.com" or host:sub(-11) == ".github.com" then return "GitHub" end
if host == "openalex.org" or host:match("%.openalex%.org$") then return "OpenAlex" end
if host == "europepmc.org" or host:match("%.europepmc%.org$") then return "Europe PMC" end
if host == "openlibrary.org" or host:match("%.openlibrary%.org$") then return "Open Library" end
local label = T.lower(fallback or "")
if label:find("wikipedia", 1, true) or label:find("wikidata", 1, true)
or label:find("wiktionary", 1, true) or label:find("wikimedia", 1, true) then
return "Wikimedia"
end
return fallback or host
end
function SEARCH.jsonArrayObjects(text, key, maxItems)
local pat = '"' .. key .. '"%s*:%s*%['
local _, listEnd = text:find(pat, 1)
if not listEnd then return {} end
local out, depth, start = {}, 0, nil
local inString, escaped = false, false
for i = listEnd + 1, #text do
local c = text:sub(i, i)
if inString then
if escaped then escaped = false
elseif c == "\\" then escaped = true
elseif c == '"' then inString = false end
else
if c == '"' then inString = true
elseif c == "{" then
depth = depth + 1
if depth == 1 then start = i end
elseif c == "}" and depth > 0 then
depth = depth - 1
if depth == 0 and start then
out[#out + 1] = text:sub(start, i)
start = nil
if maxItems and #out >= maxItems then break end
end
elseif c == "]" and depth == 0 then break end
end
end
return out
end
function SEARCH.jsonNumberAfter(text, key)
local pat = '"' .. key .. '"%s*:%s*(%-?%d+)'
return tonumber(text:match(pat))
end
function SEARCH.cleanHtml(text)
local s = tostring(text or "")
s = s:gsub("<br%s*/?>", ". "):gsub("</p%s*>", ". "):gsub("</li%s*>", ". ")
s = s:gsub("<[^>]+>", " ")
s = s:gsub("&#(%d+);", function(n)
local cp = tonumber(n)
if cp == 39 then return "'" end
if cp == 34 then return '"' end
if cp == 160 then return " " end
if cp == 8211 then return "–" end
if cp == 8212 then return "—" end
if cp == 8217 then return "’" end
return " "
end)
s = s:gsub("&#x([%x]+);", function(h)
local cp = tonumber(h, 16)
if cp == 39 then return "'" end
if cp == 34 then return '"' end
return " "
end)
s = s:gsub(" ", " "):gsub(""", '"'):gsub("'", "'")
s = s:gsub("<", "<"):gsub(">", ">")
s = s:gsub("&", "&"):gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "")
return s
end
function SEARCH.urlDecode(url)
local s = tostring(url or ""):gsub("&", "&")
local target = s:match("[?&]uddg=([^&]+)")
if target then s = target end
s = s:gsub("%%(%x%x)", function(h) return string.char(tonumber(h, 16)) end)
s = s:gsub("%+", " ")
if s:sub(1, 2) == "//" then s = "https:" .. s end
return s
end
function SEARCH.duckDuckGoSearch(query, limit)
local out = {}
local url = "https://api.duckduckgo.com/?q=" .. urlencode(query)
.. "&format=json&no_html=1&skip_disambig=1&no_redirect=1"
local raw = httpGet(url, 100000)
if not raw then return out end
local heading = jsonStringAfter(raw, "Heading")
if not heading or heading == "" then heading = query end
local abstract = jsonStringAfter(raw, "AbstractText") or ""
local source = jsonStringAfter(raw, "AbstractSource")
if not source or source == "" then source = "DuckDuckGo Instant Answers" end
local target = jsonStringAfter(raw, "AbstractURL")
if not target or target == "" then target = "https://duckduckgo.com/?q=" .. urlencode(query) end
if abstract == "" then
abstract = jsonStringAfter(raw, "Definition") or ""
local definitionSource = jsonStringAfter(raw, "DefinitionSource")
local definitionURL = jsonStringAfter(raw, "DefinitionURL")
if definitionSource and definitionSource ~= "" then source = definitionSource end
if definitionURL and definitionURL ~= "" then target = definitionURL end
end
if T.ulen(abstract) >= 24 then
local publisher = SEARCH.publisherFor(target, source)
out[#out + 1] = {title = heading, text = abstract, url = target,
site = "https://" .. SEARCH.hostOf(target), provider = "DuckDuckGo Instant Answer", publisher = publisher}
end
local texts, urls = jsonValues(raw, "Text"), jsonValues(raw, "FirstURL")
for i = 1, math.min(#texts, #urls, limit or 3) do
if T.ulen(texts[i]) >= 24 then
local title = texts[i]:match("^(.-)%s+%-%s+") or heading
local publisher = SEARCH.publisherFor(urls[i], "DuckDuckGo")
out[#out + 1] = {title = title, text = texts[i], url = urls[i],
site = "https://" .. SEARCH.hostOf(urls[i]), provider = "DuckDuckGo related topic", publisher = publisher}
end
end
return out
end
function SEARCH.duckDuckGoWebSearch(query, limit)
local out = {}
local url = "https://html.duckduckgo.com/html/?q=" .. urlencode(query)
local raw = httpGet(url, 160000)
if not raw then return out end
local titles, snippets = {}, {}
for attrs, body in raw:gmatch("<a([^>]*)>(.-)</a>") do
local low = T.lower(attrs)
local href = attrs:match('href%s*=%s*"([^"]+)"') or attrs:match("href%s*=%s*'([^']+)'")
if href and low:find("result__a", 1, true) then
local link = href:gsub("&", "&")
local target = link:match("[?&]uddg=([^&]+)")
if target then link = SEARCH.urlDecode(target)
elseif link:sub(1, 2) == "//" then link = "https:" .. link end
local title = SEARCH.cleanHtml(body)
local host = SEARCH.hostOf(link)
if title ~= "" and host ~= "" and not T.lower(host):find("duckduckgo.com", 1, true) then
titles[#titles + 1] = {title = title, url = link, host = host}
end
elseif low:find("result__snippet", 1, true) then
local snippet = SEARCH.cleanHtml(body)
if snippet ~= "" then snippets[#snippets + 1] = snippet end
end
end
for i = 1, math.min(#titles, #snippets, limit or 3) do
if T.ulen(snippets[i]) >= 24 then
local item = titles[i]
out[#out + 1] = {title = item.title, text = snippets[i], url = item.url,
site = "https://" .. item.host, provider = "DuckDuckGo Web Index",
publisher = SEARCH.publisherFor(item.url, item.host), kind = "web_snippet"}
end
end
return out
end
function SEARCH.openLibrarySearch(query, intent)
local out = {}
local bookQuery = SEARCH.queryHasAny(query, "книга|книг|роман|повест|автор|писател|литератур|поэм|произвед")
if not bookQuery then return out end
local url = "https://openlibrary.org/search.json?q=" .. urlencode(query)
.. "&limit=3&fields=title,first_publish_year,key"
local raw = httpGet(url, 100000)
if not raw then return out end
local records = SEARCH.jsonArrayObjects(raw, "docs", 6)
for _, record in ipairs(records) do
if #out >= 3 then break end
local title = jsonStringAfter(record, "title") or ""
local year = tonumber(record:match('"first_publish_year"%s*:%s*(%-?%d+)'))
local key = jsonStringAfter(record, "key") or ""
if title ~= "" and year and key ~= "" then
local text = format("Книга «%s» впервые опубликована в %d году.", title, year)
key = key:gsub("^/", "")
out[#out + 1] = {title = title, text = text,
url = "https://openlibrary.org/" .. key, site = "https://openlibrary.org",
provider = "Open Library", publisher = "Open Library", kind = "book"}
end
end
return out
end
function SEARCH.githubSearch(query, limit)
local out = {}
if not SEARCH.queryHasAny(query, "lua|opencomputers|openos|minecraft|github|код|программ|скрипт|api|библиотек|репозитор|приложен|python|javascript|java|мод") then return out end
local url = "https://api.github.com/search/repositories?q=" .. urlencode(query)
.. "&per_page=" .. math.min(limit or 3, 3) .. "&sort=stars&order=desc"
local headers = {Accept = "application/vnd.github+json", ["User-Agent"] = "NanoAI-OpenComputers/3.2"}
local raw = httpGet(url, 120000, headers)
if not raw then return out end
local records = SEARCH.jsonArrayObjects(raw, "items", math.min(limit or 3, 3))
for _, record in ipairs(records) do
local title = jsonStringAfter(record, "full_name") or ""
local description = jsonStringAfter(record, "description") or ""
local language = jsonStringAfter(record, "language") or ""
local target = jsonStringAfter(record, "html_url") or ""
local text = description
if language ~= "" then text = (text ~= "" and (text .. ". ") or "") .. "Язык проекта: " .. language end
if title ~= "" and T.ulen(text) >= 16 and target ~= "" then
out[#out + 1] = {title = title, text = text, url = target,
site = "https://github.com", provider = "GitHub repositories",
publisher = "GitHub", kind = "repository"}
end
end
return out
end
function SEARCH.stackExchangeSearch(query, limit)
local out = {}
if not SEARCH.queryHasAny(query, "lua|opencomputers|openos|код|программ|скрипт|api|ошибк|функц|компьютер|linux|ubuntu|bash|python|javascript|java|сервер|сеть|network|nginx") then return out end
local sites = {"stackoverflow"}
if SEARCH.queryHasAny(query, "linux|ubuntu|apt|bash|shell|терминал|командн|операционн") then
sites[#sites + 1] = "askubuntu"
sites[#sites + 1] = "superuser"
end
if SEARCH.queryHasAny(query, "сервер|server|network|сеть|nginx|apache|dns") then
sites[#sites + 1] = "serverfault"
end
local headers = {Accept = "application/json", ["User-Agent"] = "NanoAI-OpenComputers/3.2"}
for _, site in ipairs(sites) do
local url = "https://api.stackexchange.com/2.3/search/advanced?order=desc&sort=relevance&site="
.. site .. "&pagesize=" .. math.min(limit or 3, 3) .. "&filter=withbody&q=" .. urlencode(query)
local raw = httpGet(url, 150000, headers)
if raw then
local questions = SEARCH.jsonArrayObjects(raw, "items", math.min(limit or 3, 3))
local accepted, acceptedIds = {}, {}
for _, question in ipairs(questions) do
local answerId = SEARCH.jsonNumberAfter(question, "accepted_answer_id")
local title = jsonStringAfter(question, "title") or ""
local link = jsonStringAfter(question, "link") or ""
if answerId and title ~= "" and link ~= "" then
local key = tostring(answerId)
accepted[key] = {title = title, url = link}
acceptedIds[#acceptedIds + 1] = key
end
end
if #acceptedIds > 0 then
local answersUrl = "https://api.stackexchange.com/2.3/answers/" .. concat(acceptedIds, ";")
.. "?order=desc&sort=votes&site=" .. site .. "&filter=withbody"
local answerRaw = httpGet(answersUrl, 180000, headers)
if answerRaw then
for _, answer in ipairs(SEARCH.jsonArrayObjects(answerRaw, "items", #acceptedIds)) do
local answerId = SEARCH.jsonNumberAfter(answer, "answer_id")
local row = answerId and accepted[tostring(answerId)] or nil
local body = SEARCH.cleanHtml(jsonStringAfter(answer, "body") or "")
if row and T.ulen(body) >= 40 then
out[#out + 1] = {title = row.title, text = body,
url = row.url .. "#answer-" .. tostring(answerId),
site = "https://" .. site .. ".com", provider = "Stack Exchange accepted answers",
publisher = "Stack Exchange", kind = "community_answer"}
end
end
end
end
end
end
return out
end
function SEARCH.openAlexSearch(query, intent, limit)
local out = {}
if not SEARCH.queryHasAny(query, "науч|исслед|стат|публикац|doi|журнал|диссертац|академ|ген|клинич|биолог|физик|хим|математ|science|research|paper|academic") then return out end
local url = "https://api.openalex.org/works?search=" .. urlencode(query)
.. "&per-page=" .. math.min(limit or 3, 3) .. "&select=id,doi,title,publication_year"
local raw = httpGet(url, 150000)
if not raw then return out end
for _, record in ipairs(SEARCH.jsonArrayObjects(raw, "results", math.min(limit or 3, 3))) do
local title = jsonStringAfter(record, "title") or ""
local doi = jsonStringAfter(record, "doi") or ""
local id = jsonStringAfter(record, "id") or ""
local year = SEARCH.jsonNumberAfter(record, "publication_year")
local text = title
if year and intent and intent.kind == "release_year" then
text = format("Статья «%s» опубликована в %d году.", title, year)
elseif year then
text = text .. format(" (год публикации: %d).", year)
end
local target = (doi ~= "" and doi) or id
if title ~= "" and target ~= "" then
out[#out + 1] = {title = title, text = text, url = target,
site = "https://openalex.org", provider = "OpenAlex research index",
publisher = "OpenAlex", kind = "academic"}
end
end
return out
end
function SEARCH.europePmcSearch(query, intent, limit)
local out = {}
if not SEARCH.queryHasAny(query, "медицин|болез|заболев|вирус|бактери|ген|клетк|лекарств|вакцин|здоров|пациент|клиническ|онкол|протеин|фарма|биолог|medical|medicine|cancer|virus|vaccine|gene|clinical") then return out end
local url = "https://www.ebi.ac.uk/europepmc/webservices/rest/search?query="
.. urlencode(query) .. "&format=json&pageSize=" .. math.min(limit or 3, 3) .. "&resultType=core"
local raw = httpGet(url, 180000)
if not raw then return out end
for _, record in ipairs(SEARCH.jsonArrayObjects(raw, "result", math.min(limit or 3, 3))) do
local title = jsonStringAfter(record, "title") or ""
local abstract = SEARCH.cleanHtml(jsonStringAfter(record, "abstractText") or "")
local pmid = jsonStringAfter(record, "pmid") or ""
if pmid == "" then
local pmidNumber = SEARCH.jsonNumberAfter(record, "pmid")
if pmidNumber then pmid = tostring(pmidNumber) end
end
local pmcid = jsonStringAfter(record, "pmcid") or ""
local yearText = jsonStringAfter(record, "firstPublicationDate") or ""
local year = yearText:match("^(%d%d%d%d)")
local text = title
if abstract ~= "" then text = text .. ". " .. abstract end
if year and intent and intent.kind == "release_year" then
text = format("Статья «%s» опубликована в %s году. %s", title, year, abstract)
end
local target = pmid ~= "" and ("https://europepmc.org/article/MED/" .. pmid)
or (pmcid ~= "" and ("https://europepmc.org/article/PMC/" .. pmcid)
or "https://europepmc.org/search?query=" .. urlencode(query))
if title ~= "" and T.ulen(text) >= 40 then
out[#out + 1] = {title = title, text = text, url = target,
site = "https://europepmc.org", provider = "Europe PMC biomedical literature",
publisher = "Europe PMC", kind = "academic"}
end
end
return out
end
function SEARCH.wikiSearch(query, limit)
if not hasInternet then return nil end
query = query:gsub("^%s+", ""):gsub("%s+$", "")
if query == "" then return nil end
local results, intent = {}, T.classifyIntent(query)
local function addAll(items)
for _, item in ipairs(items or {}) do results[#results + 1] = item end
end
local pageLimit = math.min(limit or 3, 3)
for _, source in ipairs(SEARCH.wikis) do
if not source.trigger or SEARCH.queryHasAny(query, source.trigger) then
local sourceQuery = query
if source.site == "https://en.wikipedia.org"
and T.lower(query):find("майнкрафт", 1, true) then sourceQuery = "Minecraft" end
local url = source.site .. "/w/api.php?action=query&format=json&generator=search&gsrsearch="
.. urlencode(sourceQuery) .. "&gsrlimit=" .. pageLimit .. "&prop=extracts|info&exintro=1&explaintext=1&exchars=3000&redirects=1&inprop=url"
local raw = httpGet(url, 120000)
if raw then
local titles, extracts = jsonValues(raw, "title"), jsonValues(raw, "extract")
local urls = jsonValues(raw, "fullurl")
for i = 1, math.min(#titles, #extracts, pageLimit) do
results[#results + 1] = {
title = titles[i], text = extracts[i],
url = urls[i] or (source.site .. "/wiki/" .. urlencode(titles[i])),
site = source.site, provider = source.provider, publisher = source.publisher,
kind = "mediawiki",
}
end
end
end
end
addAll(SEARCH.wikidataSearch(query, intent, pageLimit))
addAll(SEARCH.duckDuckGoSearch(query, pageLimit))
addAll(SEARCH.duckDuckGoWebSearch(query, pageLimit))
addAll(SEARCH.openLibrarySearch(query, intent))
addAll(SEARCH.stackExchangeSearch(query, pageLimit))
addAll(SEARCH.githubSearch(query, pageLimit))
addAll(SEARCH.openAlexSearch(query, intent, pageLimit))
addAll(SEARCH.europePmcSearch(query, intent, pageLimit))
if #results == 0 then return nil end
local q = makeQuery(query)
local seen = {}
local unique = {}
for _, r in ipairs(results) do
local relevanceText = (r.title or "") .. " " .. (r.text or "")
if T.lower(relevanceText):find("minecraft", 1, true)
and T.lower(query):find("майнкрафт", 1, true) then
relevanceText = relevanceText .. " майнкрафт"
end
r.cover = coverageOf(relevanceText, q)
r.titleHit = (T.lower(r.title or "") == T.lower(query)) and 1 or 0
r.autoPriority = (r.kind == "mediawiki" and r.site == SEARCH.autotrainSite) and 1 or 0
local key = r.url or (tostring(r.site) .. ":" .. tostring(r.title))
if not seen[key] then seen[key] = true; unique[#unique + 1] = r end
end
table.sort(unique, function(a, b)
if a.cover ~= b.cover then return a.cover > b.cover end
if a.titleHit ~= b.titleHit then return a.titleHit > b.titleHit end
if a.autoPriority ~= b.autoPriority then return a.autoPriority > b.autoPriority end
return (a.provider or "") < (b.provider or "")
end)
return unique
end
-- Подтверждаем не только совпадение запроса: ищем тот же факт в результате
-- с другого домена. Для типизированного вопроса совпасть должен и год;
-- для общего — минимум два содержательных понятия карточек.
local function corroborateWiki(top, results, query, intent, answer, facts, typedSupport)
local sources = {top.url}
if not top or (top.cover or 0) < CFG.wikiTrust then return false, sources end
local primaryYears = {}
for year in (answer or ""):gmatch("%d%d%d%d") do primaryYears[year] = true end
local primaryTerms = {}
for _, card in ipairs(facts or {}) do
for _, word in ipairs(card.keywords or {}) do
local st = T.norm(word)
if not STOP[st] then primaryTerms[st] = true end
end
end
for _, other in ipairs(results) do
local samePublisher = top.publisher and other.publisher and top.publisher == other.publisher
if other ~= top and other.site and top.site and other.site ~= top.site
and not samePublisher and other.cover >= CFG.wikiTrust then
local otherAnswer, otherFacts, otherSupport = T.distillArticle(other.title, other.text, query)
local confirmed = false
if intent then
if typedSupport and otherSupport then
for year in (otherAnswer or ""):gmatch("%d%d%d%d") do
if primaryYears[year] then confirmed = true; break end
end
end
else
local hits, otherTerms = 0, {}
for _, card in ipairs(otherFacts or {}) do
for _, word in ipairs(card.keywords or {}) do
local st = T.norm(word)
if not STOP[st] then otherTerms[st] = true end
end
end
for st in pairs(primaryTerms) do if otherTerms[st] then hits = hits + 1 end end
confirmed = hits >= 2
end
if confirmed then
sources[#sources + 1] = other.url
return true, sources
end
end
end
return false, sources
end
-- сохранить найденное в Википедии
local function saveWiki(query, answer, title, verified, sources, autotrain, facts)
local lq = T.lower(query)
for i = #dbWiki, 1, -1 do
if T.lower(dbWiki[i].q) == lq then table.remove(dbWiki, i) end
end
dbWiki[#dbWiki + 1] = {q = query, a = answer, title = title,
ok = verified and true or false, dv = 2, sources = sources or {},
autotrain = autotrain == true, facts = facts or {}}
if #dbWiki > MAX_WIKI then table.remove(dbWiki, 1) end
T.touch()
end
-- полный путь: собрать внешние источники, извлечь факт и сохранить ответ
local function wikiAnswerFor(query, verbose)
if not hasInternet then return nil end
local res = SEARCH.wikiSearch(query, 3)
if not res or #res == 0 then return nil end
local top = res[1]
local intent = T.classifyIntent(query)
-- Сначала извлекаем компактное представление, затем проверяем факт на
-- независимом домене. Совпадение слов само по себе не считается проверкой.
local distilled, factCards, typedSupport = T.distillArticle(top.title, top.text, query)
local verified, sources = corroborateWiki(top, res, query, intent, distilled, factCards, typedSupport)
local learned = false
if top.kind == "mediawiki" and top.site == SEARCH.autotrainSite and T.learnArticle then
learned = T.learnArticle(top.title, top.text, top.url, verified)
end
if intent and not typedSupport then
buildIndex()
pcall(selfLearn)
pcall(T.saveAll)
return {rejected = true, title = top.title, cover = top.cover, learned = learned}
end
if not distilled or distilled == "" then
distilled = T.distillArticle(top.title, top.text, top.title)
end
saveWiki(query, distilled, top.title, verified, sources,
top.kind == "mediawiki" and top.site == SEARCH.autotrainSite, factCards)
buildIndex()
pcall(selfLearn)
pcall(T.saveAll)
local ei
for i = 1, #ans do
if srcOf[i] == "wiki" and entryQ[i] and T.lower(entryQ[i]) == T.lower(query) then
ei = i
end
end
return {saved = distilled, title = top.title, verified = verified,
cover = top.cover, sources = sources, url = top.url, ei = ei,
learned = learned, facts = factCards}
end
-- обучение по произвольной ссылке
local function learnFromInternet(url)
if not hasInternet then
return "Интернет-карты не вижу. Вставь её в компьютер, и команда /train заработает."
end
io.write(" скачиваю " .. url .. " ... ")
local text, sizeOrErr = httpGet(url, 131072)
print("")
if not text then
return "Не удалось скачать: " .. tostring(sizeOrErr)
end
local size = sizeOrErr
local pairsN, sentN, lines = 0, 0, 0
for line in text:gmatch("[^\r\n]+") do
line = line:gsub("^%s+", ""):gsub("%s+$", "")
lines = lines + 1
local q, a = line:match("^(.-)%s*=>%s*(.+)$")
if q and q ~= "" and a and a ~= "" and #q < 200 and #a < 400 then
if pairsN < MAX_NET then
local lq = T.lower(q)
for i = #dbLearned, 1, -1 do
if T.lower(dbLearned[i].q) == lq then table.remove(dbLearned, i) end
end
dbLearned[#dbLearned + 1] = {q = q, a = a}
pairsN = pairsN + 1
end
elseif #line > 20 and #line < 300 and not line:find("^[%-=#%*]") then
trainMarkov(line)
lexAdd(line)
sentN = sentN + 1
end
if lines % 25 == 0 then os.sleep(0) end
end
text = nil
gc()
if pairsN > 0 then buildAll() end
pcall(T.saveAll)
return format(
"Обучение завершено: скачано %d КБ.\nВыучено пар вопрос-ответ: %d (сохранено в learned.db). Предложений для генератора фраз: %d.%s",
floor(size / 1024 + 0.5), pairsN, sentN,
lite and "\nВнимание: мало памяти, генератор фраз ограничен." or "")
end
-- КОМАНДА /wiki: найти и запомнить
local function wikiLookupCommand(query)
if not hasInternet then
return "Интернет-карты не вижу. Вставь её в компьютер, и команда /wiki заработает."
end
io.write(" ищу и разбираю материал... ")
local res = SEARCH.wikiSearch(query, 4)
print("")
if not res or #res == 0 then return "Ничего не нашёл по этому запросу." end
local lines = {}
for i, r in ipairs(res) do
local mark = (r.cover >= CFG.wikiTrust) and "[совпадение сильное]" or "[нужно проверить]"
local domain = (r.site or ""):gsub("^https?://", "")
lines[#lines + 1] = format("%d. %s %s [%s] (%d%%)", i, mark, r.title, domain, floor(r.cover * 100))
end
local top = res[1]
local intent = T.classifyIntent(query)
local distilled, facts, typedSupport = T.distillArticle(top.title, top.text, query)
local verified, sources = corroborateWiki(top, res, query, intent, distilled, facts, typedSupport)
local wikiAutotrain = top.kind == "mediawiki" and top.site == SEARCH.autotrainSite
local learned = false
if wikiAutotrain and T.learnArticle then
learned = T.learnArticle(top.title, top.text, top.url, verified)
end
if intent and not typedSupport then
buildIndex()
pcall(selfLearn)
pcall(T.saveAll)
local storage = wikiAutotrain and "Материал русской Википедии добавлен в её корпус автообучения."
or "Внешний материал не добавлялся в корпус автообучения."
return format("Материал «%s» разобрал, но не нашёл в нём подтверждённого факта нужного типа. Поэтому не буду угадывать. %s\n%s",
top.title, storage, concat(lines, "\n"))
end
if not distilled or distilled == "" then distilled = T.distillArticle(top.title, top.text, top.title) end
saveWiki(query, distilled, top.title, verified, sources, wikiAutotrain, facts)
lastWikiQ = query
buildIndex()
pcall(selfLearn)
pcall(T.saveAll)
local status = verified and "факт подтверждён совпадением на независимом домене" or "пока есть только один источник; сохранено как непроверенное"
local sourceLine = "Источник: " .. (top.url or top.site or "неизвестен")
if verified and #sources > 1 then sourceLine = "Сверенные источники: " .. concat(sources, "; ") end
local storage = wikiAutotrain and "Статья русской Википедии добавлена в корпус автообучения и сохранена как выжимка."
or "Выжимка сохранена в wiki.db; внешний материал не добавлялся в корпус автообучения."
return format("Разобрал материал «%s» (%s).\nКороткая выжимка, не копия статьи: %s\n%s\n%s\n%s",
top.title, status, distilled, storage, sourceLine, concat(lines, "\n"))
end
---------------------------------------------------------------------------
-- ИНТЕРФЕЙС
---------------------------------------------------------------------------
local gpu = nil
local scrW, scrH = 80, 25
if component.isAvailable("gpu") then
gpu = component.gpu
local okR, w, h = pcall(gpu.getResolution)
if okR and w then scrW, scrH = w, h end
end
local C = {
border = 0x00B0C8, -- рамка
title = 0x55FFFF, -- заголовок
dim = 0x707070, -- служебный текст
user = 0xFFD75F, -- твои реплики
ai = 0x5FFF87, -- мои реплики
text = 0xDDDDDD, -- текст ответа
warn = 0xFF8A5F, -- предупреждения
}
local function color(c) if gpu then pcall(gpu.setForeground, c) end end
-- перенос по словам под ширину экрана
local function wrap(text, width)
if width < 8 then width = 8 end
local lines, cur, curLen = {}, {}, 0
for word in text:gmatch("%S+") do
local wl = T.ulen(word)
if curLen > 0 and curLen + 1 + wl > width then
lines[#lines + 1] = concat(cur, " ")
cur, curLen = {word}, wl
else
cur[#cur + 1] = word
curLen = curLen + (curLen > 0 and 1 or 0) + wl
end
end
if #cur > 0 then lines[#lines + 1] = concat(cur, " ") end
return lines
end
-- вывести реплику с цветным префиксом и выравниванием продолжений
local function say(prefix, pcol, text, tcol)
local indent = string.rep(" ", T.ulen(prefix))
local first = true
for seg in (text .. "\n"):gmatch("(.-)\n") do
if not seg:find("%S") then
if not first then print("") end
else
for _, ln in ipairs(wrap(seg, scrW - T.ulen(prefix) - 1)) do
color(pcol); io.write(first and prefix or indent)
color(tcol or C.text); print(ln)
first = false
end
end
end
color(0xFFFFFF)
end
-- служебная строка "хода размышления"
local function thinkNote(msg)
color(C.dim)
print(" · " .. msg)
color(0xFFFFFF)
end
---------------------------------------------------------------------------
-- АВТООБУЧЕНИЕ: обход русской Википедии
---------------------------------------------------------------------------
local function cutBytes(s, n)
if #s <= n then return s end
local t = s:sub(1, n)
while #t > 0 do
local b = t:byte(#t)
if b < 128 or b >= 192 then break end
t = t:sub(1, -2)
end
return t
end
-- выучить одну статью: корпус, база знаний, язык, лексикон
local function learnPage(title, text, url, verified)
if not title or not text or type(url) ~= "string"
or not url:match("^https?://ru%.wikipedia%.org/") then return false end
title = title:gsub("^%s+", ""):gsub("%s+$", "")
if title == "" or T.ulen(text) < 40 then return false end
local clean = text:gsub("==%s*([^=]+)%s*==", " %1. ")
clean = clean:gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "")
local summary, facts = T.distillArticle(title, clean, title)
if not summary or summary == "" then summary = "Материал о «" .. title .. "» сохранён для дальнейшего изучения." end
local excerpt = (computer.freeMemory() > 300000) and cutBytes(clean, 1200) or ""
local existing
for _, e in ipairs(dbCorpus) do
if T.lower(e.t) == T.lower(title) then existing = e; break end
end
if existing then
existing.s, existing.a, existing.u = excerpt, summary, url or existing.u or ""
existing.facts, existing.dv, existing.autotrain = facts or {}, 2, true
if verified then existing.ok = true elseif existing.ok == nil then existing.ok = false end
else
if #dbCorpus >= MAX_CORPUS then table.remove(dbCorpus, 1) end
dbCorpus[#dbCorpus + 1] = {t = title, s = excerpt, a = summary, u = url or "",
facts = facts or {}, dv = 2, ok = verified and true or false, autotrain = true}
end
trainMarkov(summary)
lexAdd(clean)
lexAdd(title)
lexAdd(summary)
return not existing
end
T.learnArticle = learnPage
-- взять ссылки со страниц, чтобы обход шёл вглубь
local function crawlLinks(titles)
if #titles == 0 then return 0 end
local url = "https://ru.wikipedia.org/w/api.php?action=query&format=json&prop=links&titles="
.. urlencode(concat(titles, "|")) .. "&plnamespace=0&pllimit=50"
local text = httpGet(url, 100000)
if not text then return 0 end
local ls = jsonValues(text, "title")
local added = 0
for _, t in ipairs(ls) do
if t ~= "" and not dbCrawl.seen[t] then
dbCrawl.seen[t] = true
dbCrawl.queue[#dbCrawl.queue + 1] = t
added = added + 1
end
end
return added
end
-- один раунд обучения: сперва очередь, потом случайные статьи
local function crawlRound(want, note)
if not hasInternet then return 0, "нет интернет-карты" end
local added, learned = 0, {}
-- 1. разбираем очередь
local batch = {}
for i = 1, math.min(#dbCrawl.queue, want) do batch[#batch + 1] = dbCrawl.queue[i] end
if #batch > 0 then
local url = "https://ru.wikipedia.org/w/api.php?action=query&format=json&prop=extracts|info"
.. "&explaintext=1&exchars=2500&inprop=url&redirects=1&titles=" .. urlencode(concat(batch, "|"))
local text = httpGet(url, 200000)
if text then
local tt = jsonValues(text, "title")
local ex = jsonValues(text, "extract")
local uu = jsonValues(text, "fullurl")
for i = 1, math.min(#tt, #ex) do
if learnPage(tt[i], ex[i], uu[i] or ("https://ru.wikipedia.org/wiki/" .. urlencode(tt[i]))) then
added = added + 1
learned[#learned + 1] = tt[i]
end
end
end
-- убираем обработанные из очереди
local left = {}
for i = #dbCrawl.queue, 1, -1 do
local t = dbCrawl.queue[i]
local found = false
for _, b in ipairs(batch) do if b == t then found = true end end
if not found then left[#left + 1] = t end
end
local tmp = {}
for i = #left, 1, -1 do tmp[#tmp + 1] = left[i] end
dbCrawl.queue = tmp
end
-- 2. добираем случайными статьями
if added < want then
local url = "https://ru.wikipedia.org/w/api.php?action=query&format=json&generator=random"
.. "&grnnamespace=0&grnlimit=" .. (want - added) .. "&prop=extracts|info&explaintext=1&exchars=2500&inprop=url&redirects=1"
local text = httpGet(url, 220000)
if text then
local tt = jsonValues(text, "title")
local ex = jsonValues(text, "extract")
local uu = jsonValues(text, "fullurl")
for i = 1, math.min(#tt, #ex) do
if learnPage(tt[i], ex[i], uu[i] or ("https://ru.wikipedia.org/wiki/" .. urlencode(tt[i]))) then
added = added + 1
learned[#learned + 1] = tt[i]
end
end
end
end
-- 3. собираем ссылки на будущее
if #learned > 0 then
local sub = {}
for i = 1, math.min(#learned, 5) do sub[#sub + 1] = learned[i] end
crawlLinks(sub)
end
if added > 0 then
buildIndex()
pcall(selfLearn)
pcall(T.saveAll)
end
return added, learned
end
---------------------------------------------------------------------------
-- КОМАНДЫ (только на английском)
---------------------------------------------------------------------------
local HELP = [[
Просто пиши мне на русском: спрашивай про Майнкрафт, OpenComputers, Lua, науку,
считай примеры (12*(3+4)). Я отвечаю по-русски, а команды - на английском.
Я не тороплюсь: переписываю вопрос, ищу кандидатов, спрашиваю своё ансамбль
нейросетей и проверяю, подходит ли ответ. Глубину размышления меняет /think.
Если я ничего не знаю, но есть интернет-карта, я ищу в широком наборе источников:
Wikimedia, поисковая выдача DDG, Stack Exchange, GitHub, OpenAlex, Europe PMC
и каталоги книг. Для отдельных источников использую открытые API только по теме.
Извлекаю выжимки и карточки фактов, сверяю разные издательские группы и показываю URL.
Фоновый обход и автообучаемый корпус ограничены русской Википедией; внешняя выжимка
может сохраниться в wiki.db для ответов, но не попадает в corpus.db или crawl.db.
Команды (после /):
/help список команд
/exit сохранить память и выйти (или просто напиши "пока")
/save сохранить всё прямо сейчас
/stats размеры баз, нейросети и памяти
/notes что я помню о тебе
/neuro состояние всех нейросетей
/net [light|core|wide|deep|all] переключить пресет нейросетей
/think 0..5 глубина размышления
/train <url> скачать текст по ссылке и выучить его
/wiki <запрос> поискать по профильным источникам, сверить и сохранить выжимку
/autotrain on|off|N автообучение: обход только русской Википедии
/right похвалить последний ответ (учит нейросети)
/wrong сказать, что ответ неверный, и дать правильный
/verify подтвердить ответ из Википедии
/lex <слово> что я знаю о слове
/forget <текст> забыть всё выученное с этим текстом
/reset стереть вообще всё, чему я научился
Научить меня вручную: teach <вопрос> => <ответ>
Сказать "меня зовут ..." или "запомни что ..." - я запомню.
]]
local VALID_CMD = {help = 1, exit = 1, quit = 1, save = 1, stats = 1, notes = 1,
wrong = 1, right = 1, forget = 1, reset = 1, train = 1, neuro = 1, wiki = 1,
autotrain = 1, think = 1, net = 1, verify = 1, lex = 1}
local function listNotes()
local out = {}
if dbNotes.name then out[#out + 1] = "Твоё имя: " .. dbNotes.name end
for i, n in ipairs(dbNotes.list) do out[#out + 1] = i .. ". " .. n end
if #out == 0 then return "Заметок пока нет. Напиши: запомни что <что-нибудь>" end
return concat(out, "\n")
end
local function ask(prompt)
io.write(prompt)
local l = io.read()
if l then l = l:gsub("^%s+", ""):gsub("%s+$", "") end
return l
end
local function doTeach(q, a)
q = q:gsub("^%s+", ""):gsub("%s+$", "")
a = a:gsub("^%s+", ""):gsub("%s+$", "")
if q == "" or a == "" then return "Формат: teach <вопрос> => <ответ>" end
local lq, replaced = T.lower(q), false
for i = #dbLearned, 1, -1 do
if T.lower(dbLearned[i].q) == lq then table.remove(dbLearned, i); replaced = true end
end
dbLearned[#dbLearned + 1] = {q = q, a = a}
lexAdd(q); lexAdd(a)
trainMarkov(a)
if replaced then
buildAll() -- старый ответ нужно убрать из индекса
else
T.addEntry(q, a, "user", false, nil)
T.finalize()
pcall(selfLearn)
end
pcall(T.saveAll)
return "Запомнил в learned.db! Спроси меня ещё раз, и я отвечу."
end
local function netsReport()
local out = {}
local totalNeurons, totalWeights, totalSteps = 0, 0, 0
for _, net in ipairs(NETS) do
local n, w = T.netNeurons(net), T.netWeights(net)
totalNeurons = totalNeurons + n
totalWeights = totalWeights + w
totalSteps = totalSteps + net.trained
local sizes = {}
for i, s in ipairs(net.sizes) do sizes[i] = tostring(s) end
out[#out + 1] = format(" %-6s %-28s нейронов: %5d, весов: %6d, обучений: %5d",
net.name, concat(sizes, "-"), n, w, net.trained)
end
out[#out + 1] = format(" ВСЕГО: нейронов %d, весов %d, шагов обучения %d",
totalNeurons, totalWeights, totalSteps)
return concat(out, "\n")
end
local function pickPreset()
if CFG.netset ~= "auto" then return CFG.netset end
local free = computer.freeMemory()
if free > 1800000 then return "deep" end
if free > 180000 then return "wide" end
if free > 60000 then return "core" end
return "light"
end
local function command(raw)
local cmd, arg = raw:match("^/(%a+)%s*(.*)$")
cmd = T.lower(cmd or "")
if not VALID_CMD[cmd] then
return "Неизвестная команда. Команды только на английском, напиши /help."
end
if cmd == "help" then
return HELP
elseif cmd == "exit" or cmd == "quit" then
return "Сохраняю память. До свидания!", true
elseif cmd == "save" then
return T.saveAll() and ("Память сохранена в папке " .. DB_DIR) or "Не удалось записать на диск."
elseif cmd == "stats" then
return format(
"Ответов во встроенной базе + выученных: %d\n вручную/по ссылке (learned.db): %d, из Википедии (wiki.db): %d, из обхода (corpus.db): %d\nПоисковых фраз в индексе: %d\nСлов в лексиконе: %d | запомненных фраз: %d | заметок: %d\nГлубина размышления: %d | пресет нейросетей: %s\n%s\nПапка базы знаний: %s\nИнтернет-карта: %s | Свободно ОЗУ: %d из %d КБ%s",
#ans, #dbLearned, #dbWiki, #dbCorpus, nDocs, lexTotal, #dbSentences, #dbNotes.list,
CFG.think, CUR_PRESET,
netsReport(),
DB_DIR,
hasInternet and "есть (/train, /wiki, /autotrain)" or "нет",
floor(computer.freeMemory() / 1024), floor(computer.totalMemory() / 1024),
lite and "\nОБЛЕГЧЁННЫЙ РЕЖИМ: мало памяти, генератор фраз ограничен." or "")
elseif cmd == "neuro" then
return format("Мой мозг - ансамбль из семи настоящих многослойных перцептронов.\n%s\nАктивации: tanh в скрытых слоях, сигмоида на выходе.\nКаждая сеть получает 20 признаков: сходство запроса и ответа, источник и проверенность,\nа также совпадение сущности, отношения и требуемого типа факта (например, год или количество).\nСтарые 16-входные веса переносятся при обновлении. /right и /wrong дообучают ансамбль.",
netsReport())
elseif cmd == "net" then
arg = T.lower(arg or "")
if arg == "" then
return format("Работает пресет: %s (в конфиге: %s).\nДоступные: light, core, wide, deep, all.\nwide - быстрый ансамбль; deep/all - 7 сетей, около 5700 нейронов и до 6 скрытых слоёв (медленнее, но проверяет глубже).\n%s",
CUR_PRESET, CFG.netset, netsReport())
end
if not NET_PRESETS[arg] then
return "Не знаю такого пресета. Доступные: light, core, wide, deep, all."
end
local saved = T.neuroLoad()
CFG.netset = arg
T.buildNets(arg, saved)
pcall(T.neuroSave)
return format("Переключил на пресет %s.\n%s", arg, netsReport())
elseif cmd == "think" then
local n = tonumber(arg or "")
if not n or n < 0 or n > 5 then
return "Формат: /think 0..5 (0 - быстро, 4 - глубокий разбор, 5 - максимальная проверка)"
end
CFG.think = floor(n)
pcall(dbWrite, DB_CFG, CFG)
return format("Глубина размышления: %d. %s", CFG.think,
CFG.think == 0 and "Отвечаю быстро, почти без размышлений." or
CFG.think == 1 and "Переписываю вопрос и ищу кандидатов." or
CFG.think == 2 and "Оцениваю варианты и проверяю ответ." or
CFG.think == 3 and "Сравниваю несколько кандидатов ансамблем." or
CFG.think == 4 and "Глубокий разбор: дополнительные запросы, проверка сущности и типа факта." or
"Максимальный разбор: больше кандидатов, глубокий ансамбль и повторная проверка ответа.")
elseif cmd == "train" then
if not hasInternet then
return "Интернет-карты не вижу. Вставь её в компьютер, и команда /train заработает."
end
arg = (arg or ""):gsub("^%s+", ""):gsub("%s+$", "")
if arg == "" then
arg = ask(" адрес (URL) текста для обучения: ") or ""
arg = arg:gsub("^%s+", ""):gsub("%s+$", "")
end
if arg == "" then return "Отменено." end
if not arg:find("^https?://") then arg = "http://" .. arg end
return learnFromInternet(arg)
elseif cmd == "wiki" then
arg = (arg or ""):gsub("^%s+", ""):gsub("%s+$", "")
if arg == "" then
arg = ask(" что искать в Википедии: ") or ""
arg = arg:gsub("^%s+", ""):gsub("%s+$", "")
end
if arg == "" then return "Отменено." end
return wikiLookupCommand(arg)
elseif cmd == "autotrain" then
arg = T.lower(arg or "")
if arg == "on" then
CFG.autotrain = "on"
pcall(dbWrite, DB_CFG, CFG)
return "Автообучение включено. Теперь я буду между ответами сам вычитывать статьи русской Википедии. Выключить: /autotrain off."
elseif arg == "off" then
CFG.autotrain = "off"
pcall(dbWrite, DB_CFG, CFG)
return "Автообучение выключено."
elseif arg == "status" or arg == "" then
return format("Автообучение: %s. Статей в corpus.db: %d, в очереди обхода: %d, слов в лексиконе: %d.",
CFG.autotrain, #dbCorpus, #dbCrawl.queue, lexTotal)
else
local n = tonumber(arg)
if not n or n < 1 or n > 50 then return "Формат: /autotrain on | off | status | <число статей 1..50>" end
if not hasInternet then return "Интернет-карты не вижу, обходить нечего." end
local added, learned = crawlRound(floor(n), true)
if added == 0 then return "Ничего нового не выучил (или нет интернета)." end
local names = {}
for i = 1, math.min(#learned, 10) do names[#names + 1] = learned[i] end
return format("Выучено %d статей: %s\nСтатей в corpus.db теперь: %d, слов в лексиконе: %d.",
added, concat(names, ", "), #dbCorpus, lexTotal)
end
elseif cmd == "right" then
if not lastFeat then return "Сначала задай вопрос, а потом оцени мой ответ." end
for _, net in ipairs(NETS) do T.netTrain(net, lastFeat, 1) end
T.touch()
return "Спасибо! Усилил нейронные связи, которые привели к этому ответу."
elseif cmd == "verify" then
if not lastWikiQ then return "Последний веб-ответ не ожидает подтверждения; сначала задай вопрос или используй /wiki." end
local found, title = false, nil
for _, e in ipairs(dbWiki) do
if T.lower(e.q) == T.lower(lastWikiQ) then
e.ok = true; found = true; title = e.title
end
end
if not found then return "Не нашёл тот ответ в wiki.db." end
for _, e in ipairs(dbCorpus) do
if title and T.lower(e.t) == T.lower(title) then e.ok = true end
end
buildIndex()
pcall(selfLearn)
pcall(T.saveAll)
return "Подтвердил и запомнил выжимку и карточки источника; теперь эта запись считается проверенной."
elseif cmd == "lex" then
arg = T.lower(arg or "")
if arg == "" then return "Формат: /lex <слово>. Показывает, что я знаю о слове." end
local w, e = lexAbout(arg)
if not e then return "Я вообще не встречал слово «" .. arg .. "»." end
local rel = {}
for k, c in pairs(e.c) do rel[#rel + 1] = {k, c} end
table.sort(rel, function(a, b) return a[2] > b[2] end)
local parts = {}
for i = 1, math.min(#rel, 6) do parts[#parts + 1] = rel[i][1] end
return format("Слово «%s»: встречалось %d раз.\nРядом с ним чаще всего: %s\nВсего слов в моём лексиконе: %d.",
w, e.n, #parts > 0 and concat(parts, ", ") or "пока ничего не замечено", lexTotal)
elseif cmd == "notes" then
return listNotes()
elseif cmd == "wrong" then
if not lastQ then return "Сначала задай мне вопрос." end
if lastFeat then
for _, net in ipairs(NETS) do T.netTrain(net, lastFeat, 0) end
T.touch()
end
local a = ask(" как надо было ответить? ")
if not a or a == "" then return "Хорошо, я ослабил неверные связи в нейросетях." end
return doTeach(lastQ, a)
elseif cmd == "forget" then
arg = T.lower(arg or "")
if arg == "" then return "Использование: /forget <текст>" end
local removed = 0
for i = #dbLearned, 1, -1 do
if T.lower(dbLearned[i].q):find(arg, 1, true) then table.remove(dbLearned, i); removed = removed + 1 end
end
for i = #dbWiki, 1, -1 do
if T.lower(dbWiki[i].q):find(arg, 1, true) then table.remove(dbWiki, i); removed = removed + 1 end
end
for i = #dbCorpus, 1, -1 do
if T.lower(dbCorpus[i].t):find(arg, 1, true) then table.remove(dbCorpus, i); removed = removed + 1 end
end
for i = #dbNotes.list, 1, -1 do
if T.lower(dbNotes.list[i]):find(arg, 1, true) then table.remove(dbNotes.list, i); removed = removed + 1 end
end
for i = #dbSentences, 1, -1 do
if T.lower(dbSentences[i]):find(arg, 1, true) then table.remove(dbSentences, i); removed = removed + 1 end
end
if removed > 0 then buildAll(); pcall(T.saveAll) end
return "Забыто записей: " .. removed .. "."
elseif cmd == "reset" then
local c = ask(" напиши YES, чтобы стереть всё, чему я научился: ")
if c and T.lower(c) == "yes" then
dbLearned, dbWiki, dbSentences, dbCorpus = {}, {}, {}, {}
dbNotes = {name = nil, list = {}}
dbCrawl = {queue = {}, seen = {}}
lex = {}
lexTotal = 0
for _, net in ipairs(NETS) do net.trained = 0 end
T.buildNets(pickPreset(), nil)
buildAll()
pcall(T.saveAll)
return "Память, лексикон и нейросети стёрты. Вернулся к заводским знаниям."
end
return "Отменено."
end
end
---------------------------------------------------------------------------
-- ОТВЕТ ИИ
---------------------------------------------------------------------------
local HEDGE_PRE = {"Кажется, ты об этом: ", "Возможно, это то, что нужно: ", "Не уверен, но: "}
local GEN_PRE = {"Точного ответа у меня нет, но вот что я могу предположить: ", "Размышляю вслух: ",
"Собираю по кусочкам из того, что знаю: "}
local RECALL_PRE = {"Ты говорил мне: ", "Я помню, ты как-то сказал: "}
local WIKI_PRE = {"В своей базе знаний этого не было, но я разобрал и независимо сверил статью: ",
"Сам не знал, но нашёл и сверил факт на другом домене; сохранил выжимку: ",
"Заглянул в Википедию, проверил, что статья про твой вопрос, и запомнил: "}
local UNKNOWN_Q = {
"Пока не знаю. Научи меня: teach <вопрос> => <ответ>",
"Это для меня новое! Можешь научить: teach <вопрос> => <ответ>",
"В моей памяти пусто. Переформулируй или научи: teach <вопрос> => <ответ>"}
local UNKNOWN_S = {"Интересно. Расскажи ещё.", "Понятно. Продолжай.", "Хм, я слушаю.", "Принял. Что ещё?"}
local function learnSentence(s)
if #s > 200 then return end
for _, t in ipairs(dbSentences) do if t == s then return end end
dbSentences[#dbSentences + 1] = s
if #dbSentences > MAX_LEARNED then table.remove(dbSentences, 1) end
T.touch()
end
-- вспомнить, что пользователь говорил раньше на похожую тему
local function recall(content)
if #content == 0 or #dbSentences == 0 then return nil, 0 end
local set = {}
for _, s in ipairs(content) do set[s] = true end
local best, bestN = nil, 0
for _, sent in ipairs(dbSentences) do
local seen, n = {}, 0
for _, t in ipairs(T.words(sent)) do
local s = T.norm(t)
if set[s] and not seen[s] then seen[s] = true; n = n + 1 end
end
if n > bestN then bestN, best = n, sent end
end
return best, bestN
end
local function tryMath(x)
local e = x
e = e:gsub("^%s*сколько%s+будет%s+", ""):gsub("^%s*сколько%s+", ""):gsub("^%s*посчитай%s+", "")
e = e:gsub("^%s*вычисли%s+", ""):gsub("^%s*реши%s+", ""):gsub("^%s*чему%s+равно%s+", "")
e = e:gsub("^%s*что%s+такое%s+", ""):gsub("^%s*calc%a*%s+", "")
e = e:gsub("%s*=%s*%??%s*$", ""):gsub("%s*%?+%s*$", "")
e = e:gsub("(%d),(%d)", "%1.%2")
e = e:gsub("%s+плюс%s+", "+"):gsub("%s+минус%s+", "-"):gsub("%s+умножить%s+на%s+", "*")
e = e:gsub("%s+умножь%s+на%s+", "*"):gsub("%s+помножить%s+на%s+", "*"):gsub("%s+разделить%s+на%s+", "/")
e = e:gsub("%s+поделить%s+на%s+", "/"):gsub("%s+делить%s+на%s+", "/"):gsub("%s+в%s+степени%s+", "^")
e = e:gsub("(%d)%s*x%s*(%d)", "%1*%2")
if e:match("^[%d%s%+%-%*/%%%^%(%)%.]+$") and e:find("%d") and e:find("[%+%-%*/%%%^]") then
local f = load("return " .. e, "=calc", "t", {})
if f then
local ok, r = pcall(f)
if ok and type(r) == "number" and r == r and r ~= huge and r ~= -huge then
return (format("%.10g", r):gsub("%.", ","))
end
end
return false
end
end
local DAYS = {"воскресенье", "понедельник", "вторник", "среда", "четверг", "пятница", "суббота"}
local MONTHS = {"января", "февраля", "марта", "апреля", "мая", "июня", "июля", "августа", "сентября",
"октября", "ноября", "декабря"}
local EXIT_LAST = {["пока"] = 1, ["прощай"] = 1, ["свидания"] = 1, ["досвидания"] = 1, ["bye"] = 1, ["goodbye"] = 1}
local EXIT_FIRST = {["выход"] = 1, ["выйти"] = 1, ["exit"] = 1, ["quit"] = 1}
local function fill(a)
a = a:gsub("{,name}", function() return dbNotes.name and (", " .. dbNotes.name) or "" end)
a = a:gsub("{name}", function() return dbNotes.name or "друг" end)
return a
end
-- текст после фразы (с сохранением регистра оригинала)
local function after(raw, lraw, pat)
local s, e = lraw:find(pat)
if s then return (raw:sub(e + 1):gsub("^%s+", "")) end
end
local function respond(raw)
raw = raw:gsub("^%s+", ""):gsub("%s+$", "")
if raw == "" then return nil end
local l = T.lower(raw):gsub("\209\145", "\208\181") -- нижний регистр, ё -> е
-- обучение ключевым словом "teach" (только на английском)
local tq, ta = raw:match("^/?%S+%s+(.-)%s*=>%s*(.+)$")
if tq and tq ~= "" and l:find("^/?teach%s") then
lastWikiQ = nil
return doTeach(tq, ta)
end
if raw:sub(1, 1) == "/" then
local commandName = T.lower(raw:match("^/%s*([^%s]+)") or "")
if commandName ~= "verify" then lastWikiQ = nil end
return command(raw)
end
lastWikiQ = nil
local tl = T.words(raw)
local j = " " .. concat(tl, " ") .. " "
local function has(phrase) return j:find(" " .. phrase .. " ", 1, true) ~= nil end
local last = tl[#tl]
if #tl > 0 and ((#tl <= 2 and EXIT_FIRST[tl[1]]) or (#tl <= 3 and EXIT_LAST[last])) then
return "Сохраняю память. До свидания" .. (dbNotes.name and (", " .. dbNotes.name) or "") .. "!", true
end
-- личная память
local nm = after(raw, l, "меня зовут%s+") or after(raw, l, "^зови меня%s+") or after(raw, l, "^называй меня%s+")
or after(raw, l, "мое имя%s+")
if nm then
if T.lower(nm):find("^это%s") then nm = nm:gsub("^%S+%s+", "") end
nm = nm:gsub("^[%-:]+%s*", "")
local tok = nm:match("^(%S+)")
if tok then
tok = tok:gsub("[%p]+$", "")
if tok ~= "" and #tok <= 40 then
dbNotes.name = tok; T.touch()
return "Приятно познакомиться, " .. tok .. "! Я запомню твоё имя."
end
end
end
if has("как меня зовут") or has("как меня звать") or has("кто я") or has("какое мое имя")
or has("знаешь мое имя") or has("помнишь мое имя") or has("помнишь как меня зовут") then
if dbNotes.name then return "Тебя зовут " .. dbNotes.name .. "." end
return "Ты ещё не говорил мне. Напиши: меня зовут <имя>"
end
local note = after(raw, l, "^запомни%s+что%s+") or after(raw, l, "^запомни:%s*") or after(raw, l, "^запиши:%s*")
or after(raw, l, "^заметка:%s*") or after(raw, l, "^запомни%s+") or after(raw, l, "^запиши%s+")
if note and #note > 2 then
dbNotes.list[#dbNotes.list + 1] = note; T.touch()
return "Записал в notes.db. Я запомню: " .. note
end
if has("что ты помнишь") or has("что ты обо мне знаешь") or has("что ты знаешь обо мне")
or has("что я тебе говорил") or has("мои заметки") or has("покажи заметки") then
return listNotes()
end
-- подсказка про самообучение из интернета обычной фразой
if has("начни обучение") or has("начать обучение") or has("учись из интернета")
or has("обучись из интернета") or has("обучение из интернета") or has("скачай знания")
or has("обойди википедию") or has("выучи википедию") then
if hasInternet then
return "Готов! Напиши /autotrain on - и я сам буду обходить русскую Википедию и учиться. Или /wiki <запрос> - найти ответ прямо сейчас."
end
return "Я бы рад, но интернет-карты в компьютере нет. Вставь её - и команды /autotrain, /wiki и /train заработают. Без карты я отвечаю только тем, что уже знаю."
end
-- маленькие игры
if has("подбрось монетку") or has("брось монетку") or has("орел или решка") or has("кинь монетку") then
return "Подбрасываю... " .. (random(2) == 1 and "Орёл!" or "Решка!")
end
if has("кинь кубик") or has("брось кубик") or has("кинь кость") or has("брось кость") then
return "Кубик показывает " .. random(6) .. "."
end
-- математика
local m = tryMath(l)
if m then return "= " .. m end
if m == false then return "Не смог это посчитать." end
-- часы и календарь
if has("который час") or has("сколько времени") or has("сколько сейчас времени") or has("сколько время")
or has("скажи время") then
local ok, t = pcall(os.date, "%H:%M")
if ok then return "Сейчас " .. t .. " по часам сервера." end
end
if has("какое сегодня число") or has("какая сегодня дата") or has("какой сегодня день") or has("какое число")
or has("какой день недели") or has("какая дата") or has("сегодняшняя дата") then
local ok, d = pcall(os.date, "*t")
if ok and d then
return "Сегодня " .. d.day .. " " .. MONTHS[d.month] .. " " .. d.year .. " года, " .. DAYS[d.wday] .. " (по часам сервера)."
end
end
-- УЧИМСЯ НА КАЖДОМ СЛОВЕ: всё, что ты пишешь, попадает в лексикон и в языковую модель
lexAdd(raw)
if #tl >= 4 then trainMarkov(raw) end
if #tl >= 4 then learnSentence(raw) end
-- РАЗМЫШЛЕНИЕ: переформулируем вопрос, ищем кандидатов, спрашиваем нейросети
local depth = CFG.think
local t0 = os.clock()
if depth >= 1 then
thinkNote(format("думаю, глубина %d ...", depth))
os.sleep(0.12 * depth)
end
local noteFn = nil
if depth >= 1 then noteFn = thinkNote end
local best, conf, feat, order = deliberate(raw, depth, noteFn)
-- Неподтверждённые записи Википедии остаются в памяти как гипотезы,
-- но не становятся обычными ответами до проверки (/verify).
if best and srcOf[best.ei] == "wiki" and not verOf[best.ei] then
best, conf, feat = nil, 0, nil
elseif best and srcOf[best.ei] == "corpus" and not verOf[best.ei]
and best.q and best.q.intent then
best, conf, feat = nil, 0, nil
end
local q = makeQuery(raw)
if #q.content > 0 then
ctxStems = {}
for i = 1, math.min(#q.content, 4) do ctxStems[i] = q.content[i] end
end
lastQ = raw
-- составной вопрос: "привет, как дела?"
if not (best and conf >= WHOLE) then
local parts, modifier = {}, false
local joined = (l .. " "):gsub("[%.!%?;,]+%s+", "\n")
joined = joined:gsub("%s+и%s+", "\n")
for c in joined:gmatch("[^\n]+") do
local cc = c:gsub("^%s+", ""):gsub("%s+$", "")
if cc ~= "" then
parts[#parts + 1] = cc
local w1 = T.words(cc)[1]
if w1 and MODIFIER[w1] then modifier = true end
end
end
if #parts >= 2 and #parts <= 3 and not modifier then
local outs, seenE = {}, {}
for _, part in ipairs(parts) do
local e2, s2 = deliberate(part, math.max(1, depth - 1), nil)
if not (e2 and s2 >= SPLIT) then outs = nil; break end
if not seenE[e2.ei] then
seenE[e2.ei] = true
local a = ans[e2.ei]
if type(a) == "table" then a = a[random(#a)] end
outs[#outs + 1] = fill(a)
end
end
if outs and #outs >= 2 then
lastFeat = nil
return concat(outs, " ")
end
end
end
if best and conf >= HEDGE then
lastFeat = best.feat
local ei = best.ei
local a = ans[ei]
if type(a) == "table" then a = a[random(#a)] end
a = fill(a)
if srcOf[ei] == "wiki" and not verOf[ei] then
a = "Это я нашёл в веб-источнике, но пока не проверил: " .. a
elseif srcOf[ei] == "corpus" and not verOf[ei] then
a = "Это предварительная, не подтверждённая независимым источником выжимка из корпуса: " .. a
end
if srcOf[ei] == "wiki" then
local entryText = T.lower(entryQ[ei] or "")
for _, item in ipairs(dbWiki) do
local itemTitle = T.lower(item.title or "")
if entryText == T.lower(item.q or "")
or (itemTitle ~= "" and entryText:find(itemTitle, 1, true)) then
if item.sources and #item.sources > 0 then
a = a .. "\nИсточник(и): " .. concat(item.sources, "; ")
end
break
end
end
end
if conf < DIRECT then a = T.pick(HEDGE_PRE) .. a end
if depth >= 2 then thinkNote(format("думал %.1f с, уверенность %d%%", os.clock() - t0, floor(conf * 100))) end
return a
end
lastFeat = nil
lastWikiQ = nil
-- вспоминания
local selfRef = false
for _, w in ipairs(tl) do if FIRST_PERSON[w] then selfRef = true end end
local isQ = q.isQ or (#tl <= 5 and not selfRef)
local rs, rn = recall(q.content)
if rs and concat(T.words(rs), " ") == concat(tl, " ") then rs = nil end
if not q.intent and rs and (rn >= 2 or (isQ and rn >= 1)) then
return T.pick(RECALL_PRE) .. "«" .. rs .. "»"
end
-- ПОСЛЕДНИЙ РУБЕЖ: поиск по профильным онлайн-источникам с проверкой.
-- Не выдаю extract или чужую страницу целиком: сначала проверю релевантность,
-- выжимку и независимое подтверждение; при сомнении честно воздержусь.
if isQ and hasInternet and #dbWiki < MAX_WIKI then
local w = wikiAnswerFor(raw)
if w and w.rejected then
if depth >= 2 then thinkNote("в найденном материале нет факта с нужными сущностью, событием и годом") end
w = nil
elseif w and q.intent and w.ei and entryDoc[w.ei] then
local wm = T.intentMetrics(q.intent, entryDoc[w.ei], w.ei)
if wm.reject then
if depth >= 2 then thinkNote("источник не подтвердил нужные сущность, событие и год") end
w = nil
end
end
if w and w.cover >= 0.16 then
lastWikiQ = raw
local sourceLine = ""
if w.sources and #w.sources > 1 then
sourceLine = "\nИсточники: " .. concat(w.sources, "; ")
elseif w.url then
sourceLine = "\nИсточник: " .. w.url
end
if w.verified then
return T.pick(WIKI_PRE) .. "(" .. (w.title or "") .. ") " .. w.saved .. sourceLine
end
return "Я разобрал материал «" .. (w.title or "без названия")
.. "», но не смог достаточно проверить его связь с вопросом (совпадение "
.. floor(w.cover * 100) .. "%). Не буду выдавать непроверенную выжимку за ответ."
.. "\nЕсли материал действительно подходит, подтверди его командой /verify."
.. sourceLine
end
end
-- Для типизированного временного факта лучше признать незнание, чем выдать
-- случайную фразу, совпавшую только по слову «год».
if q.intent then
if depth >= 2 then thinkNote("не нашёл кандидата с совпавшими сущностью, событием и годом") end
return "Не нашёл надёжный ответ о годе именно этого события. Не буду угадывать только по слову «год»; уточни название или какой релиз имеешь в виду."
end
-- генератор фраз
local txt, hits = generate(q.content, 12)
if txt and concat(T.words(txt), " ") == concat(tl, " ") then txt = nil end
if txt and (hits >= 2 or (isQ and hits >= 1)) then return T.pick(GEN_PRE) .. txt end
if depth >= 2 then thinkNote(format("думал %.1f с, ответа в базе нет", os.clock() - t0)) end
return isQ and T.pick(UNKNOWN_Q) or T.pick(UNKNOWN_S)
end
---------------------------------------------------------------------------
-- ЗАПУСК
---------------------------------------------------------------------------
local function loadMemory()
local t
t = T.dbRead(DB_LEARNED)
if t then
for _, e in ipairs(t) do
if type(e) == "table" and type(e.q) == "string" and type(e.a) == "string" then
dbLearned[#dbLearned + 1] = e
end
end
end
t = T.dbRead(DB_WIKI)
if t then
for _, e in ipairs(t) do
if type(e) == "table" and type(e.q) == "string" and type(e.a) == "string" then
dbWiki[#dbWiki + 1] = {q = e.q, a = e.a, title = e.title, ok = e.ok and true or false,
dv = e.dv, facts = e.facts, sources = e.sources,
autotrain = e.autotrain == true or (e.autotrain == nil
and type(e.sources) == "table" and type(e.sources[1]) == "string"
and e.sources[1]:match("^https?://ru%.wikipedia%.org/") ~= nil)}
end
end
end
t = T.dbRead(DB_NOTES)
if t then
if type(t.name) == "string" then dbNotes.name = t.name end
if type(t.list) == "table" then
for _, s in ipairs(t.list) do if type(s) == "string" then dbNotes.list[#dbNotes.list + 1] = s end end
end
end
t = T.dbRead(DB_SENTENCES)
if t then
for _, s in ipairs(t) do if type(s) == "string" then dbSentences[#dbSentences + 1] = s end end
end
t = T.dbRead(DB_CORPUS)
if t then
for _, e in ipairs(t) do
if type(e) == "table" and type(e.t) == "string" then
dbCorpus[#dbCorpus + 1] = {t = e.t, s = e.s, a = e.a, u = e.u,
facts = type(e.facts) == "table" and e.facts or {}, dv = e.dv, ok = e.ok == true,
autotrain = e.autotrain == true or (e.autotrain == nil and type(e.u) == "string"
and e.u:match("^https?://ru%.wikipedia%.org/") ~= nil)}
end
end
end
t = T.dbRead(DB_CRAWL)
if t then
if type(t.queue) == "table" then
for _, s in ipairs(t.queue) do if type(s) == "string" then dbCrawl.queue[#dbCrawl.queue + 1] = s end end
end
if type(t.seen) == "table" then
for k, v in pairs(t.seen) do if v then dbCrawl.seen[k] = true end end
end
end
t = T.dbRead(DB_LEXICON)
if t then
lex = t
lexTotal = 0
for _, e in pairs(lex) do if type(e) == "table" and type(e.n) == "number" then lexTotal = lexTotal + e.n end end
end
t = T.dbRead(DB_CFG)
if t then
local oldReasonVersion = t.reasonVersion or 0
for k, v in pairs(t) do
if CFG[k] ~= nil and type(v) == type(CFG[k]) then CFG[k] = v end
end
if oldReasonVersion < 4 then
CFG.think = 4
CFG.reasonVersion = 4
pcall(T.dbWrite, DB_CFG, CFG)
end
end
-- Миграция старых записей: раньше wiki.db/corpus.db могли хранить extract
-- в поле ответа. Переписываем его в короткую выжимку и сохраняем карточки.
local wikiChanged, corpusChanged = false, false
for _, e in ipairs(dbWiki) do
if e.dv ~= 2 then
local summary, facts = T.distillArticle(e.title or "Материал", e.a or "", e.q or "")
if not summary or summary == "" then
summary, facts = T.distillArticle(e.title or "Материал", e.a or "", e.title or "Материал")
end
e.a = summary or "Материал сохранён, но короткую выжимку извлечь не удалось."
e.facts, e.dv = facts or {}, 2
-- Старое ok означало лишь лексическое совпадение, не проверку фактов.
e.ok = false
wikiChanged = true
end
end
for _, e in ipairs(dbCorpus) do
if e.dv ~= 2 then
local source = (type(e.s) == "string" and e.s ~= "") and e.s or (e.a or "")
local summary, facts = T.distillArticle(e.t or "Материал", source, e.t or "Материал")
if summary and summary ~= "" then e.a = summary end
e.facts, e.dv, e.ok = facts or {}, 2, false
corpusChanged = true
elseif e.ok ~= true then
e.ok = false
end
end
if wikiChanged then pcall(T.dbWrite, DB_WIKI, dbWiki) end
if corpusChanged then pcall(T.dbWrite, DB_CORPUS, dbCorpus) end
end
local function boxLine(text, tcol)
local pad = scrW - 3 - T.ulen(text)
if pad < 0 then pad = 0 end
color(C.border); io.write("\226\149\145 ")
color(tcol); io.write(text .. string.rep(" ", pad))
color(C.border); print("\226\149\145")
end
local function header()
term.clear()
color(C.border)
print("\226\149\148" .. string.rep("\226\149\144", scrW - 2) .. "\226\149\151")
boxLine("НаноИИ 3.1 \194\183 локальный ИИ с ансамблем нейросетей", C.title)
local neurons, weights = 0, 0
for _, net in ipairs(NETS) do
neurons = neurons + T.netNeurons(net)
weights = weights + T.netWeights(net)
end
boxLine(format("нейронов: %d, весов: %d, сетей: %d \194\183 думаю на глубине %d",
neurons, weights, #NETS, CFG.think), C.dim)
boxLine(format("ответов: %d (учено %d / wiki %d / обход %d) \194\183 слов: %d \194\183 %s",
#ans, #dbLearned, #dbWiki, #dbCorpus, lexTotal,
hasInternet and "интернет: есть" or "офлайн"), C.dim)
boxLine("база знаний: " .. DB_DIR .. format(" \194\183 ОЗУ: %dК", floor(computer.freeMemory() / 1024)), C.dim)
print("\226\149\154" .. string.rep("\226\149\144", scrW - 2) .. "\226\149\157")
color(C.dim)
print(" /help - команды \194\183 /think 0..5 - глубина размышений \194\183 /net - нейросети")
print(" /autotrain on - учиться из Википедии \194\183 /right и /wrong - учить нейросети")
print("")
color(0xFFFFFF)
end
local function main()
term.clear()
color(C.title); print("НаноИИ 3.1 DeepWiki"); color(0xFFFFFF)
loadMemory()
io.write("Загружаю базы знаний и нейросети... ")
local saved = T.neuroLoad()
-- подбираем пресет, который влезет в память
local want = pickPreset()
local tries = {want}
if want == "deep" then tries = {"deep", "wide", "core", "light"}
elseif want == "wide" then tries = {"wide", "core", "light"}
elseif want == "core" then tries = {"core", "light"} end
local used
for _, p in ipairs(tries) do
local ok = pcall(function() T.buildNets(p, saved) end)
if ok then used = p break end
gc()
end
if not used then used = "light" pcall(function() T.buildNets("light", saved) end) end
CUR_PRESET = used
trainLanguage()
buildAll()
print("готово.")
header()
if used ~= want then
say(" ! ", C.warn, format("Памяти не хватило на пресет %s, включил %s. Освободи память и попробуй /net %s.", want, used, want), C.warn)
end
if lite then
say(" ! ", C.warn, "ОБЛЕГЧЁННЫЙ РЕЖИМ: мало памяти, генератор фраз ограничен.", C.warn)
end
say(" ии \194\187 ", C.ai, fill(T.pick({
"Привет{,name}! Спрашивай что угодно.",
"С возвращением{,name}. О чём поговорим?",
"Нейроны прогреты{,name}. Задавай вопрос!"})))
while true do
color(C.user); io.write(" ты \194\187 "); color(0xFFFFFF)
local line = io.read()
if not line then break end
local ok, reply, quit = pcall(respond, line)
if not ok then reply, quit = "Ой, в моей голове что-то сломалось: " .. tostring(reply), false end
if reply then say(" ии \194\187 ", C.ai, reply) end
if quit then break end
-- фоновый автообход Википедии
if CFG.autotrain == "on" and hasInternet and dbCrawl then
local added, learned = crawlRound(1, false)
if added > 0 and learned and learned[1] then
thinkNote("фоном выучил из Википедии: " .. learned[1])
end
end
end
pcall(T.saveAll)
color(0xFFFFFF)
end
main()