580 lines
24 KiB
Python
580 lines
24 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
"""
|
|||
|
|
ГИБРИДНЫЙ АУДИТ ОТЕЛЕЙ ЧУКОТКИ
|
|||
|
|
Комбинирует 3 подхода:
|
|||
|
|
1. Семантический поиск (BGE-M3 embeddings)
|
|||
|
|
2. Регулярные выражения (точные паттерны)
|
|||
|
|
3. NER с Natasha (извлечение сущностей)
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import psycopg2
|
|||
|
|
from psycopg2.extras import RealDictCursor
|
|||
|
|
import requests
|
|||
|
|
import json
|
|||
|
|
import pandas as pd
|
|||
|
|
from datetime import datetime
|
|||
|
|
import time
|
|||
|
|
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"
|
|||
|
|
|
|||
|
|
# Инициализация Natasha
|
|||
|
|
print("🔧 Инициализация Natasha...")
|
|||
|
|
segmenter = Segmenter()
|
|||
|
|
morph_vocab = MorphVocab()
|
|||
|
|
emb = NewsEmbedding()
|
|||
|
|
morph_tagger = NewsMorphTagger(emb)
|
|||
|
|
syntax_parser = NewsSyntaxParser(emb)
|
|||
|
|
ner_tagger = NewsNERTagger(emb)
|
|||
|
|
print("✅ Natasha готова!")
|
|||
|
|
|
|||
|
|
# 18 НАСТОЯЩИХ критериев аудита с регулярками
|
|||
|
|
AUDIT_CRITERIA = [
|
|||
|
|
{
|
|||
|
|
'id': 1,
|
|||
|
|
'name': 'Юридическая идентификация и верификация',
|
|||
|
|
'query': 'полное наименование организации ОПФ ИНН ОГРН ЕГРЮЛ ЕГРИП проверить',
|
|||
|
|
'keywords': ['инн', 'огрн', 'егрюл', 'егрип', 'организация', 'ооо', 'ип'],
|
|||
|
|
'required_patterns': [
|
|||
|
|
r'\b\d{10}\b', # ИНН юридического лица (10 цифр)
|
|||
|
|
r'\b\d{12}\b', # ИНН ИП (12 цифр)
|
|||
|
|
r'\b\d{13}\b', # ОГРН (13 цифр)
|
|||
|
|
r'\b\d{15}\b', # ОГРНИП (15 цифр)
|
|||
|
|
r'инн\s*:?\s*\d{10,12}',
|
|||
|
|
r'огрн\s*:?\s*\d{13}',
|
|||
|
|
r'огрнип\s*:?\s*\d{15}',
|
|||
|
|
],
|
|||
|
|
'use_ner': True, # Использовать Natasha для извлечения названий организаций
|
|||
|
|
'weight': 1.0
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
'id': 2,
|
|||
|
|
'name': 'Адрес',
|
|||
|
|
'query': 'юридический адрес фактический адрес местонахождение',
|
|||
|
|
'keywords': ['адрес', 'address', 'местонахождение', 'г.', 'ул.'],
|
|||
|
|
'priority_patterns': [
|
|||
|
|
r'\d{6}.*?ул\.', # Индекс + ул.
|
|||
|
|
r'ул\.\s*[А-Яа-яёЁA-Za-z\s]+,?\s*\d+', # ул. Название, дом
|
|||
|
|
],
|
|||
|
|
'use_ner': True, # Использовать Natasha для извлечения адресов
|
|||
|
|
'weight': 1.0
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
'id': 3,
|
|||
|
|
'name': 'Контакты',
|
|||
|
|
'query': 'телефон email форма обратной связи чат контакты',
|
|||
|
|
'keywords': ['телефон', 'phone', 'email', '@', '+7', '8-800'],
|
|||
|
|
'priority_patterns': [
|
|||
|
|
r'(?:\+7|8)\s*\(?\d{3,5}\)?\s*\d{1,3}[-\s]?\d{2}[-\s]?\d{2}', # Телефон
|
|||
|
|
r'[\w\.-]+@[\w\.-]+\.\w{2,}', # Email
|
|||
|
|
],
|
|||
|
|
'weight': 1.0
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
'id': 4,
|
|||
|
|
'name': 'Режим работы',
|
|||
|
|
'query': 'часы работы график приема режим работы колл-центр',
|
|||
|
|
'keywords': ['часы работы', 'график работы', 'режим работы', 'круглосуточно', '24/7'],
|
|||
|
|
'priority_patterns': [
|
|||
|
|
r'(?:с|с\s+)\d{1,2}(?::|\.)\d{2}\s*(?:до|по)\s*\d{1,2}(?::|\.)\d{2}', # с 9:00 до 18:00
|
|||
|
|
r'\d{1,2}:\d{2}\s*-\s*\d{1,2}:\d{2}', # 9:00 - 18:00
|
|||
|
|
r'круглосуточно',
|
|||
|
|
r'24\s*[/\-]\s*7',
|
|||
|
|
],
|
|||
|
|
'weight': 1.0
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
'id': 5,
|
|||
|
|
'name': 'Политика ПДн (152-ФЗ)',
|
|||
|
|
'query': 'политика персональных данных обработка ПДн 152-ФЗ',
|
|||
|
|
'keywords': ['персональных данных', 'пдн', '152-фз', 'privacy'],
|
|||
|
|
'priority_patterns': [
|
|||
|
|
r'политика\s+в\s+отношении\s+обработки\s+персональных\s+данных',
|
|||
|
|
r'152[-\s]?фз',
|
|||
|
|
r'федеральный\s+закон.*?персональных\s+данных',
|
|||
|
|
],
|
|||
|
|
'weight': 1.0
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
'id': 6,
|
|||
|
|
'name': 'Роскомнадзор (реестр)',
|
|||
|
|
'query': 'роскомнадзор реестр операторов персональных данных',
|
|||
|
|
'keywords': ['роскомнадзор', 'реестр', 'оператор'],
|
|||
|
|
'weight': 1.0
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
'id': 7,
|
|||
|
|
'name': 'Договор-оферта / Правила оказания услуг',
|
|||
|
|
'query': 'договор оферта правила оказания услуг условия',
|
|||
|
|
'keywords': ['договор', 'оферта', 'правила', 'условия', 'услуг'],
|
|||
|
|
'priority_patterns': [
|
|||
|
|
r'публичная\s+оферта',
|
|||
|
|
r'договор.*?оказани.*?услуг',
|
|||
|
|
],
|
|||
|
|
'weight': 1.0
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
'id': 8,
|
|||
|
|
'name': 'Рекламации и споры',
|
|||
|
|
'query': 'рекламации споры жалобы претензии решение конфликтов',
|
|||
|
|
'keywords': ['рекламация', 'спор', 'жалоба', 'претензия', 'конфликт'],
|
|||
|
|
'weight': 1.0
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
'id': 9,
|
|||
|
|
'name': 'Цены/прайс',
|
|||
|
|
'query': 'цены прайс тарифы стоимость номера',
|
|||
|
|
'keywords': ['цена', 'прайс', 'тариф', 'стоимость', 'номер'],
|
|||
|
|
'priority_patterns': [
|
|||
|
|
r'\d+\s*(?:руб|₽)', # Цены в рублях
|
|||
|
|
],
|
|||
|
|
'weight': 1.0
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
'id': 10,
|
|||
|
|
'name': 'Способы оплаты',
|
|||
|
|
'query': 'способы оплаты платеж банковская карта наличные',
|
|||
|
|
'keywords': ['оплата', 'платеж', 'карта', 'наличные', 'способ'],
|
|||
|
|
'weight': 1.0
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
'id': 11,
|
|||
|
|
'name': 'Онлайн-оплата',
|
|||
|
|
'query': 'онлайн оплата интернет платеж карта через сайт',
|
|||
|
|
'keywords': ['онлайн', 'интернет', 'платеж', 'карта', 'сайт'],
|
|||
|
|
'weight': 1.0
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
'id': 12,
|
|||
|
|
'name': 'Онлайн-бронирование',
|
|||
|
|
'query': 'онлайн бронирование заказ номера через сайт',
|
|||
|
|
'keywords': ['бронирование', 'заказ', 'номер', 'сайт', 'онлайн'],
|
|||
|
|
'weight': 1.0
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
'id': 13,
|
|||
|
|
'name': 'FAQ',
|
|||
|
|
'query': 'часто задаваемые вопросы FAQ помощь',
|
|||
|
|
'keywords': ['faq', 'вопрос', 'ответ', 'помощь', 'часто'],
|
|||
|
|
'weight': 1.0
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
'id': 14,
|
|||
|
|
'name': 'Доступность для ЛОВЗ',
|
|||
|
|
'query': 'доступность инвалиды ЛОВЗ безбарьерная среда',
|
|||
|
|
'keywords': ['доступность', 'инвалид', 'ловз', 'безбарьерная'],
|
|||
|
|
'weight': 1.0
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
'id': 15,
|
|||
|
|
'name': 'Партнёры/бренды',
|
|||
|
|
'query': 'партнеры бренды сотрудничество франшиза',
|
|||
|
|
'keywords': ['партнер', 'бренд', 'сотрудничество', 'франшиза'],
|
|||
|
|
'weight': 1.0
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
'id': 16,
|
|||
|
|
'name': 'Команда/сотрудники',
|
|||
|
|
'query': 'команда сотрудники персонал коллектив',
|
|||
|
|
'keywords': ['команда', 'сотрудник', 'персонал', 'коллектив'],
|
|||
|
|
'weight': 1.0
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
'id': 17,
|
|||
|
|
'name': 'Уголок потребителя',
|
|||
|
|
'query': 'уголок потребителя права потребителя защита прав',
|
|||
|
|
'keywords': ['потребитель', 'права', 'защита', 'уголок'],
|
|||
|
|
'weight': 1.0
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
'id': 18,
|
|||
|
|
'name': 'Актуальность документов',
|
|||
|
|
'query': 'актуальность документов дата обновления свежая информация',
|
|||
|
|
'keywords': ['актуальность', 'документ', 'дата', 'обновление', 'свежая'],
|
|||
|
|
'weight': 1.0
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
def get_db_connection():
|
|||
|
|
"""Получить подключение к БД"""
|
|||
|
|
return psycopg2.connect(**DB_CONFIG, cursor_factory=RealDictCursor)
|
|||
|
|
|
|||
|
|
def generate_embedding(text: str) -> list:
|
|||
|
|
"""Генерация эмбеддинга для текста через API"""
|
|||
|
|
headers = {
|
|||
|
|
"X-API-Key": BGE_API_KEY,
|
|||
|
|
"Content-Type": "application/json"
|
|||
|
|
}
|
|||
|
|
payload = {"text": [text]}
|
|||
|
|
response = requests.post(BGE_API_URL, json=payload, headers=headers, timeout=30)
|
|||
|
|
response.raise_for_status()
|
|||
|
|
return response.json().get('embeddings', [[]])[0]
|
|||
|
|
|
|||
|
|
def semantic_search_for_criterion(hotel_id: str, query: str, limit: int = 3):
|
|||
|
|
"""Семантический поиск по chunks отеля"""
|
|||
|
|
try:
|
|||
|
|
query_embedding = generate_embedding(query)
|
|||
|
|
embedding_str = json.dumps(query_embedding)
|
|||
|
|
|
|||
|
|
conn = get_db_connection()
|
|||
|
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|||
|
|
|
|||
|
|
query_sql = f"""
|
|||
|
|
SELECT
|
|||
|
|
text,
|
|||
|
|
metadata->>'url' as url,
|
|||
|
|
embedding <-> %s::vector as distance
|
|||
|
|
FROM hotel_website_chunks
|
|||
|
|
WHERE metadata->>'hotel_id' = %s AND embedding IS NOT NULL
|
|||
|
|
ORDER BY embedding <-> %s::vector
|
|||
|
|
LIMIT %s;
|
|||
|
|
"""
|
|||
|
|
cur.execute(query_sql, (embedding_str, hotel_id, embedding_str, limit))
|
|||
|
|
results = cur.fetchall()
|
|||
|
|
cur.close()
|
|||
|
|
conn.close()
|
|||
|
|
return results
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"Ошибка семантического поиска: {e}")
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
def check_patterns(text: str, patterns: list) -> dict:
|
|||
|
|
"""Проверка текста на соответствие регулярным выражениям"""
|
|||
|
|
matches = []
|
|||
|
|
for pattern in patterns:
|
|||
|
|
found = re.findall(pattern, text, re.IGNORECASE)
|
|||
|
|
if found:
|
|||
|
|
matches.extend(found[:3]) # Макс 3 совпадения на паттерн
|
|||
|
|
return {
|
|||
|
|
'found': len(matches) > 0,
|
|||
|
|
'matches': matches[:5], # Макс 5 совпадений всего
|
|||
|
|
'count': len(matches)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def extract_entities_with_natasha(text: str) -> dict:
|
|||
|
|
"""Извлечение сущностей с помощью Natasha"""
|
|||
|
|
try:
|
|||
|
|
doc = Doc(text[:5000]) # Ограничиваем длину для производительности
|
|||
|
|
doc.segment(segmenter)
|
|||
|
|
doc.tag_morph(morph_tagger)
|
|||
|
|
doc.parse_syntax(syntax_parser)
|
|||
|
|
doc.tag_ner(ner_tagger)
|
|||
|
|
|
|||
|
|
entities = {
|
|||
|
|
'ORG': [], # Организации
|
|||
|
|
'PER': [], # Люди
|
|||
|
|
'LOC': [], # Локации/адреса
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for span in doc.spans:
|
|||
|
|
if span.type in entities:
|
|||
|
|
entities[span.type].append(span.text)
|
|||
|
|
|
|||
|
|
return entities
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"Ошибка Natasha: {e}")
|
|||
|
|
return {'ORG': [], 'PER': [], 'LOC': []}
|
|||
|
|
|
|||
|
|
def hybrid_audit_criterion(hotel_id: str, criterion: dict) -> dict:
|
|||
|
|
"""
|
|||
|
|
Гибридный аудит по одному критерию:
|
|||
|
|
1. Семантический поиск
|
|||
|
|
2. Проверка регулярками
|
|||
|
|
3. NER с Natasha (если включено)
|
|||
|
|
"""
|
|||
|
|
result = {
|
|||
|
|
'semantic_score': 0.0,
|
|||
|
|
'pattern_score': 0.0,
|
|||
|
|
'ner_score': 0.0,
|
|||
|
|
'final_score': 0.0,
|
|||
|
|
'evidence': [],
|
|||
|
|
'explanation': '',
|
|||
|
|
'approval_urls': [], # Ссылки на страницы
|
|||
|
|
'approval_quotes': [] # Цитаты с контекстом
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 1. СЕМАНТИЧЕСКИЙ ПОИСК
|
|||
|
|
semantic_matches = semantic_search_for_criterion(hotel_id, criterion['query'], limit=3)
|
|||
|
|
|
|||
|
|
if semantic_matches:
|
|||
|
|
best_match = semantic_matches[0]
|
|||
|
|
distance = best_match['distance']
|
|||
|
|
url = best_match.get('url', 'Нет URL')
|
|||
|
|
|
|||
|
|
if distance < 0.7:
|
|||
|
|
result['semantic_score'] = 1.0
|
|||
|
|
result['evidence'].append(f"🔍 Семантика (отлично, {distance:.3f})")
|
|||
|
|
result['approval_urls'].append(url)
|
|||
|
|
result['approval_quotes'].append({
|
|||
|
|
'url': url,
|
|||
|
|
'quote': best_match['text'][:300],
|
|||
|
|
'method': 'Семантический поиск',
|
|||
|
|
'distance': f"{distance:.3f}"
|
|||
|
|
})
|
|||
|
|
elif distance < 0.9:
|
|||
|
|
result['semantic_score'] = 0.5
|
|||
|
|
result['evidence'].append(f"🔍 Семантика (средне, {distance:.3f})")
|
|||
|
|
result['approval_urls'].append(url)
|
|||
|
|
result['approval_quotes'].append({
|
|||
|
|
'url': url,
|
|||
|
|
'quote': best_match['text'][:300],
|
|||
|
|
'method': 'Семантический поиск',
|
|||
|
|
'distance': f"{distance:.3f}"
|
|||
|
|
})
|
|||
|
|
else:
|
|||
|
|
result['semantic_score'] = 0.2
|
|||
|
|
result['evidence'].append(f"🔍 Семантика (слабо, {distance:.3f})")
|
|||
|
|
result['approval_urls'].append(url)
|
|||
|
|
result['approval_quotes'].append({
|
|||
|
|
'url': url,
|
|||
|
|
'quote': best_match['text'][:300],
|
|||
|
|
'method': 'Семантический поиск',
|
|||
|
|
'distance': f"{distance:.3f}"
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
# 2. ПРОВЕРКА РЕГУЛЯРКАМИ
|
|||
|
|
if 'priority_patterns' in criterion or 'required_patterns' in criterion:
|
|||
|
|
patterns = criterion.get('priority_patterns', []) + criterion.get('required_patterns', [])
|
|||
|
|
|
|||
|
|
# Проверяем все найденные семантикой тексты
|
|||
|
|
for match in semantic_matches:
|
|||
|
|
pattern_check = check_patterns(match['text'], patterns)
|
|||
|
|
|
|||
|
|
if pattern_check['found']:
|
|||
|
|
result['pattern_score'] = 1.0
|
|||
|
|
result['evidence'].append(f"✅ Регулярки: найдено {pattern_check['count']} совпадений")
|
|||
|
|
|
|||
|
|
# Добавляем цитату с найденными паттернами
|
|||
|
|
url = match.get('url', 'Нет URL')
|
|||
|
|
if url not in result['approval_urls']:
|
|||
|
|
result['approval_urls'].append(url)
|
|||
|
|
|
|||
|
|
result['approval_quotes'].append({
|
|||
|
|
'url': url,
|
|||
|
|
'quote': match['text'][:300],
|
|||
|
|
'method': 'Регулярные выражения',
|
|||
|
|
'matches': ', '.join(pattern_check['matches'])
|
|||
|
|
})
|
|||
|
|
break # Нашли - хватит
|
|||
|
|
else:
|
|||
|
|
result['pattern_score'] = 0.0
|
|||
|
|
|
|||
|
|
# 3. NER С NATASHA
|
|||
|
|
if criterion.get('use_ner', False) and semantic_matches:
|
|||
|
|
all_text = " ".join([m['text'] for m in semantic_matches])
|
|||
|
|
entities = extract_entities_with_natasha(all_text)
|
|||
|
|
|
|||
|
|
if criterion['id'] == 1: # Юридическая идентификация
|
|||
|
|
if entities['ORG']:
|
|||
|
|
result['ner_score'] = 1.0
|
|||
|
|
result['evidence'].append(f"🏢 Natasha (организации): {', '.join(entities['ORG'][:3])}")
|
|||
|
|
|
|||
|
|
# Добавляем цитату с найденными организациями
|
|||
|
|
url = semantic_matches[0].get('url', 'Нет URL')
|
|||
|
|
if url not in result['approval_urls']:
|
|||
|
|
result['approval_urls'].append(url)
|
|||
|
|
|
|||
|
|
result['approval_quotes'].append({
|
|||
|
|
'url': url,
|
|||
|
|
'quote': all_text[:300],
|
|||
|
|
'method': 'Natasha NER (организации)',
|
|||
|
|
'entities': ', '.join(entities['ORG'][:3])
|
|||
|
|
})
|
|||
|
|
else:
|
|||
|
|
result['ner_score'] = 0.0
|
|||
|
|
|
|||
|
|
elif criterion['id'] == 2: # Адрес
|
|||
|
|
if entities['LOC']:
|
|||
|
|
result['ner_score'] = 1.0
|
|||
|
|
result['evidence'].append(f"📍 Natasha (адреса): {', '.join(entities['LOC'][:3])}")
|
|||
|
|
|
|||
|
|
# Добавляем цитату с найденными адресами
|
|||
|
|
url = semantic_matches[0].get('url', 'Нет URL')
|
|||
|
|
if url not in result['approval_urls']:
|
|||
|
|
result['approval_urls'].append(url)
|
|||
|
|
|
|||
|
|
result['approval_quotes'].append({
|
|||
|
|
'url': url,
|
|||
|
|
'quote': all_text[:300],
|
|||
|
|
'method': 'Natasha NER (адреса)',
|
|||
|
|
'entities': ', '.join(entities['LOC'][:3])
|
|||
|
|
})
|
|||
|
|
else:
|
|||
|
|
result['ner_score'] = 0.0
|
|||
|
|
|
|||
|
|
# ИТОГОВАЯ ОЦЕНКА (взвешенная)
|
|||
|
|
weights = {
|
|||
|
|
'semantic': 0.4,
|
|||
|
|
'pattern': 0.4,
|
|||
|
|
'ner': 0.2
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
result['final_score'] = (
|
|||
|
|
result['semantic_score'] * weights['semantic'] +
|
|||
|
|
result['pattern_score'] * weights['pattern'] +
|
|||
|
|
result['ner_score'] * weights['ner']
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Объяснение
|
|||
|
|
if result['final_score'] >= 0.8:
|
|||
|
|
result['explanation'] = "🟢 Высокая: информация найдена и подтверждена"
|
|||
|
|
elif result['final_score'] >= 0.5:
|
|||
|
|
result['explanation'] = "🟡 Средняя: информация найдена частично"
|
|||
|
|
elif result['final_score'] >= 0.3:
|
|||
|
|
result['explanation'] = "🟠 Низкая: информация найдена, но не подтверждена"
|
|||
|
|
else:
|
|||
|
|
result['explanation'] = "🔴 Очень низкая: информация не найдена"
|
|||
|
|
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
def audit_hotel_hybrid(hotel_info: dict):
|
|||
|
|
"""Проводит гибридный аудит для одного отеля"""
|
|||
|
|
hotel_id = hotel_info['id']
|
|||
|
|
hotel_name = hotel_info['full_name']
|
|||
|
|
region_name = hotel_info['region_name']
|
|||
|
|
|
|||
|
|
print(f"\n🏨 ГИБРИДНЫЙ АУДИТ: {hotel_name}")
|
|||
|
|
print("=" * 80)
|
|||
|
|
|
|||
|
|
results = {
|
|||
|
|
'hotel_id': hotel_id,
|
|||
|
|
'hotel_name': hotel_name,
|
|||
|
|
'region_name': region_name,
|
|||
|
|
'total_score': 0.0,
|
|||
|
|
'criteria_results': {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for criterion in AUDIT_CRITERIA:
|
|||
|
|
print(f" 🔍 Критерий {criterion['id']}: {criterion['name']}")
|
|||
|
|
|
|||
|
|
audit_result = hybrid_audit_criterion(hotel_id, criterion)
|
|||
|
|
|
|||
|
|
results['criteria_results'][criterion['name']] = audit_result
|
|||
|
|
results['total_score'] += audit_result['final_score']
|
|||
|
|
|
|||
|
|
print(f" {audit_result['explanation']} (Итого: {audit_result['final_score']:.2f}/1.0)")
|
|||
|
|
print(f" └─ Семантика: {audit_result['semantic_score']:.2f} | Регулярки: {audit_result['pattern_score']:.2f} | NER: {audit_result['ner_score']:.2f}")
|
|||
|
|
|
|||
|
|
for evidence in audit_result['evidence'][:2]: # Показываем первые 2 доказательства
|
|||
|
|
print(f" {evidence}")
|
|||
|
|
|
|||
|
|
time.sleep(0.5) # Небольшая пауза между критериями
|
|||
|
|
|
|||
|
|
print(f"\n📊 ИТОГОВАЯ ОЦЕНКА: {results['total_score']:.2f}/{len(AUDIT_CRITERIA)} ({results['total_score']/len(AUDIT_CRITERIA)*100:.1f}%)")
|
|||
|
|
print("=" * 80)
|
|||
|
|
return results
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
print("🚀 ГИБРИДНЫЙ АУДИТ ОТЕЛЕЙ ЧУКОТКИ")
|
|||
|
|
print("=" * 80)
|
|||
|
|
print("Методы:")
|
|||
|
|
print(" 1️⃣ Семантический поиск (BGE-M3)")
|
|||
|
|
print(" 2️⃣ Регулярные выражения")
|
|||
|
|
print(" 3️⃣ NER с Natasha")
|
|||
|
|
print("=" * 80)
|
|||
|
|
|
|||
|
|
conn = get_db_connection()
|
|||
|
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|||
|
|
|
|||
|
|
# Получаем ВСЕ отели Чукотского автономного округа с эмбеддингами
|
|||
|
|
cur.execute("""
|
|||
|
|
SELECT DISTINCT ON (hm.id) hm.id, hm.full_name, hm.region_name
|
|||
|
|
FROM hotel_main hm
|
|||
|
|
JOIN hotel_website_chunks hwc ON hm.id::text = hwc.metadata->>'hotel_id'
|
|||
|
|
WHERE hm.region_name = 'Чукотский автономный округ';
|
|||
|
|
""")
|
|||
|
|
chukotka_hotels = cur.fetchall()
|
|||
|
|
cur.close()
|
|||
|
|
conn.close()
|
|||
|
|
|
|||
|
|
if not chukotka_hotels:
|
|||
|
|
print("❌ Не найдено отелей в Чукотском автономном округе с эмбеддингами.")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
print(f"\n📊 Найдено {len(chukotka_hotels)} отелей для гибридного аудита:")
|
|||
|
|
for hotel in chukotka_hotels:
|
|||
|
|
print(f" • {hotel['full_name']}")
|
|||
|
|
print()
|
|||
|
|
|
|||
|
|
all_audit_results = []
|
|||
|
|
for hotel_info in chukotka_hotels:
|
|||
|
|
audit_results = audit_hotel_hybrid(hotel_info)
|
|||
|
|
all_audit_results.append(audit_results)
|
|||
|
|
|
|||
|
|
# Создание Excel отчета
|
|||
|
|
df_data = []
|
|||
|
|
for hotel_result in all_audit_results:
|
|||
|
|
row = {
|
|||
|
|
'ID Отеля': hotel_result['hotel_id'],
|
|||
|
|
'Название Отеля': hotel_result['hotel_name'],
|
|||
|
|
'Регион': hotel_result['region_name'],
|
|||
|
|
'Общий балл': f"{hotel_result['total_score']:.2f}/{len(AUDIT_CRITERIA)}"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for criterion_name, crit_data in hotel_result['criteria_results'].items():
|
|||
|
|
row[f'{criterion_name} (Итого)'] = f"{crit_data['final_score']:.2f}"
|
|||
|
|
row[f'{criterion_name} (Семантика)'] = f"{crit_data['semantic_score']:.2f}"
|
|||
|
|
row[f'{criterion_name} (Регулярки)'] = f"{crit_data['pattern_score']:.2f}"
|
|||
|
|
row[f'{criterion_name} (NER)'] = f"{crit_data['ner_score']:.2f}"
|
|||
|
|
row[f'{criterion_name} (Объяснение)'] = crit_data['explanation']
|
|||
|
|
row[f'{criterion_name} (Доказательства)'] = "\n".join(crit_data['evidence'])
|
|||
|
|
|
|||
|
|
# ДОБАВЛЯЕМ URL И ЦИТАТЫ!
|
|||
|
|
if crit_data.get('approval_urls'):
|
|||
|
|
row[f'{criterion_name} (URL)'] = "\n".join(crit_data['approval_urls'])
|
|||
|
|
else:
|
|||
|
|
row[f'{criterion_name} (URL)'] = "Не найдено"
|
|||
|
|
|
|||
|
|
if crit_data.get('approval_quotes'):
|
|||
|
|
quotes_text = []
|
|||
|
|
for quote_data in crit_data['approval_quotes']:
|
|||
|
|
quote_str = f"[{quote_data['method']}]\n"
|
|||
|
|
quote_str += f"URL: {quote_data['url']}\n"
|
|||
|
|
quote_str += f"Цитата: {quote_data['quote']}\n"
|
|||
|
|
if 'matches' in quote_data:
|
|||
|
|
quote_str += f"Найдено: {quote_data['matches']}\n"
|
|||
|
|
if 'entities' in quote_data:
|
|||
|
|
quote_str += f"Сущности: {quote_data['entities']}\n"
|
|||
|
|
if 'distance' in quote_data:
|
|||
|
|
quote_str += f"Distance: {quote_data['distance']}\n"
|
|||
|
|
quotes_text.append(quote_str)
|
|||
|
|
row[f'{criterion_name} (Цитаты)'] = "\n---\n".join(quotes_text)
|
|||
|
|
else:
|
|||
|
|
row[f'{criterion_name} (Цитаты)'] = "Не найдено"
|
|||
|
|
|
|||
|
|
df_data.append(row)
|
|||
|
|
|
|||
|
|
df = pd.DataFrame(df_data)
|
|||
|
|
|
|||
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|||
|
|
output_filename = f"hybrid_audit_chukotka_{timestamp}.xlsx"
|
|||
|
|
df.to_excel(output_filename, index=False)
|
|||
|
|
print(f"\n✅ Гибридный отчет сохранен в {output_filename}")
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|
|||
|
|
|