# app.py
import gradio as gr
from context.db_loader import SQLiteContextLoader
from models.gpt4o import ModelClient
import constants
from helpers import load_json_file


class NeuroRavenApp:
    def __init__(self):
        self.system_prompt = None
        self.current_theme = None
        self.client = ModelClient()
        self.context_loader = SQLiteContextLoader()
        self.conversation_themes = self.get_conversation_themes()
        if constants.NO_CONTEXT not in self.conversation_themes:
            self.conversation_themes.append(constants.NO_CONTEXT)

    @staticmethod
    def get_conversation_themes():
        themes = load_json_file(constants.CONVERSATION_THEMES_JSON)
        return themes

    def handle_query(self, message, history, context_theme, should_save):
        # Сохранение сообщения в базу данных, если чекбокс активирован
        if should_save and isinstance(self.context_loader, SQLiteContextLoader):
            if context_theme == constants.NO_CONTEXT:
                return constants.ERROR_CANNOT_SAVE_MESSAGE
            self.context_loader.save_message(context_theme, message)
            self.current_theme = None
            return constants.MESSAGE_SAVED_TO_CONTEXT

        if context_theme != self.current_theme:
            context = self.context_loader.load_context(context_theme)
            if context:
                self.system_prompt = constants.ANSWER_USING_CONTEXT_PROMPT.format(context=context)
            else:
                self.system_prompt = constants.DEFAULT_SYSTEM_PROMPT
            self.current_theme = context_theme

        response = self.client.get_response(
            message=message,
            history=history,
            sys_prompt=self.system_prompt
        )
        return response

    def launch_app(self):
        gr.ChatInterface(
            fn=self.handle_query,
            chatbot=gr.Chatbot(height=500),
            textbox=gr.Textbox(
                placeholder=constants.INPUT_PLACEHOLDER,
                container=False,
                scale=7
            ),
            title="NeuroRaven",
            theme="soft",
            retry_btn=None,
            clear_btn=constants.CLEAR_BUTTON_LABEL,
            additional_inputs=[
                gr.Dropdown(
                    self.conversation_themes,
                    label=constants.SELECT_CONTEXT_THEME_LABEL,
                    value=constants.NO_CONTEXT
                ),
                gr.Checkbox(
                    label=constants.SAVE_MESSAGE_LABEL,
                    value=False
                )
            ],
            css="footer{display:none !important}"
        ).launch(server_name=constants.SERVER_NAME, server_port=constants.SERVER_PORT)


if __name__ == "__main__":
    app = NeuroRavenApp()
    app.launch_app()

# constants.py
DEFAULT_SYSTEM_PROMPT = "You are a helpful assistant."
NO_CONTEXT = "Без контекста"
CONVERSATION_THEMES_JSON = "conversation_themes.json"
DATABASE_PATH = "context.db"
SERVER_NAME = "192.168.0.105"
SERVER_PORT = 7846

# Сообщения и тексты ошибок
FILE_DOES_NOT_CONTAIN_LIST = "Файл JSON не содержит списка."
ERROR_LOADING_JSON_DATA = "Ошибка при загрузке данных из {json_path}: {e}"
ERROR_CANNOT_SAVE_MESSAGE = "Невозможно сохранить сообщение для текущей темы..."
MESSAGE_SAVED_TO_CONTEXT = "Сообщение сохранено в контекст!"
ANSWER_USING_CONTEXT_PROMPT = "Отвечай на вопросы, используя следующий контекст:\n{context}"
INPUT_PLACEHOLDER = "Напиши свой вопрос"
SELECT_CONTEXT_THEME_LABEL = "Выбери тему контекста"
SAVE_MESSAGE_LABEL = "Запомнить сообщение"
CLEAR_BUTTON_LABEL = "Clear"


# helpers.py
import json
import constants


def load_json_file(json_path):
    """Загружает данные из JSON-файла."""
    try:
        with open(json_path, 'r', encoding='utf-8') as file:
            data = json.load(file)
            if isinstance(data, list):
                return data
            else:
                print(constants.FILE_DOES_NOT_CONTAIN_LIST)
                return []
    except (FileNotFoundError, json.JSONDecodeError) as e:
        print(constants.ERROR_LOADING_JSON_DATA.format(json_path=json_path, e=e))
        return []


# context\base_loader.py
from abc import ABC, abstractmethod


class ContextLoader(ABC):
    @abstractmethod
    def load_context(self, context_theme):
        pass


# context\db_loader.py
import sqlite3
from context.base_loader import ContextLoader
import constants


