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)