presentation layer

This commit is contained in:
bokho 2025-12-14 22:57:54 +03:00
parent 4a043f8e70
commit 036741c0bf
59 changed files with 3226 additions and 1865 deletions

View File

@ -1,13 +1,13 @@
fastapi==0.104.1 fastapi==0.104.1
uvicorn[standard]==0.24.0 uvicorn[standard]==0.24.0
sqlalchemy[asyncio]==2.0.23 sqlalchemy[asyncio]==2.0.23
asyncpg==0.29.0 asyncpg==0.29.0
alembic==1.12.1 alembic==1.12.1
pydantic==2.5.0 pydantic==2.5.0
pydantic-settings==2.1.0 pydantic-settings==2.1.0
python-multipart==0.0.6 python-multipart==0.0.6
httpx==0.25.2 httpx==0.25.2
PyMuPDF==1.23.8 PyMuPDF==1.23.8
Pillow==10.2.0 Pillow==10.2.0
dishka==0.7.0 dishka==0.7.0

View File

@ -1,4 +1,4 @@
""" """
ИИ-юрист система ИИ-юрист система
""" """

View File

@ -1,4 +1,4 @@
""" """
Application layer Application layer
""" """

View File

@ -0,0 +1,4 @@
"""
Application services
"""

View File

@ -0,0 +1,72 @@
"""
Сервис парсинга документов
"""
from typing import BinaryIO
from src.infrastructure.external.yandex_ocr import YandexOCRService, YandexOCRError
class DocumentParserService:
"""Сервис для парсинга документов"""
def __init__(self, ocr_service: YandexOCRService):
self.ocr_service = ocr_service
async def parse_pdf(self, file: BinaryIO, filename: str) -> tuple[str, str]:
"""
Парсинг PDF файла
Args:
file: Файловый объект
filename: Имя файла
Returns:
Кортеж (title, content)
Raises:
YandexOCRError: При ошибке распознавания
"""
try:
content = await self.ocr_service.parse_pdf(file)
title = filename.rsplit(".", 1)[0] if "." in filename else filename
if not content or not content.strip() or content.startswith("Ошибка распознавания:"):
if not content or content.startswith("Ошибка распознавания:"):
pass
else:
content = f"Документ {filename} загружен, но текст не был распознан."
return title, content
except YandexOCRError as e:
title = filename.rsplit(".", 1)[0] if "." in filename else filename
content = f" Ошибка распознавания документа: {str(e)}"
return title, content
except Exception as e:
title = filename.rsplit(".", 1)[0] if "." in filename else filename
content = f" Ошибка при парсинге документа: {str(e)}"
return title, content
async def parse_image(self, file: BinaryIO, filename: str) -> tuple[str, str]:
"""
Парсинг изображения
Args:
file: Файловый объект изображения
filename: Имя файла
Returns:
Кортеж (title, content)
"""
try:
content = await self.ocr_service.parse_image(file)
title = filename.rsplit(".", 1)[0] if "." in filename else filename
if not content or not content.strip():
content = f"Изображение {filename} загружено, но текст не был распознан."
return title, content
except YandexOCRError:
raise
except Exception as e:
raise YandexOCRError(f"Ошибка при парсинге изображения: {str(e)}") from e

View File

@ -0,0 +1,4 @@
"""
Application use cases
"""

View File

@ -0,0 +1,141 @@
"""
Use cases для работы с коллекциями
"""
from uuid import UUID
from typing import Optional
from src.domain.entities.collection import Collection
from src.domain.entities.collection_access import CollectionAccess
from src.domain.repositories.collection_repository import ICollectionRepository
from src.domain.repositories.collection_access_repository import ICollectionAccessRepository
from src.domain.repositories.user_repository import IUserRepository
from src.shared.exceptions import NotFoundError, ForbiddenError
class CollectionUseCases:
"""Use cases для коллекций"""
def __init__(
self,
collection_repository: ICollectionRepository,
access_repository: ICollectionAccessRepository,
user_repository: IUserRepository
):
self.collection_repository = collection_repository
self.access_repository = access_repository
self.user_repository = user_repository
async def create_collection(
self,
name: str,
owner_id: UUID,
description: str = "",
is_public: bool = False
) -> Collection:
"""Создать коллекцию"""
owner = await self.user_repository.get_by_id(owner_id)
if not owner:
raise NotFoundError(f"Пользователь {owner_id} не найден")
collection = Collection(
name=name,
owner_id=owner_id,
description=description,
is_public=is_public
)
return await self.collection_repository.create(collection)
async def get_collection(self, collection_id: UUID) -> Collection:
"""Получить коллекцию по ID"""
collection = await self.collection_repository.get_by_id(collection_id)
if not collection:
raise NotFoundError(f"Коллекция {collection_id} не найдена")
return collection
async def update_collection(
self,
collection_id: UUID,
user_id: UUID,
name: str | None = None,
description: str | None = None,
is_public: bool | None = None
) -> Collection:
"""Обновить коллекцию"""
collection = await self.get_collection(collection_id)
if collection.owner_id != user_id:
raise ForbiddenError("Только владелец может изменять коллекцию")
if name is not None:
collection.name = name
if description is not None:
collection.description = description
if is_public is not None:
collection.is_public = is_public
return await self.collection_repository.update(collection)
async def delete_collection(self, collection_id: UUID, user_id: UUID) -> bool:
"""Удалить коллекцию"""
collection = await self.get_collection(collection_id)
if collection.owner_id != user_id:
raise ForbiddenError("Только владелец может удалять коллекцию")
return await self.collection_repository.delete(collection_id)
async def grant_access(self, collection_id: UUID, user_id: UUID, owner_id: UUID) -> CollectionAccess:
"""Предоставить доступ пользователю к коллекции"""
collection = await self.get_collection(collection_id)
if collection.owner_id != owner_id:
raise ForbiddenError("Только владелец может предоставлять доступ")
user = await self.user_repository.get_by_id(user_id)
if not user:
raise NotFoundError(f"Пользователь {user_id} не найден")
existing_access = await self.access_repository.get_by_user_and_collection(user_id, collection_id)
if existing_access:
return existing_access
access = CollectionAccess(user_id=user_id, collection_id=collection_id)
return await self.access_repository.create(access)
async def revoke_access(self, collection_id: UUID, user_id: UUID, owner_id: UUID) -> bool:
"""Отозвать доступ пользователя к коллекции"""
collection = await self.get_collection(collection_id)
if collection.owner_id != owner_id:
raise ForbiddenError("Только владелец может отзывать доступ")
return await self.access_repository.delete_by_user_and_collection(user_id, collection_id)
async def check_access(self, collection_id: UUID, user_id: UUID) -> bool:
"""Проверить доступ пользователя к коллекции"""
collection = await self.get_collection(collection_id)
if collection.owner_id == user_id:
return True
if collection.is_public:
return True
access = await self.access_repository.get_by_user_and_collection(user_id, collection_id)
return access is not None
async def list_user_collections(self, user_id: UUID, skip: int = 0, limit: int = 100) -> list[Collection]:
"""Получить коллекции, доступные пользователю"""
owned = await self.collection_repository.list_by_owner(user_id, skip=skip, limit=limit)
public = await self.collection_repository.list_public(skip=skip, limit=limit)
accesses = await self.access_repository.list_by_user(user_id)
accessed_collections = []
for access in accesses:
collection = await self.collection_repository.get_by_id(access.collection_id)
if collection:
accessed_collections.append(collection)
all_collections = {c.collection_id: c for c in owned + public + accessed_collections}
return list(all_collections.values())[skip:skip+limit]

View File

@ -0,0 +1,68 @@
"""
Use cases для работы с беседами
"""
from uuid import UUID
from src.domain.entities.conversation import Conversation
from src.domain.repositories.conversation_repository import IConversationRepository
from src.domain.repositories.collection_repository import ICollectionRepository
from src.domain.repositories.collection_access_repository import ICollectionAccessRepository
from src.shared.exceptions import NotFoundError, ForbiddenError
class ConversationUseCases:
"""Use cases для бесед"""
def __init__(
self,
conversation_repository: IConversationRepository,
collection_repository: ICollectionRepository,
access_repository: ICollectionAccessRepository
):
self.conversation_repository = conversation_repository
self.collection_repository = collection_repository
self.access_repository = access_repository
async def create_conversation(self, user_id: UUID, collection_id: UUID) -> Conversation:
"""Создать беседу"""
collection = await self.collection_repository.get_by_id(collection_id)
if not collection:
raise NotFoundError(f"Коллекция {collection_id} не найдена")
has_access = await self._check_collection_access(user_id, collection)
if not has_access:
raise ForbiddenError("Нет доступа к коллекции")
conversation = Conversation(user_id=user_id, collection_id=collection_id)
return await self.conversation_repository.create(conversation)
async def get_conversation(self, conversation_id: UUID, user_id: UUID) -> Conversation:
"""Получить беседу по ID"""
conversation = await self.conversation_repository.get_by_id(conversation_id)
if not conversation:
raise NotFoundError(f"Беседа {conversation_id} не найдена")
if conversation.user_id != user_id:
raise ForbiddenError("Нет доступа к этой беседе")
return conversation
async def delete_conversation(self, conversation_id: UUID, user_id: UUID) -> bool:
"""Удалить беседу"""
conversation = await self.get_conversation(conversation_id, user_id)
return await self.conversation_repository.delete(conversation_id)
async def list_user_conversations(self, user_id: UUID, skip: int = 0, limit: int = 100) -> list[Conversation]:
"""Получить беседы пользователя"""
return await self.conversation_repository.list_by_user(user_id, skip=skip, limit=limit)
async def _check_collection_access(self, user_id: UUID, collection) -> bool:
"""Проверить доступ пользователя к коллекции"""
if collection.owner_id == user_id:
return True
if collection.is_public:
return True
access = await self.access_repository.get_by_user_and_collection(user_id, collection.collection_id)
return access is not None

View File

@ -0,0 +1,119 @@
"""
Use cases для работы с документами
"""
from uuid import UUID
from typing import BinaryIO, Optional
from src.domain.entities.document import Document
from src.domain.repositories.document_repository import IDocumentRepository
from src.domain.repositories.collection_repository import ICollectionRepository
from src.application.services.document_parser_service import DocumentParserService
from src.shared.exceptions import NotFoundError, ForbiddenError
class DocumentUseCases:
"""Use cases для документов"""
def __init__(
self,
document_repository: IDocumentRepository,
collection_repository: ICollectionRepository,
parser_service: DocumentParserService
):
self.document_repository = document_repository
self.collection_repository = collection_repository
self.parser_service = parser_service
async def create_document(
self,
collection_id: UUID,
title: str,
content: str,
metadata: dict | None = None
) -> Document:
"""Создать документ"""
collection = await self.collection_repository.get_by_id(collection_id)
if not collection:
raise NotFoundError(f"Коллекция {collection_id} не найдена")
document = Document(
collection_id=collection_id,
title=title,
content=content,
metadata=metadata or {}
)
return await self.document_repository.create(document)
async def upload_and_parse_document(
self,
collection_id: UUID,
file: BinaryIO,
filename: str,
user_id: UUID
) -> Document:
"""Загрузить и распарсить документ"""
collection = await self.collection_repository.get_by_id(collection_id)
if not collection:
raise NotFoundError(f"Коллекция {collection_id} не найдена")
if collection.owner_id != user_id:
raise ForbiddenError("Только владелец может добавлять документы")
title, content = await self.parser_service.parse_pdf(file, filename)
document = Document(
collection_id=collection_id,
title=title,
content=content,
metadata={"filename": filename}
)
return await self.document_repository.create(document)
async def get_document(self, document_id: UUID) -> Document:
"""Получить документ по ID"""
document = await self.document_repository.get_by_id(document_id)
if not document:
raise NotFoundError(f"Документ {document_id} не найден")
return document
async def update_document(
self,
document_id: UUID,
user_id: UUID,
title: str | None = None,
content: str | None = None,
metadata: dict | None = None
) -> Document:
"""Обновить документ"""
document = await self.get_document(document_id)
collection = await self.collection_repository.get_by_id(document.collection_id)
if not collection or collection.owner_id != user_id:
raise ForbiddenError("Только владелец коллекции может изменять документы")
if title is not None:
document.title = title
if content is not None:
document.content = content
if metadata is not None:
document.metadata = metadata
return await self.document_repository.update(document)
async def delete_document(self, document_id: UUID, user_id: UUID) -> bool:
"""Удалить документ"""
document = await self.get_document(document_id)
collection = await self.collection_repository.get_by_id(document.collection_id)
if not collection or collection.owner_id != user_id:
raise ForbiddenError("Только владелец коллекции может удалять документы")
return await self.document_repository.delete(document_id)
async def list_collection_documents(self, collection_id: UUID, skip: int = 0, limit: int = 100) -> list[Document]:
"""Получить документы коллекции"""
collection = await self.collection_repository.get_by_id(collection_id)
if not collection:
raise NotFoundError(f"Коллекция {collection_id} не найдена")
return await self.document_repository.list_by_collection(collection_id, skip=skip, limit=limit)

View File

@ -0,0 +1,93 @@
"""
Use cases для работы с сообщениями
"""
from uuid import UUID
from src.domain.entities.message import Message, MessageRole
from src.domain.repositories.message_repository import IMessageRepository
from src.domain.repositories.conversation_repository import IConversationRepository
from src.shared.exceptions import NotFoundError, ForbiddenError
class MessageUseCases:
"""Use cases для сообщений"""
def __init__(
self,
message_repository: IMessageRepository,
conversation_repository: IConversationRepository
):
self.message_repository = message_repository
self.conversation_repository = conversation_repository
async def create_message(
self,
conversation_id: UUID,
content: str,
role: MessageRole,
user_id: UUID,
sources: dict | None = None
) -> Message:
"""Создать сообщение"""
conversation = await self.conversation_repository.get_by_id(conversation_id)
if not conversation:
raise NotFoundError(f"Беседа {conversation_id} не найдена")
if conversation.user_id != user_id:
raise ForbiddenError("Нет доступа к этой беседе")
message = Message(
conversation_id=conversation_id,
content=content,
role=role,
sources=sources or {}
)
conversation.update_timestamp()
await self.conversation_repository.update(conversation)
return await self.message_repository.create(message)
async def get_message(self, message_id: UUID) -> Message:
"""Получить сообщение по ID"""
message = await self.message_repository.get_by_id(message_id)
if not message:
raise NotFoundError(f"Сообщение {message_id} не найдено")
return message
async def update_message(
self,
message_id: UUID,
content: str | None = None,
sources: dict | None = None
) -> Message:
"""Обновить сообщение"""
message = await self.get_message(message_id)
if content is not None:
message.content = content
if sources is not None:
message.sources = sources
return await self.message_repository.update(message)
async def delete_message(self, message_id: UUID) -> bool:
"""Удалить сообщение"""
return await self.message_repository.delete(message_id)
async def list_conversation_messages(
self,
conversation_id: UUID,
user_id: UUID,
skip: int = 0,
limit: int = 100
) -> list[Message]:
"""Получить сообщения беседы"""
conversation = await self.conversation_repository.get_by_id(conversation_id)
if not conversation:
raise NotFoundError(f"Беседа {conversation_id} не найдена")
if conversation.user_id != user_id:
raise ForbiddenError("Нет доступа к этой беседе")
return await self.message_repository.list_by_conversation(conversation_id, skip=skip, limit=limit)

View File