class SQLiteContextLoader(ContextLoader):
    def __init__(self):
        self.db_path = constants.DATABASE_PATH
        self._initialize_database()

    def _initialize_database(self):
        """Создает таблицу в базе данных, если она не существует."""
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS contexts (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                theme TEXT NOT NULL,
                date_added DATE DEFAULT (date('now')),
                text TEXT NOT NULL
            )
        ''')
        conn.commit()
        conn.close()

    def load_context(self, context_theme):
        if context_theme == constants.NO_CONTEXT:
            return ""

        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()

        query = "SELECT text FROM contexts WHERE theme = ?"
        cursor.execute(query, (context_theme,))
        rows = cursor.fetchall()
        conn.close()

        all_text = "\n".join(row[0] for row in rows)
        return all_text

    def save_message(self, context_theme, message):
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()

        query = "INSERT INTO contexts (theme, text) VALUES (?, ?)"
        cursor.execute(query, (context_theme, message))
        conn.commit()
        conn.close()


# context\file_loader.py
import os
from context.base_loader import ContextLoader


class FileContextLoader(ContextLoader):
    def __init__(self, base_path="docs/"):
        self.base_path = base_path

    def get_directory_by_theme(self, context_theme):
        if context_theme == "Без контекста":
            return None
        return os.path.join(self.base_path, context_theme)

    def load_context(self, context_theme):
        directory = self.get_directory_by_theme(context_theme)
        if not directory:
            return ""
        all_text = ""
        for filename in os.listdir(directory):
            with open(os.path.join(directory, filename), "r", encoding="utf-8") as file:
                all_text += file.read() + "\n"
        return all_text


# models\gemma_model.py
from gradio_client import Client


class ModelClient:
    def __init__(self):
        self.model = Client("gokaygokay/Gemma-2-llamacpp")

    def get_response(self, message, history, sys_prompt):
        result = self.model.predict(
            message=message,
            model="gemma-2-27b-it-Q5_K_M.gguf",
            system_message=sys_prompt,
            max_tokens=2048,
            temperature=0.7,
            top_p=0.95,
            top_k=40,
            repeat_penalty=1.1,
            api_name="/chat"
        )
        return result


# models\gpt4o.py
import requests
import json
import secrets


class ModelClient:
    def __init__(self):
        self.session_hash = None
        self.stream_url = None
        self.join_url = "https://yuntian-deng-chatgpt4turbo.hf.space/queue/join?__theme=light"
        self.headers = {
            "accept": "*/*",
            "accept-language": "ru,en;q=0.9",
            "content-type": "application/json",
            "priority": "u=1, i",
            "sec-ch-ua": "\"Not/A)Brand\";v=\"8\", \"Chromium\";v=\"126\", \"YaBrowser\";v=\"24.7\", "
                         "\"Yowser\";v=\"2.5\"",
            "sec-ch-ua-mobile": "?0",
            "sec-ch-ua-platform": "\"Windows\"",
            "sec-fetch-dest": "empty",
            "sec-fetch-mode": "cors",
            "sec-fetch-site": "same-origin"
        }

    @staticmethod
    def _generate_random_hash(length=10):
        """Генерирует случайный хэш указанной длины"""
        characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
        return ''.join(secrets.choice(characters) for _ in range(length))

    def _make_join_request(self, body):
        try:
            response = requests.post(self.join_url, headers=self.headers, json=body)
            response.raise_for_status()
        except requests.exceptions.HTTPError as e:
            print(f"HTTP error occurred: {e}")
        except Exception as e:
            print(f"An error occurred: {e}")
        return None

    def _make_stream_request(self):
        try:
            response = requests.get(self.stream_url, headers={**self.headers, 'accept': "text/event-stream"},
                                    stream=True)
            response.raise_for_status()
            return response
        except requests.exceptions.HTTPError as e:
            print(f"HTTP error occurred: {e}")
        except Exception as e:
            print(f"An error occurred: {e}")
        return None

    @staticmethod
    def _process_stream_response(response):
        response_message = ""
        if response:
            for chunk in response.iter_lines():
                if chunk:
                    try:
                        event_data = json.loads(chunk.decode('utf-8').replace("data: ", ""))
                        if event_data.get('msg') == "process_generating":
                            if event_data['output']['data'][0]:
                                output = event_data['output']['data'][0][-1]
                                response_message += output[-1]
                    except json.JSONDecodeError:
                        continue
        return response_message

    def get_response(self, message, history, sys_prompt):
        if not history:
            self.session_hash = self._generate_random_hash()
            self.stream_url = f"https://yuntian-deng-chatgpt4turbo.hf.space/queue/data?session_hash={self.session_hash}"
            # Добавляем sys_prompt только к первому сообщению
            input_message = sys_prompt + "\n" + message
        else:
            input_message = message

        body = {
            "data": [input_message, 1, 1, len(history), history, None],
            "event_data": None,
            "fn_index": 3,
            "trigger_id": 5,
            "session_hash": self.session_hash
        }
        self._make_join_request(body)
        stream_response = self._make_stream_request()
        response = self._process_stream_response(stream_response)
        return response


Я хочу добавить новую тему, которая будет использовать не базу данных, а langchain. Напиши такую реализацию и расскажи как это будет работать