#!/usr/bin/env python3 """ Тестирование гибридного аудита для отеля "Комфорт" (Камчатка) Сравнение с результатами n8n AI Agent """ import psycopg2 from psycopg2.extras import RealDictCursor import requests import json from urllib.parse import unquote import re # Natasha для NER from natasha import ( Segmenter, MorphVocab, NewsEmbedding, NewsMorphTagger, NewsSyntaxParser, NewsNERTagger, Doc ) # Конфигурация БД DB_CONFIG = { 'host': "147.45.189.234", 'port': 5432, 'database': "default_db", 'user': "gen_user", 'password': unquote("2~~9_%5EkVsU%3F2%5CS") } # BGE-M3 API BGE_API_URL = "http://147.45.146.17:8002/embed" BGE_API_KEY = "22564b177aa73b6ac0b8642d7773350ff4c01d4983f028beff15ea247f09fa89" # ID отеля Комфорт HOTEL_ID = "303958ee-c607-11ef-92da-f9f9e6a4072b" # Инициализация Natasha print("🔧 Инициализация Natasha...") segmenter = Segmenter() morph_vocab = MorphVocab() emb = NewsEmbedding() morph_tagger = NewsMorphTagger(emb) syntax_parser = NewsSyntaxParser(emb) ner_tagger = NewsNERTagger(emb) print("✅ Natasha готова!\n") # Результаты от n8n AI Agent для сравнения N8N_RESULTS = { 1: {"score": 0.0, "found": False}, 2: {"score": 1.0, "found": True, "url": "https://hotelcomfort41.ru/o-kompanii"}, 3: {"score": 0.5, "found": True}, 4: {"score": 1.0, "found": True, "url": "https://hotelcomfort41.ru"}, 5: {"score": 0.5, "found": True}, 7: {"score": 1.0, "found": True}, 8: {"score": 0.0, "found": False}, 9: {"score": 0.5, "found": True}, 10: {"score": 0.0, "found": False}, 11: {"score": 0.0, "found": False}, 12: {"score": 1.0, "found": True}, 13: {"score": 0.0, "found": False}, 14: {"score": 0.0, "found": False}, 15: {"score": 0.0, "found": False}, 16: {"score": 0.0, "found": False}, 17: {"score": 0.0, "found": False}, 18: {"score": 0.0, "found": False} } def get_db_connection(): return psycopg2.connect(**DB_CONFIG, cursor_factory=RealDictCursor) def get_all_hotel_text(hotel_id: str) -> list: """Получить весь текст отеля из chunks""" conn = get_db_connection() cur = conn.cursor() cur.execute(""" SELECT text, metadata->>'url' as url FROM hotel_website_chunks WHERE metadata->>'hotel_id' = %s ORDER BY id; """, (hotel_id,)) chunks = cur.fetchall() cur.close() conn.close() return chunks def check_criterion_with_regex(chunks: list, criterion: dict) -> dict: """Проверка критерия регулярными выражениями""" result = { 'found': False, 'matches': [], 'urls': [], 'quotes': [] } patterns = criterion.get('patterns', []) keywords = criterion.get('keywords', []) for chunk in chunks: text = chunk['text'] url = chunk.get('url', 'Нет URL') # Проверяем регулярки for pattern in patterns: matches = re.findall(pattern, text, re.IGNORECASE) if matches: result['found'] = True result['matches'].extend(matches[:3]) if url not in result['urls']: result['urls'].append(url) # Находим контекст match_obj = re.search(pattern, text, re.IGNORECASE) if match_obj: idx = match_obj.start() start = max(0, idx - 100) end = min(len(text), idx + 200) quote = text[start:end].strip() result['quotes'].append({ 'url': url, 'quote': quote, 'match': matches[0] }) # Проверяем ключевые слова if not result['found']: text_lower = text.lower() for keyword in keywords: if keyword.lower() in text_lower: result['found'] = True if url not in result['urls']: result['urls'].append(url) # Находим контекст вокруг ключевого слова idx = text_lower.find(keyword.lower()) if idx >= 0: start = max(0, idx - 100) end = min(len(text), idx + 200) quote = text[start:end].strip() result['quotes'].append({ 'url': url, 'quote': quote, 'match': keyword }) break return result # 17 критериев с регулярками CRITERIA = [ { 'id': 1, 'name': 'Юридическая идентификация и верификация', 'patterns': [ r'\b\d{10}\b', # ИНН юр.лица r'\b\d{12}\b', # ИНН ИП r'\b\d{13}\b', # ОГРН r'инн\s*:?\s*\d{10,12}', r'огрн\s*:?\s*\d{13}', ], 'keywords': ['инн', 'огрн', 'егрюл', 'реквизиты'] }, { 'id': 2, 'name': 'Адрес', 'patterns': [ r'\d{6}.*?ул\.', r'ул\.\s*[А-Яа-яёЁA-Za-z\s]+,?\s*\d+', r'\d{6},?\s*г\.\s*[А-Яа-яёЁ-]+', ], 'keywords': ['адрес', 'местонахождение', 'г.', 'ул.'] }, { 'id': 3, 'name': 'Контакты', 'patterns': [ r'(?:\+7|8)\s*\(?\d{3,5}\)?\s*\d{1,3}[-\s]?\d{2}[-\s]?\d{2}', r'[\w\.-]+@[\w\.-]+\.\w{2,}', ], 'keywords': ['телефон', 'email', 'контакт'] }, { 'id': 4, 'name': 'Режим работы', 'patterns': [ r'(?:с|с\s+)\d{1,2}(?::|\.)\d{2}\s*(?:до|по)\s*\d{1,2}(?::|\.)\d{2}', r'круглосуточно', r'24\s*[/\-]\s*7', ], 'keywords': ['режим работы', 'часы работы', 'график'] }, { 'id': 5, 'name': 'Политика ПДн (152-ФЗ)', 'patterns': [ r'152[-\s]?фз', r'политика\s+в\s+отношении\s+обработки\s+персональных\s+данных', ], 'keywords': ['персональных данных', 'пдн', '152-фз', 'политика конфиденциальности'] }, { 'id': 7, 'name': 'Договор-оферта / Правила оказания услуг', 'patterns': [ r'публичная\s+оферта', r'договор.*?оказани.*?услуг', r'пользовательское\s+соглашение', ], 'keywords': ['оферта', 'договор', 'правила', 'соглашение'] }, { 'id': 8, 'name': 'Рекламации и споры', 'patterns': [], 'keywords': ['рекламация', 'претензия', 'жалоба', 'спор'] }, { 'id': 9, 'name': 'Цены/прайс', 'patterns': [ r'\d+\s*(?:руб|₽)', r'(?:от|цена|стоимость)\s*\d+', ], 'keywords': ['цена', 'прайс', 'стоимость', 'тариф'] }, { 'id': 10, 'name': 'Способы оплаты', 'patterns': [], 'keywords': ['оплата картой', 'наличные', 'безналичный', 'карта', 'visa', 'mastercard'] }, { 'id': 11, 'name': 'Онлайн-оплата', 'patterns': [], 'keywords': ['онлайн оплата', 'оплатить онлайн', 'эквайринг'] }, { 'id': 12, 'name': 'Онлайн-бронирование', 'patterns': [], 'keywords': ['забронировать', 'бронирование', 'booking', 'форма заявки'] }, { 'id': 13, 'name': 'FAQ', 'patterns': [], 'keywords': ['faq', 'частые вопросы', 'часто задаваемые'] }, { 'id': 14, 'name': 'Доступность для ЛОВЗ', 'patterns': [], 'keywords': ['ловз', 'инвалид', 'доступность', 'безбарьерная'] }, { 'id': 15, 'name': 'Партнёры/бренды', 'patterns': [], 'keywords': ['партнер', 'партнёр', 'бренд', 'сотрудничество'] }, { 'id': 16, 'name': 'Команда/сотрудники', 'patterns': [], 'keywords': ['команда', 'сотрудник', 'персонал', 'руководство'] }, { 'id': 17, 'name': 'Уголок потребителя', 'patterns': [], 'keywords': ['уголок потребителя', 'права потребителя', 'защита прав'] }, { 'id': 18, 'name': 'Актуальность документов', 'patterns': [ r'\d{4}[-/.]\d{1,2}[-/.]\d{1,2}', # Даты r'обновлено', r'актуализировано', ], 'keywords': ['дата обновления', 'актуально', 'обновлено'] } ] print('🏨 ТЕСТИРОВАНИЕ ОТЕЛЯ: Городской отель \"Комфорт\" (Камчатский край)') print('='*80) print(f'ID отеля: {HOTEL_ID}') print() # Получаем chunks chunks = get_all_hotel_text(HOTEL_ID) print(f'📊 Загружено {len(chunks)} chunks') print() # Проверяем каждый критерий print('🔍 ПРОВЕРКА ПО РЕГУЛЯРКАМИ И КЛЮЧЕВЫМ СЛОВАМ:') print('='*80) total_score = 0.0 for criterion in CRITERIA: crit_id = criterion['id'] crit_name = criterion['name'] print(f'\n{crit_id}. {crit_name}') print('-'*80) result = check_criterion_with_regex(chunks, criterion) # Оценка score = 0.0 if result['found'] and result['matches']: score = 1.0 elif result['found']: score = 0.5 total_score += score # n8n результат для сравнения n8n_score = N8N_RESULTS.get(crit_id, {}).get('score', 0.0) print(f'Регулярки: {score}/1.0') print(f'n8n AI: {n8n_score}/1.0') if score != n8n_score: diff = score - n8n_score if diff > 0: print(f'✅ Регулярки ЛУЧШЕ на {diff:.1f}') else: print(f'❌ n8n AI ЛУЧШЕ на {-diff:.1f}') else: print('🟰 Одинаково') if result['found']: print(f'Найдено совпадений: {len(result["matches"])}') if result['matches']: print(f'Примеры: {result["matches"][:3]}') if result['urls']: print(f'URL: {result["urls"][0]}') if result['quotes']: quote_text = result['quotes'][0]['quote'][:150] print(f'Цитата: {quote_text}...') else: print('❌ Не найдено') print() print('='*80) print(f'📊 ИТОГО:') print(f'Регулярки: {total_score}/17 ({total_score/17*100:.1f}%)') print(f'n8n AI: 6.0/17 (35.3%)') print() if total_score > 6.0: print(f'✅ Регулярки лучше на {total_score - 6.0:.1f} баллов!') elif total_score < 6.0: print(f'❌ n8n AI лучше на {6.0 - total_score:.1f} баллов!') else: print('🟰 Результаты одинаковые!')