@ -0,0 +1,55 @@
"""
Use cases для работы с пользователями
"""
from uuid import UUID
from typing import Optional
from src.domain.entities.user import User, UserRole
from src.domain.repositories.user_repository import IUserRepository
from src.shared.exceptions import NotFoundError, ValidationError
class UserUseCases:
"""Use cases для пользователей"""
def __init__(self, user_repository: IUserRepository):
self.user_repository = user_repository
async def create_user(self, telegram_id: str, role: UserRole = UserRole.USER) -> User:
"""Создать пользователя"""
existing_user = await self.user_repository.get_by_telegram_id(telegram_id)
if existing_user:
raise ValidationError(f"Пользователь с telegram_id {telegram_id} уже существует")
user = User(telegram_id=telegram_id, role=role)
return await self.user_repository.create(user)
async def get_user(self, user_id: UUID) -> User:
"""Получить пользователя по ID"""
user = await self.user_repository.get_by_id(user_id)
if not user:
raise NotFoundError(f"Пользователь {user_id} не найден")
return user
async def get_user_by_telegram_id(self, telegram_id: str) -> Optional[User]:
"""Получить пользователя по Telegram ID"""
return await self.user_repository.get_by_telegram_id(telegram_id)
async def update_user(self, user_id: UUID, telegram_id: str | None = None, role: UserRole | None = None) -> User:
"""Обновить пользователя"""
user = await self.get_user(user_id)
if telegram_id is not None:
user.telegram_id = telegram_id
if role is not None:
user.role = role
return await self.user_repository.update(user)
async def delete_user(self, user_id: UUID) -> bool:
"""Удалить пользователя"""
return await self.user_repository.delete(user_id)
async def list_users(self, skip: int = 0, limit: int = 100) -> list[User]:
"""Получить список пользователей"""
return await self.user_repository.list_all(skip=skip, limit=limit)

View File

@ -1,4 +1,4 @@
""" """
Domain layer Domain layer
""" """

View File

@ -1,4 +1,4 @@
""" """
Domain entities Domain entities
""" """

View File

@ -1,26 +1,26 @@
""" """
Доменная сущность Collection Доменная сущность Collection
""" """
from datetime import datetime from datetime import datetime
from uuid import UUID, uuid4 from uuid import UUID, uuid4
class Collection: class Collection:
"""Каталог документов""" """Каталог документов"""
def __init__( def __init__(
self, self,
name: str, name: str,
owner_id: UUID, owner_id: UUID,
description: str = "", description: str = "",
is_public: bool = False, is_public: bool = False,
collection_id: UUID | None = None, collection_id: UUID | None = None,
created_at: datetime | None = None created_at: datetime | None = None
): ):
self.collection_id = collection_id or uuid4() self.collection_id = collection_id or uuid4()
self.name = name self.name = name
self.description = description self.description = description
self.owner_id = owner_id self.owner_id = owner_id
self.is_public = is_public self.is_public = is_public
self.created_at = created_at or datetime.utcnow() self.created_at = created_at or datetime.utcnow()

View File

@ -1,22 +1,22 @@
""" """
Доменная сущность CollectionAccess Доменная сущность CollectionAccess
""" """
from datetime import datetime from datetime import datetime
from uuid import UUID, uuid4 from uuid import UUID, uuid4
class CollectionAccess: class CollectionAccess:
"""Доступ пользователя к коллекции""" """Доступ пользователя к коллекции"""
def __init__( def __init__(
self, self,
user_id: UUID, user_id: UUID,
collection_id: UUID, collection_id: UUID,
access_id: UUID | None = None, access_id: UUID | None = None,
created_at: datetime | None = None created_at: datetime | None = None
): ):
self.access_id = access_id or uuid4() self.access_id = access_id or uuid4()
self.user_id = user_id self.user_id = user_id
self.collection_id = collection_id self.collection_id = collection_id
self.created_at = created_at or datetime.utcnow() self.created_at = created_at or datetime.utcnow()

View File

@ -1,28 +1,28 @@
""" """
Доменная сущность Conversation Доменная сущность Conversation
""" """
from datetime import datetime from datetime import datetime
from uuid import UUID, uuid4 from uuid import UUID, uuid4
class Conversation: class Conversation:
"""Беседа пользователя с ИИ""" """Беседа пользователя с ИИ"""
def __init__( def __init__(
self, self,
user_id: UUID, user_id: UUID,
collection_id: UUID, collection_id: UUID,
conversation_id: UUID | None = None, conversation_id: UUID | None = None,
created_at: datetime | None = None, created_at: datetime | None = None,
updated_at: datetime | None = None updated_at: datetime | None = None
): ):
self.conversation_id = conversation_id or uuid4() self.conversation_id = conversation_id or uuid4()
self.user_id = user_id self.user_id = user_id
self.collection_id = collection_id self.collection_id = collection_id
self.created_at = created_at or datetime.utcnow() self.created_at = created_at or datetime.utcnow()
self.updated_at = updated_at or datetime.utcnow() self.updated_at = updated_at or datetime.utcnow()
def update_timestamp(self): def update_timestamp(self):
"""Обновить время последнего изменения""" """Обновить время последнего изменения"""
self.updated_at = datetime.utcnow() self.updated_at = datetime.utcnow()

View File

@ -1,27 +1,27 @@
""" """
Доменная сущность Document Доменная сущность Document
""" """
from datetime import datetime from datetime import datetime
from uuid import UUID, uuid4 from uuid import UUID, uuid4
from typing import Any from typing import Any
class Document: class Document:
"""Документ в коллекции""" """Документ в коллекции"""
def __init__( def __init__(
self, self,
collection_id: UUID, collection_id: UUID,
title: str, title: str,
content: str, content: str,
metadata: dict[str, Any] | None = None, metadata: dict[str, Any] | None = None,
document_id: UUID | None = None, document_id: UUID | None = None,
created_at: datetime | None = None created_at: datetime | None = None
): ):
self.document_id = document_id or uuid4() self.document_id = document_id or uuid4()
self.collection_id = collection_id self.collection_id = collection_id
self.title = title self.title = title
self.content = content self.content = content
self.metadata = metadata or {} self.metadata = metadata or {}
self.created_at = created_at or datetime.utcnow() self.created_at = created_at or datetime.utcnow()

View File

@ -1,25 +1,25 @@
""" """
Доменная сущность Embedding Доменная сущность Embedding
""" """
from datetime import datetime from datetime import datetime
from uuid import UUID, uuid4 from uuid import UUID, uuid4
from typing import Any from typing import Any
class Embedding: class Embedding:
"""Эмбеддинг документа""" """Эмбеддинг документа"""
def __init__( def __init__(
self, self,
document_id: UUID, document_id: UUID,
embedding: list[float] | None = None, embedding: list[float] | None = None,
model_version: str = "", model_version: str = "",
embedding_id: UUID | None = None, embedding_id: UUID | None = None,
created_at: datetime | None = None created_at: datetime | None = None
): ):
self.embedding_id = embedding_id or uuid4() self.embedding_id = embedding_id or uuid4()
self.document_id = document_id self.document_id = document_id
self.embedding = embedding or [] self.embedding = embedding or []
self.model_version = model_version self.model_version = model_version
self.created_at = created_at or datetime.utcnow() self.created_at = created_at or datetime.utcnow()

View File

@ -1,35 +1,35 @@
""" """
Доменная сущность Message Доменная сущность Message
""" """
from datetime import datetime from datetime import datetime
from uuid import UUID, uuid4 from uuid import UUID, uuid4
from typing import Any from typing import Any
from enum import Enum from enum import Enum
class MessageRole(str, Enum): class MessageRole(str, Enum):
"""Роли сообщений""" """Роли сообщений"""
USER = "user" USER = "user"
ASSISTANT = "assistant" ASSISTANT = "assistant"
SYSTEM = "system" SYSTEM = "system"
class Message: class Message:
"""Сообщение в беседе""" """Сообщение в беседе"""
def __init__( def __init__(
self, self,
conversation_id: UUID, conversation_id: UUID,
content: str, content: str,
role: MessageRole, role: MessageRole,
sources: dict[str, Any] | None = None, sources: dict[str, Any] | None = None,
message_id: UUID | None = None, message_id: UUID | None = None,
created_at: datetime | None = None created_at: datetime | None = None
): ):
self.message_id = message_id or uuid4() self.message_id = message_id or uuid4()
self.conversation_id = conversation_id self.conversation_id = conversation_id
self.content = content self.content = content
self.role = role self.role = role
self.sources = sources or {} self.sources = sources or {}
self.created_at = created_at or datetime.utcnow() self.created_at = created_at or datetime.utcnow()

View File

@ -1,33 +1,33 @@
""" """
Доменная сущность User Доменная сущность User
""" """
from datetime import datetime from datetime import datetime
from uuid import UUID, uuid4 from uuid import UUID, uuid4
from enum import Enum from enum import Enum
class UserRole(str, Enum): class UserRole(str, Enum):
"""Роли пользователей""" """Роли пользователей"""
USER = "user" USER = "user"
ADMIN = "admin" ADMIN = "admin"
class User: class User:
"""Пользователь системы""" """Пользователь системы"""
def __init__( def __init__(
self, self,
telegram_id: str, telegram_id: str,
role: UserRole = UserRole.USER, role: UserRole = UserRole.USER,
user_id: UUID | None = None, user_id: UUID | None = None,
created_at: datetime | None = None created_at: datetime | None = None
): ):
self.user_id = user_id or uuid4() self.user_id = user_id or uuid4()
self.telegram_id = telegram_id self.telegram_id = telegram_id
self.role = role self.role = role
self.created_at = created_at or datetime.utcnow() self.created_at = created_at or datetime.utcnow()
def is_admin(self) -> bool: def is_admin(self) -> bool:
"""проверка, является ли пользователь администратором""" """проверка, является ли пользователь администратором"""
return self.role == UserRole.ADMIN return self.role == UserRole.ADMIN

View File

@ -1,4 +1,4 @@
""" """
Domain repositories interfaces Domain repositories interfaces
""" """

View File

@ -1,47 +1,47 @@
""" """
Интерфейс репозитория для CollectionAccess Интерфейс репозитория для CollectionAccess
""" """
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from uuid import UUID from uuid import UUID
from typing import Optional from typing import Optional
from src.domain.entities.collection_access import CollectionAccess from src.domain.entities.collection_access import CollectionAccess
class ICollectionAccessRepository(ABC): class ICollectionAccessRepository(ABC):
"""Интерфейс репозитория доступа к коллекциям""" """Интерфейс репозитория доступа к коллекциям"""
@abstractmethod @abstractmethod
async def create(self, access: CollectionAccess) -> CollectionAccess: async def create(self, access: CollectionAccess) -> CollectionAccess:
"""Создать доступ""" """Создать доступ"""
pass pass
@abstractmethod @abstractmethod
async def get_by_id(self, access_id: UUID) -> Optional[CollectionAccess]: async def get_by_id(self, access_id: UUID) -> Optional[CollectionAccess]:
"""Получить доступ по ID""" """Получить доступ по ID"""
pass pass
@abstractmethod @abstractmethod
async def delete(self, access_id: UUID) -> bool: async def delete(self, access_id: UUID) -> bool:
"""Удалить доступ""" """Удалить доступ"""
pass pass
@abstractmethod @abstractmethod
async def delete_by_user_and_collection(self, user_id: UUID, collection_id: UUID) -> bool: async def delete_by_user_and_collection(self, user_id: UUID, collection_id: UUID) -> bool:
"""Удалить доступ пользователя к коллекции""" """Удалить доступ пользователя к коллекции"""
pass pass
@abstractmethod @abstractmethod
async def get_by_user_and_collection(self, user_id: UUID, collection_id: UUID) -> Optional[CollectionAccess]: async def get_by_user_and_collection(self, user_id: UUID, collection_id: UUID) -> Optional[CollectionAccess]:
"""Получить доступ пользователя к коллекции""" """Получить доступ пользователя к коллекции"""
pass pass
@abstractmethod @abstractmethod
async def list_by_user(self, user_id: UUID) -> list[CollectionAccess]: async def list_by_user(self, user_id: UUID) -> list[CollectionAccess]:
"""Получить доступы пользователя""" """Получить доступы пользователя"""
pass pass
@abstractmethod @abstractmethod
async def list_by_collection(self, collection_id: UUID) -> list[CollectionAccess]: async def list_by_collection(self, collection_id: UUID) -> list[CollectionAccess]:
"""Получить доступы к коллекции""" """Получить доступы к коллекции"""
pass pass

View File

@ -1,42 +1,42 @@
""" """
Интерфейс репозитория для Collection Интерфейс репозитория для Collection
""" """
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from uuid import UUID from uuid import UUID
from typing import Optional from typing import Optional
from src.domain.entities.collection import Collection from src.domain.entities.collection import Collection
class ICollectionRepository(ABC): class ICollectionRepository(ABC):
"""Интерфейс репозитория коллекций""" """Интерфейс репозитория коллекций"""
@abstractmethod @abstractmethod
async def create(self, collection: Collection) -> Collection: async def create(self, collection: Collection) -> Collection:
"""Создать коллекцию""" """Создать коллекцию"""
pass pass
@abstractmethod @abstractmethod
async def get_by_id(self, collection_id: UUID) -> Optional[Collection]: async def get_by_id(self, collection_id: UUID) -> Optional[Collection]:
"""Получить коллекцию по ID""" """Получить коллекцию по ID"""
pass pass
@abstractmethod @abstractmethod
async def update(self, collection: Collection) -> Collection: async def update(self, collection: Collection) -> Collection:
"""Обновить коллекцию""" """Обновить коллекцию"""
pass pass
@abstractmethod @abstractmethod
async def delete(self, collection_id: UUID) -> bool: async def delete(self, collection_id: UUID) -> bool:
"""Удалить коллекцию""" """Удалить коллекцию"""
pass pass
@abstractmethod @abstractmethod
async def list_by_owner(self, owner_id: UUID, skip: int = 0, limit: int = 100) -> list[Collection]: async def list_by_owner(self, owner_id: UUID, skip: int = 0, limit: int = 100) -> list[Collection]:
"""Получить коллекции владельца""" """Получить коллекции владельца"""
pass pass
@abstractmethod @abstractmethod
async def list_public(self, skip: int = 0, limit: int = 100) -> list[Collection]: async def list_public(self, skip: int = 0, limit: int = 100) -> list[Collection]:
"""Получить публичные коллекции""" """Получить публичные коллекции"""
pass pass

View File

@ -1,42 +1,42 @@
""" """
Интерфейс репозитория для Conversation Интерфейс репозитория для Conversation
""" """
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from uuid import UUID from uuid import UUID
from typing import Optional from typing import Optional
from src.domain.entities.conversation import Conversation from src.domain.entities.conversation import Conversation
class IConversationRepository(ABC): class IConversationRepository(ABC):
"""Интерфейс репозитория бесед""" """Интерфейс репозитория бесед"""
@abstractmethod @abstractmethod
async def create(self, conversation: Conversation) -> Conversation: async def create(self, conversation: Conversation) -> Conversation:
"""Создать беседу""" """Создать беседу"""
pass pass
@abstractmethod @abstractmethod
async def get_by_id(self, conversation_id: UUID) -> Optional[Conversation]: async def get_by_id(self, conversation_id: UUID) -> Optional[Conversation]:
"""Получить беседу по ID""" """Получить беседу по ID"""
pass pass
@abstractmethod @abstractmethod
async def update(self, conversation: Conversation) -> Conversation: async def update(self, conversation: Conversation) -> Conversation:
"""Обновить беседу""" """Обновить беседу"""
pass pass
@abstractmethod @abstractmethod
async def delete(self, conversation_id: UUID) -> bool: async def delete(self, conversation_id: UUID) -> bool:
"""Удалить беседу""" """Удалить беседу"""
pass pass
@abstractmethod @abstractmethod
async def list_by_user(self, user_id: UUID, skip: int = 0, limit: int = 100) -> list[Conversation]: async def list_by_user(self, user_id: UUID, skip: int = 0, limit: int = 100) -> list[Conversation]:
"""Получить беседы пользователя""" """Получить беседы пользователя"""
pass pass
@abstractmethod @abstractmethod
async def list_by_collection(self, collection_id: UUID, skip: int = 0, limit: int = 100) -> list[Conversation]: async def list_by_collection(self, collection_id: UUID, skip: int = 0, limit: int = 100) -> list[Conversation]:
"""Получить беседы по коллекции""" """Получить беседы по коллекции"""
pass pass

View File

