forked from HSE_team/BetterCallPraskovia
presentation layer
This commit is contained in:
parent
4a043f8e70
commit
036741c0bf
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
"""
|
"""
|
||||||
ИИ-юрист система
|
ИИ-юрист система
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
"""
|
"""
|
||||||
Application layer
|
Application layer
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
4
backend/src/application/services/__init__.py
Normal file
4
backend/src/application/services/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
"""
|
||||||
|
Application services
|
||||||
|
"""
|
||||||
|
|
||||||
72
backend/src/application/services/document_parser_service.py
Normal file
72
backend/src/application/services/document_parser_service.py
Normal 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
|
||||||
|
|
||||||
4
backend/src/application/use_cases/__init__.py
Normal file
4
backend/src/application/use_cases/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
"""
|
||||||
|
Application use cases
|
||||||
|
"""
|
||||||
|
|
||||||
141
backend/src/application/use_cases/collection_use_cases.py
Normal file
141
backend/src/application/use_cases/collection_use_cases.py
Normal 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]
|
||||||
|
|
||||||
68
backend/src/application/use_cases/conversation_use_cases.py
Normal file
68
backend/src/application/use_cases/conversation_use_cases.py
Normal 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
|
||||||
|
|
||||||
119
backend/src/application/use_cases/document_use_cases.py
Normal file
119
backend/src/application/use_cases/document_use_cases.py
Normal 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)
|
||||||
|
|
||||||
93
backend/src/application/use_cases/message_use_cases.py
Normal file
93
backend/src/application/use_cases/message_use_cases.py
Normal 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)
|
||||||
|
|
||||||
55
backend/src/application/use_cases/user_use_cases.py
Normal file
55
backend/src/application/use_cases/user_use_cases.py
Normal 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)
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
"""
|
"""
|
||||||
Domain layer
|
Domain layer
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
"""
|
"""
|
||||||
Domain entities
|
Domain entities
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
"""
|
"""
|
||||||
Domain repositories interfaces
|
Domain repositories interfaces
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
"""
|
"""
|
||||||
Infrastructure layer
|
Infrastructure layer
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
"""
|
"""
|
||||||
Database infrastructure
|
Database infrastructure
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
|
||||||
|
|||||||
@ -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": "Уникальный доступ пользователя к коллекции"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
"""
|
"""
|
||||||
External services
|
External services
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
560
backend/src/infrastructure/external/yandex_ocr.py
vendored
560
backend/src/infrastructure/external/yandex_ocr.py
vendored
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
"""
|
"""
|
||||||
PostgreSQL repository implementations
|
PostgreSQL repository implementations
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
"""
|
"""
|
||||||
Presentation layer
|
Presentation layer
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
4
backend/src/presentation/api/v1/__init__.py
Normal file
4
backend/src/presentation/api/v1/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
"""
|
||||||
|
API v1 роутеры
|
||||||
|
"""
|
||||||
|
|
||||||
56
backend/src/presentation/api/v1/admin.py
Normal file
56
backend/src/presentation/api/v1/admin.py
Normal 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]
|
||||||
|
|
||||||
120
backend/src/presentation/api/v1/collections.py
Normal file
120
backend/src/presentation/api/v1/collections.py
Normal 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)
|
||||||
|
|
||||||
69
backend/src/presentation/api/v1/conversations.py
Normal file
69
backend/src/presentation/api/v1/conversations.py
Normal 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]
|
||||||
|
|
||||||
121
backend/src/presentation/api/v1/documents.py
Normal file
121
backend/src/presentation/api/v1/documents.py
Normal 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]
|
||||||
|
|
||||||
88
backend/src/presentation/api/v1/messages.py
Normal file
88
backend/src/presentation/api/v1/messages.py
Normal 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]
|
||||||
|
|
||||||
81
backend/src/presentation/api/v1/users.py
Normal file
81
backend/src/presentation/api/v1/users.py
Normal 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]
|
||||||
|
|
||||||
4
backend/src/presentation/schemas/__init__.py
Normal file
4
backend/src/presentation/schemas/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
"""
|
||||||
|
Pydantic schemas
|
||||||
|
"""
|
||||||
|
|
||||||
77
backend/src/presentation/schemas/collection_schemas.py
Normal file
77
backend/src/presentation/schemas/collection_schemas.py
Normal 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
|
||||||
|
|
||||||
35
backend/src/presentation/schemas/conversation_schemas.py
Normal file
35
backend/src/presentation/schemas/conversation_schemas.py
Normal 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
|
||||||
|
|
||||||
52
backend/src/presentation/schemas/document_schemas.py
Normal file
52
backend/src/presentation/schemas/document_schemas.py
Normal 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
|
||||||
|
|
||||||
52
backend/src/presentation/schemas/message_schemas.py
Normal file
52
backend/src/presentation/schemas/message_schemas.py
Normal 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
|
||||||
|
|
||||||
46
backend/src/presentation/schemas/user_schemas.py
Normal file
46
backend/src/presentation/schemas/user_schemas.py
Normal 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
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
"""
|
"""
|
||||||
Shared utilities
|
Shared utilities
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user