#!/usr/bin/env python3 """ Семантический аудит отелей Чукотки с использованием эмбеддингов 18 критериев аудита """ import psycopg2 from psycopg2.extras import RealDictCursor from urllib.parse import unquote import requests import json import pandas as pd from datetime import datetime import time # Конфигурация DB_CONFIG = { 'host': "147.45.189.234", 'port': 5432, 'database': "default_db", 'user': "gen_user", 'password': unquote("2~~9_%5EkVsU%3F2%5CS") } BGE_API_URL = "http://147.45.146.17:8002/embed" BGE_API_KEY = "22564b177aa73b6ac0b8642d7773350ff4c01d4983f028beff15ea247f09fa89" # 18 НАСТОЯЩИХ критериев аудита из audit_system_new.py AUDIT_CRITERIA = [ { 'id': 1, 'name': 'Юридическая идентификация и верификация', 'query': 'полное наименование организации ОПФ ИНН ОГРН ЕГРЮЛ ЕГРИП проверить', 'keywords': ['инн', 'огрн', 'егрюл', 'егрип', 'организация', 'ооо', 'ип'], 'weight': 1.0 }, { 'id': 2, 'name': 'Адрес', 'query': 'юридический адрес фактический адрес местонахождение', 'keywords': ['адрес', 'address', 'местонахождение', 'г.', 'ул.'], 'weight': 1.0 }, { 'id': 3, 'name': 'Контакты', 'query': 'телефон email форма обратной связи чат контакты', 'keywords': ['телефон', 'phone', 'email', '@', '+7', '8-800'], 'weight': 1.0 }, { 'id': 4, 'name': 'Режим работы', 'query': 'часы работы график приема режим работы колл-центр', 'keywords': ['часы работы', 'график работы', 'режим работы', 'круглосуточно', '24/7', 'пн-пт', 'пн-вс', 'время работы'], 'weight': 1.0 }, { 'id': 5, 'name': 'Политика ПДн (152-ФЗ)', 'query': 'политика персональных данных обработка ПДн 152-ФЗ', 'keywords': ['персональных данных', 'пдн', '152-фз', 'privacy'], 'weight': 1.0 }, { 'id': 6, 'name': 'Роскомнадзор (реестр)', 'query': 'роскомнадзор реестр операторов персональных данных', 'keywords': ['роскомнадзор', 'реестр', 'оператор'], 'weight': 1.0 }, { 'id': 7, 'name': 'Договор-оферта / Правила оказания услуг', 'query': 'договор оферта правила оказания услуг условия', 'keywords': ['договор', 'оферта', 'правила', 'условия', 'услуг'], 'weight': 1.0 }, { 'id': 8, 'name': 'Рекламации и споры', 'query': 'рекламации споры жалобы претензии решение конфликтов', 'keywords': ['рекламация', 'спор', 'жалоба', 'претензия', 'конфликт'], 'weight': 1.0 }, { 'id': 9, 'name': 'Цены/прайс', 'query': 'цены прайс тарифы стоимость номера', 'keywords': ['цена', 'прайс', 'тариф', 'стоимость', 'номер'], '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 semantic_search_hotel(hotel_id: str, query: str, limit: int = 5): """Семантический поиск для конкретного отеля""" try: # Генерируем эмбеддинг для запроса headers = { "X-API-Key": BGE_API_KEY, "Content-Type": "application/json" } payload = {"text": [query]} response = requests.post(BGE_API_URL, json=payload, headers=headers, timeout=30) if response.status_code != 200: return [] result = response.json() query_embedding = result.get('embeddings', [[]])[0] if not query_embedding: return [] embedding_str = json.dumps(query_embedding) # Поиск по конкретному отелю conn = get_db_connection() cur = conn.cursor() query_sql = """ SELECT metadata->>'hotel_name' as hotel_name, metadata->>'url' as url, text, embedding <-> %s::vector as distance FROM hotel_website_chunks WHERE metadata->>'hotel_id' = %s 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"Ошибка семантического поиска для отеля {hotel_id}: {e}") return [] def evaluate_criterion(hotel_id: str, hotel_name: str, criterion: dict): """Оценка критерия для отеля""" print(f" 🔍 Критерий {criterion['id']}: {criterion['name']}") # Семантический поиск search_results = semantic_search_hotel(hotel_id, criterion['query'], limit=3) if not search_results: return { 'criterion_id': criterion['id'], 'criterion_name': criterion['name'], 'score': 0, 'max_score': criterion['weight'], 'found_text': None, 'relevance': 0.0, 'url': None } # Анализируем результаты best_result = search_results[0] distance = best_result['distance'] # Конвертируем расстояние в оценку (чем меньше расстояние, тем выше оценка) if distance < 0.7: score = criterion['weight'] # Отлично relevance = "🟢 Высокая" elif distance < 0.9: score = criterion['weight'] * 0.7 # Хорошо relevance = "🟡 Средняя" elif distance < 1.1: score = criterion['weight'] * 0.4 # Удовлетворительно relevance = "🟠 Низкая" else: score = 0 # Не найдено relevance = "🔴 Очень низкая" # Проверяем наличие ключевых слов в тексте text_lower = best_result['text'].lower() keywords_found = [kw for kw in criterion['keywords'] if kw in text_lower] if keywords_found: score = min(score + 0.1 * len(keywords_found), criterion['weight']) return { 'criterion_id': criterion['id'], 'criterion_name': criterion['name'], 'score': round(score, 2), 'max_score': criterion['weight'], 'found_text': best_result['text'][:200] + "..." if len(best_result['text']) > 200 else best_result['text'], 'relevance': relevance, 'distance': round(distance, 3), 'keywords_found': keywords_found, 'url': best_result['url'] } def audit_hotel(hotel_id: str, hotel_name: str): """Аудит одного отеля""" print(f"\n🏨 АУДИТ ОТЕЛЯ: {hotel_name}") print("=" * 60) results = [] total_score = 0 max_total_score = sum(criterion['weight'] for criterion in AUDIT_CRITERIA) for criterion in AUDIT_CRITERIA: result = evaluate_criterion(hotel_id, hotel_name, criterion) results.append(result) total_score += result['score'] print(f" ✅ {criterion['name']}: {result['score']}/{result['max_score']} {result['relevance']}") if result['found_text']: print(f" 📄 {result['found_text']}") print() # Небольшая пауза между запросами time.sleep(0.5) percentage = (total_score / max_total_score) * 100 if max_total_score > 0 else 0 print(f"📊 ИТОГОВАЯ ОЦЕНКА: {total_score:.2f}/{max_total_score} ({percentage:.1f}%)") return { 'hotel_id': hotel_id, 'hotel_name': hotel_name, 'total_score': round(total_score, 2), 'max_score': max_total_score, 'percentage': round(percentage, 1), 'criteria_results': results } def main(): """Основная функция""" print("🚀 СЕМАНТИЧЕСКИЙ АУДИТ ОТЕЛЕЙ ЧУКОТКИ") print("=" * 50) # Получаем отели Чукотки с эмбеддингами conn = get_db_connection() cur = conn.cursor() cur.execute(""" SELECT DISTINCT h.id, h.full_name, COUNT(c.id) as chunks_count FROM hotel_main h JOIN hotel_website_chunks c ON h.id::text = c.metadata->>'hotel_id' WHERE h.region_name ILIKE '%чукот%' AND c.id IS NOT NULL GROUP BY h.id, h.full_name HAVING COUNT(c.id) > 0 ORDER BY chunks_count DESC; """) hotels = cur.fetchall() cur.close() conn.close() print(f"📊 Найдено {len(hotels)} отелей с эмбеддингами:") for hotel in hotels: print(f" • {hotel['full_name']} ({hotel['chunks_count']} chunks)") # Проводим аудит audit_results = [] for hotel in hotels: result = audit_hotel(hotel['id'], hotel['full_name']) audit_results.append(result) # Создаем Excel отчет create_excel_report(audit_results) print(f"\n✅ АУДИТ ЗАВЕРШЕН! Результаты сохранены в Excel.") def create_excel_report(audit_results): """Создание Excel отчета""" timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"semantic_audit_chukotka_{timestamp}.xlsx" with pd.ExcelWriter(filename, engine='openpyxl') as writer: # Сводная таблица summary_data = [] for result in audit_results: summary_data.append({ 'Отель': result['hotel_name'], 'Общая оценка': result['total_score'], 'Максимальная оценка': result['max_score'], 'Процент': f"{result['percentage']}%", 'Статус': 'Отлично' if result['percentage'] >= 80 else 'Хорошо' if result['percentage'] >= 60 else 'Удовлетворительно' if result['percentage'] >= 40 else 'Неудовлетворительно' }) summary_df = pd.DataFrame(summary_data) summary_df.to_excel(writer, sheet_name='Сводка', index=False) # Детальные результаты по каждому отелю for result in audit_results: sheet_name = result['hotel_name'][:30] # Ограничиваем длину имени листа detailed_data = [] for criterion in result['criteria_results']: detailed_data.append({ 'Критерий': criterion['criterion_name'], 'Оценка': criterion['score'], 'Максимальная оценка': criterion['max_score'], 'Релевантность': criterion['relevance'], 'Расстояние': criterion['distance'], 'Найденный текст': criterion['found_text'], 'Найденные ключевые слова': ', '.join(criterion.get('keywords_found', [])), 'URL': criterion['url'] }) detailed_df = pd.DataFrame(detailed_data) detailed_df.to_excel(writer, sheet_name=sheet_name, index=False) print(f"📊 Excel отчет сохранен: {filename}") if __name__ == "__main__": main()