Напиши одной командой установку всех библиотек // Импорт необходимых библиотек
const axios = require('axios');
const cheerio = require('cheerio');
const fs = require('fs').promises; // Используем промисы для асинхронных операций с файлами
const natural = require('natural');
const tf = require('@tensorflow/tfjs-node-gpu'); // Использование GPU версии TensorFlow
const path = require('path');

// Константы
const WIKIPEDIA_RANDOM_URL = 'https://ru.wikipedia.org/wiki/Special:Random';
const DATA_FILE = path.join(__dirname, 'bd.json');
const MODEL_DIR = 'file://./model';
const MAX_VOCAB_SIZE = 9999;
const MAX_SEQUENCE_LENGTH = 500;
const CONCURRENCY = 5; // Максимальное количество одновременных запросов

// Функция для получения URL случайной статьи Википедии
async function fetchRandomWikiTitle() {
    try {
        // Пытаемся получить URL через редирект
        const response = await axios.get(WIKIPEDIA_RANDOM_URL, { maxRedirects: 0, validateStatus: (status) => status < 400 });
        return response.headers.location || response.request.res.responseUrl;
    } catch (error) {
        if (error.response && error.response.status === 302) {
            // Обработка редиректа
            return error.response.headers.location;
        }
        console.error("Ошибка при запросе случайной статьи:", error.message);
        return null;
    }
}

// Функция для получения заголовка и текста статьи по её URL
async function fetchWikiArticle(url) {
    if (!url) return null;
    try {
        const response = await axios.get(url);
        const $ = cheerio.load(response.data);

        const title = $('#firstHeading').text().trim();
        const paragraphs = [];
        $('p').each((i, element) => {
            const text = $(element).text().trim();
            if (text.length > 50) paragraphs.push(text);
        });

        const article = paragraphs.join(' ');
        return { url, title, article };
    } catch (error) {
        console.error(`Ошибка при запросе статьи (${url}):`, error.message);
        return null;
    }
}

// Параллельная загрузка статей с ограничением количества одновременных запросов
async function getWikiArticles(count, concurrency = CONCURRENCY) {
    const articles = [];
    console.log(`Загружается ${count} статей с Википедии...`);

    // Получаем все URL случайных статей
    const urlPromises = Array.from({ length: count }, () => fetchRandomWikiTitle());
    const urls = await Promise.all(urlPromises);

    // Функция для обработки очереди URL с ограничением параллелизма
    const processBatch = async (batch) => {
        const fetchPromises = batch.map(url => fetchWikiArticle(url));
        const results = await Promise.all(fetchPromises);
        results.forEach(articleData => {
            if (articleData) {
                articles.push(articleData);
                console.log(`Загружена статья: ${articleData.url}`);
            }
        });
    };

    // Разбиваем URL на батчи по размеру concurrency
    for (let i = 0; i < urls.length; i += concurrency) {
        const batch = urls.slice(i, i + concurrency);
        await processBatch(batch);
    }

    // Сохраняем статьи в файл
    try {
        await fs.writeFile(DATA_FILE, JSON.stringify(articles, null, 2), 'utf8');
        console.log(`Загрузка завершена. Статьи сохранены в "${DATA_FILE}".`);
    } catch (error) {
        console.error("Ошибка при записи в 'bd.json':", error.message);
    }
}

// Загрузка статей из bd.json
async function loadArticlesFromJson() {
    try {
        const data = await fs.readFile(DATA_FILE, 'utf8');
        return JSON.parse(data);
    } catch (error) {
        console.error("Ошибка при чтении 'bd.json':", error.message);
        return [];
    }
}

