File size: 11,842 Bytes
f4be6cd
 
3070830
e9ed2db
 
 
 
 
 
 
 
 
 
 
 
 
 
3070830
 
e9ed2db
 
 
 
 
 
 
 
 
f4be6cd
e9ed2db
 
 
6d16343
e9ed2db
 
 
 
d2c9bd5
e9ed2db
d2c9bd5
e9ed2db
 
 
 
6d16343
e9ed2db
 
 
 
 
 
 
6d16343
 
e9ed2db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0af0611
 
f4be6cd
0af0611
 
 
 
4184656
f4be6cd
0af0611
f4be6cd
0af0611
 
ba2e935
0af0611
f4be6cd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0af0611
0ce0393
e9ed2db
 
0ce0393
e9ed2db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f4be6cd
 
 
 
ba2e935
 
 
 
 
 
 
 
 
 
f4be6cd
ba2e935
 
 
 
e9ed2db
f4be6cd
 
 
 
 
e9ed2db
 
f4be6cd
 
 
 
 
65ca105
e9ed2db
6135950
 
 
 
 
 
 
e9ed2db
 
6d16343
 
e9ed2db
 
6135950
e9ed2db
6135950
9f19a3d
e9ed2db
 
6135950
e9ed2db
 
 
 
6d16343
 
 
 
ba2e935
6d16343
 
 
 
 
 
ba2e935
 
6d16343
 
d377253
9f19a3d
 
6135950
f4be6cd
6135950
 
f4be6cd
b236f5b
9f19a3d
 
f4be6cd
9f19a3d
 
 
3070830
f4be6cd
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# app.py: Основной файл для работы с Hugging Face Space

import gradio as gr
import json
import pandas as pd
from transformers import AutoTokenizer, AutoModelForQuestionAnswering
import torch
import re
from pymorphy2 import MorphAnalyzer
import logging

# Настройка логгера
logging.basicConfig(
    filename="dialog_logs.log",  # Имя файла для сохранения логов
    level=logging.INFO,         # Уровень логирования
    format="%(asctime)s - %(message)s",  # Формат записи (дата + сообщение)
    datefmt="%Y-%m-%d %H:%M:%S"  # Формат даты и времени
)

# Функция для записи диалога
def log_dialog(user_input, model_answer):
    if user_input.strip() == "" or model_answer.strip() == "":
        return  # Не записываем пустые диалоги
    logging.info(f"User: {user_input}")
    logging.info(f"Model: {model_answer}")

# Загрузка предобученной модели
model_name = "timpal0l/mdeberta-v3-base-squad2"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForQuestionAnswering.from_pretrained(model_name)

# Функция для загрузки данных из JSON
def load_questions_from_json(file_path="questions.json"):
    with open(file_path, "r", encoding="utf-8") as file:
        return json.load(file)

# Поиск ответа в JSON
def find_answer_in_json(questions_data, message):
    for item in questions_data:
        if message.lower() in item["question"].lower():
            return item["answer"]
    return None

# Загрузка данных из CSV
def load_real_estate_data(file_path="real_estate.csv"):
    try:
        data = pd.read_csv(file_path, sep=";", encoding="utf-8")
        if data.empty:
            raise ValueError("Файл данных пустой или поврежден.")
        return data
    except Exception as e:
        print(f"Ошибка при загрузке файла данных: {e}")
        return pd.DataFrame(columns=["item_id", "address", "metro", "met_range", "price", "description", "type", "area", "RENT"])

# Инициализация морфологического анализатора
morph = MorphAnalyzer()
base_types = ["офис", "квартира", "апартаменты", "свободное назначение", "студия", "дом", "ОСЗ"]