@ -1,37 +1,37 @@
""" """
Интерфейс репозитория для Document Интерфейс репозитория для Document
""" """
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from uuid import UUID from uuid import UUID
from typing import Optional from typing import Optional
from src.domain.entities.document import Document from src.domain.entities.document import Document
class IDocumentRepository(ABC): class IDocumentRepository(ABC):
"""Интерфейс репозитория документов""" """Интерфейс репозитория документов"""
@abstractmethod @abstractmethod
async def create(self, document: Document) -> Document: async def create(self, document: Document) -> Document:
"""Создать документ""" """Создать документ"""
pass pass
@abstractmethod @abstractmethod
async def get_by_id(self, document_id: UUID) -> Optional[Document]: async def get_by_id(self, document_id: UUID) -> Optional[Document]:
"""Получить документ по ID""" """Получить документ по ID"""
pass pass
@abstractmethod @abstractmethod
async def update(self, document: Document) -> Document: async def update(self, document: Document) -> Document:
"""Обновить документ""" """Обновить документ"""
pass pass
@abstractmethod @abstractmethod
async def delete(self, document_id: UUID) -> bool: async def delete(self, document_id: UUID) -> bool:
"""Удалить документ""" """Удалить документ"""
pass pass
@abstractmethod @abstractmethod
async def list_by_collection(self, collection_id: UUID, skip: int = 0, limit: int = 100) -> list[Document]: async def list_by_collection(self, collection_id: UUID, skip: int = 0, limit: int = 100) -> list[Document]:
"""Получить документы коллекции""" """Получить документы коллекции"""
pass pass

View File

@ -1,37 +1,37 @@
""" """
Интерфейс репозитория для Message Интерфейс репозитория для Message
""" """
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from uuid import UUID from uuid import UUID
from typing import Optional from typing import Optional
from src.domain.entities.message import Message from src.domain.entities.message import Message
class IMessageRepository(ABC): class IMessageRepository(ABC):
"""Интерфейс репозитория сообщений""" """Интерфейс репозитория сообщений"""
@abstractmethod @abstractmethod
async def create(self, message: Message) -> Message: async def create(self, message: Message) -> Message:
"""Создать сообщение""" """Создать сообщение"""
pass pass
@abstractmethod @abstractmethod
async def get_by_id(self, message_id: UUID) -> Optional[Message]: async def get_by_id(self, message_id: UUID) -> Optional[Message]:
"""Получить сообщение по ID""" """Получить сообщение по ID"""
pass pass
@abstractmethod @abstractmethod
async def update(self, message: Message) -> Message: async def update(self, message: Message) -> Message:
"""Обновить сообщение""" """Обновить сообщение"""
pass pass
@abstractmethod @abstractmethod
async def delete(self, message_id: UUID) -> bool: async def delete(self, message_id: UUID) -> bool:
"""Удалить сообщение""" """Удалить сообщение"""
pass pass
@abstractmethod @abstractmethod
async def list_by_conversation(self, conversation_id: UUID, skip: int = 0, limit: int = 100) -> list[Message]: async def list_by_conversation(self, conversation_id: UUID, skip: int = 0, limit: int = 100) -> list[Message]:
"""Получить сообщения беседы""" """Получить сообщения беседы"""
pass pass

View File

@ -1,42 +1,42 @@
""" """
Интерфейс репозитория для User Интерфейс репозитория для User
""" """
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from uuid import UUID from uuid import UUID
from typing import Optional from typing import Optional
from src.domain.entities.user import User from src.domain.entities.user import User
class IUserRepository(ABC): class IUserRepository(ABC):
"""Интерфейс репозитория пользователей""" """Интерфейс репозитория пользователей"""
@abstractmethod @abstractmethod
async def create(self, user: User) -> User: async def create(self, user: User) -> User:
"""Создать пользователя""" """Создать пользователя"""
pass pass
@abstractmethod @abstractmethod
async def get_by_id(self, user_id: UUID) -> Optional[User]: async def get_by_id(self, user_id: UUID) -> Optional[User]:
"""Получить пользователя по ID""" """Получить пользователя по ID"""
pass pass
@abstractmethod @abstractmethod
async def get_by_telegram_id(self, telegram_id: str) -> Optional[User]: async def get_by_telegram_id(self, telegram_id: str) -> Optional[User]:
"""Получить пользователя по Telegram ID""" """Получить пользователя по Telegram ID"""
pass pass
@abstractmethod @abstractmethod
async def update(self, user: User) -> User: async def update(self, user: User) -> User:
"""Обновить пользователя""" """Обновить пользователя"""
pass pass
@abstractmethod @abstractmethod
async def delete(self, user_id: UUID) -> bool: async def delete(self, user_id: UUID) -> bool:
"""Удалить пользователя""" """Удалить пользователя"""
pass pass
@abstractmethod @abstractmethod
async def list_all(self, skip: int = 0, limit: int = 100) -> list[User]: async def list_all(self, skip: int = 0, limit: int = 100) -> list[User]:
"""Получить список всех пользователей""" """Получить список всех пользователей"""
pass pass

View File

@ -1,4 +1,4 @@
""" """
Infrastructure layer Infrastructure layer
""" """

View File

@ -1,4 +1,4 @@
""" """
Database infrastructure Database infrastructure
""" """

View File

@ -1,32 +1,32 @@
""" """
Базовые настройки базы данных Базовые настройки базы данных
""" """
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy.orm import declarative_base from sqlalchemy.orm import declarative_base
from src.shared.config import settings from src.shared.config import settings
engine = create_async_engine( engine = create_async_engine(
settings.database_url.replace("postgresql://", "postgresql+asyncpg://"), settings.database_url.replace("postgresql://", "postgresql+asyncpg://"),
echo=settings.DEBUG, echo=settings.DEBUG,
future=True future=True
) )
AsyncSessionLocal = async_sessionmaker( AsyncSessionLocal = async_sessionmaker(
engine, engine,
class_=AsyncSession, class_=AsyncSession,
expire_on_commit=False, expire_on_commit=False,
autocommit=False, autocommit=False,
autoflush=False autoflush=False
) )
Base = declarative_base() Base = declarative_base()
async def get_db() -> AsyncSession: async def get_db() -> AsyncSession:
"""Dependency для получения сессии БД""" """Dependency для получения сессии БД"""
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
try: try:
yield session yield session
finally: finally:
await session.close() await session.close()

View File

@ -1,109 +1,109 @@
""" """
SQLAlchemy модели для базы данных SQLAlchemy модели для базы данных
""" """
from sqlalchemy import Column, String, Text, Boolean, DateTime, ForeignKey, JSON, Integer from sqlalchemy import Column, String, Text, Boolean, DateTime, ForeignKey, JSON, Integer
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from datetime import datetime from datetime import datetime
import uuid import uuid
from src.infrastructure.database.base import Base from src.infrastructure.database.base import Base
class UserModel(Base): class UserModel(Base):
"""Модель пользователя""" """Модель пользователя"""
__tablename__ = "users" __tablename__ = "users"
user_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
telegram_id = Column(String, unique=True, nullable=False, index=True) telegram_id = Column(String, unique=True, nullable=False, index=True)
role = Column(String, nullable=False, default="user") role = Column(String, nullable=False, default="user")
created_at = Column(DateTime, nullable=False, default=datetime.utcnow) created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
collections = relationship("CollectionModel", back_populates="owner", cascade="all, delete-orphan") collections = relationship("CollectionModel", back_populates="owner", cascade="all, delete-orphan")
conversations = relationship("ConversationModel", back_populates="user", 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") collection_accesses = relationship("CollectionAccessModel", back_populates="user", cascade="all, delete-orphan")
class CollectionModel(Base): class CollectionModel(Base):
"""Модель коллекции""" """Модель коллекции"""
__tablename__ = "collections" __tablename__ = "collections"
collection_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) collection_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String, nullable=False) name = Column(String, nullable=False)
description = Column(Text, nullable=True) description = Column(Text, nullable=True)
owner_id = Column(UUID(as_uuid=True), ForeignKey("users.user_id"), nullable=False) owner_id = Column(UUID(as_uuid=True), ForeignKey("users.user_id"), nullable=False)
is_public = Column(Boolean, nullable=False, default=False) is_public = Column(Boolean, nullable=False, default=False)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow) created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
owner = relationship("UserModel", back_populates="collections") owner = relationship("UserModel", back_populates="collections")
documents = relationship("DocumentModel", back_populates="collection", cascade="all, delete-orphan") documents = relationship("DocumentModel", back_populates="collection", cascade="all, delete-orphan")
conversations = relationship("ConversationModel", 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") accesses = relationship("CollectionAccessModel", back_populates="collection", cascade="all, delete-orphan")
class DocumentModel(Base): class DocumentModel(Base):
"""Модель документа""" """Модель документа"""
__tablename__ = "documents" __tablename__ = "documents"
document_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) 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) collection_id = Column(UUID(as_uuid=True), ForeignKey("collections.collection_id"), nullable=False)
title = Column(String, nullable=False) title = Column(String, nullable=False)
content = Column(Text, nullable=False) content = Column(Text, nullable=False)
document_metadata = Column("metadata", JSON, nullable=True, default={}) document_metadata = Column("metadata", JSON, nullable=True, default={})
created_at = Column(DateTime, nullable=False, default=datetime.utcnow) created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
collection = relationship("CollectionModel", back_populates="documents") collection = relationship("CollectionModel", back_populates="documents")
embeddings = relationship("EmbeddingModel", back_populates="document", cascade="all, delete-orphan") embeddings = relationship("EmbeddingModel", back_populates="document", cascade="all, delete-orphan")
class EmbeddingModel(Base): class EmbeddingModel(Base):
"""Модель эмбеддинга (заглушка)""" """Модель эмбеддинга (заглушка)"""
__tablename__ = "embeddings" __tablename__ = "embeddings"
embedding_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) 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) document_id = Column(UUID(as_uuid=True), ForeignKey("documents.document_id"), nullable=False)
embedding = Column(JSON, nullable=True) embedding = Column(JSON, nullable=True)
model_version = Column(String, nullable=True) model_version = Column(String, nullable=True)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow) created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
document = relationship("DocumentModel", back_populates="embeddings") document = relationship("DocumentModel", back_populates="embeddings")
class ConversationModel(Base): class ConversationModel(Base):
"""Модель беседы""" """Модель беседы"""
__tablename__ = "conversations" __tablename__ = "conversations"
conversation_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) 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) 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) collection_id = Column(UUID(as_uuid=True), ForeignKey("collections.collection_id"), nullable=False)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow) created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
updated_at = Column(DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow) updated_at = Column(DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow)
user = relationship("UserModel", back_populates="conversations") user = relationship("UserModel", back_populates="conversations")
collection = relationship("CollectionModel", back_populates="conversations") collection = relationship("CollectionModel", back_populates="conversations")
messages = relationship("MessageModel", back_populates="conversation", cascade="all, delete-orphan") messages = relationship("MessageModel", back_populates="conversation", cascade="all, delete-orphan")
class MessageModel(Base): class MessageModel(Base):
"""Модель сообщения""" """Модель сообщения"""
__tablename__ = "messages" __tablename__ = "messages"
message_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) 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) conversation_id = Column(UUID(as_uuid=True), ForeignKey("conversations.conversation_id"), nullable=False)
content = Column(Text, nullable=False) content = Column(Text, nullable=False)
role = Column(String, nullable=False) role = Column(String, nullable=False)
sources = Column(JSON, nullable=True, default={}) sources = Column(JSON, nullable=True, default={})
created_at = Column(DateTime, nullable=False, default=datetime.utcnow) created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
conversation = relationship("ConversationModel", back_populates="messages") conversation = relationship("ConversationModel", back_populates="messages")
class CollectionAccessModel(Base): class CollectionAccessModel(Base):
"""Модель доступа к коллекции""" """Модель доступа к коллекции"""
__tablename__ = "collection_access" __tablename__ = "collection_access"
access_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) 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) 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) collection_id = Column(UUID(as_uuid=True), ForeignKey("collections.collection_id"), nullable=False)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow) created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
user = relationship("UserModel", back_populates="collection_accesses") user = relationship("UserModel", back_populates="collection_accesses")
collection = relationship("CollectionModel", back_populates="accesses") collection = relationship("CollectionModel", back_populates="accesses")
__table_args__ = ( __table_args__ = (
{"comment": "Уникальный доступ пользователя к коллекции"}, {"comment": "Уникальный доступ пользователя к коллекции"},
) )

View File

@ -1,4 +1,4 @@
""" """
External services External services
""" """

View File