// Токенизация и обработка текста для модели
function preprocessData(articles, maxVocabSize = MAX_VOCAB_SIZE, maxSequenceLength = MAX_SEQUENCE_LENGTH) {
    const tokenizer = new natural.WordTokenizer();
    const allTokens = articles.map(articleObj => tokenizer.tokenize(articleObj.article.toLowerCase()));

    // Построение карты частот
    const freqMap = {};
    allTokens.flat().forEach(token => {
        freqMap[token] = (freqMap[token] || 0) + 1;
    });

    // Сортировка токенов по частоте и выбор топ N
    const sortedTokens = Object.keys(freqMap).sort((a, b) => freqMap[b] - freqMap[a]);
    const limitedVocab = sortedTokens.slice(0, maxVocabSize);

    // Создание словаря: слово => индекс
    const wordIndex = {};
    limitedVocab.forEach((word, index) => {
        wordIndex[word] = index + 1; // Индекс 0 зарезервирован для "неизвестных" токенов
    });

    // Преобразование токенов в индексы
    const data = allTokens.map(tokens => ({
        tokenIndexes: tokens.map(token => wordIndex[token] || 0),
        label: Math.random() > 0.5 ? 1 : 0 // Случайные метки (замените на реальные при необходимости)
    }));

    // Ограничение длины последовательностей и паддинг
    const inputs = data.map(d => {
        let padded = d.tokenIndexes;
        if (padded.length > maxSequenceLength) {
            padded = padded.slice(0, maxSequenceLength);
        } else {
            while (padded.length < maxSequenceLength) {
                padded.push(0);
            }
        }
        return padded;
    });

    const labels = data.map(d => d.label);

    // Поиск максимального индекса токена без использования spread оператора
    let maxTokenIndex = 0;
    for (let i = 0; i < inputs.length; i++) {
        for (let j = 0; j < inputs[i].length; j++) {
            if (inputs[i][j] > maxTokenIndex) {
                maxTokenIndex = inputs[i][j];
            }
        }
    }
    console.log(`Максимальный индекс токена: ${maxTokenIndex}`);
    if (maxTokenIndex >= maxVocabSize + 1) {
        console.warn(`Внимание: Максимальный индекс токена (${maxTokenIndex}) превышает inputDim (${maxVocabSize + 1})`);
    }

    return { inputs, labels, wordIndex };
}

// Создание модели TensorFlow
function createModel(inputShape, vocabSize = MAX_VOCAB_SIZE + 1) { // +1 для неизвестных токенов
    const model = tf.sequential();
    model.add(tf.layers.embedding({ 
        inputDim: vocabSize, 
        outputDim: 128, 
        inputLength: inputShape 
    }));
    model.add(tf.layers.flatten());
    model.add(tf.layers.dense({ units: 256, activation: 'relu' }));
    model.add(tf.layers.dropout({ rate: 0.3 }));
    model.add(tf.layers.dense({ units: 128, activation: 'relu' }));
    model.add(tf.layers.dropout({ rate: 0.3 }));
    model.add(tf.layers.dense({ units: 1, activation: 'sigmoid' }));
    model.compile({ optimizer: 'adam', loss: 'binaryCrossentropy', metrics: ['accuracy'] });
    return model;
}

// Обучение модели
async function trainModel(model, inputs, labels) {
    const xs = tf.tensor2d(inputs);
    const ys = tf.tensor2d(labels, [labels.length, 1]);

    console.log("Обучение модели на GPU...");
    try {
        await model.fit(xs, ys, {
            epochs: 20, // Количество эпох
            batchSize: 64,
            validationSplit: 0.2,
            callbacks: {
                onEpochEnd: (epoch, logs) => {
                    console.log(`Эпоха ${epoch + 1}: loss=${logs.loss.toFixed(4)}, accuracy=${(logs.acc * 100).toFixed(2)}%, val_loss=${logs.val_loss.toFixed(4)}, val_accuracy=${(logs.val_acc * 100).toFixed(2)}%`);
                }
            }
        });
        console.log("Модель успешно обучена.");
    } catch (error) {
        console.error("Ошибка при обучении модели:", error.message);
    } finally {
        xs.dispose();
        ys.dispose();
    }
}