# Анализ запроса пользователя
def analyze_query(query):
    query = query.lower()
    filters = {}
    words = query.split()
    lemmas = [morph.parse(word)[0].normal_form for word in words]

    # Поиск типа недвижимости
    types_found = []
    for base_type in base_types:
        if any(morph.parse(lemma)[0].normal_form == base_type for lemma in lemmas):
            types_found.append(base_type)
    if types_found:
        filters["type"] = types_found

    # Поиск вида сделки ("аренда" или "продажа")
    if any(word in query for word in ["аренда", "снять", "аренду", "арендовать", "сниму"]):
        filters["rent"] = "Аренда"
    elif any(word in query for word in ["продажа", "купить", "покупке", "покупки", "продаю", "продажи"]):
        filters["rent"] = "Продажа"

    # Нормализация числовых значений (цена и площадь)
    def normalize_number(text):
        match = re.search(r"(\d+[\s]*[.,]?\d*)\s*(млн|тр|т\.р|тыс|тысруб|тыс\.руб|тр|т\.р.|тыр|тыр.|тыщ|тыш|тысяч|млнруб|млн\.руб|мн|М|миллионов|милионов|лямов|лимонов)?", text)
        if not match:
            return None
        number = float(match.group(1).replace(" ", "").replace(",", "."))
        unit = match.group(2)
        if unit in ["млн", "м", "млнруб", "млн.руб", "мн", "М", "миллионов", "милионов", "лямов", "лимонов"]:
            return int(number * 1_000_000)
        elif unit in ["тр", "т.р", "т.р.", "тыс", "тысруб", "тыс.руб", "тыр", "тыр.", "тыщ", "тыш", "тысяч"]:
            return int(number * 1_000)
        else:
            return int(number)

    # Поиск цены
    price_keywords = ["руб", "р.", "рублей", "рубля"]
    price_match = re.findall(rf"(до|<|>\s*)(\d+[\s]*[.,]?\d*)\s*(млн|тр|т\.р|тыс|тысруб|тыс\.руб|тр|т\.р.|тыр|тыр.|тыщ|тыш|тысяч|млнруб|млн\.руб|мн|М|миллионов|милионов|лямов|лимонов)?\s*({'|'.join(price_keywords)})\b", query)
    if price_match:
        operator = price_match[0][0]
        raw_price = price_match[0][1] + (price_match[0][2] or "")
        normalized_price = normalize_number(raw_price)
        if normalized_price is not None:
            if operator in ["до", "<"]:
                filters["price_max"] = normalized_price
            elif operator in ["от", ">"]:
                filters["price_min"] = normalized_price

    # Поиск площади
    area_keywords = ["квм", "кв.м", "кв метров", "кв м", "кв.м.", "квадратов", "квадрата", "м2", "метров", "м²"]
    area_range_match_1 = re.findall(rf"(\d+)\s*-\s*(\d+)\s*({'|'.join(area_keywords)})", query)
    area_range_match_2 = re.findall(rf"(\d+)\s*[.\s]+\s*(\d+)\s*({'|'.join(area_keywords)})", query)
    if area_range_match_1:
        filters["area_min"] = int(area_range_match_1[0][0])
        filters["area_max"] = int(area_range_match_1[0][1])
    elif area_range_match_2:
        filters["area_min"] = int(area_range_match_2[0][0])
        filters["area_max"] = int(area_range_match_2[0][1])
    else:
        area_min_match = re.findall(rf"\b(от)\s*(\d+)\s*({'|'.join(area_keywords)})", query)
        area_max_match = re.findall(rf"\b(до)\s*(\d+)\s*({'|'.join(area_keywords)})", query)
        if area_min_match:
            filters["area_min"] = int(area_min_match[0][1])
        if area_max_match:
            filters["area_max"] = int(area_max_match[0][1])

    return filters

