🚀 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:
Фёдор
2025-10-27 22:49:42 +03:00
parent 0cf3297290
commit 684fada337
94 changed files with 14891 additions and 911 deletions

579
hybrid_audit_spb.py Normal file
View File

@@ -0,0 +1,579 @@
#!/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()