Files
hotels/n8n_code_merge_audit_results.js
Фёдор 684fada337 🚀 Full project sync: Hotels RAG & Audit System
 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
2025-10-27 22:49:42 +03:00

230 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ============================================================
// 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. На выходе - объединённый результат с итоговой оценкой
// ============================================================