@ -1,223 +1,223 @@
""" """
Клиент для работы с DeepSeek API Клиент для работы с DeepSeek API
""" """
import json import json
from typing import Optional, AsyncIterator from typing import Optional, AsyncIterator
import httpx import httpx
from src.shared.config import settings from src.shared.config import settings
class DeepSeekAPIError(Exception): class DeepSeekAPIError(Exception):
"""Ошибка при работе с DeepSeek API""" """Ошибка при работе с DeepSeek API"""
pass pass
class DeepSeekClient: class DeepSeekClient:
"""Клиент для работы с DeepSeek API""" """Клиент для работы с DeepSeek API"""
def __init__(self, api_key: str | None = None, api_url: str | None = None): def __init__(self, api_key: str | None = None, api_url: str | None = None):
self.api_key = api_key or settings.DEEPSEEK_API_KEY self.api_key = api_key or settings.DEEPSEEK_API_KEY
self.api_url = api_url or settings.DEEPSEEK_API_URL self.api_url = api_url or settings.DEEPSEEK_API_URL
self.timeout = 60.0 self.timeout = 60.0
def _get_headers(self) -> dict[str, str]: def _get_headers(self) -> dict[str, str]:
"""Получить заголовки для запроса""" """Получить заголовки для запроса"""
if not self.api_key: if not self.api_key:
raise DeepSeekAPIError("DEEPSEEK_API_KEY не установлен в настройках") raise DeepSeekAPIError("DEEPSEEK_API_KEY не установлен в настройках")
return { return {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}" "Authorization": f"Bearer {self.api_key}"
} }
async def chat_completion( async def chat_completion(
self, self,
messages: list[dict[str, str]], messages: list[dict[str, str]],
model: str = "deepseek-chat", model: str = "deepseek-chat",
temperature: float = 0.7, temperature: float = 0.7,
max_tokens: Optional[int] = None, max_tokens: Optional[int] = None,
stream: bool = False stream: bool = False
) -> dict: ) -> dict:
""" """
Отправка запроса на генерацию ответа Отправка запроса на генерацию ответа
Args: Args:
messages: Список сообщений в формате [{"role": "user", "content": "..."}] messages: Список сообщений в формате [{"role": "user", "content": "..."}]
model: Модель для использования (по умолчанию "deepseek-chat") model: Модель для использования (по умолчанию "deepseek-chat")
temperature: Температура генерации (0.0-2.0) temperature: Температура генерации (0.0-2.0)
max_tokens: Максимальное количество токенов в ответе max_tokens: Максимальное количество токенов в ответе
stream: Использовать ли потоковую генерацию stream: Использовать ли потоковую генерацию
Returns: Returns:
Ответ от API в формате: Ответ от API в формате:
{ {
"content": "текст ответа", "content": "текст ответа",
"usage": { "usage": {
"prompt_tokens": int, "prompt_tokens": int,
"completion_tokens": int, "completion_tokens": int,
"total_tokens": int "total_tokens": int
} }
} }
Raises: Raises:
DeepSeekAPIError: При ошибке API DeepSeekAPIError: При ошибке API
""" """
if not self.api_key: if not self.api_key:
return { return {
"content": " DEEPSEEK_API_KEY не установлен. Установите ключ в настройках для работы с DeepSeek API.", "content": " DEEPSEEK_API_KEY не установлен. Установите ключ в настройках для работы с DeepSeek API.",
"usage": { "usage": {
"prompt_tokens": 0, "prompt_tokens": 0,
"completion_tokens": 0, "completion_tokens": 0,
"total_tokens": 0 "total_tokens": 0
} }
} }
payload = { payload = {
"model": model, "model": model,
"messages": messages, "messages": messages,
"temperature": temperature, "temperature": temperature,
"stream": stream "stream": stream
} }
if max_tokens is not None: if max_tokens is not None:
payload["max_tokens"] = max_tokens payload["max_tokens"] = max_tokens
try: try:
async with httpx.AsyncClient(timeout=self.timeout) as client: async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.post( response = await client.post(
self.api_url, self.api_url,
headers=self._get_headers(), headers=self._get_headers(),
json=payload json=payload
) )
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
if "choices" in data and len(data["choices"]) > 0: if "choices" in data and len(data["choices"]) > 0:
content = data["choices"][0]["message"]["content"] content = data["choices"][0]["message"]["content"]
else: else:
raise DeepSeekAPIError("Неожиданный формат ответа от DeepSeek API") raise DeepSeekAPIError("Неожиданный формат ответа от DeepSeek API")
usage = data.get("usage", {}) usage = data.get("usage", {})
return { return {
"content": content, "content": content,
"usage": { "usage": {
"prompt_tokens": usage.get("prompt_tokens", 0), "prompt_tokens": usage.get("prompt_tokens", 0),
"completion_tokens": usage.get("completion_tokens", 0), "completion_tokens": usage.get("completion_tokens", 0),
"total_tokens": usage.get("total_tokens", 0) "total_tokens": usage.get("total_tokens", 0)
} }
} }
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
error_msg = f"Ошибка DeepSeek API: {e.response.status_code}" error_msg = f"Ошибка DeepSeek API: {e.response.status_code}"
try: try:
error_data = e.response.json() error_data = e.response.json()
if "error" in error_data: if "error" in error_data:
error_msg = f"Ошибка DeepSeek API: {error_data['error'].get('message', error_msg)}" error_msg = f"Ошибка DeepSeek API: {error_data['error'].get('message', error_msg)}"
except: except:
pass pass
raise DeepSeekAPIError(error_msg) from e raise DeepSeekAPIError(error_msg) from e
except httpx.RequestError as e: except httpx.RequestError as e:
raise DeepSeekAPIError(f"Ошибка подключения к DeepSeek API: {str(e)}") from e raise DeepSeekAPIError(f"Ошибка подключения к DeepSeek API: {str(e)}") from e
except Exception as e: except Exception as e:
raise DeepSeekAPIError(f"Неожиданная ошибка при работе с DeepSeek API: {str(e)}") from e raise DeepSeekAPIError(f"Неожиданная ошибка при работе с DeepSeek API: {str(e)}") from e
async def stream_chat_completion( async def stream_chat_completion(
self, self,
messages: list[dict[str, str]], messages: list[dict[str, str]],
model: str = "deepseek-chat", model: str = "deepseek-chat",
temperature: float = 0.7, temperature: float = 0.7,
max_tokens: Optional[int] = None max_tokens: Optional[int] = None
) -> AsyncIterator[str]: ) -> AsyncIterator[str]:
""" """
Потоковая генерация ответа Потоковая генерация ответа
Args: Args:
messages: Список сообщений в формате [{"role": "user", "content": "..."}] messages: Список сообщений в формате [{"role": "user", "content": "..."}]
model: Модель для использования model: Модель для использования
temperature: Температура генерации temperature: Температура генерации
max_tokens: Максимальное количество токенов max_tokens: Максимальное количество токенов
Yields: Yields:
Части ответа (chunks) по мере генерации Части ответа (chunks) по мере генерации
Raises: Raises:
DeepSeekAPIError: При ошибке API DeepSeekAPIError: При ошибке API
""" """
if not self.api_key: if not self.api_key:
yield "⚠️ DEEPSEEK_API_KEY не установлен. Установите ключ в настройках для работы с DeepSeek API." yield "⚠️ DEEPSEEK_API_KEY не установлен. Установите ключ в настройках для работы с DeepSeek API."
return return
payload = { payload = {
"model": model, "model": model,
"messages": messages, "messages": messages,
"temperature": temperature, "temperature": temperature,
"stream": True "stream": True
} }
if max_tokens is not None: if max_tokens is not None:
payload["max_tokens"] = max_tokens payload["max_tokens"] = max_tokens
try: try:
async with httpx.AsyncClient(timeout=self.timeout) as client: async with httpx.AsyncClient(timeout=self.timeout) as client:
async with client.stream( async with client.stream(
"POST", "POST",
self.api_url, self.api_url,
headers=self._get_headers(), headers=self._get_headers(),
json=payload json=payload
) as response: ) as response:
response.raise_for_status() response.raise_for_status()
async for line in response.aiter_lines(): async for line in response.aiter_lines():
if not line.strip(): if not line.strip():
continue continue
if line.startswith("data: "): if line.startswith("data: "):
line = line[6:] line = line[6:]
if line.strip() == "[DONE]": if line.strip() == "[DONE]":
break break
try: try:
data = json.loads(line) data = json.loads(line)
if "choices" in data and len(data["choices"]) > 0: if "choices" in data and len(data["choices"]) > 0:
delta = data["choices"][0].get("delta", {}) delta = data["choices"][0].get("delta", {})
content = delta.get("content", "") content = delta.get("content", "")
if content: if content:
yield content yield content
except json.JSONDecodeError: except json.JSONDecodeError:
continue continue
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
error_msg = f"Ошибка DeepSeek API: {e.response.status_code}" error_msg = f"Ошибка DeepSeek API: {e.response.status_code}"
try: try:
error_data = e.response.json() error_data = e.response.json()
if "error" in error_data: if "error" in error_data:
error_msg = f"Ошибка DeepSeek API: {error_data['error'].get('message', error_msg)}" error_msg = f"Ошибка DeepSeek API: {error_data['error'].get('message', error_msg)}"
except: except:
pass pass
raise DeepSeekAPIError(error_msg) from e raise DeepSeekAPIError(error_msg) from e
except httpx.RequestError as e: except httpx.RequestError as e:
raise DeepSeekAPIError(f"Ошибка подключения к DeepSeek API: {str(e)}") from e raise DeepSeekAPIError(f"Ошибка подключения к DeepSeek API: {str(e)}") from e
except Exception as e: except Exception as e:
raise DeepSeekAPIError(f"Неожиданная ошибка при потоковой генерации: {str(e)}") from e raise DeepSeekAPIError(f"Неожиданная ошибка при потоковой генерации: {str(e)}") from e
async def health_check(self) -> bool: async def health_check(self) -> bool:
""" """
Проверка доступности API Проверка доступности API
Returns: Returns:
True если API доступен, False иначе True если API доступен, False иначе
""" """
if not self.api_key: if not self.api_key:
return False return False
try: try:
test_messages = [{"role": "user", "content": "test"}] test_messages = [{"role": "user", "content": "test"}]
await self.chat_completion(test_messages, max_tokens=1) await self.chat_completion(test_messages, max_tokens=1)
return True return True
except Exception: except Exception:
return False return False

View File

@ -1,35 +1,35 @@
""" """
Сервис для работы с Telegram Bot API Сервис для работы с Telegram Bot API
""" """
from typing import Optional from typing import Optional
from src.shared.config import settings from src.shared.config import settings
class TelegramAuthService: class TelegramAuthService:
""" """
Сервис для работы с Telegram Bot API Сервис для работы с Telegram Bot API
""" """
def __init__(self, bot_token: str | None = None): def __init__(self, bot_token: str | None = None):
self.bot_token = bot_token or settings.TELEGRAM_BOT_TOKEN self.bot_token = bot_token or settings.TELEGRAM_BOT_TOKEN
async def get_user_info(self, telegram_id: str) -> Optional[dict]: async def get_user_info(self, telegram_id: str) -> Optional[dict]:
""" """
Получение информации о пользователе через Telegram Bot API Получение информации о пользователе через Telegram Bot API
Args: Args:
telegram_id: ID пользователя в Telegram telegram_id: ID пользователя в Telegram
Returns: Returns:
Информация о пользователе или None Информация о пользователе или None
""" """
if not self.bot_token: if not self.bot_token:
return None return None
return { return {
"id": telegram_id, "id": telegram_id,
"first_name": "User", "first_name": "User",
"username": None "username": None
} }

View File

@ -1,280 +1,280 @@
""" """
Интеграция с Yandex Vision OCR для парсинга документов Интеграция с Yandex Vision OCR для парсинга документов
""" """
import base64 import base64
import io import io
from typing import BinaryIO from typing import BinaryIO
import httpx import httpx
import fitz import fitz
from PIL import Image from PIL import Image
from src.shared.config import settings from src.shared.config import settings
class YandexOCRError(Exception): class YandexOCRError(Exception):
"""Ошибка при работе с Yandex OCR API""" """Ошибка при работе с Yandex OCR API"""
pass pass
class YandexOCRService: class YandexOCRService:
"""Сервис для работы с Yandex Vision OCR""" """Сервис для работы с Yandex Vision OCR"""
def __init__(self, api_key: str | None = None): def __init__(self, api_key: str | None = None):
self.api_key = api_key or settings.YANDEX_OCR_API_KEY self.api_key = api_key or settings.YANDEX_OCR_API_KEY
self.api_url = settings.YANDEX_OCR_API_URL self.api_url = settings.YANDEX_OCR_API_URL
self.timeout = 120.0 self.timeout = 120.0
self.max_file_size = 10 * 1024 * 1024 self.max_file_size = 10 * 1024 * 1024
def _get_headers(self) -> dict[str, str]: def _get_headers(self) -> dict[str, str]:
"""Получить заголовки для запроса""" """Получить заголовки для запроса"""
if not self.api_key: if not self.api_key:
raise YandexOCRError("YANDEX_OCR_API_KEY не установлен в настройках") raise YandexOCRError("YANDEX_OCR_API_KEY не установлен в настройках")
return { return {
"Authorization": f"Api-Key {self.api_key}", "Authorization": f"Api-Key {self.api_key}",
"Content-Type": "application/json" "Content-Type": "application/json"
} }
def _validate_file_size(self, file_content: bytes) -> None: def _validate_file_size(self, file_content: bytes) -> None:
"""Проверка размера файла""" """Проверка размера файла"""
if len(file_content) > self.max_file_size: if len(file_content) > self.max_file_size:
raise YandexOCRError( raise YandexOCRError(
f"Файл слишком большой: {len(file_content)} байт. " f"Файл слишком большой: {len(file_content)} байт. "
f"Максимальный размер: {self.max_file_size} байт (10 МБ)" f"Максимальный размер: {self.max_file_size} байт (10 МБ)"
) )
async def extract_text( async def extract_text(
self, self,
file_content: bytes, file_content: bytes,
file_type: str = "pdf", file_type: str = "pdf",
language_codes: list[str] | None = None language_codes: list[str] | None = None
) -> str: ) -> str:
""" """
Извлечение текста из файла через Yandex Vision OCR Извлечение текста из файла через Yandex Vision OCR
Args: Args:
file_content: Содержимое файла в байтах file_content: Содержимое файла в байтах
file_type: Тип файла (pdf, image) file_type: Тип файла (pdf, image)
language_codes: Коды языков для распознавания (по умолчанию ['ru', 'en']) language_codes: Коды языков для распознавания (по умолчанию ['ru', 'en'])
Returns: Returns:
Извлеченный текст Извлеченный текст
Raises: Raises:
YandexOCRError: При ошибке API YandexOCRError: При ошибке API
""" """
if not self.api_key: if not self.api_key:
return " YANDEX_OCR_API_KEY не установлен. Установите ключ в настройках для распознавания документов." return " YANDEX_OCR_API_KEY не установлен. Установите ключ в настройках для распознавания документов."
self._validate_file_size(file_content) self._validate_file_size(file_content)
image_data = base64.b64encode(file_content).decode('utf-8') image_data = base64.b64encode(file_content).decode('utf-8')
if language_codes is None: if language_codes is None:
language_codes = ['ru', 'en'] language_codes = ['ru', 'en']
model = 'page' model = 'page'
payload = { payload = {
"analyze_specs": [{ "analyze_specs": [{
"content": image_data, "content": image_data,
"features": [{ "features": [{
"type": "TEXT_DETECTION", "type": "TEXT_DETECTION",
"text_detection_config": { "text_detection_config": {
"model": model, "model": model,
"language_codes": language_codes "language_codes": language_codes
} }
}] }]
}] }]
} }
try: try:
async with httpx.AsyncClient(timeout=self.timeout) as client: async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.post( response = await client.post(
self.api_url, self.api_url,
headers=self._get_headers(), headers=self._get_headers(),
json=payload json=payload
) )
response.raise_for_status() response.raise_for_status()
result = response.json() result = response.json()
return self._extract_text_from_response(result) return self._extract_text_from_response(result)
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
error_msg = f"Ошибка Yandex OCR API: {e.response.status_code}" error_msg = f"Ошибка Yandex OCR API: {e.response.status_code}"
try: try:
error_data = e.response.json() error_data = e.response.json()
if "message" in error_data: if "message" in error_data:
error_msg = f"Ошибка Yandex OCR API: {error_data['message']}" error_msg = f"Ошибка Yandex OCR API: {error_data['message']}"
except: except:
pass pass
raise YandexOCRError(error_msg) from e raise YandexOCRError(error_msg) from e
except httpx.RequestError as e: except httpx.RequestError as e:
raise YandexOCRError(f"Ошибка подключения к Yandex OCR API: {str(e)}") from e raise YandexOCRError(f"Ошибка подключения к Yandex OCR API: {str(e)}") from e
except YandexOCRError: except YandexOCRError:
raise raise
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
raise YandexOCRError(f"Неожиданная ошибка при работе с Yandex OCR: {str(e)}\n{error_details}") from e raise YandexOCRError(f"Неожиданная ошибка при работе с Yandex OCR: {str(e)}\n{error_details}") from e
def _extract_text_from_response(self, response: dict) -> str: def _extract_text_from_response(self, response: dict) -> str:
""" """
Извлечение текста из ответа Yandex Vision API Извлечение текста из ответа Yandex Vision API
Args: Args:
response: JSON ответ от API response: JSON ответ от API
Returns: Returns:
Извлеченный текст Извлеченный текст
""" """
import json import json
if not self.api_key: if not self.api_key:
return " YANDEX_OCR_API_KEY не установлен. Установите ключ в настройках для распознавания документов." return " YANDEX_OCR_API_KEY не установлен. Установите ключ в настройках для распознавания документов."
text_parts = [] text_parts = []
if "results" not in response: if "results" not in response:
if "error" in response: if "error" in response:
error_msg = response.get("error", {}).get("message", "Неизвестная ошибка") error_msg = response.get("error", {}).get("message", "Неизвестная ошибка")
raise YandexOCRError(f"Ошибка Yandex OCR API: {error_msg}") raise YandexOCRError(f"Ошибка Yandex OCR API: {error_msg}")
raise YandexOCRError(f"Неожиданный формат ответа от Yandex OCR API. Структура: {list(response.keys())}") raise YandexOCRError(f"Неожиданный формат ответа от Yandex OCR API. Структура: {list(response.keys())}")
for result in response["results"]: for result in response["results"]:
if "results" not in result: if "results" not in result:
continue continue
for annotation in result["results"]: for annotation in result["results"]:
if "textDetection" not in annotation: if "textDetection" not in annotation:
continue continue
text_detection = annotation["textDetection"] text_detection = annotation["textDetection"]
if "pages" in text_detection: if "pages" in text_detection:
for page in text_detection["pages"]: for page in text_detection["pages"]:
if "blocks" in page: if "blocks" in page:
for block in page["blocks"]: for block in page["blocks"]:
if "lines" in block: if "lines" in block:
for line in block["lines"]: for line in block["lines"]:
if "words" in line: if "words" in line:
line_text = " ".join([ line_text = " ".join([
word.get("text", "") word.get("text", "")
for word in line["words"] for word in line["words"]
]) ])
if line_text: if line_text:
text_parts.append(line_text) text_parts.append(line_text)
full_text = "\n".join(text_parts) full_text = "\n".join(text_parts)
if not full_text.strip(): if not full_text.strip():
return f" Не удалось извлечь текст из документа. Возможно, документ пустой или нечитаемый. Структура ответа: {json.dumps(response, indent=2, ensure_ascii=False)[:500]}" return f" Не удалось извлечь текст из документа. Возможно, документ пустой или нечитаемый. Структура ответа: {json.dumps(response, indent=2, ensure_ascii=False)[:500]}"
return full_text return full_text
async def parse_pdf(self, file: BinaryIO) -> str: async def parse_pdf(self, file: BinaryIO) -> str:
""" """
Парсинг PDF документа через YandexOCR Парсинг PDF документа через YandexOCR
Yandex Vision API не поддерживает PDF напрямую, поэтому Yandex Vision API не поддерживает PDF напрямую, поэтому
конвертируем каждую страницу PDF в изображение и распознаем отдельно. конвертируем каждую страницу PDF в изображение и распознаем отдельно.
Args: Args:
file: Файловый объект PDF file: Файловый объект PDF
Returns: Returns:
Текст из документа (объединенный текст со всех страниц) Текст из документа (объединенный текст со всех страниц)
""" """
file_content = await self._read_file(file) file_content = await self._read_file(file)
images = await self._pdf_to_images(file_content) images = await self._pdf_to_images(file_content)
if not images: if not images:
return " Не удалось конвертировать PDF в изображения. Возможно, файл поврежден." return " Не удалось конвертировать PDF в изображения. Возможно, файл поврежден."
all_text_parts = [] all_text_parts = []
for i, image_bytes in enumerate(images, 1): for i, image_bytes in enumerate(images, 1):
try: try:
page_text = await self.extract_text(image_bytes, file_type="image") page_text = await self.extract_text(image_bytes, file_type="image")
if page_text and not page_text.startswith("Ошибка распознавания:"): if page_text and not page_text.startswith("Ошибка распознавания:"):
all_text_parts.append(f"--- Страница {i} ---\n{page_text}") all_text_parts.append(f"--- Страница {i} ---\n{page_text}")
except YandexOCRError as e: except YandexOCRError as e:
all_text_parts.append(f"--- Страница {i} ---\n Ошибка распознавания: {str(e)}") all_text_parts.append(f"--- Страница {i} ---\n Ошибка распознавания: {str(e)}")
if not all_text_parts: if not all_text_parts:
return " Не удалось распознать текст ни с одной страницы PDF." return " Не удалось распознать текст ни с одной страницы PDF."
return "\n\n".join(all_text_parts) return "\n\n".join(all_text_parts)
async def _pdf_to_images(self, pdf_content: bytes) -> list[bytes]: async def _pdf_to_images(self, pdf_content: bytes) -> list[bytes]:
""" """
Конвертация PDF в список изображений (по одной на страницу) Конвертация PDF в список изображений (по одной на страницу)
Args: Args:
pdf_content: Содержимое PDF файла в байтах pdf_content: Содержимое PDF файла в байтах
Returns: Returns:
Список изображений в формате PNG (каждое в байтах) Список изображений в формате PNG (каждое в байтах)
""" """
try: try:
pdf_document = fitz.open(stream=pdf_content, filetype="pdf") pdf_document = fitz.open(stream=pdf_content, filetype="pdf")
images = [] images = []
for page_num in range(len(pdf_document)): for page_num in range(len(pdf_document)):
page = pdf_document[page_num] page = pdf_document[page_num]
mat = fitz.Matrix(2.0, 2.0) mat = fitz.Matrix(2.0, 2.0)
pix = page.get_pixmap(matrix=mat) pix = page.get_pixmap(matrix=mat)
img_data = pix.tobytes("png") img_data = pix.tobytes("png")
images.append(img_data) images.append(img_data)
pdf_document.close() pdf_document.close()
return images return images
except Exception as e: except Exception as e:
raise YandexOCRError(f"Ошибка при конвертации PDF в изображения: {str(e)}") from e raise YandexOCRError(f"Ошибка при конвертации PDF в изображения: {str(e)}") from e
async def parse_image(self, file: BinaryIO) -> str: async def parse_image(self, file: BinaryIO) -> str:
""" """
Парсинг изображения через YandexOCR Парсинг изображения через YandexOCR
Args: Args:
file: Файловый объект изображения (PNG, JPEG, etc.) file: Файловый объект изображения (PNG, JPEG, etc.)
Returns: Returns:
Текст из изображения Текст из изображения
""" """
file_content = await self._read_file(file) file_content = await self._read_file(file)
return await self.extract_text(file_content, file_type="image") return await self.extract_text(file_content, file_type="image")
async def _read_file(self, file: BinaryIO) -> bytes: async def _read_file(self, file: BinaryIO) -> bytes:
""" """
Чтение содержимого файла в байты Чтение содержимого файла в байты
Args: Args:
file: Файловый объект file: Файловый объект
Returns: Returns:
Содержимое файла в байтах Содержимое файла в байтах
""" """
if hasattr(file, 'read'): if hasattr(file, 'read'):
content = file.read() content = file.read()
if hasattr(file, 'seek'): if hasattr(file, 'seek'):
file.seek(0) file.seek(0)
return content return content
else: else:
raise YandexOCRError("Некорректный файловый объект") raise YandexOCRError("Некорректный файловый объект")
async def health_check(self) -> bool: async def health_check(self) -> bool:
""" """
Проверка доступности API Проверка доступности API
Returns: Returns:
True если API доступен, False иначе True если API доступен, False иначе
""" """
if not self.api_key: if not self.api_key:
return False return False
return True return True

