diff --git a/services/backend/README.md b/services/backend/README.md new file mode 100644 index 0000000..8743345 --- /dev/null +++ b/services/backend/README.md @@ -0,0 +1,17 @@ +# Backend +### Flask + Celery + PostgreSQL + +Веб-приложение, построенное с использованием Flask, Celery для асинхронной обработки задач и PostgreSQL для хранения данных. +Включает в себя управление беседами, отправку сообщений, загрузку файлов и обработку задач с использованием внешнего ML-сервиса. + +### Структура + +- `models/`: Папка с моделями SQLAlchemy. +- `routes/`: Папка с маршрутами приложения. +- `schemas`: Схемы для Swagger. +- `services/`: Папка с сервисами для обработки задач и сообщений. +- `utils`: Вспомогательные утилиты. +- `app.py`: Основной файл приложения Flask. +- `celery_init.py`: Инициализация Celery. +- `celery_worker.py`: Настройка и запуск Celery worker. +- `config.py`: Конфигурационные параметры. \ No newline at end of file diff --git a/services/backend/models/__init__.py b/services/backend/models/__init__.py index 3291920..021d5d7 100644 --- a/services/backend/models/__init__.py +++ b/services/backend/models/__init__.py @@ -1,4 +1,4 @@ from flask_sqlalchemy import SQLAlchemy +# Инициализация объекта SQLAlchemy db = SQLAlchemy() - diff --git a/services/backend/models/conversation_model.py b/services/backend/models/conversation_model.py index 64133b8..2890d0d 100644 --- a/services/backend/models/conversation_model.py +++ b/services/backend/models/conversation_model.py @@ -1,11 +1,22 @@ from models import db -import uuid class ConversationModel(db.Model): + """ + Модель для хранения данных о беседах (разговорах). + + Таблица: conversations + + Атрибуты: + - id (int): Идентификатор беседы (целое число, первичный ключ). + - user_id (str): Идентификатор пользователя, связанный с беседой (строка до 50 символов). + """ __tablename__ = 'conversations' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.String(50)) def __repr__(self): + """ + :return: Строка, представляющая объект беседы, включая её идентификатор и идентификатор пользователя. + """ return f"" diff --git a/services/backend/models/file_model.py b/services/backend/models/file_model.py index c1fafee..86f4ffa 100644 --- a/services/backend/models/file_model.py +++ b/services/backend/models/file_model.py @@ -1,10 +1,22 @@ from models import db class FileModel(db.Model): + """ + Модель для хранения данных о файлах. + + Таблица: files + + Атрибуты: + - id (int): Идентификатор файла (целое число, первичный ключ). + - file_path (str): Путь к файлу. + """ __tablename__ = 'files' id = db.Column(db.Integer, primary_key=True) file_path = db.Column(db.String, nullable=False) # Путь к файлу def __repr__(self): + """ + :return:Строка, представляющая объект файла, включая его идентификатор и путь к файлу. + """ return f"" diff --git a/services/backend/models/message_model.py b/services/backend/models/message_model.py index 9e810ee..29c75bf 100644 --- a/services/backend/models/message_model.py +++ b/services/backend/models/message_model.py @@ -2,6 +2,21 @@ class MessageModel(db.Model): + """ + Модель для хранения данных о сообщениях. + + Таблица: messages + + Атрибуты: + - id (int): Идентификатор сообщения (целое число, первичный ключ). + - conversation_id (int): Идентификатор беседы, к которой относится сообщение (целое число, внешний ключ на таблицу conversations). + - file_id (int): Идентификатор файла, связанного с сообщением (целое число, внешний ключ на таблицу files, может быть пустым). + - task_id (str): Идентификатор задачи. + - role (int): Роль отправителя сообщения (1 - 'system', 2 - 'user'). + - text (str): Текст сообщения. + - created_at (datetime): Дата и время создания сообщения. + - updated_at (datetime): Дата и время последнего обновления сообщения. + """ __tablename__ = 'messages' id = db.Column(db.Integer, primary_key=True) @@ -17,4 +32,8 @@ class MessageModel(db.Model): updated_at = db.Column(db.DateTime, default=db.func.current_timestamp(), onupdate=db.func.current_timestamp()) def __repr__(self): + """ + :return:Строка, представляющая объект сообщения, включая его идентификатор, + идентификатор беседы, идентификатор файла, роль отправителя и текст сообщения. + """ return f"" diff --git a/services/backend/routes/__init__.py b/services/backend/routes/__init__.py index bc6e3b0..5bf12d4 100644 --- a/services/backend/routes/__init__.py +++ b/services/backend/routes/__init__.py @@ -2,6 +2,11 @@ def register_blueprints(app): + """ + Регистрирует все Blueprints в приложении Flask. + + :param app: Экземпляр приложения Flask, к которому будут привязаны Blueprints. + """ app.register_blueprint(api) diff --git a/services/backend/routes/conversation/delete_conversation.py b/services/backend/routes/conversation/delete_conversation.py index 9c49411..faf5e2c 100644 --- a/services/backend/routes/conversation/delete_conversation.py +++ b/services/backend/routes/conversation/delete_conversation.py @@ -7,6 +7,12 @@ @api.route('/conversation/delete/', methods=['DELETE']) def delete_message(conversation_id): + """ + Удаляет беседу по идентификатору беседы. + + :param conversation_id: Идентификатор беседы + :return:JSON-ответ с сообщением об успешном удалении или ошибке + """ try: ConversationService.from_id(conversation_id).delete_conversation() return jsonify({'message': 'Conversation deleted successfully'}), 200 diff --git a/services/backend/routes/conversation/get_conversations.py b/services/backend/routes/conversation/get_conversations.py index a0f3d78..a8be724 100644 --- a/services/backend/routes/conversation/get_conversations.py +++ b/services/backend/routes/conversation/get_conversations.py @@ -11,6 +11,11 @@ @api.route('/conversations/get', methods=['POST']) @swag_from(get_conversation_swagger) def get_conversation(): + """ + Получает список бесед для пользователя по его идентификатору, который хранится в cookie. + + :return:JSON-ответ с данными бесед или сообщением об ошибке. + """ try: user_id = request.cookies.get('user_id') diff --git a/services/backend/routes/message/get_message.py b/services/backend/routes/message/get_message.py index 6643a9a..5c1db9e 100644 --- a/services/backend/routes/message/get_message.py +++ b/services/backend/routes/message/get_message.py @@ -10,6 +10,12 @@ @api.route('/messages/', methods=['GET']) @swag_from(get_message_swagger) def get_user_messages(conversation_id): + """ + Получает список сообщений для заданной беседы. + + :param conversation_id:Идентификатор беседы + :return:JSON-ответ со списком сообщений для указанной беседы или сообщение об ошибке. + """ try: result = MessageService.get_message(conversation_id) diff --git a/services/backend/services/conversation_service.py b/services/backend/services/conversation_service.py index 4dd4912..7d85482 100644 --- a/services/backend/services/conversation_service.py +++ b/services/backend/services/conversation_service.py @@ -10,12 +10,30 @@ project_root = os.path.dirname(os.path.abspath(__file__)) class ConversationService: + """ + Сервис для работы с беседами. + + Предоставляет методы для создания беседы, обработки сообщений и файлов, + удаления беседы и получения всех бесед пользователя. + """ def __init__(self, model: ConversationModel = None): + """ + Инициализирует ConversationService с данным объектом ConversationModel. + + :param model: Экземпляр ConversationModel, с которым будет работать сервис. + """ self.model = model @staticmethod def from_id(conversation_id: int): + """ + Создает ConversationService на основе идентификатора беседы. + + :param conversation_id: Идентификатор беседы. + :return: Экземпляр ConversationService. + :raises Exception: Если беседа с указанным идентификатором не найдена. + """ conversation = ConversationModel.query.get(conversation_id) if not conversation: @@ -26,6 +44,12 @@ def from_id(conversation_id: int): @staticmethod def create_conversation(user_id: uuid) -> ConversationModel: + """ + Создает новую беседу. + + :param user_id: Идентификатор пользователя, создающего беседу. + :return: Созданный экземпляр ConversationModel. + """ new_conversation = ConversationModel(user_id = user_id) @@ -35,6 +59,15 @@ def create_conversation(user_id: uuid) -> ConversationModel: return new_conversation def handle_message(self, text: str) -> str: + """ + Обрабатывает сообщение и сохраняет его в базе данных. + + Создает задачу для обработки сообщения и сохраняет сообщение с этой задачей. + + :param text: Текст сообщения. + :return: Идентификатор задачи, созданной для обработки сообщения. + """ + task_id = RagService.create_task(text) MessageService.save_message( @@ -44,6 +77,15 @@ def handle_message(self, text: str) -> str: return task_id def handle_file(self, file: FileModel, text: str) -> str: + """ + Обрабатывает файл и сохраняет сообщение с файлом в базе данных. + + Создает задачу для обработки файла и сообщения и сохраняет сообщение с этой задачей и файлом. + + :param file: Экземпляр FileModel, представляющий загруженный файл. + :param text: Текст сообщения. + :return: Идентификатор задачи, созданной для обработки файла и сообщения. + """ task_id = RagService.create_file_question_task(file, text) @@ -55,6 +97,9 @@ def handle_file(self, file: FileModel, text: str) -> str: return task_id def delete_conversation(self): + """ + Удаляет беседу и связанные с ней сообщения. + """ delete_result, code = MessageService.delete_message(self.model.id) @@ -66,6 +111,12 @@ def delete_conversation(self): @staticmethod def get_conversations(user_id): + """ + Получает список идентификаторов бесед для указанного пользователя. + + :param user_id: Идентификатор пользователя. + :return: Список идентификаторов бесед или сообщение об ошибке, если беседы не найдены. + """ conversations = ConversationModel.query.filter_by(user_id=user_id).all() print(conversations) if not conversations: diff --git a/services/backend/services/file_service.py b/services/backend/services/file_service.py index df1b3ff..62d27c2 100644 --- a/services/backend/services/file_service.py +++ b/services/backend/services/file_service.py @@ -6,15 +6,24 @@ UPLOAD_FOLDER = 'uploads' class FileService: + """ + Сервис для работы с загруженными файлами. + """ @staticmethod def check_upload_folder(self): + """ + Проверяет наличие папки для загрузки файлов и создает её, если она отсутствует. + """ + if not os.path.exists(UPLOAD_FOLDER): os.makedirs(UPLOAD_FOLDER) - @staticmethod def save_file(file): + """ + Сохраняет загруженный файл на сервере и сохраняет информацию о файле в базе данных. + """ filename = secure_filename(file.filename) filepath = os.path.join(UPLOAD_FOLDER, filename) diff --git a/services/backend/services/message_service.py b/services/backend/services/message_service.py index df50dc1..f62a206 100644 --- a/services/backend/services/message_service.py +++ b/services/backend/services/message_service.py @@ -12,6 +12,16 @@ class Role(Enum): @dataclass class SaveMessageOptions: + """ + Класс для хранения параметров сохранения сообщения. + + :param text: Текст сообщения. + :param conversation_id: Идентификатор беседы. + :param task_id: Идентификатор задачи. + :param role: Роль отправителя сообщения. + :param file_id: Идентификатор файла. + """ + text: str conversation_id: int task_id: str @@ -20,6 +30,9 @@ class SaveMessageOptions: class MessageService: + """ + Класс для работы с сообщениями. + """ def __init__(self, model: MessageModel = None): self.model = model @@ -36,6 +49,10 @@ def from_task_id(task_id): @staticmethod def save_message(options: SaveMessageOptions) -> MessageModel: + """ + Сохраняет сообщение в базе данных. + """ + new_message = MessageModel( text = options.text, conversation_id = options.conversation_id, @@ -51,6 +68,13 @@ def save_message(options: SaveMessageOptions) -> MessageModel: @staticmethod def delete_message(conversation_id): + """ + Удаляет сообщения по идентификатору беседы. + + :param conversation_id: Идентификатор беседы. + :return: Сообщение об успешном удалении или ошибке. + """ + if not conversation_id: return 'Conversation ID not found', 400 @@ -61,6 +85,13 @@ def delete_message(conversation_id): @staticmethod def get_message(conversation_id): + """ + Получает все сообщения для заданного идентификатора беседы. + + :param conversation_id: Идентификатор беседы. + :return: Словарь с сообщениями, сгруппированными по идентификатору беседы, код ответа. + """ + messages = MessageModel.query.filter(MessageModel.conversation_id.in_([conversation_id])).all() if not messages: diff --git a/services/backend/services/rag_service.py b/services/backend/services/rag_service.py index 71f45e9..c3ad08a 100644 --- a/services/backend/services/rag_service.py +++ b/services/backend/services/rag_service.py @@ -51,7 +51,7 @@ def on_success(self, retval, task_id, args, kwargs): message = MessageService.from_task_id(task_id).model MessageService.save_message( - SaveMessageOptions(text=retval['output']['result'], task_id=task_id, conversation_id=message.conversation_id, role=Role.SYSTEM, file_id = retval['file_id']) + SaveMessageOptions(text=retval['output']['result'], task_id=task_id, conversation_id=message.conversation_id, role=Role.SYSTEM) ) super().on_success(retval, task_id, args, kwargs) @@ -59,16 +59,38 @@ def on_success(self, retval, task_id, args, kwargs): class RagService: @staticmethod def create_task(question: str) -> str: + """ + Создает задачу для обработки вопроса. + + :param question: Вопрос для обработки. + :return: Идентификатор созданной задачи. + """ + return run_question_task.delay(question).id @staticmethod def create_file_question_task(file: FileModel, question: str) -> str: + """ + Создает задачу для обработки вопроса с файлом. + + :param file: Объект FileModel. + :param question: Вопрос для обработки. + :return: Идентификатор созданной задачи. + """ + return run_file_question_task.delay(file.id, file.file_path, question).id @shared_task(base = QuestionTask,ignore_result=False) def run_question_task(question: str): + """ + Обрабатывает вопрос. + + :param question: Вопрос для обработки. + :return: Результат обработки вопроса. + """ + try: data = { "input":{ @@ -89,14 +111,25 @@ def run_question_task(question: str): @shared_task(base = QuestionTaskWithFile,ignore_result=False) def run_file_question_task(file_id, file_path, question: str) -> dict: + """ + Обрабатывает вопрос с загруженным файлом. + + :param file_id: Идентификатор файла. + :param file_path: Путь к файлу. + :param question: Вопрос для обработки. + :return: Результат обработки вопроса. + """ + try: - result = { - "output":{ - "result": f'Ответ на вопрос с файлом: {question}', - "file_id": file_id, - "file_path": file_path - } + result = { + "metadata":{ + "feedback_tokens": [], + "run_id": "426ba193-5406-40af-b5d4-be1f8ede18bb" + }, + "output":{ + "result": "OTVET NA TEXT." } + } json.dumps(result) return result diff --git a/services/backend/validators/upload_request.py b/services/backend/validators/upload_request.py index b352fcc..4db85a6 100644 --- a/services/backend/validators/upload_request.py +++ b/services/backend/validators/upload_request.py @@ -1,8 +1,8 @@ -from flask_wtf import FlaskForm -from wtforms import IntegerField, StringField, FileField -from wtforms.validators import DataRequired, Length - -class UploadForm(FlaskForm): - conversation_id = IntegerField('Conversation ID', validators=[DataRequired()]) - message = StringField('Message', validators=[DataRequired(), Length(min=1)]) - file = FileField('File', validators=[DataRequired()]) +# from flask_wtf import FlaskForm +# from wtforms import IntegerField, StringField, FileField +# from wtforms.validators import DataRequired, Length +# +# class UploadForm(FlaskForm): +# conversation_id = IntegerField('Conversation ID', validators=[DataRequired()]) +# message = StringField('Message', validators=[DataRequired(), Length(min=1)]) +# file = FileField('File', validators=[DataRequired()])