diff --git a/backend/src/domain/entities/__init__.py b/backend/src/domain/entities/__init__.py new file mode 100644 index 0000000..c3a6d2e --- /dev/null +++ b/backend/src/domain/entities/__init__.py @@ -0,0 +1,4 @@ +""" +Domain entities +""" + diff --git a/backend/src/domain/entities/collection.py b/backend/src/domain/entities/collection.py new file mode 100644 index 0000000..abfc3a0 --- /dev/null +++ b/backend/src/domain/entities/collection.py @@ -0,0 +1,26 @@ +""" +Доменная сущность Collection +""" +from datetime import datetime +from uuid import UUID, uuid4 + + +class Collection: + """Каталог документов""" + + def __init__( + self, + name: str, + owner_id: UUID, + description: str = "", + is_public: bool = False, + collection_id: UUID | None = None, + created_at: datetime | None = None + ): + self.collection_id = collection_id or uuid4() + self.name = name + self.description = description + self.owner_id = owner_id + self.is_public = is_public + self.created_at = created_at or datetime.utcnow() + diff --git a/backend/src/domain/entities/collection_access.py b/backend/src/domain/entities/collection_access.py new file mode 100644 index 0000000..df4cd0a --- /dev/null +++ b/backend/src/domain/entities/collection_access.py @@ -0,0 +1,22 @@ +""" +Доменная сущность CollectionAccess +""" +from datetime import datetime +from uuid import UUID, uuid4 + + +class CollectionAccess: + """Доступ пользователя к коллекции""" + + def __init__( + self, + user_id: UUID, + collection_id: UUID, + access_id: UUID | None = None, + created_at: datetime | None = None + ): + self.access_id = access_id or uuid4() + self.user_id = user_id + self.collection_id = collection_id + self.created_at = created_at or datetime.utcnow() + diff --git a/backend/src/domain/entities/conversation.py b/backend/src/domain/entities/conversation.py new file mode 100644 index 0000000..ffe42cc --- /dev/null +++ b/backend/src/domain/entities/conversation.py @@ -0,0 +1,28 @@ +""" +Доменная сущность Conversation +""" +from datetime import datetime +from uuid import UUID, uuid4 + + +class Conversation: + """Беседа пользователя с ИИ""" + + def __init__( + self, + user_id: UUID, + collection_id: UUID, + conversation_id: UUID | None = None, + created_at: datetime | None = None, + updated_at: datetime | None = None + ): + self.conversation_id = conversation_id or uuid4() + self.user_id = user_id + self.collection_id = collection_id + self.created_at = created_at or datetime.utcnow() + self.updated_at = updated_at or datetime.utcnow() + + def update_timestamp(self): + """Обновить время последнего изменения""" + self.updated_at = datetime.utcnow() + diff --git a/backend/src/domain/entities/document.py b/backend/src/domain/entities/document.py new file mode 100644 index 0000000..48bc95d --- /dev/null +++ b/backend/src/domain/entities/document.py @@ -0,0 +1,27 @@ +""" +Доменная сущность Document +""" +from datetime import datetime +from uuid import UUID, uuid4 +from typing import Any + + +class Document: + """Документ в коллекции""" + + def __init__( + self, + collection_id: UUID, + title: str, + content: str, + metadata: dict[str, Any] | None = None, + document_id: UUID | None = None, + created_at: datetime | None = None + ): + self.document_id = document_id or uuid4() + self.collection_id = collection_id + self.title = title + self.content = content + self.metadata = metadata or {} + self.created_at = created_at or datetime.utcnow() + diff --git a/backend/src/domain/entities/embedding.py b/backend/src/domain/entities/embedding.py new file mode 100644 index 0000000..4da5977 --- /dev/null +++ b/backend/src/domain/entities/embedding.py @@ -0,0 +1,25 @@ +""" +Доменная сущность Embedding +""" +from datetime import datetime +from uuid import UUID, uuid4 +from typing import Any + + +class Embedding: + """Эмбеддинг документа""" + + def __init__( + self, + document_id: UUID, + embedding: list[float] | None = None, + model_version: str = "", + embedding_id: UUID | None = None, + created_at: datetime | None = None + ): + self.embedding_id = embedding_id or uuid4() + self.document_id = document_id + self.embedding = embedding or [] + self.model_version = model_version + self.created_at = created_at or datetime.utcnow() + diff --git a/backend/src/domain/entities/message.py b/backend/src/domain/entities/message.py new file mode 100644 index 0000000..967f439 --- /dev/null +++ b/backend/src/domain/entities/message.py @@ -0,0 +1,35 @@ +""" +Доменная сущность Message +""" +from datetime import datetime +from uuid import UUID, uuid4 +from typing import Any +from enum import Enum + + +class MessageRole(str, Enum): + """Роли сообщений""" + USER = "user" + ASSISTANT = "assistant" + SYSTEM = "system" + + +class Message: + """Сообщение в беседе""" + + def __init__( + self, + conversation_id: UUID, + content: str, + role: MessageRole, + sources: dict[str, Any] | None = None, + message_id: UUID | None = None, + created_at: datetime | None = None + ): + self.message_id = message_id or uuid4() + self.conversation_id = conversation_id + self.content = content + self.role = role + self.sources = sources or {} + self.created_at = created_at or datetime.utcnow() + diff --git a/backend/src/domain/entities/user.py b/backend/src/domain/entities/user.py new file mode 100644 index 0000000..a9cc815 --- /dev/null +++ b/backend/src/domain/entities/user.py @@ -0,0 +1,33 @@ +""" +Доменная сущность User +""" +from datetime import datetime +from uuid import UUID, uuid4 +from enum import Enum + + +class UserRole(str, Enum): + """Роли пользователей""" + USER = "user" + ADMIN = "admin" + + +class User: + """Пользователь системы""" + + def __init__( + self, + telegram_id: str, + role: UserRole = UserRole.USER, + user_id: UUID | None = None, + created_at: datetime | None = None + ): + self.user_id = user_id or uuid4() + self.telegram_id = telegram_id + self.role = role + self.created_at = created_at or datetime.utcnow() + + def is_admin(self) -> bool: + """проверка, является ли пользователь администратором""" + return self.role == UserRole.ADMIN + diff --git a/backend/src/domain/repositories/__init__.py b/backend/src/domain/repositories/__init__.py new file mode 100644 index 0000000..0647a45 --- /dev/null +++ b/backend/src/domain/repositories/__init__.py @@ -0,0 +1,4 @@ +""" +Domain repositories interfaces +""" + diff --git a/backend/src/domain/repositories/collection_access_repository.py b/backend/src/domain/repositories/collection_access_repository.py new file mode 100644 index 0000000..765649e --- /dev/null +++ b/backend/src/domain/repositories/collection_access_repository.py @@ -0,0 +1,47 @@ +""" +Интерфейс репозитория для CollectionAccess +""" +from abc import ABC, abstractmethod +from uuid import UUID +from typing import Optional +from src.domain.entities.collection_access import CollectionAccess + + +class ICollectionAccessRepository(ABC): + """Интерфейс репозитория доступа к коллекциям""" + + @abstractmethod + async def create(self, access: CollectionAccess) -> CollectionAccess: + """Создать доступ""" + pass + + @abstractmethod + async def get_by_id(self, access_id: UUID) -> Optional[CollectionAccess]: + """Получить доступ по ID""" + pass + + @abstractmethod + async def delete(self, access_id: UUID) -> bool: + """Удалить доступ""" + pass + + @abstractmethod + async def delete_by_user_and_collection(self, user_id: UUID, collection_id: UUID) -> bool: + """Удалить доступ пользователя к коллекции""" + pass + + @abstractmethod + async def get_by_user_and_collection(self, user_id: UUID, collection_id: UUID) -> Optional[CollectionAccess]: + """Получить доступ пользователя к коллекции""" + pass + + @abstractmethod + async def list_by_user(self, user_id: UUID) -> list[CollectionAccess]: + """Получить доступы пользователя""" + pass + + @abstractmethod + async def list_by_collection(self, collection_id: UUID) -> list[CollectionAccess]: + """Получить доступы к коллекции""" + pass + diff --git a/backend/src/domain/repositories/collection_repository.py b/backend/src/domain/repositories/collection_repository.py new file mode 100644 index 0000000..6e4e0a3 --- /dev/null +++ b/backend/src/domain/repositories/collection_repository.py @@ -0,0 +1,42 @@ +""" +Интерфейс репозитория для Collection +""" +from abc import ABC, abstractmethod +from uuid import UUID +from typing import Optional +from src.domain.entities.collection import Collection + + +class ICollectionRepository(ABC): + """Интерфейс репозитория коллекций""" + + @abstractmethod + async def create(self, collection: Collection) -> Collection: + """Создать коллекцию""" + pass + + @abstractmethod + async def get_by_id(self, collection_id: UUID) -> Optional[Collection]: + """Получить коллекцию по ID""" + pass + + @abstractmethod + async def update(self, collection: Collection) -> Collection: + """Обновить коллекцию""" + pass + + @abstractmethod + async def delete(self, collection_id: UUID) -> bool: + """Удалить коллекцию""" + pass + + @abstractmethod + async def list_by_owner(self, owner_id: UUID, skip: int = 0, limit: int = 100) -> list[Collection]: + """Получить коллекции владельца""" + pass + + @abstractmethod + async def list_public(self, skip: int = 0, limit: int = 100) -> list[Collection]: + """Получить публичные коллекции""" + pass + diff --git a/backend/src/domain/repositories/conversation_repository.py b/backend/src/domain/repositories/conversation_repository.py new file mode 100644 index 0000000..7eccd6a --- /dev/null +++ b/backend/src/domain/repositories/conversation_repository.py @@ -0,0 +1,42 @@ +""" +Интерфейс репозитория для Conversation +""" +from abc import ABC, abstractmethod +from uuid import UUID +from typing import Optional +from src.domain.entities.conversation import Conversation + + +class IConversationRepository(ABC): + """Интерфейс репозитория бесед""" + + @abstractmethod + async def create(self, conversation: Conversation) -> Conversation: + """Создать беседу""" + pass + + @abstractmethod + async def get_by_id(self, conversation_id: UUID) -> Optional[Conversation]: + """Получить беседу по ID""" + pass + + @abstractmethod + async def update(self, conversation: Conversation) -> Conversation: + """Обновить беседу""" + pass + + @abstractmethod + async def delete(self, conversation_id: UUID) -> bool: + """Удалить беседу""" + pass + + @abstractmethod + async def list_by_user(self, user_id: UUID, skip: int = 0, limit: int = 100) -> list[Conversation]: + """Получить беседы пользователя""" + pass + + @abstractmethod + async def list_by_collection(self, collection_id: UUID, skip: int = 0, limit: int = 100) -> list[Conversation]: + """Получить беседы по коллекции""" + pass + diff --git a/backend/src/domain/repositories/document_repository.py b/backend/src/domain/repositories/document_repository.py new file mode 100644 index 0000000..68f4826 --- /dev/null +++ b/backend/src/domain/repositories/document_repository.py @@ -0,0 +1,37 @@ +""" +Интерфейс репозитория для Document +""" +from abc import ABC, abstractmethod +from uuid import UUID +from typing import Optional +from src.domain.entities.document import Document + + +class IDocumentRepository(ABC): + """Интерфейс репозитория документов""" + + @abstractmethod + async def create(self, document: Document) -> Document: + """Создать документ""" + pass + + @abstractmethod + async def get_by_id(self, document_id: UUID) -> Optional[Document]: + """Получить документ по ID""" + pass + + @abstractmethod + async def update(self, document: Document) -> Document: + """Обновить документ""" + pass + + @abstractmethod + async def delete(self, document_id: UUID) -> bool: + """Удалить документ""" + pass + + @abstractmethod + async def list_by_collection(self, collection_id: UUID, skip: int = 0, limit: int = 100) -> list[Document]: + """Получить документы коллекции""" + pass + diff --git a/backend/src/domain/repositories/message_repository.py b/backend/src/domain/repositories/message_repository.py new file mode 100644 index 0000000..d40d477 --- /dev/null +++ b/backend/src/domain/repositories/message_repository.py @@ -0,0 +1,37 @@ +""" +Интерфейс репозитория для Message +""" +from abc import ABC, abstractmethod +from uuid import UUID +from typing import Optional +from src.domain.entities.message import Message + + +class IMessageRepository(ABC): + """Интерфейс репозитория сообщений""" + + @abstractmethod + async def create(self, message: Message) -> Message: + """Создать сообщение""" + pass + + @abstractmethod + async def get_by_id(self, message_id: UUID) -> Optional[Message]: + """Получить сообщение по ID""" + pass + + @abstractmethod + async def update(self, message: Message) -> Message: + """Обновить сообщение""" + pass + + @abstractmethod + async def delete(self, message_id: UUID) -> bool: + """Удалить сообщение""" + pass + + @abstractmethod + async def list_by_conversation(self, conversation_id: UUID, skip: int = 0, limit: int = 100) -> list[Message]: + """Получить сообщения беседы""" + pass + diff --git a/backend/src/domain/repositories/user_repository.py b/backend/src/domain/repositories/user_repository.py new file mode 100644 index 0000000..a724816 --- /dev/null +++ b/backend/src/domain/repositories/user_repository.py @@ -0,0 +1,42 @@ +""" +Интерфейс репозитория для User +""" +from abc import ABC, abstractmethod +from uuid import UUID +from typing import Optional +from src.domain.entities.user import User + + +class IUserRepository(ABC): + """Интерфейс репозитория пользователей""" + + @abstractmethod + async def create(self, user: User) -> User: + """Создать пользователя""" + pass + + @abstractmethod + async def get_by_id(self, user_id: UUID) -> Optional[User]: + """Получить пользователя по ID""" + pass + + @abstractmethod + async def get_by_telegram_id(self, telegram_id: str) -> Optional[User]: + """Получить пользователя по Telegram ID""" + pass + + @abstractmethod + async def update(self, user: User) -> User: + """Обновить пользователя""" + pass + + @abstractmethod + async def delete(self, user_id: UUID) -> bool: + """Удалить пользователя""" + pass + + @abstractmethod + async def list_all(self, skip: int = 0, limit: int = 100) -> list[User]: + """Получить список всех пользователей""" + pass + diff --git a/backend/src/infrastructure/database/__init__.py b/backend/src/infrastructure/database/__init__.py new file mode 100644 index 0000000..0049886 --- /dev/null +++ b/backend/src/infrastructure/database/__init__.py @@ -0,0 +1,4 @@ +""" +Database infrastructure +""" + diff --git a/backend/src/infrastructure/database/base.py b/backend/src/infrastructure/database/base.py new file mode 100644 index 0000000..8c19b37 --- /dev/null +++ b/backend/src/infrastructure/database/base.py @@ -0,0 +1,32 @@ +""" +Базовые настройки базы данных +""" +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker +from sqlalchemy.orm import declarative_base +from src.shared.config import settings + +engine = create_async_engine( + settings.database_url.replace("postgresql://", "postgresql+asyncpg://"), + echo=settings.DEBUG, + future=True +) + +AsyncSessionLocal = async_sessionmaker( + engine, + class_=AsyncSession, + expire_on_commit=False, + autocommit=False, + autoflush=False +) + +Base = declarative_base() + + +async def get_db() -> AsyncSession: + """Dependency для получения сессии БД""" + async with AsyncSessionLocal() as session: + try: + yield session + finally: + await session.close() + diff --git a/backend/src/infrastructure/database/models.py b/backend/src/infrastructure/database/models.py new file mode 100644 index 0000000..b2312fe --- /dev/null +++ b/backend/src/infrastructure/database/models.py @@ -0,0 +1,109 @@ +""" +SQLAlchemy модели для базы данных +""" +from sqlalchemy import Column, String, Text, Boolean, DateTime, ForeignKey, JSON, Integer +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import relationship +from datetime import datetime +import uuid +from src.infrastructure.database.base import Base + + +class UserModel(Base): + """Модель пользователя""" + __tablename__ = "users" + + user_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + telegram_id = Column(String, unique=True, nullable=False, index=True) + role = Column(String, nullable=False, default="user") + created_at = Column(DateTime, nullable=False, default=datetime.utcnow) + collections = relationship("CollectionModel", back_populates="owner", cascade="all, delete-orphan") + conversations = relationship("ConversationModel", back_populates="user", cascade="all, delete-orphan") + collection_accesses = relationship("CollectionAccessModel", back_populates="user", cascade="all, delete-orphan") + + +class CollectionModel(Base): + """Модель коллекции""" + __tablename__ = "collections" + + collection_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + name = Column(String, nullable=False) + description = Column(Text, nullable=True) + owner_id = Column(UUID(as_uuid=True), ForeignKey("users.user_id"), nullable=False) + is_public = Column(Boolean, nullable=False, default=False) + created_at = Column(DateTime, nullable=False, default=datetime.utcnow) + owner = relationship("UserModel", back_populates="collections") + documents = relationship("DocumentModel", back_populates="collection", cascade="all, delete-orphan") + conversations = relationship("ConversationModel", back_populates="collection", cascade="all, delete-orphan") + accesses = relationship("CollectionAccessModel", back_populates="collection", cascade="all, delete-orphan") + + +class DocumentModel(Base): + """Модель документа""" + __tablename__ = "documents" + + document_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + collection_id = Column(UUID(as_uuid=True), ForeignKey("collections.collection_id"), nullable=False) + title = Column(String, nullable=False) + content = Column(Text, nullable=False) + document_metadata = Column("metadata", JSON, nullable=True, default={}) + created_at = Column(DateTime, nullable=False, default=datetime.utcnow) + collection = relationship("CollectionModel", back_populates="documents") + embeddings = relationship("EmbeddingModel", back_populates="document", cascade="all, delete-orphan") + + +class EmbeddingModel(Base): + """Модель эмбеддинга (заглушка)""" + __tablename__ = "embeddings" + + embedding_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + document_id = Column(UUID(as_uuid=True), ForeignKey("documents.document_id"), nullable=False) + embedding = Column(JSON, nullable=True) + model_version = Column(String, nullable=True) + created_at = Column(DateTime, nullable=False, default=datetime.utcnow) + document = relationship("DocumentModel", back_populates="embeddings") + + +class ConversationModel(Base): + """Модель беседы""" + __tablename__ = "conversations" + + conversation_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + user_id = Column(UUID(as_uuid=True), ForeignKey("users.user_id"), nullable=False) + collection_id = Column(UUID(as_uuid=True), ForeignKey("collections.collection_id"), nullable=False) + created_at = Column(DateTime, nullable=False, default=datetime.utcnow) + updated_at = Column(DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow) + + user = relationship("UserModel", back_populates="conversations") + collection = relationship("CollectionModel", back_populates="conversations") + messages = relationship("MessageModel", back_populates="conversation", cascade="all, delete-orphan") + + +class MessageModel(Base): + """Модель сообщения""" + __tablename__ = "messages" + + message_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + conversation_id = Column(UUID(as_uuid=True), ForeignKey("conversations.conversation_id"), nullable=False) + content = Column(Text, nullable=False) + role = Column(String, nullable=False) + sources = Column(JSON, nullable=True, default={}) + created_at = Column(DateTime, nullable=False, default=datetime.utcnow) + conversation = relationship("ConversationModel", back_populates="messages") + + +class CollectionAccessModel(Base): + """Модель доступа к коллекции""" + __tablename__ = "collection_access" + + access_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + user_id = Column(UUID(as_uuid=True), ForeignKey("users.user_id"), nullable=False) + collection_id = Column(UUID(as_uuid=True), ForeignKey("collections.collection_id"), nullable=False) + created_at = Column(DateTime, nullable=False, default=datetime.utcnow) + user = relationship("UserModel", back_populates="collection_accesses") + collection = relationship("CollectionModel", back_populates="accesses") + + __table_args__ = ( + {"comment": "Уникальный доступ пользователя к коллекции"}, + ) +