View File

@ -1,4 +1,4 @@
""" """
PostgreSQL repository implementations PostgreSQL repository implementations
""" """

View File

@ -1,107 +1,107 @@
""" """
Реализация репозитория доступа к коллекциям для PostgreSQL Реализация репозитория доступа к коллекциям для PostgreSQL
""" """
from uuid import UUID from uuid import UUID
from typing import Optional from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
from src.domain.entities.collection_access import CollectionAccess from src.domain.entities.collection_access import CollectionAccess
from src.domain.repositories.collection_access_repository import ICollectionAccessRepository from src.domain.repositories.collection_access_repository import ICollectionAccessRepository
from src.infrastructure.database.models import CollectionAccessModel from src.infrastructure.database.models import CollectionAccessModel
from src.shared.exceptions import NotFoundError from src.shared.exceptions import NotFoundError
class PostgreSQLCollectionAccessRepository(ICollectionAccessRepository): class PostgreSQLCollectionAccessRepository(ICollectionAccessRepository):
"""PostgreSQL реализация репозитория доступа к коллекциям""" """PostgreSQL реализация репозитория доступа к коллекциям"""
def __init__(self, session: AsyncSession): def __init__(self, session: AsyncSession):
self.session = session self.session = session
async def create(self, access: CollectionAccess) -> CollectionAccess: async def create(self, access: CollectionAccess) -> CollectionAccess:
"""Создать доступ""" """Создать доступ"""
db_access = CollectionAccessModel( db_access = CollectionAccessModel(
access_id=access.access_id, access_id=access.access_id,
user_id=access.user_id, user_id=access.user_id,
collection_id=access.collection_id, collection_id=access.collection_id,
created_at=access.created_at created_at=access.created_at
) )
self.session.add(db_access) self.session.add(db_access)
await self.session.commit() await self.session.commit()
await self.session.refresh(db_access) await self.session.refresh(db_access)
return self._to_entity(db_access) return self._to_entity(db_access)
async def get_by_id(self, access_id: UUID) -> Optional[CollectionAccess]: async def get_by_id(self, access_id: UUID) -> Optional[CollectionAccess]:
"""Получить доступ по ID""" """Получить доступ по ID"""
result = await self.session.execute( result = await self.session.execute(
select(CollectionAccessModel).where(CollectionAccessModel.access_id == access_id) select(CollectionAccessModel).where(CollectionAccessModel.access_id == access_id)
) )
db_access = result.scalar_one_or_none() db_access = result.scalar_one_or_none()
return self._to_entity(db_access) if db_access else None return self._to_entity(db_access) if db_access else None
async def delete(self, access_id: UUID) -> bool: async def delete(self, access_id: UUID) -> bool:
"""Удалить доступ""" """Удалить доступ"""
result = await self.session.execute( result = await self.session.execute(
select(CollectionAccessModel).where(CollectionAccessModel.access_id == access_id) select(CollectionAccessModel).where(CollectionAccessModel.access_id == access_id)
) )
db_access = result.scalar_one_or_none() db_access = result.scalar_one_or_none()
if not db_access: if not db_access:
return False return False
await self.session.delete(db_access) await self.session.delete(db_access)
await self.session.commit() await self.session.commit()
return True return True
async def delete_by_user_and_collection(self, user_id: UUID, collection_id: UUID) -> bool: async def delete_by_user_and_collection(self, user_id: UUID, collection_id: UUID) -> bool:
"""Удалить доступ пользователя к коллекции""" """Удалить доступ пользователя к коллекции"""
result = await self.session.execute( result = await self.session.execute(
select(CollectionAccessModel).where( select(CollectionAccessModel).where(
CollectionAccessModel.user_id == user_id, CollectionAccessModel.user_id == user_id,
CollectionAccessModel.collection_id == collection_id CollectionAccessModel.collection_id == collection_id
) )
) )
db_access = result.scalar_one_or_none() db_access = result.scalar_one_or_none()
if not db_access: if not db_access:
return False return False
await self.session.delete(db_access) await self.session.delete(db_access)
await self.session.commit() await self.session.commit()
return True return True
async def get_by_user_and_collection(self, user_id: UUID, collection_id: UUID) -> Optional[CollectionAccess]: async def get_by_user_and_collection(self, user_id: UUID, collection_id: UUID) -> Optional[CollectionAccess]:
"""Получить доступ пользователя к коллекции""" """Получить доступ пользователя к коллекции"""
result = await self.session.execute( result = await self.session.execute(
select(CollectionAccessModel).where( select(CollectionAccessModel).where(
CollectionAccessModel.user_id == user_id, CollectionAccessModel.user_id == user_id,
CollectionAccessModel.collection_id == collection_id CollectionAccessModel.collection_id == collection_id
) )
) )
db_access = result.scalar_one_or_none() db_access = result.scalar_one_or_none()
return self._to_entity(db_access) if db_access else None return self._to_entity(db_access) if db_access else None
async def list_by_user(self, user_id: UUID) -> list[CollectionAccess]: async def list_by_user(self, user_id: UUID) -> list[CollectionAccess]:
"""Получить доступы пользователя""" """Получить доступы пользователя"""
result = await self.session.execute( result = await self.session.execute(
select(CollectionAccessModel).where(CollectionAccessModel.user_id == user_id) select(CollectionAccessModel).where(CollectionAccessModel.user_id == user_id)
) )
db_accesses = result.scalars().all() db_accesses = result.scalars().all()
return [self._to_entity(db_access) for db_access in db_accesses] return [self._to_entity(db_access) for db_access in db_accesses]
async def list_by_collection(self, collection_id: UUID) -> list[CollectionAccess]: async def list_by_collection(self, collection_id: UUID) -> list[CollectionAccess]:
"""Получить доступы к коллекции""" """Получить доступы к коллекции"""
result = await self.session.execute( result = await self.session.execute(
select(CollectionAccessModel).where(CollectionAccessModel.collection_id == collection_id) select(CollectionAccessModel).where(CollectionAccessModel.collection_id == collection_id)
) )
db_accesses = result.scalars().all() db_accesses = result.scalars().all()
return [self._to_entity(db_access) for db_access in db_accesses] return [self._to_entity(db_access) for db_access in db_accesses]
def _to_entity(self, db_access: CollectionAccessModel | None) -> CollectionAccess | None: def _to_entity(self, db_access: CollectionAccessModel | None) -> CollectionAccess | None:
"""Преобразовать модель БД в доменную сущность""" """Преобразовать модель БД в доменную сущность"""
if not db_access: if not db_access:
return None return None
return CollectionAccess( return CollectionAccess(
access_id=db_access.access_id, access_id=db_access.access_id,
user_id=db_access.user_id, user_id=db_access.user_id,
collection_id=db_access.collection_id, collection_id=db_access.collection_id,
created_at=db_access.created_at created_at=db_access.created_at
) )

View File

@ -1,106 +1,106 @@
""" """
Реализация репозитория коллекций для PostgreSQL Реализация репозитория коллекций для PostgreSQL
""" """
from uuid import UUID from uuid import UUID
from typing import Optional from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
from src.domain.entities.collection import Collection from src.domain.entities.collection import Collection
from src.domain.repositories.collection_repository import ICollectionRepository from src.domain.repositories.collection_repository import ICollectionRepository
from src.infrastructure.database.models import CollectionModel from src.infrastructure.database.models import CollectionModel
from src.shared.exceptions import NotFoundError from src.shared.exceptions import NotFoundError
class PostgreSQLCollectionRepository(ICollectionRepository): class PostgreSQLCollectionRepository(ICollectionRepository):
"""PostgreSQL реализация репозитория коллекций""" """PostgreSQL реализация репозитория коллекций"""
def __init__(self, session: AsyncSession): def __init__(self, session: AsyncSession):
self.session = session self.session = session
async def create(self, collection: Collection) -> Collection: async def create(self, collection: Collection) -> Collection:
"""Создать коллекцию""" """Создать коллекцию"""
db_collection = CollectionModel( db_collection = CollectionModel(
collection_id=collection.collection_id, collection_id=collection.collection_id,
name=collection.name, name=collection.name,
description=collection.description, description=collection.description,
owner_id=collection.owner_id, owner_id=collection.owner_id,
is_public=collection.is_public, is_public=collection.is_public,
created_at=collection.created_at created_at=collection.created_at
) )
self.session.add(db_collection) self.session.add(db_collection)
await self.session.commit() await self.session.commit()
await self.session.refresh(db_collection) await self.session.refresh(db_collection)
return self._to_entity(db_collection) return self._to_entity(db_collection)
async def get_by_id(self, collection_id: UUID) -> Optional[Collection]: async def get_by_id(self, collection_id: UUID) -> Optional[Collection]:
"""Получить коллекцию по ID""" """Получить коллекцию по ID"""
result = await self.session.execute( result = await self.session.execute(
select(CollectionModel).where(CollectionModel.collection_id == collection_id) select(CollectionModel).where(CollectionModel.collection_id == collection_id)
) )
db_collection = result.scalar_one_or_none() db_collection = result.scalar_one_or_none()
return self._to_entity(db_collection) if db_collection else None return self._to_entity(db_collection) if db_collection else None
async def update(self, collection: Collection) -> Collection: async def update(self, collection: Collection) -> Collection:
"""Обновить коллекцию""" """Обновить коллекцию"""
result = await self.session.execute( result = await self.session.execute(
select(CollectionModel).where(CollectionModel.collection_id == collection.collection_id) select(CollectionModel).where(CollectionModel.collection_id == collection.collection_id)
) )
db_collection = result.scalar_one_or_none() db_collection = result.scalar_one_or_none()
if not db_collection: if not db_collection:
raise NotFoundError(f"Коллекция {collection.collection_id} не найдена") raise NotFoundError(f"Коллекция {collection.collection_id} не найдена")
db_collection.name = collection.name db_collection.name = collection.name
db_collection.description = collection.description db_collection.description = collection.description
db_collection.is_public = collection.is_public db_collection.is_public = collection.is_public
await self.session.commit() await self.session.commit()
await self.session.refresh(db_collection) await self.session.refresh(db_collection)
return self._to_entity(db_collection) return self._to_entity(db_collection)
async def delete(self, collection_id: UUID) -> bool: async def delete(self, collection_id: UUID) -> bool:
"""Удалить коллекцию""" """Удалить коллекцию"""
result = await self.session.execute( result = await self.session.execute(
select(CollectionModel).where(CollectionModel.collection_id == collection_id) select(CollectionModel).where(CollectionModel.collection_id == collection_id)
) )
db_collection = result.scalar_one_or_none() db_collection = result.scalar_one_or_none()
if not db_collection: if not db_collection:
return False return False
await self.session.delete(db_collection) await self.session.delete(db_collection)
await self.session.commit() await self.session.commit()
return True return True
async def list_by_owner(self, owner_id: UUID, skip: int = 0, limit: int = 100) -> list[Collection]: async def list_by_owner(self, owner_id: UUID, skip: int = 0, limit: int = 100) -> list[Collection]:
"""Получить коллекции владельца""" """Получить коллекции владельца"""
result = await self.session.execute( result = await self.session.execute(
select(CollectionModel) select(CollectionModel)
.where(CollectionModel.owner_id == owner_id) .where(CollectionModel.owner_id == owner_id)
.offset(skip) .offset(skip)
.limit(limit) .limit(limit)
) )
db_collections = result.scalars().all() db_collections = result.scalars().all()
return [self._to_entity(db_collection) for db_collection in db_collections] return [self._to_entity(db_collection) for db_collection in db_collections]
async def list_public(self, skip: int = 0, limit: int = 100) -> list[Collection]: async def list_public(self, skip: int = 0, limit: int = 100) -> list[Collection]:
"""Получить публичные коллекции""" """Получить публичные коллекции"""
result = await self.session.execute( result = await self.session.execute(
select(CollectionModel) select(CollectionModel)
.where(CollectionModel.is_public == True) .where(CollectionModel.is_public == True)
.offset(skip) .offset(skip)
.limit(limit) .limit(limit)
) )
db_collections = result.scalars().all() db_collections = result.scalars().all()
return [self._to_entity(db_collection) for db_collection in db_collections] return [self._to_entity(db_collection) for db_collection in db_collections]
def _to_entity(self, db_collection: CollectionModel | None) -> Collection | None: def _to_entity(self, db_collection: CollectionModel | None) -> Collection | None:
"""Преобразовать модель БД в доменную сущность""" """Преобразовать модель БД в доменную сущность"""
if not db_collection: if not db_collection:
return None return None
return Collection( return Collection(
collection_id=db_collection.collection_id, collection_id=db_collection.collection_id,
name=db_collection.name, name=db_collection.name,
description=db_collection.description or "", description=db_collection.description or "",
owner_id=db_collection.owner_id, owner_id=db_collection.owner_id,
is_public=db_collection.is_public, is_public=db_collection.is_public,
created_at=db_collection.created_at created_at=db_collection.created_at
) )

