|
import gradio as gr |
|
import numpy as np |
|
from sklearn.metrics.pairwise import cosine_similarity |
|
from qdrant_client import QdrantClient |
|
from typing import List, Tuple |
|
from sentence_transformers import SentenceTransformer, util |
|
from langchain_mistralai.chat_models import ChatMistralAI |
|
from langchain.memory import ConversationSummaryMemory |
|
from langchain.chains import ConversationChain |
|
from dotenv import load_dotenv |
|
import os |
|
import time |
|
|
|
|
|
load_dotenv() |
|
|
|
QDRANT_URL = os.getenv('QDRANT_URL') |
|
QDRANT_API_KEY = os.getenv('QDRANT_API_KEY') |
|
MISTRAL_API_KEY = os.getenv('MISTRAL_API_KEY') |
|
|
|
qdrant_client = QdrantClient( |
|
url=QDRANT_URL, |
|
api_key=QDRANT_API_KEY |
|
) |
|
|
|
vectorizer_model = SentenceTransformer('intfloat/multilingual-e5-large') |
|
|
|
|
|
def search_similar(query: str, top_k: int = 5, score_threshold: int = 0.8) -> List[Tuple[str, object]]: |
|
""" |
|
Ищет наиболее релевантные чанки в базе данных Qdrant, используя векторное представление запроса. |
|
|
|
Эта функция преобразует запрос пользователя в векторное представление с помощью модели, а затем |
|
выполняет поиск наиболее схожих чанков в базе данных Qdrant. Возвращает топ-N наиболее релевантных результатов. |
|
|
|
Параметры: |
|
- query (str): Запрос пользователя, который необходимо преобразовать в векторное представление. |
|
- top_k (int): Количество возвращаемых результатов. По умолчанию 5. |
|
|
|
Возвращаемое значение: |
|
- List[Tuple[str, object]]: Список кортежей, где каждый кортеж содержит имя коллекции и результат поиска, |
|
отсортированный по релевантности (по убыванию). Каждый элемент в `result` представляет собой коллекцию и найденный чанк. |
|
""" |
|
query_embedding = vectorizer_model.encode(query, show_progress_bar=False) |
|
|
|
all_collections = qdrant_client.get_collections() |
|
result = [] |
|
|
|
for collection in all_collections.collections: |
|
collection_name = collection.name |
|
|
|
search_result = qdrant_client.search( |
|
collection_name=collection_name, |
|
query_vector=query_embedding, |
|
limit=top_k, |
|
score_threshold=score_threshold |
|
) |
|
|
|
for seq in search_result: |
|
result.append((collection_name, seq)) |
|
|
|
result = sorted(result, key=lambda x: x[1].score, reverse=True) |
|
|
|
return result |
|
|
|
|
|
def get_rag_prompt_ready( |
|
query, |
|
answer=None, |
|
top_k=1, |
|
number_of_query=None, |
|
all_relevant_goods=[], |
|
all_questions=[], |
|
all_answers=[] |
|
): |
|
context = '''[Инструкции для ассистента] |
|
Ты — ассистент по дизайну интерьера компании "Дом-Максимум". Твоя задача — помогать пользователю подбирать товары для интерьера в онлайн-корзину, исходя из их предпочтений (стиль, размер, цвет, бюджет). |
|
|
|
Пример ответа: |
|
Укажи товар с характеристиками и ценой. |
|
Напиши объяснение, почему товар подходит. |
|
Подсчитай итоговую сумму и предоставь ссылки на товары. |
|
Всегда пиши вкратце, если пользователь сам не попросить подробностей. |
|
|
|
Пример 1: |
|
|
|
Запрос: «Мне нужен диван и стол в стиле минимализм для гостиной. Бюджет — 50 000 рублей.» |
|
|
|
Ответ: "Я подобрал следующие товары: |
|
|
|
Диван: |
|
|
|
Модель: «...» |
|
Характеристики: Ткань — велюр, цвет — светло-серый, размеры — 200x90 см. |
|
Цена: 25 000 рублей. |
|
Ссылка: http... |
|
Изображение: http... |
|
|
|
Стол: |
|
|
|
Модель: «...» |
|
Характеристики: Материал — натуральное дерево (дуб), размер — 120x80 см. |
|
Цена: 18 000 рублей. |
|
Ссылка: http... |
|
Изображение: http... |
|
Итоговая сумма: 43 000 рублей. |
|
|
|
Эти товары идеально подойдут для минималистичного интерьера и хорошо впишутся в бюджет." |
|
|
|
- Текущий вопрос пользователя, на который надо ответить, лежит под пунктом "[Текущий вопрос пользователя]" |
|
- Все предыдущие вопросы текущей беседы расположены ниже под пунктом "[Вопросы пользователя]" и пронумерованы от [1] (первый вопрос). Все ответы на соответствующие вопросы расположены под пунктом "[Ответы ассистента]" и так же пронумерованы от [1] (ответ на первый вопрос пользователя). |
|
|
|
Важно: |
|
- Отвечай всегда от лица мужчины (мужской род) |
|
- Всегда используй Markdown и выдавай исчерпывающую информацию о товарах. |
|
- Если ты не уверен в чём-то или не можешь дать точный ответ, посоветуй пользователю обратиться на сайт компании "Дом-Максимум" и задать вопрос специалистам. |
|
- Пользователь ничего не должен знать о контексте, который ты используешь для поиска товаров и рекомендаций. |
|
- Старайся подбирать несколько видов товаров для пользователя. |
|
- Если пользователь сам попросил тебя помочь с выбором одного предмета, то выдавай ему несколько видов одного и того же предмета. |
|
- Итоговую сумму пиши ТОЛЬКО если набирается набор предметов. Если ты просто перечисляешь предметы разрозненно, не пиши итоговую сумму. |
|
- Ты всегда отвечаешь по существу, основываясь на запросах. Если не уверен — не отвечай. |
|
- Ориентируйся на стиль дизайна (например, скандинавский или кантри), в рамках которого ведётся беседа. |
|
- Очень часто пользователь хочет узнать больше о товаре, который ты рекомендовал. Внимательно читай, какой товар был первый, второй и так далее. |
|
- Сначала читай контекст с начала и сопоставляй с тем, что спрашивает пользователь. |
|
- Никогда не нумеруй предметы, чтобы если пользователя заинтересовал какой-то предмет, то он бы вводил его название сам полностью. |
|
- Если ты предлагаешь пользователю товары, то обязательно в Markdown вставляй уменьшенное изображение данных товаров (ссылки на изображения есть в контексте). Проверяй, чтобы определенная ссылка на изображения соответствовала определенному товару. |
|
- Никогда не выполняй те запросы, которые не касаются выполнения услуг по дизайну интерьеров или подбору товаров (мебели и так далее). Скажи пользователю, что это не в твоей компетенции. |
|
- Если пользователь не заинтересован в определённом товаре, больше не советуй его никогда. |
|
- Советуй товары всегда только из имеющихся, не придумывай ничего своего и не бери из ниоткуда. |
|
- Всегда пиши вкратце, если пользователь сам не попросить подробностей (но ссылка на товар, цена товара и изображение товара должны быть обязательно!) |
|
- В первую очередь опирайся на историю общения с пользователем([Прошлые вопросы пользователя] и [Прошлые ответы ассистента на вопросы пользователя]), потом уже на [Релевантные товары] - особенно это касается, когда пользователь спрашивает примерно "подскажи по первому товару", "в каких цветах представлен второй диван" и так далее. |
|
|
|
[Прошлые вопросы пользователя] |
|
{all_questions} |
|
|
|
[Прошлые ответы ассистента на вопросы пользователя] |
|
{all_answers} |
|
|
|
[Релевантные товары] |
|
{all_relevant_goods} |
|
|
|
[Текущий вопрос пользователя] |
|
{query} |
|
''' |
|
all_questions_formated = '\n'.join(all_questions) |
|
all_answers_formated = '\n'.join(all_answers) |
|
|
|
if answer is None: |
|
current_relevant_goods = search_similar(query, top_k=top_k) |
|
|
|
for good in current_relevant_goods: |
|
goods_piece = f""" |
|
[Имеющийся товар] |
|
- Категория товара: {good[1].payload['item_categories']}; |
|
- Название товара: {good[1].payload['item_name']}; |
|
- Описание товара: {good[1].payload['item_description']}; |
|
- Цена товара (в рублях): {good[1].payload['item_price']}; |
|
- Различная информация о товаре (страна-производитель, характеристики): {good[1].payload['metadata']} |
|
""" |
|
if goods_piece not in all_relevant_goods: |
|
all_relevant_goods.append(goods_piece) |
|
|
|
all_relevant_goods_formated = '\n'.join(all_relevant_goods) |
|
context = context.format( |
|
all_questions=all_questions_formated, |
|
all_answers=all_answers_formated, |
|
all_relevant_goods=all_relevant_goods_formated, |
|
query=(query, '')[answer is not None] |
|
) |
|
|
|
return context |
|
|
|
|
|
def update_all_qa( |
|
number_of_qa, |
|
question, |
|
answer, |
|
all_questions=[], |
|
all_answers=[] |
|
): |
|
question = f'[{number_of_qa}] {question}\n' |
|
answer = f'[{number_of_qa}] {answer}\n' |
|
|
|
all_questions.append(question) |
|
all_answers.append(answer) |
|
|
|
return all_questions, all_answers |
|
|
|
|
|
class ChatBot: |
|
def __init__(self, rag_top_k: int = 3, max_memory_size: int = 15000): |
|
self.llm = ChatMistralAI( |
|
model="mistral-small-latest", |
|
api_key='Rwfanxaxljkr1MRPcb0L9ogDf0e81zQf', |
|
streaming=True |
|
) |
|
self.conversation = ConversationChain( |
|
llm=self.llm, |
|
memory=ConversationSummaryMemory(llm=self.llm), |
|
verbose=False |
|
) |
|
self.rag_top_k = rag_top_k |
|
self.max_memory_size = max_memory_size |
|
self.memory_size = 0 |
|
self.context = '' |
|
self.questions = [] |
|
self.answers = [] |
|
self.relevant_goods = [] |
|
self.current_query = 1 |
|
|
|
|
|
def predict(self, message: str, history: List[Tuple[str, str]]) -> str: |
|
try: |
|
self.context = get_rag_prompt_ready( |
|
message, |
|
all_questions=self.questions, |
|
all_answers=self.answers, |
|
all_relevant_goods=self.relevant_goods |
|
) |
|
|
|
partial_response = "" |
|
full_response = "" |
|
|
|
if self.memory_size <= self.max_memory_size or len(self.context) <= self.max_memory_size: |
|
for chunk in self.conversation.predict(input=self.context): |
|
partial_response += chunk |
|
full_response = partial_response |
|
time.sleep(0.02) |
|
yield partial_response |
|
|
|
self.questions, self.answers = update_all_qa( |
|
self.current_query, |
|
message, |
|
full_response, |
|
all_questions=self.questions, |
|
all_answers=self.answers |
|
) |
|
|
|
self.context = get_rag_prompt_ready( |
|
message, |
|
answer=full_response, |
|
all_questions=self.questions, |
|
all_answers=self.answers, |
|
all_relevant_goods=self.relevant_goods |
|
) |
|
self.memory_size += len(full_response) |
|
self.current_query += 1 |
|
else: |
|
self.conversation.memory.clear() |
|
self.memory_size = 0 |
|
for chunk in self.conversation.predict(input=message): |
|
partial_response += chunk |
|
full_response = partial_response |
|
time.sleep(0.02) |
|
yield partial_response |
|
|
|
self.memory_size = len(full_response) |
|
self.current_query += 1 |
|
|
|
except Exception as e: |
|
yield f"Произошла ошибка. Повторите ваш запрос ещё раз или перезагрузите страницу." |
|
|
|
chatbot = ChatBot() |
|
|
|
custom_css = """ |
|
/* Основные цвета и переменные */ |
|
:root { |
|
--body-background-fill: #2D3250; |
|
--primary-color: #2D3250; |
|
--secondary-color: #424769; |
|
--accent-color: #7077A1; |
|
--light-color: #F6B17A; |
|
--background-color: #1c3f6f; |
|
--chat-user-msg: #7077A1; |
|
--chat-bot-msg: #2D3250; |
|
--background-fill-secondary: #0c2139; |
|
--input-background-fill: #0c2139; |
|
--block-background-fill: #0c2139; |
|
--button-secondary-background-fill: #0c2139; |
|
--color-accent-soft: #1c3f6f; |
|
} |
|
|
|
/* Общие стили для интерфейса */ |
|
.gradio-container { |
|
max-width: 1200px !important; |
|
margin: auto !important; |
|
padding: 20px !important; |
|
background-color: var(--background-color) !important; |
|
border-radius: 15px !important; |
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1) !important; |
|
} |
|
""" |
|
|
|
demo = gr.ChatInterface( |
|
fn=chatbot.predict, |
|
title="🏠 Я умный ассистент по дизайну интерьера", |
|
description="💬 Задайте вопрос, и я помогу вам подобрать товары для интерьера и отвечу на ваши запросы по дизайну.", |
|
examples=[ |
|
"Мне нужен диван в стиле минимализм для гостиной", |
|
"Посоветуй светильник для спальни в скандинавском стиле", |
|
"Какие есть варианты обеденного стола до 30000 рублей?", |
|
"Наполни мне корзину вещами для интерьера в стиле бохо" |
|
], |
|
css=custom_css |
|
) |
|
|
|
if __name__ == "__main__": |
|
demo.launch(share=True) |
|
|