# Поиск объектов недвижимости в CSV
def search_real_estate(dataframe, filters):
    if not filters:
        return []

    mask = pd.Series([True] * len(dataframe))

    # Фильтрация по типу недвижимости
    if "type" in filters:
        mask &= dataframe["type"].str.lower().isin(filters["type"])

    # Фильтрация по виду сделки (Аренда/Продажа)
    if "rent" in filters:
        mask &= dataframe["RENT"].str.lower() == filters["rent"].lower()

    # Фильтрация по цене
    if "price_min" in filters and "price_max" in filters:
        mask &= (dataframe["price"] >= filters["price_min"]) & (dataframe["price"] <= filters["price_max"])
    elif "price_min" in filters:
        mask &= dataframe["price"] >= filters["price_min"]
    elif "price_max" in filters:
        mask &= dataframe["price"] <= filters["price_max"]

    # Фильтрация по площади
    if "area_min" in filters and "area_max" in filters:
        mask &= (dataframe["area"] >= filters["area_min"]) & (dataframe["area"] <= filters["area_max"])
    elif "area_min" in filters:
        mask &= dataframe["area"] >= filters["area_min"]
    elif "area_max" in filters:
        mask &= dataframe["area"] <= filters["area_max"]

    results = dataframe[mask]
    if not results.empty:
        return results.to_dict(orient="records")  # Возвращаем список словарей
    return []  # Возвращаем пустой список, если ничего не найдено

# Создание контекста для модели
def create_context(dataframe, filters):
    if "type" in filters and "rent" in filters:
        relevant_data = dataframe[
            (dataframe["type"].str.lower().isin(filters["type"])) &
            (dataframe["RENT"].str.lower() == filters["rent"].lower())
        ]
    else:
        relevant_data = dataframe

    context = " ".join(relevant_data["description"].dropna().tolist()) + " " + \
              " ".join(relevant_data["type"].dropna().tolist()) + " " + \
              " ".join(relevant_data["address"].dropna().tolist())
    return context

# Генерация ответа через модель
def generate_answer(question, context):
    inputs = tokenizer(question, context, return_tensors="pt", truncation=True, padding=True)
    with torch.no_grad():
        outputs = model(**inputs)
    answer_start = torch.argmax(outputs.start_logits)
    answer_end = torch.argmax(outputs.end_logits) + 1
    tokens = tokenizer.convert_ids_to_tokens(inputs["input_ids"][0])
    answer = tokenizer.convert_tokens_to_string(tokens[answer_start:answer_end])
    if answer.strip() == "[CLS]" or not answer.strip():
        return "Не удалось найти точный ответ."
    return answer

# Главная функция для Gradio

def predict(message, history=None):
    if history is None:
        history = []  # Инициализируем историю чата как пустой список

    logging.info(f"Получено сообщение: {message}")
    logging.info(f"История диалога: {history}")

    # Загрузка контекста из JSON/CVS
    questions_data = load_questions_from_json()
    real_estate_data = load_real_estate_data()

    # Поиск ответа в JSON
    json_answer = find_answer_in_json(questions_data, message)
    if json_answer:
        log_dialog(message, json_answer)
        return [{"role": "assistant", "content": json_answer}]

    # Анализ запроса пользователя
    filters = analyze_query(message)

    # Поиск объектов недвижимости
    filtered_results = search_real_estate(real_estate_data, filters)
    if filtered_results:
        response = ""
        for result in filtered_results:
            response += (
                f"ID: {result.get('item_id', 'Не указано')}\n"
                f"Тип: {result.get('type', 'Не указано')}\n"
                f"Адрес: {result.get('address', 'Не указано')}\n"
                f"Цена: {result.get('price', 'Не указано')} руб.\n"
                f"Площадь: {result.get('area', 'Не указано')} м²\n"
                f"Метро: {result.get('metro', 'Не указано')}\n"
                f"До метро: {result.get('met_range', 'Не указано')}\n"
                f"Описание: {result.get('description', 'Не указано')}\n"
                f"Фото: {result.get('foto', 'Не указано')}\n"
                f"Записаться на просмотр: {result.get('order', 'Не указано')}\n"
                f"---\n"
            )
        log_dialog(message, response.strip())
        return [{"role": "assistant", "content": response.strip()}]

    # Если ничего не найдено, используем модель
    context = create_context(real_estate_data, filters)
    model_answer = get_model_response(message, context)
    log_dialog(message, model_answer)
    return [{"role": "assistant", "content": model_answer}]

# Создание интерфейса Gradio
demo = gr.ChatInterface(
    fn=predict,
    title="ИИ-ассистент по недвижимости",
    description="Задайте вопрос о недвижимости, и я помогу вам найти подходящий объект!",
)

if __name__ == "__main__":
    demo.launch()