View File

@ -1,104 +1,104 @@
""" """
Реализация репозитория бесед для PostgreSQL Реализация репозитория бесед для PostgreSQL
""" """
from uuid import UUID from uuid import UUID
from typing import Optional from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
from src.domain.entities.conversation import Conversation from src.domain.entities.conversation import Conversation
from src.domain.repositories.conversation_repository import IConversationRepository from src.domain.repositories.conversation_repository import IConversationRepository
from src.infrastructure.database.models import ConversationModel from src.infrastructure.database.models import ConversationModel
from src.shared.exceptions import NotFoundError from src.shared.exceptions import NotFoundError
class PostgreSQLConversationRepository(IConversationRepository): class PostgreSQLConversationRepository(IConversationRepository):
"""PostgreSQL реализация репозитория бесед""" """PostgreSQL реализация репозитория бесед"""
def __init__(self, session: AsyncSession): def __init__(self, session: AsyncSession):
self.session = session self.session = session
async def create(self, conversation: Conversation) -> Conversation: async def create(self, conversation: Conversation) -> Conversation:
"""Создать беседу""" """Создать беседу"""
db_conversation = ConversationModel( db_conversation = ConversationModel(
conversation_id=conversation.conversation_id, conversation_id=conversation.conversation_id,
user_id=conversation.user_id, user_id=conversation.user_id,
collection_id=conversation.collection_id, collection_id=conversation.collection_id,
created_at=conversation.created_at, created_at=conversation.created_at,
updated_at=conversation.updated_at updated_at=conversation.updated_at
) )
self.session.add(db_conversation) self.session.add(db_conversation)
await self.session.commit() await self.session.commit()
await self.session.refresh(db_conversation) await self.session.refresh(db_conversation)
return self._to_entity(db_conversation) return self._to_entity(db_conversation)
async def get_by_id(self, conversation_id: UUID) -> Optional[Conversation]: async def get_by_id(self, conversation_id: UUID) -> Optional[Conversation]:
"""Получить беседу по ID""" """Получить беседу по ID"""
result = await self.session.execute( result = await self.session.execute(
select(ConversationModel).where(ConversationModel.conversation_id == conversation_id) select(ConversationModel).where(ConversationModel.conversation_id == conversation_id)
) )
db_conversation = result.scalar_one_or_none() db_conversation = result.scalar_one_or_none()
return self._to_entity(db_conversation) if db_conversation else None return self._to_entity(db_conversation) if db_conversation else None
async def update(self, conversation: Conversation) -> Conversation: async def update(self, conversation: Conversation) -> Conversation:
"""Обновить беседу""" """Обновить беседу"""
result = await self.session.execute( result = await self.session.execute(
select(ConversationModel).where(ConversationModel.conversation_id == conversation.conversation_id) select(ConversationModel).where(ConversationModel.conversation_id == conversation.conversation_id)
) )
db_conversation = result.scalar_one_or_none() db_conversation = result.scalar_one_or_none()
if not db_conversation: if not db_conversation:
raise NotFoundError(f"Беседа {conversation.conversation_id} не найдена") raise NotFoundError(f"Беседа {conversation.conversation_id} не найдена")
db_conversation.user_id = conversation.user_id db_conversation.user_id = conversation.user_id
db_conversation.collection_id = conversation.collection_id db_conversation.collection_id = conversation.collection_id
db_conversation.updated_at = conversation.updated_at db_conversation.updated_at = conversation.updated_at
await self.session.commit() await self.session.commit()
await self.session.refresh(db_conversation) await self.session.refresh(db_conversation)
return self._to_entity(db_conversation) return self._to_entity(db_conversation)
async def delete(self, conversation_id: UUID) -> bool: async def delete(self, conversation_id: UUID) -> bool:
"""Удалить беседу""" """Удалить беседу"""
result = await self.session.execute( result = await self.session.execute(
select(ConversationModel).where(ConversationModel.conversation_id == conversation_id) select(ConversationModel).where(ConversationModel.conversation_id == conversation_id)
) )
db_conversation = result.scalar_one_or_none() db_conversation = result.scalar_one_or_none()
if not db_conversation: if not db_conversation:
return False return False
await self.session.delete(db_conversation) await self.session.delete(db_conversation)
await self.session.commit() await self.session.commit()
return True return True
async def list_by_user(self, user_id: UUID, skip: int = 0, limit: int = 100) -> list[Conversation]: async def list_by_user(self, user_id: UUID, skip: int = 0, limit: int = 100) -> list[Conversation]:
"""Получить беседы пользователя""" """Получить беседы пользователя"""
result = await self.session.execute( result = await self.session.execute(
select(ConversationModel) select(ConversationModel)
.where(ConversationModel.user_id == user_id) .where(ConversationModel.user_id == user_id)
.offset(skip) .offset(skip)
.limit(limit) .limit(limit)
) )
db_conversations = result.scalars().all() db_conversations = result.scalars().all()
return [self._to_entity(db_conversation) for db_conversation in db_conversations] return [self._to_entity(db_conversation) for db_conversation in db_conversations]
async def list_by_collection(self, collection_id: UUID, skip: int = 0, limit: int = 100) -> list[Conversation]: async def list_by_collection(self, collection_id: UUID, skip: int = 0, limit: int = 100) -> list[Conversation]:
"""Получить беседы по коллекции""" """Получить беседы по коллекции"""
result = await self.session.execute( result = await self.session.execute(
select(ConversationModel) select(ConversationModel)
.where(ConversationModel.collection_id == collection_id) .where(ConversationModel.collection_id == collection_id)
.offset(skip) .offset(skip)
.limit(limit) .limit(limit)
) )
db_conversations = result.scalars().all() db_conversations = result.scalars().all()
return [self._to_entity(db_conversation) for db_conversation in db_conversations] return [self._to_entity(db_conversation) for db_conversation in db_conversations]
def _to_entity(self, db_conversation: ConversationModel | None) -> Conversation | None: def _to_entity(self, db_conversation: ConversationModel | None) -> Conversation | None:
"""Преобразовать модель БД в доменную сущность""" """Преобразовать модель БД в доменную сущность"""
if not db_conversation: if not db_conversation:
return None return None
return Conversation( return Conversation(
conversation_id=db_conversation.conversation_id, conversation_id=db_conversation.conversation_id,
user_id=db_conversation.user_id, user_id=db_conversation.user_id,
collection_id=db_conversation.collection_id, collection_id=db_conversation.collection_id,
created_at=db_conversation.created_at, created_at=db_conversation.created_at,
updated_at=db_conversation.updated_at updated_at=db_conversation.updated_at
) )

View File

@ -1,95 +1,95 @@
""" """
Реализация репозитория документов для PostgreSQL Реализация репозитория документов для PostgreSQL
""" """
from uuid import UUID from uuid import UUID
from typing import Optional from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
from src.domain.entities.document import Document from src.domain.entities.document import Document
from src.domain.repositories.document_repository import IDocumentRepository from src.domain.repositories.document_repository import IDocumentRepository
from src.infrastructure.database.models import DocumentModel from src.infrastructure.database.models import DocumentModel
from src.shared.exceptions import NotFoundError from src.shared.exceptions import NotFoundError
class PostgreSQLDocumentRepository(IDocumentRepository): class PostgreSQLDocumentRepository(IDocumentRepository):
"""PostgreSQL реализация репозитория документов""" """PostgreSQL реализация репозитория документов"""
def __init__(self, session: AsyncSession): def __init__(self, session: AsyncSession):
self.session = session self.session = session
async def create(self, document: Document) -> Document: async def create(self, document: Document) -> Document:
"""Создать документ""" """Создать документ"""
db_document = DocumentModel( db_document = DocumentModel(
document_id=document.document_id, document_id=document.document_id,
collection_id=document.collection_id, collection_id=document.collection_id,
title=document.title, title=document.title,
content=document.content, content=document.content,
document_metadata=document.metadata, document_metadata=document.metadata,
created_at=document.created_at created_at=document.created_at
) )
self.session.add(db_document) self.session.add(db_document)
await self.session.commit() await self.session.commit()
await self.session.refresh(db_document) await self.session.refresh(db_document)
return self._to_entity(db_document) return self._to_entity(db_document)
async def get_by_id(self, document_id: UUID) -> Optional[Document]: async def get_by_id(self, document_id: UUID) -> Optional[Document]:
"""Получить документ по ID""" """Получить документ по ID"""
result = await self.session.execute( result = await self.session.execute(
select(DocumentModel).where(DocumentModel.document_id == document_id) select(DocumentModel).where(DocumentModel.document_id == document_id)
) )
db_document = result.scalar_one_or_none() db_document = result.scalar_one_or_none()
return self._to_entity(db_document) if db_document else None return self._to_entity(db_document) if db_document else None
async def update(self, document: Document) -> Document: async def update(self, document: Document) -> Document:
"""Обновить документ""" """Обновить документ"""
result = await self.session.execute( result = await self.session.execute(
select(DocumentModel).where(DocumentModel.document_id == document.document_id) select(DocumentModel).where(DocumentModel.document_id == document.document_id)
) )
db_document = result.scalar_one_or_none() db_document = result.scalar_one_or_none()
if not db_document: if not db_document:
raise NotFoundError(f"Документ {document.document_id} не найден") raise NotFoundError(f"Документ {document.document_id} не найден")
db_document.title = document.title db_document.title = document.title
db_document.content = document.content db_document.content = document.content
db_document.document_metadata = document.metadata db_document.document_metadata = document.metadata
await self.session.commit() await self.session.commit()
await self.session.refresh(db_document) await self.session.refresh(db_document)
return self._to_entity(db_document) return self._to_entity(db_document)
async def delete(self, document_id: UUID) -> bool: async def delete(self, document_id: UUID) -> bool:
"""Удалить документ""" """Удалить документ"""
result = await self.session.execute( result = await self.session.execute(
select(DocumentModel).where(DocumentModel.document_id == document_id) select(DocumentModel).where(DocumentModel.document_id == document_id)
) )
db_document = result.scalar_one_or_none() db_document = result.scalar_one_or_none()
if not db_document: if not db_document:
return False return False
await self.session.delete(db_document) await self.session.delete(db_document)
await self.session.commit() await self.session.commit()
return True return True
async def list_by_collection(self, collection_id: UUID, skip: int = 0, limit: int = 100) -> list[Document]: async def list_by_collection(self, collection_id: UUID, skip: int = 0, limit: int = 100) -> list[Document]:
"""Получить документы коллекции""" """Получить документы коллекции"""
result = await self.session.execute( result = await self.session.execute(
select(DocumentModel) select(DocumentModel)
.where(DocumentModel.collection_id == collection_id) .where(DocumentModel.collection_id == collection_id)
.offset(skip) .offset(skip)
.limit(limit) .limit(limit)
) )
db_documents = result.scalars().all() db_documents = result.scalars().all()
return [self._to_entity(db_document) for db_document in db_documents] return [self._to_entity(db_document) for db_document in db_documents]
def _to_entity(self, db_document: DocumentModel | None) -> Document | None: def _to_entity(self, db_document: DocumentModel | None) -> Document | None:
"""Преобразовать модель БД в доменную сущность""" """Преобразовать модель БД в доменную сущность"""
if not db_document: if not db_document:
return None return None
return Document( return Document(
document_id=db_document.document_id, document_id=db_document.document_id,
collection_id=db_document.collection_id, collection_id=db_document.collection_id,
title=db_document.title, title=db_document.title,
content=db_document.content, content=db_document.content,
metadata=db_document.document_metadata or {}, metadata=db_document.document_metadata or {},
created_at=db_document.created_at created_at=db_document.created_at
) )

View File

