✨ Major Features: - Complete RAG system for hotel website analysis - Hybrid audit with BGE-M3 embeddings + Natasha NER - Universal horizontal Excel reports with dashboards - Multi-region processing (SPb, Orel, Chukotka, Kamchatka) 📊 Completed Regions: - Орловская область: 100% (36/36) - Чукотский АО: 100% (4/4) - г. Санкт-Петербург: 93% (893/960) - Камчатский край: 87% (89/102) 🔧 Infrastructure: - PostgreSQL with pgvector extension - BGE-M3 embeddings API - Browserless for web scraping - N8N workflows for automation - S3/Nextcloud file storage 📝 Documentation: - Complete DB schemas - API documentation - Setup guides - Status reports
230 lines
10 KiB
JavaScript
230 lines
10 KiB
JavaScript
// ============================================================
|
||
// N8N CODE NODE: Объединение результатов AI Agent и Regex
|
||
// ============================================================
|
||
//
|
||
// INPUT: Массив из 34 элементов
|
||
// - Первые 17: результаты от AI Agent
|
||
// - Последние 17: результаты от Regex
|
||
//
|
||
// OUTPUT: Объединённые результаты с итоговой оценкой
|
||
// ============================================================
|
||
|
||
// Определяем 17 критериев
|
||
const CRITERIA = [
|
||
{ id: 1, name: "Юридическая идентификация и верификация", description: "ИНН, ОГРН, полное наименование организации" },
|
||
{ id: 2, name: "Адрес", description: "Юридический и фактический адрес, местонахождение" },
|
||
{ id: 3, name: "Контакты", description: "Телефон, email, форма обратной связи" },
|
||
{ id: 4, name: "Режим работы", description: "Часы работы, график приема, колл-центр" },
|
||
{ id: 5, name: "Политика ПДн (152-ФЗ)", description: "Политика персональных данных, обработка ПДн" },
|
||
{ id: 7, name: "Договор-оферта / Правила оказания услуг", description: "Публичная оферта, пользовательское соглашение" },
|
||
{ id: 8, name: "Рекламации и споры", description: "Претензии, возврат, обмен, жалобы" },
|
||
{ id: 9, name: "Цены/прайс", description: "Цены, стоимость, тарифы" },
|
||
{ id: 10, name: "Способы оплаты", description: "Наличные, карта, СБП" },
|
||
{ id: 11, name: "Онлайн-оплата", description: "Эквайринг, оплата онлайн" },
|
||
{ id: 12, name: "Онлайн-бронирование", description: "Забронировать, booking" },
|
||
{ id: 13, name: "FAQ", description: "Частые вопросы, вопрос-ответ" },
|
||
{ id: 14, name: "Доступность для ЛОВЗ", description: "Инвалиды, безбарьерная среда" },
|
||
{ id: 15, name: "Партнёры/бренды", description: "Партнеры, поставщики, сотрудничество" },
|
||
{ id: 16, name: "Команда/сотрудники", description: "Команда, персонал, руководство" },
|
||
{ id: 17, name: "Уголок потребителя", description: "Права потребителей, защита" },
|
||
{ id: 18, name: "Актуальность документов", description: "Дата обновления, версия" }
|
||
];
|
||
|
||
/**
|
||
* Рассчитывает итоговую уверенность
|
||
*/
|
||
function calculateFinalConfidence(aiConf, regexConf, aiFound, regexFound) {
|
||
// Если оба нашли - очень высокая
|
||
if (aiFound && regexFound) {
|
||
return "Очень высокая";
|
||
}
|
||
|
||
// Если один нашёл с высокой уверенностью
|
||
if ((aiFound && aiConf === "Высокая") || (regexFound && regexConf === "Высокая")) {
|
||
return "Высокая";
|
||
}
|
||
|
||
// Если один нашёл со средней уверенностью
|
||
if ((aiFound && aiConf === "Средняя") || (regexFound && regexConf === "Средняя")) {
|
||
return "Средняя";
|
||
}
|
||
|
||
// Если оба не нашли с высокой уверенностью - точно нет
|
||
if (!aiFound && !regexFound && aiConf === "Высокая" && regexConf === "Высокая") {
|
||
return "Высокая (не найдено)";
|
||
}
|
||
|
||
// Иначе - низкая
|
||
return "Низкая";
|
||
}
|
||
|
||
/**
|
||
* Объединяет результаты AI и Regex
|
||
*/
|
||
function mergeResults(allResults) {
|
||
// Разделяем на AI (первые 17) и Regex (последние 17)
|
||
const aiResults = allResults.slice(0, 17);
|
||
const regexResults = allResults.slice(17, 34);
|
||
|
||
const merged = [];
|
||
|
||
for (let i = 0; i < CRITERIA.length; i++) {
|
||
const criterion = CRITERIA[i];
|
||
|
||
// AI результаты
|
||
const aiItem = aiResults[i] || {};
|
||
const aiOutput = aiItem.output || {};
|
||
const aiFound = aiOutput.found || false;
|
||
const aiScore = aiOutput.score || 0;
|
||
const aiQuote = aiOutput.quote || '';
|
||
const aiUrl = aiOutput.url || '';
|
||
const aiDetails = aiOutput.details || '';
|
||
const aiConfidence = aiOutput.confidence || 'Не определена';
|
||
const aiCheckedPages = aiOutput.checked_pages || 0;
|
||
|
||
// Regex результаты
|
||
const regexItem = regexResults[i] || {};
|
||
const regexOutput = regexItem.output || {};
|
||
const regexFound = regexOutput.found || false;
|
||
const regexAnswer = regexOutput.answer || 'НЕТ';
|
||
const regexExtracted = regexOutput.extracted || '';
|
||
const regexConfidence = regexOutput.confidence || 'Не определена';
|
||
|
||
// Итоговый результат
|
||
const found = aiFound || regexFound;
|
||
const finalScore = Math.max(aiScore, regexFound ? 1 : 0);
|
||
const finalConfidence = calculateFinalConfidence(aiConfidence, regexConfidence, aiFound, regexFound);
|
||
|
||
// Собираем объединённый результат
|
||
const mergedItem = {
|
||
criterion_id: criterion.id,
|
||
criterion_name: criterion.name,
|
||
criterion_description: criterion.description,
|
||
|
||
// Общий результат
|
||
found: found,
|
||
status: found ? "НАЙДЕНО" : "НЕ НАЙДЕНО",
|
||
score: finalScore,
|
||
final_confidence: finalConfidence,
|
||
|
||
// AI Agent результаты
|
||
ai_agent: {
|
||
found: aiFound,
|
||
score: aiScore,
|
||
quote: aiQuote,
|
||
url: aiUrl,
|
||
details: aiDetails,
|
||
confidence: aiConfidence,
|
||
checked_pages: aiCheckedPages
|
||
},
|
||
|
||
// Regex результаты
|
||
regex: {
|
||
found: regexFound,
|
||
answer: regexAnswer,
|
||
extracted: regexExtracted,
|
||
confidence: regexConfidence
|
||
}
|
||
};
|
||
|
||
merged.push(mergedItem);
|
||
}
|
||
|
||
return merged;
|
||
}
|
||
|
||
/**
|
||
* Формирует итоговую сводку
|
||
*/
|
||
function formatSummary(mergedResults, hotelName, region) {
|
||
const total = mergedResults.length;
|
||
const foundCount = mergedResults.filter(r => r.found).length;
|
||
const notFoundCount = total - foundCount;
|
||
const compliancePercentage = Math.round((foundCount / total) * 100 * 10) / 10;
|
||
|
||
return {
|
||
hotel_name: hotelName || "Не указано",
|
||
region: region || "Не указано",
|
||
audit_date: new Date().toISOString().split('T')[0],
|
||
total_criteria: total,
|
||
found: foundCount,
|
||
not_found: notFoundCount,
|
||
compliance_percentage: compliancePercentage,
|
||
criteria_results: mergedResults
|
||
};
|
||
}
|
||
|
||
// ============================================================
|
||
// ГЛАВНЫЙ КОД
|
||
// ============================================================
|
||
|
||
// Получаем входные данные
|
||
const inputData = $input.all();
|
||
|
||
// Извлекаем массив результатов
|
||
let allResults = [];
|
||
|
||
if (Array.isArray(inputData) && inputData.length > 0) {
|
||
// Вариант 1: Aggregate вернул один item с массивом внутри
|
||
if (inputData.length === 1 && inputData[0].json && Array.isArray(inputData[0].json)) {
|
||
allResults = inputData[0].json;
|
||
}
|
||
// Вариант 2: Aggregate вернул один item с полем data (массив)
|
||
else if (inputData.length === 1 && inputData[0].json && Array.isArray(inputData[0].json.data)) {
|
||
allResults = inputData[0].json.data;
|
||
}
|
||
// Вариант 3: Пришло 34 отдельных items (без Aggregate)
|
||
else if (inputData.length === 34) {
|
||
allResults = inputData.map(item => item.json || item);
|
||
}
|
||
// Вариант 4: Пришло много items, берём все
|
||
else {
|
||
allResults = inputData.map(item => item.json || item);
|
||
}
|
||
} else {
|
||
throw new Error('Неверный формат входных данных. Ожидается массив из 34 элементов.');
|
||
}
|
||
|
||
// Отладочная информация
|
||
console.log(`📊 Получено элементов: ${allResults.length}`);
|
||
console.log(`📦 Формат входных данных: ${inputData.length} items`);
|
||
|
||
// Проверяем количество
|
||
if (allResults.length !== 34) {
|
||
console.log(`⚠️ Предупреждение: получено ${allResults.length} элементов вместо 34`);
|
||
console.log(`Первый элемент:`, JSON.stringify(allResults[0], null, 2).substring(0, 200));
|
||
}
|
||
|
||
// Объединяем результаты
|
||
const mergedResults = mergeResults(allResults);
|
||
|
||
// Получаем данные об отеле из первого элемента или workflow
|
||
let hotelName = "Неизвестный отель";
|
||
let region = "Неизвестный регион";
|
||
|
||
try {
|
||
// Пытаемся получить из первого input item
|
||
const firstItem = $input.first().json;
|
||
hotelName = firstItem.hotel_name || hotelName;
|
||
region = firstItem.region || region;
|
||
} catch (e) {
|
||
// Если не получилось, используем значения по умолчанию
|
||
console.log('Не удалось получить hotel_name и region из input');
|
||
}
|
||
|
||
// Формируем итоговую сводку
|
||
const summary = formatSummary(mergedResults, hotelName, region);
|
||
|
||
// Возвращаем результат
|
||
return [{ json: summary }];
|
||
|
||
// ============================================================
|
||
// ПРИМЕЧАНИЯ:
|
||
// ============================================================
|
||
// 1. Входные данные должны быть массивом из 34 элементов
|
||
// 2. Первые 17 - от AI Agent (с детальными ответами)
|
||
// 3. Последние 17 - от Regex (с простыми ДА/НЕТ)
|
||
// 4. На выходе - объединённый результат с итоговой оценкой
|
||
// ============================================================
|
||
|