// Функция для извлечения релевантного контекста из статьи
function extractContext(article, questionTokens) {
    const sentenceTokenizer = new natural.SentenceTokenizer();
    const wordTokenizer = new natural.WordTokenizer();
    const sentences = sentenceTokenizer.tokenize(article);
    const relevantSentences = [];

    sentences.forEach(sentence => {
        const sentenceTokens = wordTokenizer.tokenize(sentence.toLowerCase());
        const matchingTokens = questionTokens.filter(token => sentenceTokens.includes(token));
        if (matchingTokens.length > 0) {
            relevantSentences.push(sentence);
        }
    });

    return relevantSentences;
}

// Функция для генерации объединенного ответа из релевантных статей
function generateUnifiedAnswer(question, articles) {
    const wordTokenizer = new natural.WordTokenizer();
    const questionTokens = wordTokenizer.tokenize(question.toLowerCase());

    let combinedContext = '';

    articles.forEach(article => {
        const context = extractContext(article.article, questionTokens);
        if (context.length > 0) {
            combinedContext += context.join(' ') + ' ';
        }
    });

    if (combinedContext.trim().length === 0) {
        return "Не найдено релевантной информации.";
    }

    return combinedContext.trim();
}

// Функция для поиска релевантных статей
function findRelevantArticles(question, articles) {
    const wordTokenizer = new natural.WordTokenizer();
    const questionTokens = wordTokenizer.tokenize(question.toLowerCase());

    return articles.map(article => {
        const articleTokens = wordTokenizer.tokenize(article.article.toLowerCase());
        const intersection = questionTokens.filter(token => articleTokens.includes(token));
        const relevance = intersection.length / questionTokens.length;
        return { ...article, relevance };
    })
    .filter(article => article.relevance > 0)
    .sort((a, b) => b.relevance - a.relevance)
    .slice(0, 5); // Ограничиваем до 5 наиболее релевантных
}

// Главная функция
async function main() {
    const [,, command, ...args] = process.argv;

    switch (command) {
        case '/getwiki':
            {
                const count = parseInt(args[0], 10);
                if (!isNaN(count) && count > 0) {
                    await getWikiArticles(count);
                } else {
                    console.log("Укажите допустимое количество статей: /getwiki <количество>");
                }
                break;
            }
        case '/getNR':
            {
                const articles = await loadArticlesFromJson();
                if (articles.length === 0) {
                    return console.log("Нет статей для обучения. Используйте /getwiki.");
                }

                const { inputs, labels, wordIndex } = preprocessData(articles);
                console.log(`Количество примеров: ${inputs.length}, Длина последовательности: ${inputs[0].length}`);

                const model = createModel(inputs[0].length);
                await trainModel(model, inputs, labels);
                try {
                    await model.save(MODEL_DIR);
                    console.log(`Модель сохранена в '${MODEL_DIR}'.`);
                } catch (error) {
                    console.error("Ошибка при сохранении модели:", error.message);
                }
                break;
            }
        case '/ask':
            {
                const question = args.join(' ');
                if (!question) {
                    return console.log("Введите вопрос: /ask <вопрос>");
                }

                const articles = await loadArticlesFromJson();
                if (articles.length === 0) {
                    return console.log("Нет статей для поиска. Используйте /getwiki.");
                }

                const relevantArticles = findRelevantArticles(question, articles);
                if (relevantArticles.length === 0) {
                    console.log("Не найдено подходящих статей.");
                } else {
                    const unifiedAnswer = generateUnifiedAnswer(question, relevantArticles);
                    console.log("Ответ:");
                    console.log(unifiedAnswer);
                }
                break;
            }
        default:
            console.log("Доступные команды:\n  /getwiki <количество> - Загрузка статей\n  /getNR - Обучение модели\n  /ask <вопрос> - Задать вопрос");
    }
}

main();