@ -1,96 +1,96 @@
""" """
Реализация репозитория сообщений для PostgreSQL Реализация репозитория сообщений для PostgreSQL
""" """
from uuid import UUID from uuid import UUID
from typing import Optional from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
from src.domain.entities.message import Message, MessageRole from src.domain.entities.message import Message, MessageRole
from src.domain.repositories.message_repository import IMessageRepository from src.domain.repositories.message_repository import IMessageRepository
from src.infrastructure.database.models import MessageModel from src.infrastructure.database.models import MessageModel
from src.shared.exceptions import NotFoundError from src.shared.exceptions import NotFoundError
class PostgreSQLMessageRepository(IMessageRepository): class PostgreSQLMessageRepository(IMessageRepository):
"""PostgreSQL реализация репозитория сообщений""" """PostgreSQL реализация репозитория сообщений"""
def __init__(self, session: AsyncSession): def __init__(self, session: AsyncSession):
self.session = session self.session = session
async def create(self, message: Message) -> Message: async def create(self, message: Message) -> Message:
"""Создать сообщение""" """Создать сообщение"""
db_message = MessageModel( db_message = MessageModel(
message_id=message.message_id, message_id=message.message_id,
conversation_id=message.conversation_id, conversation_id=message.conversation_id,
content=message.content, content=message.content,
role=message.role.value, role=message.role.value,
sources=message.sources, sources=message.sources,
created_at=message.created_at created_at=message.created_at
) )
self.session.add(db_message) self.session.add(db_message)
await self.session.commit() await self.session.commit()
await self.session.refresh(db_message) await self.session.refresh(db_message)
return self._to_entity(db_message) return self._to_entity(db_message)
async def get_by_id(self, message_id: UUID) -> Optional[Message]: async def get_by_id(self, message_id: UUID) -> Optional[Message]:
"""Получить сообщение по ID""" """Получить сообщение по ID"""
result = await self.session.execute( result = await self.session.execute(
select(MessageModel).where(MessageModel.message_id == message_id) select(MessageModel).where(MessageModel.message_id == message_id)
) )
db_message = result.scalar_one_or_none() db_message = result.scalar_one_or_none()
return self._to_entity(db_message) if db_message else None return self._to_entity(db_message) if db_message else None
async def update(self, message: Message) -> Message: async def update(self, message: Message) -> Message:
"""Обновить сообщение""" """Обновить сообщение"""
result = await self.session.execute( result = await self.session.execute(
select(MessageModel).where(MessageModel.message_id == message.message_id) select(MessageModel).where(MessageModel.message_id == message.message_id)
) )
db_message = result.scalar_one_or_none() db_message = result.scalar_one_or_none()
if not db_message: if not db_message:
raise NotFoundError(f"Сообщение {message.message_id} не найдено") raise NotFoundError(f"Сообщение {message.message_id} не найдено")
db_message.content = message.content db_message.content = message.content
db_message.role = message.role.value db_message.role = message.role.value
db_message.sources = message.sources db_message.sources = message.sources
await self.session.commit() await self.session.commit()
await self.session.refresh(db_message) await self.session.refresh(db_message)
return self._to_entity(db_message) return self._to_entity(db_message)
async def delete(self, message_id: UUID) -> bool: async def delete(self, message_id: UUID) -> bool:
"""Удалить сообщение""" """Удалить сообщение"""
result = await self.session.execute( result = await self.session.execute(
select(MessageModel).where(MessageModel.message_id == message_id) select(MessageModel).where(MessageModel.message_id == message_id)
) )
db_message = result.scalar_one_or_none() db_message = result.scalar_one_or_none()
if not db_message: if not db_message:
return False return False
await self.session.delete(db_message) await self.session.delete(db_message)
await self.session.commit() await self.session.commit()
return True return True
async def list_by_conversation(self, conversation_id: UUID, skip: int = 0, limit: int = 100) -> list[Message]: async def list_by_conversation(self, conversation_id: UUID, skip: int = 0, limit: int = 100) -> list[Message]:
"""Получить сообщения беседы""" """Получить сообщения беседы"""
result = await self.session.execute( result = await self.session.execute(
select(MessageModel) select(MessageModel)
.where(MessageModel.conversation_id == conversation_id) .where(MessageModel.conversation_id == conversation_id)
.order_by(MessageModel.created_at) .order_by(MessageModel.created_at)
.offset(skip) .offset(skip)
.limit(limit) .limit(limit)
) )
db_messages = result.scalars().all() db_messages = result.scalars().all()
return [self._to_entity(db_message) for db_message in db_messages] return [self._to_entity(db_message) for db_message in db_messages]
def _to_entity(self, db_message: MessageModel | None) -> Message | None: def _to_entity(self, db_message: MessageModel | None) -> Message | None:
"""Преобразовать модель БД в доменную сущность""" """Преобразовать модель БД в доменную сущность"""
if not db_message: if not db_message:
return None return None
return Message( return Message(
message_id=db_message.message_id, message_id=db_message.message_id,
conversation_id=db_message.conversation_id, conversation_id=db_message.conversation_id,
content=db_message.content, content=db_message.content,
role=MessageRole(db_message.role), role=MessageRole(db_message.role),
sources=db_message.sources or {}, sources=db_message.sources or {},
created_at=db_message.created_at created_at=db_message.created_at
) )

View File

@ -1,95 +1,95 @@
""" """
Реализация репозитория пользователей для PostgreSQL Реализация репозитория пользователей для PostgreSQL
""" """
from uuid import UUID from uuid import UUID
from typing import Optional from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
from src.domain.entities.user import User, UserRole from src.domain.entities.user import User, UserRole
from src.domain.repositories.user_repository import IUserRepository from src.domain.repositories.user_repository import IUserRepository
from src.infrastructure.database.models import UserModel from src.infrastructure.database.models import UserModel
from src.shared.exceptions import NotFoundError from src.shared.exceptions import NotFoundError
class PostgreSQLUserRepository(IUserRepository): class PostgreSQLUserRepository(IUserRepository):
"""PostgreSQL реализация репозитория пользователей""" """PostgreSQL реализация репозитория пользователей"""
def __init__(self, session: AsyncSession): def __init__(self, session: AsyncSession):
self.session = session self.session = session
async def create(self, user: User) -> User: async def create(self, user: User) -> User:
"""Создать пользователя""" """Создать пользователя"""
db_user = UserModel( db_user = UserModel(
user_id=user.user_id, user_id=user.user_id,
telegram_id=user.telegram_id, telegram_id=user.telegram_id,
role=user.role.value, role=user.role.value,
created_at=user.created_at created_at=user.created_at
) )
self.session.add(db_user) self.session.add(db_user)
await self.session.commit() await self.session.commit()
await self.session.refresh(db_user) await self.session.refresh(db_user)
return self._to_entity(db_user) return self._to_entity(db_user)
async def get_by_id(self, user_id: UUID) -> Optional[User]: async def get_by_id(self, user_id: UUID) -> Optional[User]:
"""Получить пользователя по ID""" """Получить пользователя по ID"""
result = await self.session.execute( result = await self.session.execute(
select(UserModel).where(UserModel.user_id == user_id) select(UserModel).where(UserModel.user_id == user_id)
) )
db_user = result.scalar_one_or_none() db_user = result.scalar_one_or_none()
return self._to_entity(db_user) if db_user else None return self._to_entity(db_user) if db_user else None
async def get_by_telegram_id(self, telegram_id: str) -> Optional[User]: async def get_by_telegram_id(self, telegram_id: str) -> Optional[User]:
"""Получить пользователя по Telegram ID""" """Получить пользователя по Telegram ID"""
result = await self.session.execute( result = await self.session.execute(
select(UserModel).where(UserModel.telegram_id == telegram_id) select(UserModel).where(UserModel.telegram_id == telegram_id)
) )
db_user = result.scalar_one_or_none() db_user = result.scalar_one_or_none()
return self._to_entity(db_user) if db_user else None return self._to_entity(db_user) if db_user else None
async def update(self, user: User) -> User: async def update(self, user: User) -> User:
"""Обновить пользователя""" """Обновить пользователя"""
result = await self.session.execute( result = await self.session.execute(
select(UserModel).where(UserModel.user_id == user.user_id) select(UserModel).where(UserModel.user_id == user.user_id)
) )
db_user = result.scalar_one_or_none() db_user = result.scalar_one_or_none()
if not db_user: if not db_user:
raise NotFoundError(f"Пользователь {user.user_id} не найден") raise NotFoundError(f"Пользователь {user.user_id} не найден")
db_user.telegram_id = user.telegram_id db_user.telegram_id = user.telegram_id
db_user.role = user.role.value db_user.role = user.role.value
await self.session.commit() await self.session.commit()
await self.session.refresh(db_user) await self.session.refresh(db_user)
return self._to_entity(db_user) return self._to_entity(db_user)
async def delete(self, user_id: UUID) -> bool: async def delete(self, user_id: UUID) -> bool:
"""Удалить пользователя""" """Удалить пользователя"""
result = await self.session.execute( result = await self.session.execute(
select(UserModel).where(UserModel.user_id == user_id) select(UserModel).where(UserModel.user_id == user_id)
) )
db_user = result.scalar_one_or_none() db_user = result.scalar_one_or_none()
if not db_user: if not db_user:
return False return False
await self.session.delete(db_user) await self.session.delete(db_user)
await self.session.commit() await self.session.commit()
return True return True
async def list_all(self, skip: int = 0, limit: int = 100) -> list[User]: async def list_all(self, skip: int = 0, limit: int = 100) -> list[User]:
"""Получить список всех пользователей""" """Получить список всех пользователей"""
result = await self.session.execute( result = await self.session.execute(
select(UserModel).offset(skip).limit(limit) select(UserModel).offset(skip).limit(limit)
) )
db_users = result.scalars().all() db_users = result.scalars().all()
return [self._to_entity(db_user) for db_user in db_users] return [self._to_entity(db_user) for db_user in db_users]
def _to_entity(self, db_user: UserModel | None) -> User | None: def _to_entity(self, db_user: UserModel | None) -> User | None:
"""Преобразовать модель БД в доменную сущность""" """Преобразовать модель БД в доменную сущность"""
if not db_user: if not db_user:
return None return None
return User( return User(
user_id=db_user.user_id, user_id=db_user.user_id,
telegram_id=db_user.telegram_id, telegram_id=db_user.telegram_id,
role=UserRole(db_user.role), role=UserRole(db_user.role),
created_at=db_user.created_at created_at=db_user.created_at
) )

View File

@ -1,4 +1,4 @@
""" """
Presentation layer Presentation layer
""" """

View File

@ -0,0 +1,4 @@
"""
API v1 роутеры
"""

View File

@ -0,0 +1,56 @@
"""
Админ-панель - упрощенная версия через API эндпоинты
В будущем можно интегрировать полноценную админ-панель
"""
from fastapi import APIRouter, HTTPException
from typing import List
from uuid import UUID
from dishka.integrations.fastapi import FromDishka
from src.presentation.schemas.user_schemas import UserResponse
from src.presentation.schemas.collection_schemas import CollectionResponse
from src.presentation.schemas.document_schemas import DocumentResponse
from src.presentation.schemas.conversation_schemas import ConversationResponse
from src.presentation.schemas.message_schemas import MessageResponse
from src.domain.entities.user import User, UserRole
from src.application.use_cases.user_use_cases import UserUseCases
from src.application.use_cases.collection_use_cases import CollectionUseCases
router = APIRouter(prefix="/admin", tags=["admin"])
@router.get("/users", response_model=List[UserResponse])
async def admin_list_users(
skip: int = 0,
limit: int = 100,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[UserUseCases] = FromDishka()
):
"""Получить список всех пользователей (только для админов)"""
if not current_user.is_admin():
raise HTTPException(status_code=403, detail="Требуются права администратора")
users = await use_cases.list_users(skip=skip, limit=limit)
return [UserResponse.from_entity(user) for user in users]
@router.get("/collections", response_model=List[CollectionResponse])
async def admin_list_collections(
skip: int = 0,
limit: int = 100,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka()
):
"""Получить список всех коллекций (только для админов)"""
from src.infrastructure.database.base import AsyncSessionLocal
from src.infrastructure.repositories.postgresql.collection_repository import PostgreSQLCollectionRepository
from sqlalchemy import select
from src.infrastructure.database.models import CollectionModel
async with AsyncSessionLocal() as session:
repo = PostgreSQLCollectionRepository(session)
result = await session.execute(
select(CollectionModel).offset(skip).limit(limit)
)
db_collections = result.scalars().all()
collections = [repo._to_entity(c) for c in db_collections if c]
return [CollectionResponse.from_entity(c) for c in collections if c]

View File

@ -0,0 +1,120 @@
"""
API роутеры для работы с коллекциями
"""
from uuid import UUID
from fastapi import APIRouter, status
from fastapi.responses import JSONResponse
from typing import List
from dishka.integrations.fastapi import FromDishka
from src.presentation.schemas.collection_schemas import (
CollectionCreate,
CollectionUpdate,
CollectionResponse,
CollectionAccessGrant,
CollectionAccessResponse
)
from src.application.use_cases.collection_use_cases import CollectionUseCases
from src.domain.entities.user import User
from src.presentation.middleware.auth_middleware import get_current_user
router = APIRouter(prefix="/collections", tags=["collections"])
@router.post("", response_model=CollectionResponse, status_code=status.HTTP_201_CREATED)
async def create_collection(
collection_data: CollectionCreate,
current_user: User = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka()
):
"""Создать коллекцию"""
collection = await use_cases.create_collection(
name=collection_data.name,
owner_id=current_user.user_id,
description=collection_data.description,
is_public=collection_data.is_public
)
return CollectionResponse.from_entity(collection)
@router.get("/{collection_id}", response_model=CollectionResponse)
async def get_collection(
collection_id: UUID,
use_cases: FromDishka[CollectionUseCases] = FromDishka()
):
"""Получить коллекцию по ID"""
collection = await use_cases.get_collection(collection_id)
return CollectionResponse.from_entity(collection)
@router.put("/{collection_id}", response_model=CollectionResponse)
async def update_collection(
collection_id: UUID,
collection_data: CollectionUpdate,
current_user: User = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka()
):
"""Обновить коллекцию"""
collection = await use_cases.update_collection(
collection_id=collection_id,
user_id=current_user.user_id,
name=collection_data.name,
description=collection_data.description,
is_public=collection_data.is_public
)
return CollectionResponse.from_entity(collection)
@router.delete("/{collection_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_collection(
collection_id: UUID,
current_user: User = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka()
):
"""Удалить коллекцию"""
await use_cases.delete_collection(collection_id, current_user.user_id)
return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None)
@router.get("", response_model=List[CollectionResponse])
async def list_collections(
skip: int = 0,
limit: int = 100,
current_user: User = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka()
):
"""Получить список коллекций, доступных пользователю"""
collections = await use_cases.list_user_collections(
user_id=current_user.user_id,
skip=skip,
limit=limit
)
return [CollectionResponse.from_entity(c) for c in collections]
@router.post("/{collection_id}/access", response_model=CollectionAccessResponse, status_code=status.HTTP_201_CREATED)
async def grant_access(
collection_id: UUID,
access_data: CollectionAccessGrant,
current_user: User = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka()
):
"""Предоставить доступ пользователю к коллекции"""
access = await use_cases.grant_access(
collection_id=collection_id,
user_id=access_data.user_id,
owner_id=current_user.user_id
)
return CollectionAccessResponse.from_entity(access)
@router.delete("/{collection_id}/access/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def revoke_access(
collection_id: UUID,
user_id: UUID,
current_user: User = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka()
):
"""Отозвать доступ пользователя к коллекции"""
await use_cases.revoke_access(collection_id, user_id, current_user.user_id)
return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None)

View File

@ -0,0 +1,69 @@
"""
API роутеры для работы с беседами
"""
from uuid import UUID
from fastapi import APIRouter, status
from fastapi.responses import JSONResponse
from typing import List
from dishka.integrations.fastapi import FromDishka
from src.presentation.schemas.conversation_schemas import (
ConversationCreate,
ConversationResponse
)
from src.application.use_cases.conversation_use_cases import ConversationUseCases
from src.domain.entities.user import User
router = APIRouter(prefix="/conversations", tags=["conversations"])
@router.post("", response_model=ConversationResponse, status_code=status.HTTP_201_CREATED)
async def create_conversation(
conversation_data: ConversationCreate,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[ConversationUseCases] = FromDishka()
):
"""Создать беседу"""
conversation = await use_cases.create_conversation(
user_id=current_user.user_id,
collection_id=conversation_data.collection_id
)
return ConversationResponse.from_entity(conversation)
@router.get("/{conversation_id}", response_model=ConversationResponse)
async def get_conversation(
conversation_id: UUID,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[ConversationUseCases] = FromDishka()
):
"""Получить беседу по ID"""
conversation = await use_cases.get_conversation(conversation_id, current_user.user_id)
return ConversationResponse.from_entity(conversation)
@router.delete("/{conversation_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_conversation(
conversation_id: UUID,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[ConversationUseCases] = FromDishka()
):
"""Удалить беседу"""
await use_cases.delete_conversation(conversation_id, current_user.user_id)
return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None)
@router.get("", response_model=List[ConversationResponse])
async def list_conversations(
skip: int = 0,
limit: int = 100,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[ConversationUseCases] = FromDishka()
):
"""Получить список бесед пользователя"""
conversations = await use_cases.list_user_conversations(
user_id=current_user.user_id,
skip=skip,
limit=limit
)
return [ConversationResponse.from_entity(c) for c in conversations]

View File

