365 lines
12 KiB
Python
365 lines
12 KiB
Python
|
|
#!/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('🟰 Результаты одинаковые!')
|