🚀 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
This commit is contained in:
229
n8n_code_merge_audit_results.js
Normal file
229
n8n_code_merge_audit_results.js
Normal file
@@ -0,0 +1,229 @@
|
||||
// ============================================================
|
||||
// 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. На выходе - объединённый результат с итоговой оценкой
|
||||
// ============================================================
|
||||
|
||||
Reference in New Issue
Block a user