@ -0,0 +1,121 @@
"""
API роутеры для работы с документами
"""
from uuid import UUID
from fastapi import APIRouter, status, UploadFile, File
from fastapi.responses import JSONResponse
from typing import List
from dishka.integrations.fastapi import FromDishka
from src.presentation.schemas.document_schemas import (
DocumentCreate,
DocumentUpdate,
DocumentResponse
)
from src.application.use_cases.document_use_cases import DocumentUseCases
from src.domain.entities.user import User
router = APIRouter(prefix="/documents", tags=["documents"])
@router.post("", response_model=DocumentResponse, status_code=status.HTTP_201_CREATED)
async def create_document(
document_data: DocumentCreate,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[DocumentUseCases] = FromDishka()
):
"""Создать документ"""
document = await use_cases.create_document(
collection_id=document_data.collection_id,
title=document_data.title,
content=document_data.content,
metadata=document_data.metadata
)
return DocumentResponse.from_entity(document)
@router.post("/upload", response_model=DocumentResponse, status_code=status.HTTP_201_CREATED)
async def upload_document(
collection_id: UUID,
file: UploadFile = File(...),
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[DocumentUseCases] = FromDishka()
):
"""Загрузить и распарсить PDF документ или изображение"""
if not file.filename:
raise JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"detail": "Имя файла не указано"}
)
supported_formats = ['.pdf', '.png', '.jpg', '.jpeg', '.tiff', '.bmp']
file_ext = file.filename.lower().rsplit('.', 1)[-1] if '.' in file.filename else ''
if f'.{file_ext}' not in supported_formats:
raise JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"detail": f"Неподдерживаемый формат файла. Поддерживаются: {', '.join(supported_formats)}"}
)
document = await use_cases.upload_and_parse_document(
collection_id=collection_id,
file=file.file,
filename=file.filename,
user_id=current_user.user_id
)
return DocumentResponse.from_entity(document)
@router.get("/{document_id}", response_model=DocumentResponse)
async def get_document(
document_id: UUID,
use_cases: FromDishka[DocumentUseCases] = FromDishka()
):
"""Получить документ по ID"""
document = await use_cases.get_document(document_id)
return DocumentResponse.from_entity(document)
@router.put("/{document_id}", response_model=DocumentResponse)
async def update_document(
document_id: UUID,
document_data: DocumentUpdate,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[DocumentUseCases] = FromDishka()
):
"""Обновить документ"""
document = await use_cases.update_document(
document_id=document_id,
user_id=current_user.user_id,
title=document_data.title,
content=document_data.content,
metadata=document_data.metadata
)
return DocumentResponse.from_entity(document)
@router.delete("/{document_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_document(
document_id: UUID,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[DocumentUseCases] = FromDishka()
):
"""Удалить документ"""
await use_cases.delete_document(document_id, current_user.user_id)
return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None)
@router.get("/collection/{collection_id}", response_model=List[DocumentResponse])
async def list_collection_documents(
collection_id: UUID,
skip: int = 0,
limit: int = 100,
use_cases: FromDishka[DocumentUseCases] = FromDishka()
):
"""Получить документы коллекции"""
documents = await use_cases.list_collection_documents(
collection_id=collection_id,
skip=skip,
limit=limit
)
return [DocumentResponse.from_entity(d) for d in documents]

View File

@ -0,0 +1,88 @@
"""
API роутеры для работы с сообщениями
"""
from uuid import UUID
from fastapi import APIRouter, status
from fastapi.responses import JSONResponse
from typing import List
from dishka.integrations.fastapi import FromDishka
from src.presentation.schemas.message_schemas import (
MessageCreate,
MessageUpdate,
MessageResponse
)
from src.application.use_cases.message_use_cases import MessageUseCases
from src.domain.entities.user import User
router = APIRouter(prefix="/messages", tags=["messages"])
@router.post("", response_model=MessageResponse, status_code=status.HTTP_201_CREATED)
async def create_message(
message_data: MessageCreate,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[MessageUseCases] = FromDishka()
):
"""Создать сообщение"""
message = await use_cases.create_message(
conversation_id=message_data.conversation_id,
content=message_data.content,
role=message_data.role,
user_id=current_user.user_id,
sources=message_data.sources
)
return MessageResponse.from_entity(message)
@router.get("/{message_id}", response_model=MessageResponse)
async def get_message(
message_id: UUID,
use_cases: FromDishka[MessageUseCases] = FromDishka()
):
"""Получить сообщение по ID"""
message = await use_cases.get_message(message_id)
return MessageResponse.from_entity(message)
@router.put("/{message_id}", response_model=MessageResponse)
async def update_message(
message_id: UUID,
message_data: MessageUpdate,
use_cases: FromDishka[MessageUseCases] = FromDishka()
):
"""Обновить сообщение"""
message = await use_cases.update_message(
message_id=message_id,
content=message_data.content,
sources=message_data.sources
)
return MessageResponse.from_entity(message)
@router.delete("/{message_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_message(
message_id: UUID,
use_cases: FromDishka[MessageUseCases] = FromDishka()
):
"""Удалить сообщение"""
await use_cases.delete_message(message_id)
return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None)
@router.get("/conversation/{conversation_id}", response_model=List[MessageResponse])
async def list_conversation_messages(
conversation_id: UUID,
skip: int = 0,
limit: int = 100,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[MessageUseCases] = FromDishka()
):
"""Получить сообщения беседы"""
messages = await use_cases.list_conversation_messages(
conversation_id=conversation_id,
user_id=current_user.user_id,
skip=skip,
limit=limit
)
return [MessageResponse.from_entity(m) for m in messages]

View File

@ -0,0 +1,81 @@
"""
API роутеры для работы с пользователями
"""
from uuid import UUID
from fastapi import APIRouter, status
from fastapi.responses import JSONResponse
from typing import List
from dishka.integrations.fastapi import FromDishka
from src.presentation.schemas.user_schemas import UserCreate, UserUpdate, UserResponse
from src.application.use_cases.user_use_cases import UserUseCases
from src.domain.entities.user import User
router = APIRouter(prefix="/users", tags=["users"])
@router.post("", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def create_user(
user_data: UserCreate,
use_cases: FromDishka[UserUseCases] = FromDishka()
):
"""Создать пользователя"""
user = await use_cases.create_user(
telegram_id=user_data.telegram_id,
role=user_data.role
)
return UserResponse.from_entity(user)
@router.get("/me", response_model=UserResponse)
async def get_current_user_info(
current_user: FromDishka[User] = FromDishka()
):
"""Получить информацию о текущем пользователе"""
return UserResponse.from_entity(current_user)
@router.get("/{user_id}", response_model=UserResponse)
async def get_user(
user_id: UUID,
use_cases: FromDishka[UserUseCases] = FromDishka()
):
"""Получить пользователя по ID"""
user = await use_cases.get_user(user_id)
return UserResponse.from_entity(user)
@router.put("/{user_id}", response_model=UserResponse)
async def update_user(
user_id: UUID,
user_data: UserUpdate,
use_cases: FromDishka[UserUseCases] = FromDishka()
):
"""Обновить пользователя"""
user = await use_cases.update_user(
user_id=user_id,
telegram_id=user_data.telegram_id,
role=user_data.role
)
return UserResponse.from_entity(user)
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(
user_id: UUID,
use_cases: FromDishka[UserUseCases] = FromDishka()
):
"""Удалить пользователя"""
await use_cases.delete_user(user_id)
return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None)
@router.get("", response_model=List[UserResponse])
async def list_users(
skip: int = 0,
limit: int = 100,
use_cases: FromDishka[UserUseCases] = FromDishka()
):
"""Получить список пользователей"""
users = await use_cases.list_users(skip=skip, limit=limit)
return [UserResponse.from_entity(user) for user in users]

View File

@ -0,0 +1,4 @@
"""
Pydantic schemas
"""

View File

@ -0,0 +1,77 @@
"""
Pydantic схемы для Collection
"""
from uuid import UUID
from datetime import datetime
from pydantic import BaseModel
class CollectionBase(BaseModel):
"""Базовая схема коллекции"""
name: str
description: str = ""
is_public: bool = False
class CollectionCreate(CollectionBase):
"""Схема создания коллекции"""
pass
class CollectionUpdate(BaseModel):
"""Схема обновления коллекции"""
name: str | None = None
description: str | None = None
is_public: bool | None = None
class CollectionResponse(BaseModel):
"""Схема ответа с коллекцией"""
collection_id: UUID
name: str
description: str
owner_id: UUID
is_public: bool
created_at: datetime
@classmethod
def from_entity(cls, collection: "Collection") -> "CollectionResponse":
"""Создать из доменной сущности"""
return cls(
collection_id=collection.collection_id,
name=collection.name,
description=collection.description,
owner_id=collection.owner_id,
is_public=collection.is_public,
created_at=collection.created_at
)
class Config:
from_attributes = True
class CollectionAccessGrant(BaseModel):
"""Схема предоставления доступа"""
user_id: UUID
class CollectionAccessResponse(BaseModel):
"""Схема ответа с доступом"""
access_id: UUID
user_id: UUID
collection_id: UUID
created_at: datetime
@classmethod
def from_entity(cls, access: "CollectionAccess") -> "CollectionAccessResponse":
"""Создать из доменной сущности"""
return cls(
access_id=access.access_id,
user_id=access.user_id,
collection_id=access.collection_id,
created_at=access.created_at
)
class Config:
from_attributes = True

View File

@ -0,0 +1,35 @@
"""
Pydantic схемы для Conversation
"""
from uuid import UUID
from datetime import datetime
from pydantic import BaseModel
class ConversationCreate(BaseModel):
"""Схема создания беседы"""
collection_id: UUID
class ConversationResponse(BaseModel):
"""Схема ответа с беседой"""
conversation_id: UUID
user_id: UUID
collection_id: UUID
created_at: datetime
updated_at: datetime
@classmethod
def from_entity(cls, conversation: "Conversation") -> "ConversationResponse":
"""Создать из доменной сущности"""
return cls(
conversation_id=conversation.conversation_id,
user_id=conversation.user_id,
collection_id=conversation.collection_id,
created_at=conversation.created_at,
updated_at=conversation.updated_at
)
class Config:
from_attributes = True

View File

@ -0,0 +1,52 @@
"""
Pydantic схемы для Document
"""
from uuid import UUID
from datetime import datetime
from typing import Any
from pydantic import BaseModel
class DocumentBase(BaseModel):
"""Базовая схема документа"""
title: str
content: str
metadata: dict[str, Any] = {}
class DocumentCreate(DocumentBase):
"""Схема создания документа"""
collection_id: UUID
class DocumentUpdate(BaseModel):
"""Схема обновления документа"""
title: str | None = None
content: str | None = None
metadata: dict[str, Any] | None = None
class DocumentResponse(BaseModel):
"""Схема ответа с документом"""
document_id: UUID
collection_id: UUID
title: str
content: str
metadata: dict[str, Any]
created_at: datetime
@classmethod
def from_entity(cls, document: "Document") -> "DocumentResponse":
"""Создать из доменной сущности"""
return cls(
document_id=document.document_id,
collection_id=document.collection_id,
title=document.title,
content=document.content,
metadata=document.metadata,
created_at=document.created_at
)
class Config:
from_attributes = True

View File

@ -0,0 +1,52 @@
"""
Pydantic схемы для Message
"""
from uuid import UUID
from datetime import datetime
from typing import Any
from pydantic import BaseModel
from src.domain.entities.message import MessageRole
class MessageBase(BaseModel):
"""Базовая схема сообщения"""
content: str
role: MessageRole
sources: dict[str, Any] = {}
class MessageCreate(MessageBase):
"""Схема создания сообщения"""
conversation_id: UUID
class MessageUpdate(BaseModel):
"""Схема обновления сообщения"""
content: str | None = None
sources: dict[str, Any] | None = None
class MessageResponse(BaseModel):
"""Схема ответа с сообщением"""
message_id: UUID
conversation_id: UUID
content: str
role: MessageRole
sources: dict[str, Any]
created_at: datetime
@classmethod
def from_entity(cls, message: "Message") -> "MessageResponse":
"""Создать из доменной сущности"""
return cls(
message_id=message.message_id,
conversation_id=message.conversation_id,
content=message.content,
role=message.role,
sources=message.sources,
created_at=message.created_at
)
class Config:
from_attributes = True

View File

@ -0,0 +1,46 @@
"""
Pydantic схемы для User
"""
from uuid import UUID
from datetime import datetime
from pydantic import BaseModel
from src.domain.entities.user import UserRole
class UserBase(BaseModel):
"""Базовая схема пользователя"""
telegram_id: str
role: UserRole
class UserCreate(UserBase):
"""Схема создания пользователя"""
pass
class UserUpdate(BaseModel):
"""Схема обновления пользователя"""
telegram_id: str | None = None
role: UserRole | None = None
class UserResponse(BaseModel):
"""Схема ответа с пользователем"""
user_id: UUID
telegram_id: str
role: UserRole
created_at: datetime
@classmethod
def from_entity(cls, user: "User") -> "UserResponse":
"""Создать из доменной сущности"""
return cls(
user_id=user.user_id,
telegram_id=user.telegram_id,
role=user.role,
created_at=user.created_at
)
class Config:
from_attributes = True

View File

@ -1,4 +1,4 @@
""" """
Shared utilities Shared utilities
""" """

View File

@ -1,48 +1,48 @@
""" """
Конфигурация приложения Конфигурация приложения
""" """
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
from typing import Optional from typing import Optional
class Settings(BaseSettings): class Settings(BaseSettings):
"""Настройки (загружаются из .env автоматически)""" """Настройки (загружаются из .env автоматически)"""
POSTGRES_HOST: str = "localhost" POSTGRES_HOST: str = "localhost"
POSTGRES_PORT: int = 5432 POSTGRES_PORT: int = 5432
POSTGRES_USER: str = "postgres" POSTGRES_USER: str = "postgres"
POSTGRES_PASSWORD: str = "postgres" POSTGRES_PASSWORD: str = "postgres"
POSTGRES_DB: str = "lawyer_ai" POSTGRES_DB: str = "lawyer_ai"
QDRANT_HOST: str = "localhost" QDRANT_HOST: str = "localhost"
QDRANT_PORT: int = 6333 QDRANT_PORT: int = 6333
REDIS_HOST: str = "localhost" REDIS_HOST: str = "localhost"
REDIS_PORT: int = 6379 REDIS_PORT: int = 6379
TELEGRAM_BOT_TOKEN: Optional[str] = None TELEGRAM_BOT_TOKEN: Optional[str] = None
YANDEX_OCR_API_KEY: Optional[str] = None YANDEX_OCR_API_KEY: Optional[str] = None
DEEPSEEK_API_KEY: Optional[str] = None DEEPSEEK_API_KEY: Optional[str] = None
YANDEX_OCR_API_URL: str = "https://vision.api.cloud.yandex.net/vision/v1/batchAnalyze" YANDEX_OCR_API_URL: str = "https://vision.api.cloud.yandex.net/vision/v1/batchAnalyze"
DEEPSEEK_API_URL: str = "https://api.deepseek.com/v1/chat/completions" DEEPSEEK_API_URL: str = "https://api.deepseek.com/v1/chat/completions"
APP_NAME: str = "ИИ-юрист" APP_NAME: str = "ИИ-юрист"
DEBUG: bool = False DEBUG: bool = False
SECRET_KEY: str = "your-secret-key-change-in-production" SECRET_KEY: str = "your-secret-key-change-in-production"
CORS_ORIGINS: list[str] = ["*"] CORS_ORIGINS: list[str] = ["*"]
@property @property
def database_url(self) -> str: def database_url(self) -> str:
"""Вычисляемый URL подключения""" """Вычисляемый URL подключения"""
return f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}" return f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
class Config: class Config:
env_file = ".env" env_file = ".env"
case_sensitive = True case_sensitive = True
settings = Settings() settings = Settings()

View File

@ -1,35 +1,35 @@
""" """
Кастомные исключения приложения Кастомные исключения приложения
""" """
class LawyerAIException(Exception): class LawyerAIException(Exception):
"""Базовое исключение приложения""" """Базовое исключение приложения"""
pass pass
class NotFoundError(LawyerAIException): class NotFoundError(LawyerAIException):
"""Ресурс не найден""" """Ресурс не найден"""
pass pass
class UnauthorizedError(LawyerAIException): class UnauthorizedError(LawyerAIException):
"""Пользователь не авторизован""" """Пользователь не авторизован"""
pass pass
class ForbiddenError(LawyerAIException): class ForbiddenError(LawyerAIException):
"""Доступ запрещен""" """Доступ запрещен"""
pass pass
class ValidationError(LawyerAIException): class ValidationError(LawyerAIException):
"""Ошибка валидации данных""" """Ошибка валидации данных"""
pass pass
class DatabaseError(LawyerAIException): class DatabaseError(LawyerAIException):
"""Ошибка базы данных""" """Ошибка базы данных"""
pass pass