forked from HSE_team/BetterCallPraskovia
presentation layer
This commit is contained in:
parent
4a043f8e70
commit
036741c0bf
@ -1,13 +1,13 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
sqlalchemy[asyncio]==2.0.23
|
||||
asyncpg==0.29.0
|
||||
alembic==1.12.1
|
||||
pydantic==2.5.0
|
||||
pydantic-settings==2.1.0
|
||||
python-multipart==0.0.6
|
||||
httpx==0.25.2
|
||||
PyMuPDF==1.23.8
|
||||
Pillow==10.2.0
|
||||
dishka==0.7.0
|
||||
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
sqlalchemy[asyncio]==2.0.23
|
||||
asyncpg==0.29.0
|
||||
alembic==1.12.1
|
||||
pydantic==2.5.0
|
||||
pydantic-settings==2.1.0
|
||||
python-multipart==0.0.6
|
||||
httpx==0.25.2
|
||||
PyMuPDF==1.23.8
|
||||
Pillow==10.2.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
|
||||
"""
|
||||
from datetime import datetime
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
|
||||
class Collection:
|
||||
"""Каталог документов"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
owner_id: UUID,
|
||||
description: str = "",
|
||||
is_public: bool = False,
|
||||
collection_id: UUID | None = None,
|
||||
created_at: datetime | None = None
|
||||
):
|
||||
self.collection_id = collection_id or uuid4()
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.owner_id = owner_id
|
||||
self.is_public = is_public
|
||||
self.created_at = created_at or datetime.utcnow()
|
||||
|
||||
"""
|
||||
Доменная сущность Collection
|
||||
"""
|
||||
from datetime import datetime
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
|
||||
class Collection:
|
||||
"""Каталог документов"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
owner_id: UUID,
|
||||
description: str = "",
|
||||
is_public: bool = False,
|
||||
collection_id: UUID | None = None,
|
||||
created_at: datetime | None = None
|
||||
):
|
||||
self.collection_id = collection_id or uuid4()
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.owner_id = owner_id
|
||||
self.is_public = is_public
|
||||
self.created_at = created_at or datetime.utcnow()
|
||||
|
||||
|
||||
@ -1,22 +1,22 @@
|
||||
"""
|
||||
Доменная сущность CollectionAccess
|
||||
"""
|
||||
from datetime import datetime
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
|
||||
class CollectionAccess:
|
||||
"""Доступ пользователя к коллекции"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user_id: UUID,
|
||||
collection_id: UUID,
|
||||
access_id: UUID | None = None,
|
||||
created_at: datetime | None = None
|
||||
):
|
||||
self.access_id = access_id or uuid4()
|
||||
self.user_id = user_id
|
||||
self.collection_id = collection_id
|
||||
self.created_at = created_at or datetime.utcnow()
|
||||
|
||||
"""
|
||||
Доменная сущность CollectionAccess
|
||||
"""
|
||||
from datetime import datetime
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
|
||||
class CollectionAccess:
|
||||
"""Доступ пользователя к коллекции"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user_id: UUID,
|
||||
collection_id: UUID,
|
||||
access_id: UUID | None = None,
|
||||
created_at: datetime | None = None
|
||||
):
|
||||
self.access_id = access_id or uuid4()
|
||||
self.user_id = user_id
|
||||
self.collection_id = collection_id
|
||||
self.created_at = created_at or datetime.utcnow()
|
||||
|
||||
|
||||
@ -1,28 +1,28 @@
|
||||
"""
|
||||
Доменная сущность Conversation
|
||||
"""
|
||||
from datetime import datetime
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
|
||||
class Conversation:
|
||||
"""Беседа пользователя с ИИ"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user_id: UUID,
|
||||
collection_id: UUID,
|
||||
conversation_id: UUID | None = None,
|
||||
created_at: datetime | None = None,
|
||||
updated_at: datetime | None = None
|
||||
):
|
||||
self.conversation_id = conversation_id or uuid4()
|
||||
self.user_id = user_id
|
||||
self.collection_id = collection_id
|
||||
self.created_at = created_at or datetime.utcnow()
|
||||
self.updated_at = updated_at or datetime.utcnow()
|
||||
|
||||
def update_timestamp(self):
|
||||
"""Обновить время последнего изменения"""
|
||||
self.updated_at = datetime.utcnow()
|
||||
|
||||
"""
|
||||
Доменная сущность Conversation
|
||||
"""
|
||||
from datetime import datetime
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
|
||||
class Conversation:
|
||||
"""Беседа пользователя с ИИ"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user_id: UUID,
|
||||
collection_id: UUID,
|
||||
conversation_id: UUID | None = None,
|
||||
created_at: datetime | None = None,
|
||||
updated_at: datetime | None = None
|
||||
):
|
||||
self.conversation_id = conversation_id or uuid4()
|
||||
self.user_id = user_id
|
||||
self.collection_id = collection_id
|
||||
self.created_at = created_at or datetime.utcnow()
|
||||
self.updated_at = updated_at or datetime.utcnow()
|
||||
|
||||
def update_timestamp(self):
|
||||
"""Обновить время последнего изменения"""
|
||||
self.updated_at = datetime.utcnow()
|
||||
|
||||
|
||||
@ -1,27 +1,27 @@
|
||||
"""
|
||||
Доменная сущность Document
|
||||
"""
|
||||
from datetime import datetime
|
||||
from uuid import UUID, uuid4
|
||||
from typing import Any
|
||||
|
||||
|
||||
class Document:
|
||||
"""Документ в коллекции"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
title: str,
|
||||
content: str,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
document_id: UUID | None = None,
|
||||
created_at: datetime | None = None
|
||||
):
|
||||
self.document_id = document_id or uuid4()
|
||||
self.collection_id = collection_id
|
||||
self.title = title
|
||||
self.content = content
|
||||
self.metadata = metadata or {}
|
||||
self.created_at = created_at or datetime.utcnow()
|
||||
|
||||
"""
|
||||
Доменная сущность Document
|
||||
"""
|
||||
from datetime import datetime
|
||||
from uuid import UUID, uuid4
|
||||
from typing import Any
|
||||
|
||||
|
||||
class Document:
|
||||
"""Документ в коллекции"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
title: str,
|
||||
content: str,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
document_id: UUID | None = None,
|
||||
created_at: datetime | None = None
|
||||
):
|
||||
self.document_id = document_id or uuid4()
|
||||
self.collection_id = collection_id
|
||||
self.title = title
|
||||
self.content = content
|
||||
self.metadata = metadata or {}
|
||||
self.created_at = created_at or datetime.utcnow()
|
||||
|
||||
|
||||
@ -1,25 +1,25 @@
|
||||
"""
|
||||
Доменная сущность Embedding
|
||||
"""
|
||||
from datetime import datetime
|
||||
from uuid import UUID, uuid4
|
||||
from typing import Any
|
||||
|
||||
|
||||
class Embedding:
|
||||
"""Эмбеддинг документа"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
document_id: UUID,
|
||||
embedding: list[float] | None = None,
|
||||
model_version: str = "",
|
||||
embedding_id: UUID | None = None,
|
||||
created_at: datetime | None = None
|
||||
):
|
||||
self.embedding_id = embedding_id or uuid4()
|
||||
self.document_id = document_id
|
||||
self.embedding = embedding or []
|
||||
self.model_version = model_version
|
||||
self.created_at = created_at or datetime.utcnow()
|
||||
|
||||
"""
|
||||
Доменная сущность Embedding
|
||||
"""
|
||||
from datetime import datetime
|
||||
from uuid import UUID, uuid4
|
||||
from typing import Any
|
||||
|
||||
|
||||
class Embedding:
|
||||
"""Эмбеддинг документа"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
document_id: UUID,
|
||||
embedding: list[float] | None = None,
|
||||
model_version: str = "",
|
||||
embedding_id: UUID | None = None,
|
||||
created_at: datetime | None = None
|
||||
):
|
||||
self.embedding_id = embedding_id or uuid4()
|
||||
self.document_id = document_id
|
||||
self.embedding = embedding or []
|
||||
self.model_version = model_version
|
||||
self.created_at = created_at or datetime.utcnow()
|
||||
|
||||
|
||||
@ -1,35 +1,35 @@
|
||||
"""
|
||||
Доменная сущность Message
|
||||
"""
|
||||
from datetime import datetime
|
||||
from uuid import UUID, uuid4
|
||||
from typing import Any
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class MessageRole(str, Enum):
|
||||
"""Роли сообщений"""
|
||||
USER = "user"
|
||||
ASSISTANT = "assistant"
|
||||
SYSTEM = "system"
|
||||
|
||||
|
||||
class Message:
|
||||
"""Сообщение в беседе"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
conversation_id: UUID,
|
||||
content: str,
|
||||
role: MessageRole,
|
||||
sources: dict[str, Any] | None = None,
|
||||
message_id: UUID | None = None,
|
||||
created_at: datetime | None = None
|
||||
):
|
||||
self.message_id = message_id or uuid4()
|
||||
self.conversation_id = conversation_id
|
||||
self.content = content
|
||||
self.role = role
|
||||
self.sources = sources or {}
|
||||
self.created_at = created_at or datetime.utcnow()
|
||||
|
||||
"""
|
||||
Доменная сущность Message
|
||||
"""
|
||||
from datetime import datetime
|
||||
from uuid import UUID, uuid4
|
||||
from typing import Any
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class MessageRole(str, Enum):
|
||||
"""Роли сообщений"""
|
||||
USER = "user"
|
||||
ASSISTANT = "assistant"
|
||||
SYSTEM = "system"
|
||||
|
||||
|
||||
class Message:
|
||||
"""Сообщение в беседе"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
conversation_id: UUID,
|
||||
content: str,
|
||||
role: MessageRole,
|
||||
sources: dict[str, Any] | None = None,
|
||||
message_id: UUID | None = None,
|
||||
created_at: datetime | None = None
|
||||
):
|
||||
self.message_id = message_id or uuid4()
|
||||
self.conversation_id = conversation_id
|
||||
self.content = content
|
||||
self.role = role
|
||||
self.sources = sources or {}
|
||||
self.created_at = created_at or datetime.utcnow()
|
||||
|
||||
|
||||
@ -1,33 +1,33 @@
|
||||
"""
|
||||
Доменная сущность User
|
||||
"""
|
||||
from datetime import datetime
|
||||
from uuid import UUID, uuid4
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class UserRole(str, Enum):
|
||||
"""Роли пользователей"""
|
||||
USER = "user"
|
||||
ADMIN = "admin"
|
||||
|
||||
|
||||
class User:
|
||||
"""Пользователь системы"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
telegram_id: str,
|
||||
role: UserRole = UserRole.USER,
|
||||
user_id: UUID | None = None,
|
||||
created_at: datetime | None = None
|
||||
):
|
||||
self.user_id = user_id or uuid4()
|
||||
self.telegram_id = telegram_id
|
||||
self.role = role
|
||||
self.created_at = created_at or datetime.utcnow()
|
||||
|
||||
def is_admin(self) -> bool:
|
||||
"""проверка, является ли пользователь администратором"""
|
||||
return self.role == UserRole.ADMIN
|
||||
|
||||
"""
|
||||
Доменная сущность User
|
||||
"""
|
||||
from datetime import datetime
|
||||
from uuid import UUID, uuid4
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class UserRole(str, Enum):
|
||||
"""Роли пользователей"""
|
||||
USER = "user"
|
||||
ADMIN = "admin"
|
||||
|
||||
|
||||
class User:
|
||||
"""Пользователь системы"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
telegram_id: str,
|
||||
role: UserRole = UserRole.USER,
|
||||
user_id: UUID | None = None,
|
||||
created_at: datetime | None = None
|
||||
):
|
||||
self.user_id = user_id or uuid4()
|
||||
self.telegram_id = telegram_id
|
||||
self.role = role
|
||||
self.created_at = created_at or datetime.utcnow()
|
||||
|
||||
def is_admin(self) -> bool:
|
||||
"""проверка, является ли пользователь администратором"""
|
||||
return self.role == UserRole.ADMIN
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""
|
||||
Domain repositories interfaces
|
||||
"""
|
||||
|
||||
"""
|
||||
Domain repositories interfaces
|
||||
"""
|
||||
|
||||
|
||||
@ -1,47 +1,47 @@
|
||||
"""
|
||||
Интерфейс репозитория для CollectionAccess
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
from src.domain.entities.collection_access import CollectionAccess
|
||||
|
||||
|
||||
class ICollectionAccessRepository(ABC):
|
||||
"""Интерфейс репозитория доступа к коллекциям"""
|
||||
|
||||
@abstractmethod
|
||||
async def create(self, access: CollectionAccess) -> CollectionAccess:
|
||||
"""Создать доступ"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_by_id(self, access_id: UUID) -> Optional[CollectionAccess]:
|
||||
"""Получить доступ по ID"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def delete(self, access_id: UUID) -> bool:
|
||||
"""Удалить доступ"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def delete_by_user_and_collection(self, user_id: UUID, collection_id: UUID) -> bool:
|
||||
"""Удалить доступ пользователя к коллекции"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_by_user_and_collection(self, user_id: UUID, collection_id: UUID) -> Optional[CollectionAccess]:
|
||||
"""Получить доступ пользователя к коллекции"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def list_by_user(self, user_id: UUID) -> list[CollectionAccess]:
|
||||
"""Получить доступы пользователя"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def list_by_collection(self, collection_id: UUID) -> list[CollectionAccess]:
|
||||
"""Получить доступы к коллекции"""
|
||||
pass
|
||||
|
||||
"""
|
||||
Интерфейс репозитория для CollectionAccess
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
from src.domain.entities.collection_access import CollectionAccess
|
||||
|
||||
|
||||
class ICollectionAccessRepository(ABC):
|
||||
"""Интерфейс репозитория доступа к коллекциям"""
|
||||
|
||||
@abstractmethod
|
||||
async def create(self, access: CollectionAccess) -> CollectionAccess:
|
||||
"""Создать доступ"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_by_id(self, access_id: UUID) -> Optional[CollectionAccess]:
|
||||
"""Получить доступ по ID"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def delete(self, access_id: UUID) -> bool:
|
||||
"""Удалить доступ"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def delete_by_user_and_collection(self, user_id: UUID, collection_id: UUID) -> bool:
|
||||
"""Удалить доступ пользователя к коллекции"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_by_user_and_collection(self, user_id: UUID, collection_id: UUID) -> Optional[CollectionAccess]:
|
||||
"""Получить доступ пользователя к коллекции"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def list_by_user(self, user_id: UUID) -> list[CollectionAccess]:
|
||||
"""Получить доступы пользователя"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def list_by_collection(self, collection_id: UUID) -> list[CollectionAccess]:
|
||||
"""Получить доступы к коллекции"""
|
||||
pass
|
||||
|
||||
|
||||
@ -1,42 +1,42 @@
|
||||
"""
|
||||
Интерфейс репозитория для Collection
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
from src.domain.entities.collection import Collection
|
||||
|
||||
|
||||
class ICollectionRepository(ABC):
|
||||
"""Интерфейс репозитория коллекций"""
|
||||
|
||||
@abstractmethod
|
||||
async def create(self, collection: Collection) -> Collection:
|
||||
"""Создать коллекцию"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_by_id(self, collection_id: UUID) -> Optional[Collection]:
|
||||
"""Получить коллекцию по ID"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def update(self, collection: Collection) -> Collection:
|
||||
"""Обновить коллекцию"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def delete(self, collection_id: UUID) -> bool:
|
||||
"""Удалить коллекцию"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def list_by_owner(self, owner_id: UUID, skip: int = 0, limit: int = 100) -> list[Collection]:
|
||||
"""Получить коллекции владельца"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def list_public(self, skip: int = 0, limit: int = 100) -> list[Collection]:
|
||||
"""Получить публичные коллекции"""
|
||||
pass
|
||||
|
||||
"""
|
||||
Интерфейс репозитория для Collection
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
from src.domain.entities.collection import Collection
|
||||
|
||||
|
||||
class ICollectionRepository(ABC):
|
||||
"""Интерфейс репозитория коллекций"""
|
||||
|
||||
@abstractmethod
|
||||
async def create(self, collection: Collection) -> Collection:
|
||||
"""Создать коллекцию"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_by_id(self, collection_id: UUID) -> Optional[Collection]:
|
||||
"""Получить коллекцию по ID"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def update(self, collection: Collection) -> Collection:
|
||||
"""Обновить коллекцию"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def delete(self, collection_id: UUID) -> bool:
|
||||
"""Удалить коллекцию"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def list_by_owner(self, owner_id: UUID, skip: int = 0, limit: int = 100) -> list[Collection]:
|
||||
"""Получить коллекции владельца"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def list_public(self, skip: int = 0, limit: int = 100) -> list[Collection]:
|
||||
"""Получить публичные коллекции"""
|
||||
pass
|
||||
|
||||
|
||||
@ -1,42 +1,42 @@
|
||||
"""
|
||||
Интерфейс репозитория для Conversation
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
from src.domain.entities.conversation import Conversation
|
||||
|
||||
|
||||
class IConversationRepository(ABC):
|
||||
"""Интерфейс репозитория бесед"""
|
||||
|
||||
@abstractmethod
|
||||
async def create(self, conversation: Conversation) -> Conversation:
|
||||
"""Создать беседу"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_by_id(self, conversation_id: UUID) -> Optional[Conversation]:
|
||||
"""Получить беседу по ID"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def update(self, conversation: Conversation) -> Conversation:
|
||||
"""Обновить беседу"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def delete(self, conversation_id: UUID) -> bool:
|
||||
"""Удалить беседу"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def list_by_user(self, user_id: UUID, skip: int = 0, limit: int = 100) -> list[Conversation]:
|
||||
"""Получить беседы пользователя"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def list_by_collection(self, collection_id: UUID, skip: int = 0, limit: int = 100) -> list[Conversation]:
|
||||
"""Получить беседы по коллекции"""
|
||||
pass
|
||||
|
||||
"""
|
||||
Интерфейс репозитория для Conversation
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
from src.domain.entities.conversation import Conversation
|
||||
|
||||
|
||||
class IConversationRepository(ABC):
|
||||
"""Интерфейс репозитория бесед"""
|
||||
|
||||
@abstractmethod
|
||||
async def create(self, conversation: Conversation) -> Conversation:
|
||||
"""Создать беседу"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_by_id(self, conversation_id: UUID) -> Optional[Conversation]:
|
||||
"""Получить беседу по ID"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def update(self, conversation: Conversation) -> Conversation:
|
||||
"""Обновить беседу"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def delete(self, conversation_id: UUID) -> bool:
|
||||
"""Удалить беседу"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def list_by_user(self, user_id: UUID, skip: int = 0, limit: int = 100) -> list[Conversation]:
|
||||
"""Получить беседы пользователя"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def list_by_collection(self, collection_id: UUID, skip: int = 0, limit: int = 100) -> list[Conversation]:
|
||||
"""Получить беседы по коллекции"""
|
||||
pass
|
||||
|
||||
|
||||
@ -1,37 +1,37 @@
|
||||
"""
|
||||
Интерфейс репозитория для Document
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
from src.domain.entities.document import Document
|
||||
|
||||
|
||||
class IDocumentRepository(ABC):
|
||||
"""Интерфейс репозитория документов"""
|
||||
|
||||
@abstractmethod
|
||||
async def create(self, document: Document) -> Document:
|
||||
"""Создать документ"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_by_id(self, document_id: UUID) -> Optional[Document]:
|
||||
"""Получить документ по ID"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def update(self, document: Document) -> Document:
|
||||
"""Обновить документ"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def delete(self, document_id: UUID) -> bool:
|
||||
"""Удалить документ"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def list_by_collection(self, collection_id: UUID, skip: int = 0, limit: int = 100) -> list[Document]:
|
||||
"""Получить документы коллекции"""
|
||||
pass
|
||||
|
||||
"""
|
||||
Интерфейс репозитория для Document
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
from src.domain.entities.document import Document
|
||||
|
||||
|
||||
class IDocumentRepository(ABC):
|
||||
"""Интерфейс репозитория документов"""
|
||||
|
||||
@abstractmethod
|
||||
async def create(self, document: Document) -> Document:
|
||||
"""Создать документ"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_by_id(self, document_id: UUID) -> Optional[Document]:
|
||||
"""Получить документ по ID"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def update(self, document: Document) -> Document:
|
||||
"""Обновить документ"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def delete(self, document_id: UUID) -> bool:
|
||||
"""Удалить документ"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def list_by_collection(self, collection_id: UUID, skip: int = 0, limit: int = 100) -> list[Document]:
|
||||
"""Получить документы коллекции"""
|
||||
pass
|
||||
|
||||
|
||||
@ -1,37 +1,37 @@
|
||||
"""
|
||||
Интерфейс репозитория для Message
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
from src.domain.entities.message import Message
|
||||
|
||||
|
||||
class IMessageRepository(ABC):
|
||||
"""Интерфейс репозитория сообщений"""
|
||||
|
||||
@abstractmethod
|
||||
async def create(self, message: Message) -> Message:
|
||||
"""Создать сообщение"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_by_id(self, message_id: UUID) -> Optional[Message]:
|
||||
"""Получить сообщение по ID"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def update(self, message: Message) -> Message:
|
||||
"""Обновить сообщение"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def delete(self, message_id: UUID) -> bool:
|
||||
"""Удалить сообщение"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def list_by_conversation(self, conversation_id: UUID, skip: int = 0, limit: int = 100) -> list[Message]:
|
||||
"""Получить сообщения беседы"""
|
||||
pass
|
||||
|
||||
"""
|
||||
Интерфейс репозитория для Message
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
from src.domain.entities.message import Message
|
||||
|
||||
|
||||
class IMessageRepository(ABC):
|
||||
"""Интерфейс репозитория сообщений"""
|
||||
|
||||
@abstractmethod
|
||||
async def create(self, message: Message) -> Message:
|
||||
"""Создать сообщение"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_by_id(self, message_id: UUID) -> Optional[Message]:
|
||||
"""Получить сообщение по ID"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def update(self, message: Message) -> Message:
|
||||
"""Обновить сообщение"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def delete(self, message_id: UUID) -> bool:
|
||||
"""Удалить сообщение"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def list_by_conversation(self, conversation_id: UUID, skip: int = 0, limit: int = 100) -> list[Message]:
|
||||
"""Получить сообщения беседы"""
|
||||
pass
|
||||
|
||||
|
||||
@ -1,42 +1,42 @@
|
||||
"""
|
||||
Интерфейс репозитория для User
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
from src.domain.entities.user import User
|
||||
|
||||
|
||||
class IUserRepository(ABC):
|
||||
"""Интерфейс репозитория пользователей"""
|
||||
|
||||
@abstractmethod
|
||||
async def create(self, user: User) -> User:
|
||||
"""Создать пользователя"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_by_id(self, user_id: UUID) -> Optional[User]:
|
||||
"""Получить пользователя по ID"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_by_telegram_id(self, telegram_id: str) -> Optional[User]:
|
||||
"""Получить пользователя по Telegram ID"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def update(self, user: User) -> User:
|
||||
"""Обновить пользователя"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def delete(self, user_id: UUID) -> bool:
|
||||
"""Удалить пользователя"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def list_all(self, skip: int = 0, limit: int = 100) -> list[User]:
|
||||
"""Получить список всех пользователей"""
|
||||
pass
|
||||
|
||||
"""
|
||||
Интерфейс репозитория для User
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
from src.domain.entities.user import User
|
||||
|
||||
|
||||
class IUserRepository(ABC):
|
||||
"""Интерфейс репозитория пользователей"""
|
||||
|
||||
@abstractmethod
|
||||
async def create(self, user: User) -> User:
|
||||
"""Создать пользователя"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_by_id(self, user_id: UUID) -> Optional[User]:
|
||||
"""Получить пользователя по ID"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_by_telegram_id(self, telegram_id: str) -> Optional[User]:
|
||||
"""Получить пользователя по Telegram ID"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def update(self, user: User) -> User:
|
||||
"""Обновить пользователя"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def delete(self, user_id: UUID) -> bool:
|
||||
"""Удалить пользователя"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def list_all(self, skip: int = 0, limit: int = 100) -> list[User]:
|
||||
"""Получить список всех пользователей"""
|
||||
pass
|
||||
|
||||
|
||||
@ -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.orm import declarative_base
|
||||
from src.shared.config import settings
|
||||
|
||||
engine = create_async_engine(
|
||||
settings.database_url.replace("postgresql://", "postgresql+asyncpg://"),
|
||||
echo=settings.DEBUG,
|
||||
future=True
|
||||
)
|
||||
|
||||
AsyncSessionLocal = async_sessionmaker(
|
||||
engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
autocommit=False,
|
||||
autoflush=False
|
||||
)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
async def get_db() -> AsyncSession:
|
||||
"""Dependency для получения сессии БД"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
await session.close()
|
||||
|
||||
"""
|
||||
Базовые настройки базы данных
|
||||
"""
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
from sqlalchemy.orm import declarative_base
|
||||
from src.shared.config import settings
|
||||
|
||||
engine = create_async_engine(
|
||||
settings.database_url.replace("postgresql://", "postgresql+asyncpg://"),
|
||||
echo=settings.DEBUG,
|
||||
future=True
|
||||
)
|
||||
|
||||
AsyncSessionLocal = async_sessionmaker(
|
||||
engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
autocommit=False,
|
||||
autoflush=False
|
||||
)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
async def get_db() -> AsyncSession:
|
||||
"""Dependency для получения сессии БД"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
await session.close()
|
||||
|
||||
|
||||
@ -1,109 +1,109 @@
|
||||
"""
|
||||
SQLAlchemy модели для базы данных
|
||||
"""
|
||||
from sqlalchemy import Column, String, Text, Boolean, DateTime, ForeignKey, JSON, Integer
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
from src.infrastructure.database.base import Base
|
||||
|
||||
|
||||
class UserModel(Base):
|
||||
"""Модель пользователя"""
|
||||
__tablename__ = "users"
|
||||
|
||||
user_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
telegram_id = Column(String, unique=True, nullable=False, index=True)
|
||||
role = Column(String, nullable=False, default="user")
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
collections = relationship("CollectionModel", back_populates="owner", cascade="all, delete-orphan")
|
||||
conversations = relationship("ConversationModel", back_populates="user", cascade="all, delete-orphan")
|
||||
collection_accesses = relationship("CollectionAccessModel", back_populates="user", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class CollectionModel(Base):
|
||||
"""Модель коллекции"""
|
||||
__tablename__ = "collections"
|
||||
|
||||
collection_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
name = Column(String, nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
owner_id = Column(UUID(as_uuid=True), ForeignKey("users.user_id"), nullable=False)
|
||||
is_public = Column(Boolean, nullable=False, default=False)
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
owner = relationship("UserModel", back_populates="collections")
|
||||
documents = relationship("DocumentModel", back_populates="collection", cascade="all, delete-orphan")
|
||||
conversations = relationship("ConversationModel", back_populates="collection", cascade="all, delete-orphan")
|
||||
accesses = relationship("CollectionAccessModel", back_populates="collection", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class DocumentModel(Base):
|
||||
"""Модель документа"""
|
||||
__tablename__ = "documents"
|
||||
|
||||
document_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
collection_id = Column(UUID(as_uuid=True), ForeignKey("collections.collection_id"), nullable=False)
|
||||
title = Column(String, nullable=False)
|
||||
content = Column(Text, nullable=False)
|
||||
document_metadata = Column("metadata", JSON, nullable=True, default={})
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
collection = relationship("CollectionModel", back_populates="documents")
|
||||
embeddings = relationship("EmbeddingModel", back_populates="document", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class EmbeddingModel(Base):
|
||||
"""Модель эмбеддинга (заглушка)"""
|
||||
__tablename__ = "embeddings"
|
||||
|
||||
embedding_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
document_id = Column(UUID(as_uuid=True), ForeignKey("documents.document_id"), nullable=False)
|
||||
embedding = Column(JSON, nullable=True)
|
||||
model_version = Column(String, nullable=True)
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
document = relationship("DocumentModel", back_populates="embeddings")
|
||||
|
||||
|
||||
class ConversationModel(Base):
|
||||
"""Модель беседы"""
|
||||
__tablename__ = "conversations"
|
||||
|
||||
conversation_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id = Column(UUID(as_uuid=True), ForeignKey("users.user_id"), nullable=False)
|
||||
collection_id = Column(UUID(as_uuid=True), ForeignKey("collections.collection_id"), nullable=False)
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
user = relationship("UserModel", back_populates="conversations")
|
||||
collection = relationship("CollectionModel", back_populates="conversations")
|
||||
messages = relationship("MessageModel", back_populates="conversation", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class MessageModel(Base):
|
||||
"""Модель сообщения"""
|
||||
__tablename__ = "messages"
|
||||
|
||||
message_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
conversation_id = Column(UUID(as_uuid=True), ForeignKey("conversations.conversation_id"), nullable=False)
|
||||
content = Column(Text, nullable=False)
|
||||
role = Column(String, nullable=False)
|
||||
sources = Column(JSON, nullable=True, default={})
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
conversation = relationship("ConversationModel", back_populates="messages")
|
||||
|
||||
|
||||
class CollectionAccessModel(Base):
|
||||
"""Модель доступа к коллекции"""
|
||||
__tablename__ = "collection_access"
|
||||
|
||||
access_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id = Column(UUID(as_uuid=True), ForeignKey("users.user_id"), nullable=False)
|
||||
collection_id = Column(UUID(as_uuid=True), ForeignKey("collections.collection_id"), nullable=False)
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
user = relationship("UserModel", back_populates="collection_accesses")
|
||||
collection = relationship("CollectionModel", back_populates="accesses")
|
||||
|
||||
__table_args__ = (
|
||||
{"comment": "Уникальный доступ пользователя к коллекции"},
|
||||
)
|
||||
|
||||
"""
|
||||
SQLAlchemy модели для базы данных
|
||||
"""
|
||||
from sqlalchemy import Column, String, Text, Boolean, DateTime, ForeignKey, JSON, Integer
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
from src.infrastructure.database.base import Base
|
||||
|
||||
|
||||
class UserModel(Base):
|
||||
"""Модель пользователя"""
|
||||
__tablename__ = "users"
|
||||
|
||||
user_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
telegram_id = Column(String, unique=True, nullable=False, index=True)
|
||||
role = Column(String, nullable=False, default="user")
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
collections = relationship("CollectionModel", back_populates="owner", cascade="all, delete-orphan")
|
||||
conversations = relationship("ConversationModel", back_populates="user", cascade="all, delete-orphan")
|
||||
collection_accesses = relationship("CollectionAccessModel", back_populates="user", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class CollectionModel(Base):
|
||||
"""Модель коллекции"""
|
||||
__tablename__ = "collections"
|
||||
|
||||
collection_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
name = Column(String, nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
owner_id = Column(UUID(as_uuid=True), ForeignKey("users.user_id"), nullable=False)
|
||||
is_public = Column(Boolean, nullable=False, default=False)
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
owner = relationship("UserModel", back_populates="collections")
|
||||
documents = relationship("DocumentModel", back_populates="collection", cascade="all, delete-orphan")
|
||||
conversations = relationship("ConversationModel", back_populates="collection", cascade="all, delete-orphan")
|
||||
accesses = relationship("CollectionAccessModel", back_populates="collection", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class DocumentModel(Base):
|
||||
"""Модель документа"""
|
||||
__tablename__ = "documents"
|
||||
|
||||
document_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
collection_id = Column(UUID(as_uuid=True), ForeignKey("collections.collection_id"), nullable=False)
|
||||
title = Column(String, nullable=False)
|
||||
content = Column(Text, nullable=False)
|
||||
document_metadata = Column("metadata", JSON, nullable=True, default={})
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
collection = relationship("CollectionModel", back_populates="documents")
|
||||
embeddings = relationship("EmbeddingModel", back_populates="document", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class EmbeddingModel(Base):
|
||||
"""Модель эмбеддинга (заглушка)"""
|
||||
__tablename__ = "embeddings"
|
||||
|
||||
embedding_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
document_id = Column(UUID(as_uuid=True), ForeignKey("documents.document_id"), nullable=False)
|
||||
embedding = Column(JSON, nullable=True)
|
||||
model_version = Column(String, nullable=True)
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
document = relationship("DocumentModel", back_populates="embeddings")
|
||||
|
||||
|
||||
class ConversationModel(Base):
|
||||
"""Модель беседы"""
|
||||
__tablename__ = "conversations"
|
||||
|
||||
conversation_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id = Column(UUID(as_uuid=True), ForeignKey("users.user_id"), nullable=False)
|
||||
collection_id = Column(UUID(as_uuid=True), ForeignKey("collections.collection_id"), nullable=False)
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
user = relationship("UserModel", back_populates="conversations")
|
||||
collection = relationship("CollectionModel", back_populates="conversations")
|
||||
messages = relationship("MessageModel", back_populates="conversation", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class MessageModel(Base):
|
||||
"""Модель сообщения"""
|
||||
__tablename__ = "messages"
|
||||
|
||||
message_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
conversation_id = Column(UUID(as_uuid=True), ForeignKey("conversations.conversation_id"), nullable=False)
|
||||
content = Column(Text, nullable=False)
|
||||
role = Column(String, nullable=False)
|
||||
sources = Column(JSON, nullable=True, default={})
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
conversation = relationship("ConversationModel", back_populates="messages")
|
||||
|
||||
|
||||
class CollectionAccessModel(Base):
|
||||
"""Модель доступа к коллекции"""
|
||||
__tablename__ = "collection_access"
|
||||
|
||||
access_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id = Column(UUID(as_uuid=True), ForeignKey("users.user_id"), nullable=False)
|
||||
collection_id = Column(UUID(as_uuid=True), ForeignKey("collections.collection_id"), nullable=False)
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
user = relationship("UserModel", back_populates="collection_accesses")
|
||||
collection = relationship("CollectionModel", back_populates="accesses")
|
||||
|
||||
__table_args__ = (
|
||||
{"comment": "Уникальный доступ пользователя к коллекции"},
|
||||
)
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""
|
||||
External services
|
||||
"""
|
||||
|
||||
"""
|
||||
External services
|
||||
"""
|
||||
|
||||
|
||||
@ -1,223 +1,223 @@
|
||||
"""
|
||||
Клиент для работы с DeepSeek API
|
||||
"""
|
||||
import json
|
||||
from typing import Optional, AsyncIterator
|
||||
import httpx
|
||||
from src.shared.config import settings
|
||||
|
||||
|
||||
class DeepSeekAPIError(Exception):
|
||||
"""Ошибка при работе с DeepSeek API"""
|
||||
pass
|
||||
|
||||
|
||||
class DeepSeekClient:
|
||||
"""Клиент для работы с DeepSeek API"""
|
||||
|
||||
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_url = api_url or settings.DEEPSEEK_API_URL
|
||||
self.timeout = 60.0
|
||||
|
||||
def _get_headers(self) -> dict[str, str]:
|
||||
"""Получить заголовки для запроса"""
|
||||
if not self.api_key:
|
||||
raise DeepSeekAPIError("DEEPSEEK_API_KEY не установлен в настройках")
|
||||
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.api_key}"
|
||||
}
|
||||
|
||||
async def chat_completion(
|
||||
self,
|
||||
messages: list[dict[str, str]],
|
||||
model: str = "deepseek-chat",
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None,
|
||||
stream: bool = False
|
||||
) -> dict:
|
||||
"""
|
||||
Отправка запроса на генерацию ответа
|
||||
|
||||
Args:
|
||||
messages: Список сообщений в формате [{"role": "user", "content": "..."}]
|
||||
model: Модель для использования (по умолчанию "deepseek-chat")
|
||||
temperature: Температура генерации (0.0-2.0)
|
||||
max_tokens: Максимальное количество токенов в ответе
|
||||
stream: Использовать ли потоковую генерацию
|
||||
|
||||
Returns:
|
||||
Ответ от API в формате:
|
||||
{
|
||||
"content": "текст ответа",
|
||||
"usage": {
|
||||
"prompt_tokens": int,
|
||||
"completion_tokens": int,
|
||||
"total_tokens": int
|
||||
}
|
||||
}
|
||||
|
||||
Raises:
|
||||
DeepSeekAPIError: При ошибке API
|
||||
"""
|
||||
if not self.api_key:
|
||||
return {
|
||||
"content": " DEEPSEEK_API_KEY не установлен. Установите ключ в настройках для работы с DeepSeek API.",
|
||||
"usage": {
|
||||
"prompt_tokens": 0,
|
||||
"completion_tokens": 0,
|
||||
"total_tokens": 0
|
||||
}
|
||||
}
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"stream": stream
|
||||
}
|
||||
|
||||
if max_tokens is not None:
|
||||
payload["max_tokens"] = max_tokens
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.post(
|
||||
self.api_url,
|
||||
headers=self._get_headers(),
|
||||
json=payload
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
|
||||
if "choices" in data and len(data["choices"]) > 0:
|
||||
content = data["choices"][0]["message"]["content"]
|
||||
else:
|
||||
raise DeepSeekAPIError("Неожиданный формат ответа от DeepSeek API")
|
||||
|
||||
usage = data.get("usage", {})
|
||||
|
||||
return {
|
||||
"content": content,
|
||||
"usage": {
|
||||
"prompt_tokens": usage.get("prompt_tokens", 0),
|
||||
"completion_tokens": usage.get("completion_tokens", 0),
|
||||
"total_tokens": usage.get("total_tokens", 0)
|
||||
}
|
||||
}
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
error_msg = f"Ошибка DeepSeek API: {e.response.status_code}"
|
||||
try:
|
||||
error_data = e.response.json()
|
||||
if "error" in error_data:
|
||||
error_msg = f"Ошибка DeepSeek API: {error_data['error'].get('message', error_msg)}"
|
||||
except:
|
||||
pass
|
||||
raise DeepSeekAPIError(error_msg) from e
|
||||
except httpx.RequestError as e:
|
||||
raise DeepSeekAPIError(f"Ошибка подключения к DeepSeek API: {str(e)}") from e
|
||||
except Exception as e:
|
||||
raise DeepSeekAPIError(f"Неожиданная ошибка при работе с DeepSeek API: {str(e)}") from e
|
||||
|
||||
async def stream_chat_completion(
|
||||
self,
|
||||
messages: list[dict[str, str]],
|
||||
model: str = "deepseek-chat",
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None
|
||||
) -> AsyncIterator[str]:
|
||||
"""
|
||||
Потоковая генерация ответа
|
||||
|
||||
Args:
|
||||
messages: Список сообщений в формате [{"role": "user", "content": "..."}]
|
||||
model: Модель для использования
|
||||
temperature: Температура генерации
|
||||
max_tokens: Максимальное количество токенов
|
||||
|
||||
Yields:
|
||||
Части ответа (chunks) по мере генерации
|
||||
|
||||
Raises:
|
||||
DeepSeekAPIError: При ошибке API
|
||||
"""
|
||||
if not self.api_key:
|
||||
yield "⚠️ DEEPSEEK_API_KEY не установлен. Установите ключ в настройках для работы с DeepSeek API."
|
||||
return
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"stream": True
|
||||
}
|
||||
|
||||
if max_tokens is not None:
|
||||
payload["max_tokens"] = max_tokens
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
async with client.stream(
|
||||
"POST",
|
||||
self.api_url,
|
||||
headers=self._get_headers(),
|
||||
json=payload
|
||||
) as response:
|
||||
response.raise_for_status()
|
||||
|
||||
async for line in response.aiter_lines():
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
if line.startswith("data: "):
|
||||
line = line[6:]
|
||||
|
||||
if line.strip() == "[DONE]":
|
||||
break
|
||||
|
||||
try:
|
||||
data = json.loads(line)
|
||||
|
||||
if "choices" in data and len(data["choices"]) > 0:
|
||||
delta = data["choices"][0].get("delta", {})
|
||||
content = delta.get("content", "")
|
||||
if content:
|
||||
yield content
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
error_msg = f"Ошибка DeepSeek API: {e.response.status_code}"
|
||||
try:
|
||||
error_data = e.response.json()
|
||||
if "error" in error_data:
|
||||
error_msg = f"Ошибка DeepSeek API: {error_data['error'].get('message', error_msg)}"
|
||||
except:
|
||||
pass
|
||||
raise DeepSeekAPIError(error_msg) from e
|
||||
except httpx.RequestError as e:
|
||||
raise DeepSeekAPIError(f"Ошибка подключения к DeepSeek API: {str(e)}") from e
|
||||
except Exception as e:
|
||||
raise DeepSeekAPIError(f"Неожиданная ошибка при потоковой генерации: {str(e)}") from e
|
||||
|
||||
async def health_check(self) -> bool:
|
||||
"""
|
||||
Проверка доступности API
|
||||
|
||||
Returns:
|
||||
True если API доступен, False иначе
|
||||
"""
|
||||
if not self.api_key:
|
||||
return False
|
||||
|
||||
try:
|
||||
test_messages = [{"role": "user", "content": "test"}]
|
||||
await self.chat_completion(test_messages, max_tokens=1)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
"""
|
||||
Клиент для работы с DeepSeek API
|
||||
"""
|
||||
import json
|
||||
from typing import Optional, AsyncIterator
|
||||
import httpx
|
||||
from src.shared.config import settings
|
||||
|
||||
|
||||
class DeepSeekAPIError(Exception):
|
||||
"""Ошибка при работе с DeepSeek API"""
|
||||
pass
|
||||
|
||||
|
||||
class DeepSeekClient:
|
||||
"""Клиент для работы с DeepSeek API"""
|
||||
|
||||
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_url = api_url or settings.DEEPSEEK_API_URL
|
||||
self.timeout = 60.0
|
||||
|
||||
def _get_headers(self) -> dict[str, str]:
|
||||
"""Получить заголовки для запроса"""
|
||||
if not self.api_key:
|
||||
raise DeepSeekAPIError("DEEPSEEK_API_KEY не установлен в настройках")
|
||||
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.api_key}"
|
||||
}
|
||||
|
||||
async def chat_completion(
|
||||
self,
|
||||
messages: list[dict[str, str]],
|
||||
model: str = "deepseek-chat",
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None,
|
||||
stream: bool = False
|
||||
) -> dict:
|
||||
"""
|
||||
Отправка запроса на генерацию ответа
|
||||
|
||||
Args:
|
||||
messages: Список сообщений в формате [{"role": "user", "content": "..."}]
|
||||
model: Модель для использования (по умолчанию "deepseek-chat")
|
||||
temperature: Температура генерации (0.0-2.0)
|
||||
max_tokens: Максимальное количество токенов в ответе
|
||||
stream: Использовать ли потоковую генерацию
|
||||
|
||||
Returns:
|
||||
Ответ от API в формате:
|
||||
{
|
||||
"content": "текст ответа",
|
||||
"usage": {
|
||||
"prompt_tokens": int,
|
||||
"completion_tokens": int,
|
||||
"total_tokens": int
|
||||
}
|
||||
}
|
||||
|
||||
Raises:
|
||||
DeepSeekAPIError: При ошибке API
|
||||
"""
|
||||
if not self.api_key:
|
||||
return {
|
||||
"content": " DEEPSEEK_API_KEY не установлен. Установите ключ в настройках для работы с DeepSeek API.",
|
||||
"usage": {
|
||||
"prompt_tokens": 0,
|
||||
"completion_tokens": 0,
|
||||
"total_tokens": 0
|
||||
}
|
||||
}
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"stream": stream
|
||||
}
|
||||
|
||||
if max_tokens is not None:
|
||||
payload["max_tokens"] = max_tokens
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.post(
|
||||
self.api_url,
|
||||
headers=self._get_headers(),
|
||||
json=payload
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
|
||||
if "choices" in data and len(data["choices"]) > 0:
|
||||
content = data["choices"][0]["message"]["content"]
|
||||
else:
|
||||
raise DeepSeekAPIError("Неожиданный формат ответа от DeepSeek API")
|
||||
|
||||
usage = data.get("usage", {})
|
||||
|
||||
return {
|
||||
"content": content,
|
||||
"usage": {
|
||||
"prompt_tokens": usage.get("prompt_tokens", 0),
|
||||
"completion_tokens": usage.get("completion_tokens", 0),
|
||||
"total_tokens": usage.get("total_tokens", 0)
|
||||
}
|
||||
}
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
error_msg = f"Ошибка DeepSeek API: {e.response.status_code}"
|
||||
try:
|
||||
error_data = e.response.json()
|
||||
if "error" in error_data:
|
||||
error_msg = f"Ошибка DeepSeek API: {error_data['error'].get('message', error_msg)}"
|
||||
except:
|
||||
pass
|
||||
raise DeepSeekAPIError(error_msg) from e
|
||||
except httpx.RequestError as e:
|
||||
raise DeepSeekAPIError(f"Ошибка подключения к DeepSeek API: {str(e)}") from e
|
||||
except Exception as e:
|
||||
raise DeepSeekAPIError(f"Неожиданная ошибка при работе с DeepSeek API: {str(e)}") from e
|
||||
|
||||
async def stream_chat_completion(
|
||||
self,
|
||||
messages: list[dict[str, str]],
|
||||
model: str = "deepseek-chat",
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None
|
||||
) -> AsyncIterator[str]:
|
||||
"""
|
||||
Потоковая генерация ответа
|
||||
|
||||
Args:
|
||||
messages: Список сообщений в формате [{"role": "user", "content": "..."}]
|
||||
model: Модель для использования
|
||||
temperature: Температура генерации
|
||||
max_tokens: Максимальное количество токенов
|
||||
|
||||
Yields:
|
||||
Части ответа (chunks) по мере генерации
|
||||
|
||||
Raises:
|
||||
DeepSeekAPIError: При ошибке API
|
||||
"""
|
||||
if not self.api_key:
|
||||
yield "⚠️ DEEPSEEK_API_KEY не установлен. Установите ключ в настройках для работы с DeepSeek API."
|
||||
return
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"stream": True
|
||||
}
|
||||
|
||||
if max_tokens is not None:
|
||||
payload["max_tokens"] = max_tokens
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
async with client.stream(
|
||||
"POST",
|
||||
self.api_url,
|
||||
headers=self._get_headers(),
|
||||
json=payload
|
||||
) as response:
|
||||
response.raise_for_status()
|
||||
|
||||
async for line in response.aiter_lines():
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
if line.startswith("data: "):
|
||||
line = line[6:]
|
||||
|
||||
if line.strip() == "[DONE]":
|
||||
break
|
||||
|
||||
try:
|
||||
data = json.loads(line)
|
||||
|
||||
if "choices" in data and len(data["choices"]) > 0:
|
||||
delta = data["choices"][0].get("delta", {})
|
||||
content = delta.get("content", "")
|
||||
if content:
|
||||
yield content
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
error_msg = f"Ошибка DeepSeek API: {e.response.status_code}"
|
||||
try:
|
||||
error_data = e.response.json()
|
||||
if "error" in error_data:
|
||||
error_msg = f"Ошибка DeepSeek API: {error_data['error'].get('message', error_msg)}"
|
||||
except:
|
||||
pass
|
||||
raise DeepSeekAPIError(error_msg) from e
|
||||
except httpx.RequestError as e:
|
||||
raise DeepSeekAPIError(f"Ошибка подключения к DeepSeek API: {str(e)}") from e
|
||||
except Exception as e:
|
||||
raise DeepSeekAPIError(f"Неожиданная ошибка при потоковой генерации: {str(e)}") from e
|
||||
|
||||
async def health_check(self) -> bool:
|
||||
"""
|
||||
Проверка доступности API
|
||||
|
||||
Returns:
|
||||
True если API доступен, False иначе
|
||||
"""
|
||||
if not self.api_key:
|
||||
return False
|
||||
|
||||
try:
|
||||
test_messages = [{"role": "user", "content": "test"}]
|
||||
await self.chat_completion(test_messages, max_tokens=1)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
@ -1,35 +1,35 @@
|
||||
"""
|
||||
Сервис для работы с Telegram Bot API
|
||||
"""
|
||||
from typing import Optional
|
||||
from src.shared.config import settings
|
||||
|
||||
|
||||
class TelegramAuthService:
|
||||
"""
|
||||
Сервис для работы с Telegram Bot API
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, bot_token: str | None = None):
|
||||
self.bot_token = bot_token or settings.TELEGRAM_BOT_TOKEN
|
||||
|
||||
async def get_user_info(self, telegram_id: str) -> Optional[dict]:
|
||||
"""
|
||||
Получение информации о пользователе через Telegram Bot API
|
||||
|
||||
Args:
|
||||
telegram_id: ID пользователя в Telegram
|
||||
|
||||
Returns:
|
||||
Информация о пользователе или None
|
||||
"""
|
||||
if not self.bot_token:
|
||||
return None
|
||||
|
||||
return {
|
||||
"id": telegram_id,
|
||||
"first_name": "User",
|
||||
"username": None
|
||||
}
|
||||
|
||||
"""
|
||||
Сервис для работы с Telegram Bot API
|
||||
"""
|
||||
from typing import Optional
|
||||
from src.shared.config import settings
|
||||
|
||||
|
||||
class TelegramAuthService:
|
||||
"""
|
||||
Сервис для работы с Telegram Bot API
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, bot_token: str | None = None):
|
||||
self.bot_token = bot_token or settings.TELEGRAM_BOT_TOKEN
|
||||
|
||||
async def get_user_info(self, telegram_id: str) -> Optional[dict]:
|
||||
"""
|
||||
Получение информации о пользователе через Telegram Bot API
|
||||
|
||||
Args:
|
||||
telegram_id: ID пользователя в Telegram
|
||||
|
||||
Returns:
|
||||
Информация о пользователе или None
|
||||
"""
|
||||
if not self.bot_token:
|
||||
return None
|
||||
|
||||
return {
|
||||
"id": telegram_id,
|
||||
"first_name": "User",
|
||||
"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 для парсинга документов
|
||||
"""
|
||||
import base64
|
||||
import io
|
||||
from typing import BinaryIO
|
||||
import httpx
|
||||
import fitz
|
||||
from PIL import Image
|
||||
from src.shared.config import settings
|
||||
|
||||
|
||||
class YandexOCRError(Exception):
|
||||
"""Ошибка при работе с Yandex OCR API"""
|
||||
pass
|
||||
|
||||
|
||||
class YandexOCRService:
|
||||
"""Сервис для работы с Yandex Vision OCR"""
|
||||
|
||||
def __init__(self, api_key: str | None = None):
|
||||
self.api_key = api_key or settings.YANDEX_OCR_API_KEY
|
||||
self.api_url = settings.YANDEX_OCR_API_URL
|
||||
self.timeout = 120.0
|
||||
self.max_file_size = 10 * 1024 * 1024
|
||||
|
||||
def _get_headers(self) -> dict[str, str]:
|
||||
"""Получить заголовки для запроса"""
|
||||
if not self.api_key:
|
||||
raise YandexOCRError("YANDEX_OCR_API_KEY не установлен в настройках")
|
||||
|
||||
return {
|
||||
"Authorization": f"Api-Key {self.api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
def _validate_file_size(self, file_content: bytes) -> None:
|
||||
"""Проверка размера файла"""
|
||||
if len(file_content) > self.max_file_size:
|
||||
raise YandexOCRError(
|
||||
f"Файл слишком большой: {len(file_content)} байт. "
|
||||
f"Максимальный размер: {self.max_file_size} байт (10 МБ)"
|
||||
)
|
||||
|
||||
async def extract_text(
|
||||
self,
|
||||
file_content: bytes,
|
||||
file_type: str = "pdf",
|
||||
language_codes: list[str] | None = None
|
||||
) -> str:
|
||||
"""
|
||||
Извлечение текста из файла через Yandex Vision OCR
|
||||
|
||||
Args:
|
||||
file_content: Содержимое файла в байтах
|
||||
file_type: Тип файла (pdf, image)
|
||||
language_codes: Коды языков для распознавания (по умолчанию ['ru', 'en'])
|
||||
|
||||
Returns:
|
||||
Извлеченный текст
|
||||
|
||||
Raises:
|
||||
YandexOCRError: При ошибке API
|
||||
"""
|
||||
if not self.api_key:
|
||||
return " YANDEX_OCR_API_KEY не установлен. Установите ключ в настройках для распознавания документов."
|
||||
|
||||
self._validate_file_size(file_content)
|
||||
|
||||
image_data = base64.b64encode(file_content).decode('utf-8')
|
||||
|
||||
if language_codes is None:
|
||||
language_codes = ['ru', 'en']
|
||||
|
||||
model = 'page'
|
||||
|
||||
payload = {
|
||||
"analyze_specs": [{
|
||||
"content": image_data,
|
||||
"features": [{
|
||||
"type": "TEXT_DETECTION",
|
||||
"text_detection_config": {
|
||||
"model": model,
|
||||
"language_codes": language_codes
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.post(
|
||||
self.api_url,
|
||||
headers=self._get_headers(),
|
||||
json=payload
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
result = response.json()
|
||||
|
||||
return self._extract_text_from_response(result)
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
error_msg = f"Ошибка Yandex OCR API: {e.response.status_code}"
|
||||
try:
|
||||
error_data = e.response.json()
|
||||
if "message" in error_data:
|
||||
error_msg = f"Ошибка Yandex OCR API: {error_data['message']}"
|
||||
except:
|
||||
pass
|
||||
raise YandexOCRError(error_msg) from e
|
||||
except httpx.RequestError as e:
|
||||
raise YandexOCRError(f"Ошибка подключения к Yandex OCR API: {str(e)}") from e
|
||||
except YandexOCRError:
|
||||
raise
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
raise YandexOCRError(f"Неожиданная ошибка при работе с Yandex OCR: {str(e)}\n{error_details}") from e
|
||||
|
||||
def _extract_text_from_response(self, response: dict) -> str:
|
||||
"""
|
||||
Извлечение текста из ответа Yandex Vision API
|
||||
|
||||
Args:
|
||||
response: JSON ответ от API
|
||||
|
||||
Returns:
|
||||
Извлеченный текст
|
||||
"""
|
||||
import json
|
||||
|
||||
if not self.api_key:
|
||||
return " YANDEX_OCR_API_KEY не установлен. Установите ключ в настройках для распознавания документов."
|
||||
|
||||
text_parts = []
|
||||
|
||||
if "results" not in response:
|
||||
if "error" in response:
|
||||
error_msg = response.get("error", {}).get("message", "Неизвестная ошибка")
|
||||
raise YandexOCRError(f"Ошибка Yandex OCR API: {error_msg}")
|
||||
raise YandexOCRError(f"Неожиданный формат ответа от Yandex OCR API. Структура: {list(response.keys())}")
|
||||
|
||||
for result in response["results"]:
|
||||
if "results" not in result:
|
||||
continue
|
||||
|
||||
for annotation in result["results"]:
|
||||
if "textDetection" not in annotation:
|
||||
continue
|
||||
|
||||
text_detection = annotation["textDetection"]
|
||||
|
||||
if "pages" in text_detection:
|
||||
for page in text_detection["pages"]:
|
||||
if "blocks" in page:
|
||||
for block in page["blocks"]:
|
||||
if "lines" in block:
|
||||
for line in block["lines"]:
|
||||
if "words" in line:
|
||||
line_text = " ".join([
|
||||
word.get("text", "")
|
||||
for word in line["words"]
|
||||
])
|
||||
if line_text:
|
||||
text_parts.append(line_text)
|
||||
|
||||
full_text = "\n".join(text_parts)
|
||||
|
||||
if not full_text.strip():
|
||||
return f" Не удалось извлечь текст из документа. Возможно, документ пустой или нечитаемый. Структура ответа: {json.dumps(response, indent=2, ensure_ascii=False)[:500]}"
|
||||
|
||||
return full_text
|
||||
|
||||
async def parse_pdf(self, file: BinaryIO) -> str:
|
||||
"""
|
||||
Парсинг PDF документа через YandexOCR
|
||||
|
||||
Yandex Vision API не поддерживает PDF напрямую, поэтому
|
||||
конвертируем каждую страницу PDF в изображение и распознаем отдельно.
|
||||
|
||||
Args:
|
||||
file: Файловый объект PDF
|
||||
|
||||
Returns:
|
||||
Текст из документа (объединенный текст со всех страниц)
|
||||
"""
|
||||
file_content = await self._read_file(file)
|
||||
|
||||
images = await self._pdf_to_images(file_content)
|
||||
|
||||
if not images:
|
||||
return " Не удалось конвертировать PDF в изображения. Возможно, файл поврежден."
|
||||
|
||||
all_text_parts = []
|
||||
for i, image_bytes in enumerate(images, 1):
|
||||
try:
|
||||
page_text = await self.extract_text(image_bytes, file_type="image")
|
||||
if page_text and not page_text.startswith("Ошибка распознавания:"):
|
||||
all_text_parts.append(f"--- Страница {i} ---\n{page_text}")
|
||||
except YandexOCRError as e:
|
||||
all_text_parts.append(f"--- Страница {i} ---\n Ошибка распознавания: {str(e)}")
|
||||
|
||||
if not all_text_parts:
|
||||
return " Не удалось распознать текст ни с одной страницы PDF."
|
||||
|
||||
return "\n\n".join(all_text_parts)
|
||||
|
||||
async def _pdf_to_images(self, pdf_content: bytes) -> list[bytes]:
|
||||
"""
|
||||
Конвертация PDF в список изображений (по одной на страницу)
|
||||
|
||||
Args:
|
||||
pdf_content: Содержимое PDF файла в байтах
|
||||
|
||||
Returns:
|
||||
Список изображений в формате PNG (каждое в байтах)
|
||||
"""
|
||||
try:
|
||||
pdf_document = fitz.open(stream=pdf_content, filetype="pdf")
|
||||
|
||||
images = []
|
||||
for page_num in range(len(pdf_document)):
|
||||
page = pdf_document[page_num]
|
||||
|
||||
mat = fitz.Matrix(2.0, 2.0)
|
||||
pix = page.get_pixmap(matrix=mat)
|
||||
|
||||
img_data = pix.tobytes("png")
|
||||
images.append(img_data)
|
||||
|
||||
pdf_document.close()
|
||||
return images
|
||||
|
||||
except Exception as e:
|
||||
raise YandexOCRError(f"Ошибка при конвертации PDF в изображения: {str(e)}") from e
|
||||
|
||||
async def parse_image(self, file: BinaryIO) -> str:
|
||||
"""
|
||||
Парсинг изображения через YandexOCR
|
||||
|
||||
Args:
|
||||
file: Файловый объект изображения (PNG, JPEG, etc.)
|
||||
|
||||
Returns:
|
||||
Текст из изображения
|
||||
"""
|
||||
file_content = await self._read_file(file)
|
||||
return await self.extract_text(file_content, file_type="image")
|
||||
|
||||
async def _read_file(self, file: BinaryIO) -> bytes:
|
||||
"""
|
||||
Чтение содержимого файла в байты
|
||||
|
||||
Args:
|
||||
file: Файловый объект
|
||||
|
||||
Returns:
|
||||
Содержимое файла в байтах
|
||||
"""
|
||||
if hasattr(file, 'read'):
|
||||
content = file.read()
|
||||
if hasattr(file, 'seek'):
|
||||
file.seek(0)
|
||||
return content
|
||||
else:
|
||||
raise YandexOCRError("Некорректный файловый объект")
|
||||
|
||||
async def health_check(self) -> bool:
|
||||
"""
|
||||
Проверка доступности API
|
||||
|
||||
Returns:
|
||||
True если API доступен, False иначе
|
||||
"""
|
||||
if not self.api_key:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
"""
|
||||
Интеграция с Yandex Vision OCR для парсинга документов
|
||||
"""
|
||||
import base64
|
||||
import io
|
||||
from typing import BinaryIO
|
||||
import httpx
|
||||
import fitz
|
||||
from PIL import Image
|
||||
from src.shared.config import settings
|
||||
|
||||
|
||||
class YandexOCRError(Exception):
|
||||
"""Ошибка при работе с Yandex OCR API"""
|
||||
pass
|
||||
|
||||
|
||||
class YandexOCRService:
|
||||
"""Сервис для работы с Yandex Vision OCR"""
|
||||
|
||||
def __init__(self, api_key: str | None = None):
|
||||
self.api_key = api_key or settings.YANDEX_OCR_API_KEY
|
||||
self.api_url = settings.YANDEX_OCR_API_URL
|
||||
self.timeout = 120.0
|
||||
self.max_file_size = 10 * 1024 * 1024
|
||||
|
||||
def _get_headers(self) -> dict[str, str]:
|
||||
"""Получить заголовки для запроса"""
|
||||
if not self.api_key:
|
||||
raise YandexOCRError("YANDEX_OCR_API_KEY не установлен в настройках")
|
||||
|
||||
return {
|
||||
"Authorization": f"Api-Key {self.api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
def _validate_file_size(self, file_content: bytes) -> None:
|
||||
"""Проверка размера файла"""
|
||||
if len(file_content) > self.max_file_size:
|
||||
raise YandexOCRError(
|
||||
f"Файл слишком большой: {len(file_content)} байт. "
|
||||
f"Максимальный размер: {self.max_file_size} байт (10 МБ)"
|
||||
)
|
||||
|
||||
async def extract_text(
|
||||
self,
|
||||
file_content: bytes,
|
||||
file_type: str = "pdf",
|
||||
language_codes: list[str] | None = None
|
||||
) -> str:
|
||||
"""
|
||||
Извлечение текста из файла через Yandex Vision OCR
|
||||
|
||||
Args:
|
||||
file_content: Содержимое файла в байтах
|
||||
file_type: Тип файла (pdf, image)
|
||||
language_codes: Коды языков для распознавания (по умолчанию ['ru', 'en'])
|
||||
|
||||
Returns:
|
||||
Извлеченный текст
|
||||
|
||||
Raises:
|
||||
YandexOCRError: При ошибке API
|
||||
"""
|
||||
if not self.api_key:
|
||||
return " YANDEX_OCR_API_KEY не установлен. Установите ключ в настройках для распознавания документов."
|
||||
|
||||
self._validate_file_size(file_content)
|
||||
|
||||
image_data = base64.b64encode(file_content).decode('utf-8')
|
||||
|
||||
if language_codes is None:
|
||||
language_codes = ['ru', 'en']
|
||||
|
||||
model = 'page'
|
||||
|
||||
payload = {
|
||||
"analyze_specs": [{
|
||||
"content": image_data,
|
||||
"features": [{
|
||||
"type": "TEXT_DETECTION",
|
||||
"text_detection_config": {
|
||||
"model": model,
|
||||
"language_codes": language_codes
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.post(
|
||||
self.api_url,
|
||||
headers=self._get_headers(),
|
||||
json=payload
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
result = response.json()
|
||||
|
||||
return self._extract_text_from_response(result)
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
error_msg = f"Ошибка Yandex OCR API: {e.response.status_code}"
|
||||
try:
|
||||
error_data = e.response.json()
|
||||
if "message" in error_data:
|
||||
error_msg = f"Ошибка Yandex OCR API: {error_data['message']}"
|
||||
except:
|
||||
pass
|
||||
raise YandexOCRError(error_msg) from e
|
||||
except httpx.RequestError as e:
|
||||
raise YandexOCRError(f"Ошибка подключения к Yandex OCR API: {str(e)}") from e
|
||||
except YandexOCRError:
|
||||
raise
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
raise YandexOCRError(f"Неожиданная ошибка при работе с Yandex OCR: {str(e)}\n{error_details}") from e
|
||||
|
||||
def _extract_text_from_response(self, response: dict) -> str:
|
||||
"""
|
||||
Извлечение текста из ответа Yandex Vision API
|
||||
|
||||
Args:
|
||||
response: JSON ответ от API
|
||||
|
||||
Returns:
|
||||
Извлеченный текст
|
||||
"""
|
||||
import json
|
||||
|
||||
if not self.api_key:
|
||||
return " YANDEX_OCR_API_KEY не установлен. Установите ключ в настройках для распознавания документов."
|
||||
|
||||
text_parts = []
|
||||
|
||||
if "results" not in response:
|
||||
if "error" in response:
|
||||
error_msg = response.get("error", {}).get("message", "Неизвестная ошибка")
|
||||
raise YandexOCRError(f"Ошибка Yandex OCR API: {error_msg}")
|
||||
raise YandexOCRError(f"Неожиданный формат ответа от Yandex OCR API. Структура: {list(response.keys())}")
|
||||
|
||||
for result in response["results"]:
|
||||
if "results" not in result:
|
||||
continue
|
||||
|
||||
for annotation in result["results"]:
|
||||
if "textDetection" not in annotation:
|
||||
continue
|
||||
|
||||
text_detection = annotation["textDetection"]
|
||||
|
||||
if "pages" in text_detection:
|
||||
for page in text_detection["pages"]:
|
||||
if "blocks" in page:
|
||||
for block in page["blocks"]:
|
||||
if "lines" in block:
|
||||
for line in block["lines"]:
|
||||
if "words" in line:
|
||||
line_text = " ".join([
|
||||
word.get("text", "")
|
||||
for word in line["words"]
|
||||
])
|
||||
if line_text:
|
||||
text_parts.append(line_text)
|
||||
|
||||
full_text = "\n".join(text_parts)
|
||||
|
||||
if not full_text.strip():
|
||||
return f" Не удалось извлечь текст из документа. Возможно, документ пустой или нечитаемый. Структура ответа: {json.dumps(response, indent=2, ensure_ascii=False)[:500]}"
|
||||
|
||||
return full_text
|
||||
|
||||
async def parse_pdf(self, file: BinaryIO) -> str:
|
||||
"""
|
||||
Парсинг PDF документа через YandexOCR
|
||||
|
||||
Yandex Vision API не поддерживает PDF напрямую, поэтому
|
||||
конвертируем каждую страницу PDF в изображение и распознаем отдельно.
|
||||
|
||||
Args:
|
||||
file: Файловый объект PDF
|
||||
|
||||
Returns:
|
||||
Текст из документа (объединенный текст со всех страниц)
|
||||
"""
|
||||
file_content = await self._read_file(file)
|
||||
|
||||
images = await self._pdf_to_images(file_content)
|
||||
|
||||
if not images:
|
||||
return " Не удалось конвертировать PDF в изображения. Возможно, файл поврежден."
|
||||
|
||||
all_text_parts = []
|
||||
for i, image_bytes in enumerate(images, 1):
|
||||
try:
|
||||
page_text = await self.extract_text(image_bytes, file_type="image")
|
||||
if page_text and not page_text.startswith("Ошибка распознавания:"):
|
||||
all_text_parts.append(f"--- Страница {i} ---\n{page_text}")
|
||||
except YandexOCRError as e:
|
||||
all_text_parts.append(f"--- Страница {i} ---\n Ошибка распознавания: {str(e)}")
|
||||
|
||||
if not all_text_parts:
|
||||
return " Не удалось распознать текст ни с одной страницы PDF."
|
||||
|
||||
return "\n\n".join(all_text_parts)
|
||||
|
||||
async def _pdf_to_images(self, pdf_content: bytes) -> list[bytes]:
|
||||
"""
|
||||
Конвертация PDF в список изображений (по одной на страницу)
|
||||
|
||||
Args:
|
||||
pdf_content: Содержимое PDF файла в байтах
|
||||
|
||||
Returns:
|
||||
Список изображений в формате PNG (каждое в байтах)
|
||||
"""
|
||||
try:
|
||||
pdf_document = fitz.open(stream=pdf_content, filetype="pdf")
|
||||
|
||||
images = []
|
||||
for page_num in range(len(pdf_document)):
|
||||
page = pdf_document[page_num]
|
||||
|
||||
mat = fitz.Matrix(2.0, 2.0)
|
||||
pix = page.get_pixmap(matrix=mat)
|
||||
|
||||
img_data = pix.tobytes("png")
|
||||
images.append(img_data)
|
||||
|
||||
pdf_document.close()
|
||||
return images
|
||||
|
||||
except Exception as e:
|
||||
raise YandexOCRError(f"Ошибка при конвертации PDF в изображения: {str(e)}") from e
|
||||
|
||||
async def parse_image(self, file: BinaryIO) -> str:
|
||||
"""
|
||||
Парсинг изображения через YandexOCR
|
||||
|
||||
Args:
|
||||
file: Файловый объект изображения (PNG, JPEG, etc.)
|
||||
|
||||
Returns:
|
||||
Текст из изображения
|
||||
"""
|
||||
file_content = await self._read_file(file)
|
||||
return await self.extract_text(file_content, file_type="image")
|
||||
|
||||
async def _read_file(self, file: BinaryIO) -> bytes:
|
||||
"""
|
||||
Чтение содержимого файла в байты
|
||||
|
||||
Args:
|
||||
file: Файловый объект
|
||||
|
||||
Returns:
|
||||
Содержимое файла в байтах
|
||||
"""
|
||||
if hasattr(file, 'read'):
|
||||
content = file.read()
|
||||
if hasattr(file, 'seek'):
|
||||
file.seek(0)
|
||||
return content
|
||||
else:
|
||||
raise YandexOCRError("Некорректный файловый объект")
|
||||
|
||||
async def health_check(self) -> bool:
|
||||
"""
|
||||
Проверка доступности API
|
||||
|
||||
Returns:
|
||||
True если API доступен, False иначе
|
||||
"""
|
||||
if not self.api_key:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""
|
||||
PostgreSQL repository implementations
|
||||
"""
|
||||
|
||||
"""
|
||||
PostgreSQL repository implementations
|
||||
"""
|
||||
|
||||
|
||||
@ -1,107 +1,107 @@
|
||||
"""
|
||||
Реализация репозитория доступа к коллекциям для PostgreSQL
|
||||
"""
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from src.domain.entities.collection_access import CollectionAccess
|
||||
from src.domain.repositories.collection_access_repository import ICollectionAccessRepository
|
||||
from src.infrastructure.database.models import CollectionAccessModel
|
||||
from src.shared.exceptions import NotFoundError
|
||||
|
||||
|
||||
class PostgreSQLCollectionAccessRepository(ICollectionAccessRepository):
|
||||
"""PostgreSQL реализация репозитория доступа к коллекциям"""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def create(self, access: CollectionAccess) -> CollectionAccess:
|
||||
"""Создать доступ"""
|
||||
db_access = CollectionAccessModel(
|
||||
access_id=access.access_id,
|
||||
user_id=access.user_id,
|
||||
collection_id=access.collection_id,
|
||||
created_at=access.created_at
|
||||
)
|
||||
self.session.add(db_access)
|
||||
await self.session.commit()
|
||||
await self.session.refresh(db_access)
|
||||
return self._to_entity(db_access)
|
||||
|
||||
async def get_by_id(self, access_id: UUID) -> Optional[CollectionAccess]:
|
||||
"""Получить доступ по ID"""
|
||||
result = await self.session.execute(
|
||||
select(CollectionAccessModel).where(CollectionAccessModel.access_id == access_id)
|
||||
)
|
||||
db_access = result.scalar_one_or_none()
|
||||
return self._to_entity(db_access) if db_access else None
|
||||
|
||||
async def delete(self, access_id: UUID) -> bool:
|
||||
"""Удалить доступ"""
|
||||
result = await self.session.execute(
|
||||
select(CollectionAccessModel).where(CollectionAccessModel.access_id == access_id)
|
||||
)
|
||||
db_access = result.scalar_one_or_none()
|
||||
if not db_access:
|
||||
return False
|
||||
|
||||
await self.session.delete(db_access)
|
||||
await self.session.commit()
|
||||
return True
|
||||
|
||||
async def delete_by_user_and_collection(self, user_id: UUID, collection_id: UUID) -> bool:
|
||||
"""Удалить доступ пользователя к коллекции"""
|
||||
result = await self.session.execute(
|
||||
select(CollectionAccessModel).where(
|
||||
CollectionAccessModel.user_id == user_id,
|
||||
CollectionAccessModel.collection_id == collection_id
|
||||
)
|
||||
)
|
||||
db_access = result.scalar_one_or_none()
|
||||
if not db_access:
|
||||
return False
|
||||
|
||||
await self.session.delete(db_access)
|
||||
await self.session.commit()
|
||||
return True
|
||||
|
||||
async def get_by_user_and_collection(self, user_id: UUID, collection_id: UUID) -> Optional[CollectionAccess]:
|
||||
"""Получить доступ пользователя к коллекции"""
|
||||
result = await self.session.execute(
|
||||
select(CollectionAccessModel).where(
|
||||
CollectionAccessModel.user_id == user_id,
|
||||
CollectionAccessModel.collection_id == collection_id
|
||||
)
|
||||
)
|
||||
db_access = result.scalar_one_or_none()
|
||||
return self._to_entity(db_access) if db_access else None
|
||||
|
||||
async def list_by_user(self, user_id: UUID) -> list[CollectionAccess]:
|
||||
"""Получить доступы пользователя"""
|
||||
result = await self.session.execute(
|
||||
select(CollectionAccessModel).where(CollectionAccessModel.user_id == user_id)
|
||||
)
|
||||
db_accesses = result.scalars().all()
|
||||
return [self._to_entity(db_access) for db_access in db_accesses]
|
||||
|
||||
async def list_by_collection(self, collection_id: UUID) -> list[CollectionAccess]:
|
||||
"""Получить доступы к коллекции"""
|
||||
result = await self.session.execute(
|
||||
select(CollectionAccessModel).where(CollectionAccessModel.collection_id == collection_id)
|
||||
)
|
||||
db_accesses = result.scalars().all()
|
||||
return [self._to_entity(db_access) for db_access in db_accesses]
|
||||
|
||||
def _to_entity(self, db_access: CollectionAccessModel | None) -> CollectionAccess | None:
|
||||
"""Преобразовать модель БД в доменную сущность"""
|
||||
if not db_access:
|
||||
return None
|
||||
return CollectionAccess(
|
||||
access_id=db_access.access_id,
|
||||
user_id=db_access.user_id,
|
||||
collection_id=db_access.collection_id,
|
||||
created_at=db_access.created_at
|
||||
)
|
||||
|
||||
"""
|
||||
Реализация репозитория доступа к коллекциям для PostgreSQL
|
||||
"""
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from src.domain.entities.collection_access import CollectionAccess
|
||||
from src.domain.repositories.collection_access_repository import ICollectionAccessRepository
|
||||
from src.infrastructure.database.models import CollectionAccessModel
|
||||
from src.shared.exceptions import NotFoundError
|
||||
|
||||
|
||||
class PostgreSQLCollectionAccessRepository(ICollectionAccessRepository):
|
||||
"""PostgreSQL реализация репозитория доступа к коллекциям"""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def create(self, access: CollectionAccess) -> CollectionAccess:
|
||||
"""Создать доступ"""
|
||||
db_access = CollectionAccessModel(
|
||||
access_id=access.access_id,
|
||||
user_id=access.user_id,
|
||||
collection_id=access.collection_id,
|
||||
created_at=access.created_at
|
||||
)
|
||||
self.session.add(db_access)
|
||||
await self.session.commit()
|
||||
await self.session.refresh(db_access)
|
||||
return self._to_entity(db_access)
|
||||
|
||||
async def get_by_id(self, access_id: UUID) -> Optional[CollectionAccess]:
|
||||
"""Получить доступ по ID"""
|
||||
result = await self.session.execute(
|
||||
select(CollectionAccessModel).where(CollectionAccessModel.access_id == access_id)
|
||||
)
|
||||
db_access = result.scalar_one_or_none()
|
||||
return self._to_entity(db_access) if db_access else None
|
||||
|
||||
async def delete(self, access_id: UUID) -> bool:
|
||||
"""Удалить доступ"""
|
||||
result = await self.session.execute(
|
||||
select(CollectionAccessModel).where(CollectionAccessModel.access_id == access_id)
|
||||
)
|
||||
db_access = result.scalar_one_or_none()
|
||||
if not db_access:
|
||||
return False
|
||||
|
||||
await self.session.delete(db_access)
|
||||
await self.session.commit()
|
||||
return True
|
||||
|
||||
async def delete_by_user_and_collection(self, user_id: UUID, collection_id: UUID) -> bool:
|
||||
"""Удалить доступ пользователя к коллекции"""
|
||||
result = await self.session.execute(
|
||||
select(CollectionAccessModel).where(
|
||||
CollectionAccessModel.user_id == user_id,
|
||||
CollectionAccessModel.collection_id == collection_id
|
||||
)
|
||||
)
|
||||
db_access = result.scalar_one_or_none()
|
||||
if not db_access:
|
||||
return False
|
||||
|
||||
await self.session.delete(db_access)
|
||||
await self.session.commit()
|
||||
return True
|
||||
|
||||
async def get_by_user_and_collection(self, user_id: UUID, collection_id: UUID) -> Optional[CollectionAccess]:
|
||||
"""Получить доступ пользователя к коллекции"""
|
||||
result = await self.session.execute(
|
||||
select(CollectionAccessModel).where(
|
||||
CollectionAccessModel.user_id == user_id,
|
||||
CollectionAccessModel.collection_id == collection_id
|
||||
)
|
||||
)
|
||||
db_access = result.scalar_one_or_none()
|
||||
return self._to_entity(db_access) if db_access else None
|
||||
|
||||
async def list_by_user(self, user_id: UUID) -> list[CollectionAccess]:
|
||||
"""Получить доступы пользователя"""
|
||||
result = await self.session.execute(
|
||||
select(CollectionAccessModel).where(CollectionAccessModel.user_id == user_id)
|
||||
)
|
||||
db_accesses = result.scalars().all()
|
||||
return [self._to_entity(db_access) for db_access in db_accesses]
|
||||
|
||||
async def list_by_collection(self, collection_id: UUID) -> list[CollectionAccess]:
|
||||
"""Получить доступы к коллекции"""
|
||||
result = await self.session.execute(
|
||||
select(CollectionAccessModel).where(CollectionAccessModel.collection_id == collection_id)
|
||||
)
|
||||
db_accesses = result.scalars().all()
|
||||
return [self._to_entity(db_access) for db_access in db_accesses]
|
||||
|
||||
def _to_entity(self, db_access: CollectionAccessModel | None) -> CollectionAccess | None:
|
||||
"""Преобразовать модель БД в доменную сущность"""
|
||||
if not db_access:
|
||||
return None
|
||||
return CollectionAccess(
|
||||
access_id=db_access.access_id,
|
||||
user_id=db_access.user_id,
|
||||
collection_id=db_access.collection_id,
|
||||
created_at=db_access.created_at
|
||||
)
|
||||
|
||||
|
||||
@ -1,106 +1,106 @@
|
||||
"""
|
||||
Реализация репозитория коллекций для PostgreSQL
|
||||
"""
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from src.domain.entities.collection import Collection
|
||||
from src.domain.repositories.collection_repository import ICollectionRepository
|
||||
from src.infrastructure.database.models import CollectionModel
|
||||
from src.shared.exceptions import NotFoundError
|
||||
|
||||
|
||||
class PostgreSQLCollectionRepository(ICollectionRepository):
|
||||
"""PostgreSQL реализация репозитория коллекций"""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def create(self, collection: Collection) -> Collection:
|
||||
"""Создать коллекцию"""
|
||||
db_collection = CollectionModel(
|
||||
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
|
||||
)
|
||||
self.session.add(db_collection)
|
||||
await self.session.commit()
|
||||
await self.session.refresh(db_collection)
|
||||
return self._to_entity(db_collection)
|
||||
|
||||
async def get_by_id(self, collection_id: UUID) -> Optional[Collection]:
|
||||
"""Получить коллекцию по ID"""
|
||||
result = await self.session.execute(
|
||||
select(CollectionModel).where(CollectionModel.collection_id == collection_id)
|
||||
)
|
||||
db_collection = result.scalar_one_or_none()
|
||||
return self._to_entity(db_collection) if db_collection else None
|
||||
|
||||
async def update(self, collection: Collection) -> Collection:
|
||||
"""Обновить коллекцию"""
|
||||
result = await self.session.execute(
|
||||
select(CollectionModel).where(CollectionModel.collection_id == collection.collection_id)
|
||||
)
|
||||
db_collection = result.scalar_one_or_none()
|
||||
if not db_collection:
|
||||
raise NotFoundError(f"Коллекция {collection.collection_id} не найдена")
|
||||
|
||||
db_collection.name = collection.name
|
||||
db_collection.description = collection.description
|
||||
db_collection.is_public = collection.is_public
|
||||
await self.session.commit()
|
||||
await self.session.refresh(db_collection)
|
||||
return self._to_entity(db_collection)
|
||||
|
||||
async def delete(self, collection_id: UUID) -> bool:
|
||||
"""Удалить коллекцию"""
|
||||
result = await self.session.execute(
|
||||
select(CollectionModel).where(CollectionModel.collection_id == collection_id)
|
||||
)
|
||||
db_collection = result.scalar_one_or_none()
|
||||
if not db_collection:
|
||||
return False
|
||||
|
||||
await self.session.delete(db_collection)
|
||||
await self.session.commit()
|
||||
return True
|
||||
|
||||
async def list_by_owner(self, owner_id: UUID, skip: int = 0, limit: int = 100) -> list[Collection]:
|
||||
"""Получить коллекции владельца"""
|
||||
result = await self.session.execute(
|
||||
select(CollectionModel)
|
||||
.where(CollectionModel.owner_id == owner_id)
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
db_collections = result.scalars().all()
|
||||
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]:
|
||||
"""Получить публичные коллекции"""
|
||||
result = await self.session.execute(
|
||||
select(CollectionModel)
|
||||
.where(CollectionModel.is_public == True)
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
db_collections = result.scalars().all()
|
||||
return [self._to_entity(db_collection) for db_collection in db_collections]
|
||||
|
||||
def _to_entity(self, db_collection: CollectionModel | None) -> Collection | None:
|
||||
"""Преобразовать модель БД в доменную сущность"""
|
||||
if not db_collection:
|
||||
return None
|
||||
return Collection(
|
||||
collection_id=db_collection.collection_id,
|
||||
name=db_collection.name,
|
||||
description=db_collection.description or "",
|
||||
owner_id=db_collection.owner_id,
|
||||
is_public=db_collection.is_public,
|
||||
created_at=db_collection.created_at
|
||||
)
|
||||
|
||||
"""
|
||||
Реализация репозитория коллекций для PostgreSQL
|
||||
"""
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from src.domain.entities.collection import Collection
|
||||
from src.domain.repositories.collection_repository import ICollectionRepository
|
||||
from src.infrastructure.database.models import CollectionModel
|
||||
from src.shared.exceptions import NotFoundError
|
||||
|
||||
|
||||
class PostgreSQLCollectionRepository(ICollectionRepository):
|
||||
"""PostgreSQL реализация репозитория коллекций"""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def create(self, collection: Collection) -> Collection:
|
||||
"""Создать коллекцию"""
|
||||
db_collection = CollectionModel(
|
||||
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
|
||||
)
|
||||
self.session.add(db_collection)
|
||||
await self.session.commit()
|
||||
await self.session.refresh(db_collection)
|
||||
return self._to_entity(db_collection)
|
||||
|
||||
async def get_by_id(self, collection_id: UUID) -> Optional[Collection]:
|
||||
"""Получить коллекцию по ID"""
|
||||
result = await self.session.execute(
|
||||
select(CollectionModel).where(CollectionModel.collection_id == collection_id)
|
||||
)
|
||||
db_collection = result.scalar_one_or_none()
|
||||
return self._to_entity(db_collection) if db_collection else None
|
||||
|
||||
async def update(self, collection: Collection) -> Collection:
|
||||
"""Обновить коллекцию"""
|
||||
result = await self.session.execute(
|
||||
select(CollectionModel).where(CollectionModel.collection_id == collection.collection_id)
|
||||
)
|
||||
db_collection = result.scalar_one_or_none()
|
||||
if not db_collection:
|
||||
raise NotFoundError(f"Коллекция {collection.collection_id} не найдена")
|
||||
|
||||
db_collection.name = collection.name
|
||||
db_collection.description = collection.description
|
||||
db_collection.is_public = collection.is_public
|
||||
await self.session.commit()
|
||||
await self.session.refresh(db_collection)
|
||||
return self._to_entity(db_collection)
|
||||
|
||||
async def delete(self, collection_id: UUID) -> bool:
|
||||
"""Удалить коллекцию"""
|
||||
result = await self.session.execute(
|
||||
select(CollectionModel).where(CollectionModel.collection_id == collection_id)
|
||||
)
|
||||
db_collection = result.scalar_one_or_none()
|
||||
if not db_collection:
|
||||
return False
|
||||
|
||||
await self.session.delete(db_collection)
|
||||
await self.session.commit()
|
||||
return True
|
||||
|
||||
async def list_by_owner(self, owner_id: UUID, skip: int = 0, limit: int = 100) -> list[Collection]:
|
||||
"""Получить коллекции владельца"""
|
||||
result = await self.session.execute(
|
||||
select(CollectionModel)
|
||||
.where(CollectionModel.owner_id == owner_id)
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
db_collections = result.scalars().all()
|
||||
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]:
|
||||
"""Получить публичные коллекции"""
|
||||
result = await self.session.execute(
|
||||
select(CollectionModel)
|
||||
.where(CollectionModel.is_public == True)
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
db_collections = result.scalars().all()
|
||||
return [self._to_entity(db_collection) for db_collection in db_collections]
|
||||
|
||||
def _to_entity(self, db_collection: CollectionModel | None) -> Collection | None:
|
||||
"""Преобразовать модель БД в доменную сущность"""
|
||||
if not db_collection:
|
||||
return None
|
||||
return Collection(
|
||||
collection_id=db_collection.collection_id,
|
||||
name=db_collection.name,
|
||||
description=db_collection.description or "",
|
||||
owner_id=db_collection.owner_id,
|
||||
is_public=db_collection.is_public,
|
||||
created_at=db_collection.created_at
|
||||
)
|
||||
|
||||
|
||||
@ -1,104 +1,104 @@
|
||||
"""
|
||||
Реализация репозитория бесед для PostgreSQL
|
||||
"""
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from src.domain.entities.conversation import Conversation
|
||||
from src.domain.repositories.conversation_repository import IConversationRepository
|
||||
from src.infrastructure.database.models import ConversationModel
|
||||
from src.shared.exceptions import NotFoundError
|
||||
|
||||
|
||||
class PostgreSQLConversationRepository(IConversationRepository):
|
||||
"""PostgreSQL реализация репозитория бесед"""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def create(self, conversation: Conversation) -> Conversation:
|
||||
"""Создать беседу"""
|
||||
db_conversation = ConversationModel(
|
||||
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
|
||||
)
|
||||
self.session.add(db_conversation)
|
||||
await self.session.commit()
|
||||
await self.session.refresh(db_conversation)
|
||||
return self._to_entity(db_conversation)
|
||||
|
||||
async def get_by_id(self, conversation_id: UUID) -> Optional[Conversation]:
|
||||
"""Получить беседу по ID"""
|
||||
result = await self.session.execute(
|
||||
select(ConversationModel).where(ConversationModel.conversation_id == conversation_id)
|
||||
)
|
||||
db_conversation = result.scalar_one_or_none()
|
||||
return self._to_entity(db_conversation) if db_conversation else None
|
||||
|
||||
async def update(self, conversation: Conversation) -> Conversation:
|
||||
"""Обновить беседу"""
|
||||
result = await self.session.execute(
|
||||
select(ConversationModel).where(ConversationModel.conversation_id == conversation.conversation_id)
|
||||
)
|
||||
db_conversation = result.scalar_one_or_none()
|
||||
if not db_conversation:
|
||||
raise NotFoundError(f"Беседа {conversation.conversation_id} не найдена")
|
||||
|
||||
db_conversation.user_id = conversation.user_id
|
||||
db_conversation.collection_id = conversation.collection_id
|
||||
db_conversation.updated_at = conversation.updated_at
|
||||
await self.session.commit()
|
||||
await self.session.refresh(db_conversation)
|
||||
return self._to_entity(db_conversation)
|
||||
|
||||
async def delete(self, conversation_id: UUID) -> bool:
|
||||
"""Удалить беседу"""
|
||||
result = await self.session.execute(
|
||||
select(ConversationModel).where(ConversationModel.conversation_id == conversation_id)
|
||||
)
|
||||
db_conversation = result.scalar_one_or_none()
|
||||
if not db_conversation:
|
||||
return False
|
||||
|
||||
await self.session.delete(db_conversation)
|
||||
await self.session.commit()
|
||||
return True
|
||||
|
||||
async def list_by_user(self, user_id: UUID, skip: int = 0, limit: int = 100) -> list[Conversation]:
|
||||
"""Получить беседы пользователя"""
|
||||
result = await self.session.execute(
|
||||
select(ConversationModel)
|
||||
.where(ConversationModel.user_id == user_id)
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
db_conversations = result.scalars().all()
|
||||
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]:
|
||||
"""Получить беседы по коллекции"""
|
||||
result = await self.session.execute(
|
||||
select(ConversationModel)
|
||||
.where(ConversationModel.collection_id == collection_id)
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
db_conversations = result.scalars().all()
|
||||
return [self._to_entity(db_conversation) for db_conversation in db_conversations]
|
||||
|
||||
def _to_entity(self, db_conversation: ConversationModel | None) -> Conversation | None:
|
||||
"""Преобразовать модель БД в доменную сущность"""
|
||||
if not db_conversation:
|
||||
return None
|
||||
return Conversation(
|
||||
conversation_id=db_conversation.conversation_id,
|
||||
user_id=db_conversation.user_id,
|
||||
collection_id=db_conversation.collection_id,
|
||||
created_at=db_conversation.created_at,
|
||||
updated_at=db_conversation.updated_at
|
||||
)
|
||||
|
||||
"""
|
||||
Реализация репозитория бесед для PostgreSQL
|
||||
"""
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from src.domain.entities.conversation import Conversation
|
||||
from src.domain.repositories.conversation_repository import IConversationRepository
|
||||
from src.infrastructure.database.models import ConversationModel
|
||||
from src.shared.exceptions import NotFoundError
|
||||
|
||||
|
||||
class PostgreSQLConversationRepository(IConversationRepository):
|
||||
"""PostgreSQL реализация репозитория бесед"""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def create(self, conversation: Conversation) -> Conversation:
|
||||
"""Создать беседу"""
|
||||
db_conversation = ConversationModel(
|
||||
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
|
||||
)
|
||||
self.session.add(db_conversation)
|
||||
await self.session.commit()
|
||||
await self.session.refresh(db_conversation)
|
||||
return self._to_entity(db_conversation)
|
||||
|
||||
async def get_by_id(self, conversation_id: UUID) -> Optional[Conversation]:
|
||||
"""Получить беседу по ID"""
|
||||
result = await self.session.execute(
|
||||
select(ConversationModel).where(ConversationModel.conversation_id == conversation_id)
|
||||
)
|
||||
db_conversation = result.scalar_one_or_none()
|
||||
return self._to_entity(db_conversation) if db_conversation else None
|
||||
|
||||
async def update(self, conversation: Conversation) -> Conversation:
|
||||
"""Обновить беседу"""
|
||||
result = await self.session.execute(
|
||||
select(ConversationModel).where(ConversationModel.conversation_id == conversation.conversation_id)
|
||||
)
|
||||
db_conversation = result.scalar_one_or_none()
|
||||
if not db_conversation:
|
||||
raise NotFoundError(f"Беседа {conversation.conversation_id} не найдена")
|
||||
|
||||
db_conversation.user_id = conversation.user_id
|
||||
db_conversation.collection_id = conversation.collection_id
|
||||
db_conversation.updated_at = conversation.updated_at
|
||||
await self.session.commit()
|
||||
await self.session.refresh(db_conversation)
|
||||
return self._to_entity(db_conversation)
|
||||
|
||||
async def delete(self, conversation_id: UUID) -> bool:
|
||||
"""Удалить беседу"""
|
||||
result = await self.session.execute(
|
||||
select(ConversationModel).where(ConversationModel.conversation_id == conversation_id)
|
||||
)
|
||||
db_conversation = result.scalar_one_or_none()
|
||||
if not db_conversation:
|
||||
return False
|
||||
|
||||
await self.session.delete(db_conversation)
|
||||
await self.session.commit()
|
||||
return True
|
||||
|
||||
async def list_by_user(self, user_id: UUID, skip: int = 0, limit: int = 100) -> list[Conversation]:
|
||||
"""Получить беседы пользователя"""
|
||||
result = await self.session.execute(
|
||||
select(ConversationModel)
|
||||
.where(ConversationModel.user_id == user_id)
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
db_conversations = result.scalars().all()
|
||||
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]:
|
||||
"""Получить беседы по коллекции"""
|
||||
result = await self.session.execute(
|
||||
select(ConversationModel)
|
||||
.where(ConversationModel.collection_id == collection_id)
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
db_conversations = result.scalars().all()
|
||||
return [self._to_entity(db_conversation) for db_conversation in db_conversations]
|
||||
|
||||
def _to_entity(self, db_conversation: ConversationModel | None) -> Conversation | None:
|
||||
"""Преобразовать модель БД в доменную сущность"""
|
||||
if not db_conversation:
|
||||
return None
|
||||
return Conversation(
|
||||
conversation_id=db_conversation.conversation_id,
|
||||
user_id=db_conversation.user_id,
|
||||
collection_id=db_conversation.collection_id,
|
||||
created_at=db_conversation.created_at,
|
||||
updated_at=db_conversation.updated_at
|
||||
)
|
||||
|
||||
|
||||
@ -1,95 +1,95 @@
|
||||
"""
|
||||
Реализация репозитория документов для PostgreSQL
|
||||
"""
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from src.domain.entities.document import Document
|
||||
from src.domain.repositories.document_repository import IDocumentRepository
|
||||
from src.infrastructure.database.models import DocumentModel
|
||||
from src.shared.exceptions import NotFoundError
|
||||
|
||||
|
||||
class PostgreSQLDocumentRepository(IDocumentRepository):
|
||||
"""PostgreSQL реализация репозитория документов"""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def create(self, document: Document) -> Document:
|
||||
"""Создать документ"""
|
||||
db_document = DocumentModel(
|
||||
document_id=document.document_id,
|
||||
collection_id=document.collection_id,
|
||||
title=document.title,
|
||||
content=document.content,
|
||||
document_metadata=document.metadata,
|
||||
created_at=document.created_at
|
||||
)
|
||||
self.session.add(db_document)
|
||||
await self.session.commit()
|
||||
await self.session.refresh(db_document)
|
||||
return self._to_entity(db_document)
|
||||
|
||||
async def get_by_id(self, document_id: UUID) -> Optional[Document]:
|
||||
"""Получить документ по ID"""
|
||||
result = await self.session.execute(
|
||||
select(DocumentModel).where(DocumentModel.document_id == document_id)
|
||||
)
|
||||
db_document = result.scalar_one_or_none()
|
||||
return self._to_entity(db_document) if db_document else None
|
||||
|
||||
async def update(self, document: Document) -> Document:
|
||||
"""Обновить документ"""
|
||||
result = await self.session.execute(
|
||||
select(DocumentModel).where(DocumentModel.document_id == document.document_id)
|
||||
)
|
||||
db_document = result.scalar_one_or_none()
|
||||
if not db_document:
|
||||
raise NotFoundError(f"Документ {document.document_id} не найден")
|
||||
|
||||
db_document.title = document.title
|
||||
db_document.content = document.content
|
||||
db_document.document_metadata = document.metadata
|
||||
await self.session.commit()
|
||||
await self.session.refresh(db_document)
|
||||
return self._to_entity(db_document)
|
||||
|
||||
async def delete(self, document_id: UUID) -> bool:
|
||||
"""Удалить документ"""
|
||||
result = await self.session.execute(
|
||||
select(DocumentModel).where(DocumentModel.document_id == document_id)
|
||||
)
|
||||
db_document = result.scalar_one_or_none()
|
||||
if not db_document:
|
||||
return False
|
||||
|
||||
await self.session.delete(db_document)
|
||||
await self.session.commit()
|
||||
return True
|
||||
|
||||
async def list_by_collection(self, collection_id: UUID, skip: int = 0, limit: int = 100) -> list[Document]:
|
||||
"""Получить документы коллекции"""
|
||||
result = await self.session.execute(
|
||||
select(DocumentModel)
|
||||
.where(DocumentModel.collection_id == collection_id)
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
db_documents = result.scalars().all()
|
||||
return [self._to_entity(db_document) for db_document in db_documents]
|
||||
|
||||
def _to_entity(self, db_document: DocumentModel | None) -> Document | None:
|
||||
"""Преобразовать модель БД в доменную сущность"""
|
||||
if not db_document:
|
||||
return None
|
||||
return Document(
|
||||
document_id=db_document.document_id,
|
||||
collection_id=db_document.collection_id,
|
||||
title=db_document.title,
|
||||
content=db_document.content,
|
||||
metadata=db_document.document_metadata or {},
|
||||
created_at=db_document.created_at
|
||||
)
|
||||
|
||||
"""
|
||||
Реализация репозитория документов для PostgreSQL
|
||||
"""
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from src.domain.entities.document import Document
|
||||
from src.domain.repositories.document_repository import IDocumentRepository
|
||||
from src.infrastructure.database.models import DocumentModel
|
||||
from src.shared.exceptions import NotFoundError
|
||||
|
||||
|
||||
class PostgreSQLDocumentRepository(IDocumentRepository):
|
||||
"""PostgreSQL реализация репозитория документов"""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def create(self, document: Document) -> Document:
|
||||
"""Создать документ"""
|
||||
db_document = DocumentModel(
|
||||
document_id=document.document_id,
|
||||
collection_id=document.collection_id,
|
||||
title=document.title,
|
||||
content=document.content,
|
||||
document_metadata=document.metadata,
|
||||
created_at=document.created_at
|
||||
)
|
||||
self.session.add(db_document)
|
||||
await self.session.commit()
|
||||
await self.session.refresh(db_document)
|
||||
return self._to_entity(db_document)
|
||||
|
||||
async def get_by_id(self, document_id: UUID) -> Optional[Document]:
|
||||
"""Получить документ по ID"""
|
||||
result = await self.session.execute(
|
||||
select(DocumentModel).where(DocumentModel.document_id == document_id)
|
||||
)
|
||||
db_document = result.scalar_one_or_none()
|
||||
return self._to_entity(db_document) if db_document else None
|
||||
|
||||
async def update(self, document: Document) -> Document:
|
||||
"""Обновить документ"""
|
||||
result = await self.session.execute(
|
||||
select(DocumentModel).where(DocumentModel.document_id == document.document_id)
|
||||
)
|
||||
db_document = result.scalar_one_or_none()
|
||||
if not db_document:
|
||||
raise NotFoundError(f"Документ {document.document_id} не найден")
|
||||
|
||||
db_document.title = document.title
|
||||
db_document.content = document.content
|
||||
db_document.document_metadata = document.metadata
|
||||
await self.session.commit()
|
||||
await self.session.refresh(db_document)
|
||||
return self._to_entity(db_document)
|
||||
|
||||
async def delete(self, document_id: UUID) -> bool:
|
||||
"""Удалить документ"""
|
||||
result = await self.session.execute(
|
||||
select(DocumentModel).where(DocumentModel.document_id == document_id)
|
||||
)
|
||||
db_document = result.scalar_one_or_none()
|
||||
if not db_document:
|
||||
return False
|
||||
|
||||
await self.session.delete(db_document)
|
||||
await self.session.commit()
|
||||
return True
|
||||
|
||||
async def list_by_collection(self, collection_id: UUID, skip: int = 0, limit: int = 100) -> list[Document]:
|
||||
"""Получить документы коллекции"""
|
||||
result = await self.session.execute(
|
||||
select(DocumentModel)
|
||||
.where(DocumentModel.collection_id == collection_id)
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
db_documents = result.scalars().all()
|
||||
return [self._to_entity(db_document) for db_document in db_documents]
|
||||
|
||||
def _to_entity(self, db_document: DocumentModel | None) -> Document | None:
|
||||
"""Преобразовать модель БД в доменную сущность"""
|
||||
if not db_document:
|
||||
return None
|
||||
return Document(
|
||||
document_id=db_document.document_id,
|
||||
collection_id=db_document.collection_id,
|
||||
title=db_document.title,
|
||||
content=db_document.content,
|
||||
metadata=db_document.document_metadata or {},
|
||||
created_at=db_document.created_at
|
||||
)
|
||||
|
||||
|
||||
@ -1,96 +1,96 @@
|
||||
"""
|
||||
Реализация репозитория сообщений для PostgreSQL
|
||||
"""
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from src.domain.entities.message import Message, MessageRole
|
||||
from src.domain.repositories.message_repository import IMessageRepository
|
||||
from src.infrastructure.database.models import MessageModel
|
||||
from src.shared.exceptions import NotFoundError
|
||||
|
||||
|
||||
class PostgreSQLMessageRepository(IMessageRepository):
|
||||
"""PostgreSQL реализация репозитория сообщений"""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def create(self, message: Message) -> Message:
|
||||
"""Создать сообщение"""
|
||||
db_message = MessageModel(
|
||||
message_id=message.message_id,
|
||||
conversation_id=message.conversation_id,
|
||||
content=message.content,
|
||||
role=message.role.value,
|
||||
sources=message.sources,
|
||||
created_at=message.created_at
|
||||
)
|
||||
self.session.add(db_message)
|
||||
await self.session.commit()
|
||||
await self.session.refresh(db_message)
|
||||
return self._to_entity(db_message)
|
||||
|
||||
async def get_by_id(self, message_id: UUID) -> Optional[Message]:
|
||||
"""Получить сообщение по ID"""
|
||||
result = await self.session.execute(
|
||||
select(MessageModel).where(MessageModel.message_id == message_id)
|
||||
)
|
||||
db_message = result.scalar_one_or_none()
|
||||
return self._to_entity(db_message) if db_message else None
|
||||
|
||||
async def update(self, message: Message) -> Message:
|
||||
"""Обновить сообщение"""
|
||||
result = await self.session.execute(
|
||||
select(MessageModel).where(MessageModel.message_id == message.message_id)
|
||||
)
|
||||
db_message = result.scalar_one_or_none()
|
||||
if not db_message:
|
||||
raise NotFoundError(f"Сообщение {message.message_id} не найдено")
|
||||
|
||||
db_message.content = message.content
|
||||
db_message.role = message.role.value
|
||||
db_message.sources = message.sources
|
||||
await self.session.commit()
|
||||
await self.session.refresh(db_message)
|
||||
return self._to_entity(db_message)
|
||||
|
||||
async def delete(self, message_id: UUID) -> bool:
|
||||
"""Удалить сообщение"""
|
||||
result = await self.session.execute(
|
||||
select(MessageModel).where(MessageModel.message_id == message_id)
|
||||
)
|
||||
db_message = result.scalar_one_or_none()
|
||||
if not db_message:
|
||||
return False
|
||||
|
||||
await self.session.delete(db_message)
|
||||
await self.session.commit()
|
||||
return True
|
||||
|
||||
async def list_by_conversation(self, conversation_id: UUID, skip: int = 0, limit: int = 100) -> list[Message]:
|
||||
"""Получить сообщения беседы"""
|
||||
result = await self.session.execute(
|
||||
select(MessageModel)
|
||||
.where(MessageModel.conversation_id == conversation_id)
|
||||
.order_by(MessageModel.created_at)
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
db_messages = result.scalars().all()
|
||||
return [self._to_entity(db_message) for db_message in db_messages]
|
||||
|
||||
def _to_entity(self, db_message: MessageModel | None) -> Message | None:
|
||||
"""Преобразовать модель БД в доменную сущность"""
|
||||
if not db_message:
|
||||
return None
|
||||
return Message(
|
||||
message_id=db_message.message_id,
|
||||
conversation_id=db_message.conversation_id,
|
||||
content=db_message.content,
|
||||
role=MessageRole(db_message.role),
|
||||
sources=db_message.sources or {},
|
||||
created_at=db_message.created_at
|
||||
)
|
||||
|
||||
"""
|
||||
Реализация репозитория сообщений для PostgreSQL
|
||||
"""
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from src.domain.entities.message import Message, MessageRole
|
||||
from src.domain.repositories.message_repository import IMessageRepository
|
||||
from src.infrastructure.database.models import MessageModel
|
||||
from src.shared.exceptions import NotFoundError
|
||||
|
||||
|
||||
class PostgreSQLMessageRepository(IMessageRepository):
|
||||
"""PostgreSQL реализация репозитория сообщений"""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def create(self, message: Message) -> Message:
|
||||
"""Создать сообщение"""
|
||||
db_message = MessageModel(
|
||||
message_id=message.message_id,
|
||||
conversation_id=message.conversation_id,
|
||||
content=message.content,
|
||||
role=message.role.value,
|
||||
sources=message.sources,
|
||||
created_at=message.created_at
|
||||
)
|
||||
self.session.add(db_message)
|
||||
await self.session.commit()
|
||||
await self.session.refresh(db_message)
|
||||
return self._to_entity(db_message)
|
||||
|
||||
async def get_by_id(self, message_id: UUID) -> Optional[Message]:
|
||||
"""Получить сообщение по ID"""
|
||||
result = await self.session.execute(
|
||||
select(MessageModel).where(MessageModel.message_id == message_id)
|
||||
)
|
||||
db_message = result.scalar_one_or_none()
|
||||
return self._to_entity(db_message) if db_message else None
|
||||
|
||||
async def update(self, message: Message) -> Message:
|
||||
"""Обновить сообщение"""
|
||||
result = await self.session.execute(
|
||||
select(MessageModel).where(MessageModel.message_id == message.message_id)
|
||||
)
|
||||
db_message = result.scalar_one_or_none()
|
||||
if not db_message:
|
||||
raise NotFoundError(f"Сообщение {message.message_id} не найдено")
|
||||
|
||||
db_message.content = message.content
|
||||
db_message.role = message.role.value
|
||||
db_message.sources = message.sources
|
||||
await self.session.commit()
|
||||
await self.session.refresh(db_message)
|
||||
return self._to_entity(db_message)
|
||||
|
||||
async def delete(self, message_id: UUID) -> bool:
|
||||
"""Удалить сообщение"""
|
||||
result = await self.session.execute(
|
||||
select(MessageModel).where(MessageModel.message_id == message_id)
|
||||
)
|
||||
db_message = result.scalar_one_or_none()
|
||||
if not db_message:
|
||||
return False
|
||||
|
||||
await self.session.delete(db_message)
|
||||
await self.session.commit()
|
||||
return True
|
||||
|
||||
async def list_by_conversation(self, conversation_id: UUID, skip: int = 0, limit: int = 100) -> list[Message]:
|
||||
"""Получить сообщения беседы"""
|
||||
result = await self.session.execute(
|
||||
select(MessageModel)
|
||||
.where(MessageModel.conversation_id == conversation_id)
|
||||
.order_by(MessageModel.created_at)
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
db_messages = result.scalars().all()
|
||||
return [self._to_entity(db_message) for db_message in db_messages]
|
||||
|
||||
def _to_entity(self, db_message: MessageModel | None) -> Message | None:
|
||||
"""Преобразовать модель БД в доменную сущность"""
|
||||
if not db_message:
|
||||
return None
|
||||
return Message(
|
||||
message_id=db_message.message_id,
|
||||
conversation_id=db_message.conversation_id,
|
||||
content=db_message.content,
|
||||
role=MessageRole(db_message.role),
|
||||
sources=db_message.sources or {},
|
||||
created_at=db_message.created_at
|
||||
)
|
||||
|
||||
|
||||
@ -1,95 +1,95 @@
|
||||
"""
|
||||
Реализация репозитория пользователей для PostgreSQL
|
||||
"""
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from src.domain.entities.user import User, UserRole
|
||||
from src.domain.repositories.user_repository import IUserRepository
|
||||
from src.infrastructure.database.models import UserModel
|
||||
from src.shared.exceptions import NotFoundError
|
||||
|
||||
|
||||
class PostgreSQLUserRepository(IUserRepository):
|
||||
"""PostgreSQL реализация репозитория пользователей"""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def create(self, user: User) -> User:
|
||||
"""Создать пользователя"""
|
||||
db_user = UserModel(
|
||||
user_id=user.user_id,
|
||||
telegram_id=user.telegram_id,
|
||||
role=user.role.value,
|
||||
created_at=user.created_at
|
||||
)
|
||||
self.session.add(db_user)
|
||||
await self.session.commit()
|
||||
await self.session.refresh(db_user)
|
||||
return self._to_entity(db_user)
|
||||
|
||||
async def get_by_id(self, user_id: UUID) -> Optional[User]:
|
||||
"""Получить пользователя по ID"""
|
||||
result = await self.session.execute(
|
||||
select(UserModel).where(UserModel.user_id == user_id)
|
||||
)
|
||||
db_user = result.scalar_one_or_none()
|
||||
return self._to_entity(db_user) if db_user else None
|
||||
|
||||
async def get_by_telegram_id(self, telegram_id: str) -> Optional[User]:
|
||||
"""Получить пользователя по Telegram ID"""
|
||||
result = await self.session.execute(
|
||||
select(UserModel).where(UserModel.telegram_id == telegram_id)
|
||||
)
|
||||
db_user = result.scalar_one_or_none()
|
||||
return self._to_entity(db_user) if db_user else None
|
||||
|
||||
async def update(self, user: User) -> User:
|
||||
"""Обновить пользователя"""
|
||||
result = await self.session.execute(
|
||||
select(UserModel).where(UserModel.user_id == user.user_id)
|
||||
)
|
||||
db_user = result.scalar_one_or_none()
|
||||
if not db_user:
|
||||
raise NotFoundError(f"Пользователь {user.user_id} не найден")
|
||||
|
||||
db_user.telegram_id = user.telegram_id
|
||||
db_user.role = user.role.value
|
||||
await self.session.commit()
|
||||
await self.session.refresh(db_user)
|
||||
return self._to_entity(db_user)
|
||||
|
||||
async def delete(self, user_id: UUID) -> bool:
|
||||
"""Удалить пользователя"""
|
||||
result = await self.session.execute(
|
||||
select(UserModel).where(UserModel.user_id == user_id)
|
||||
)
|
||||
db_user = result.scalar_one_or_none()
|
||||
if not db_user:
|
||||
return False
|
||||
|
||||
await self.session.delete(db_user)
|
||||
await self.session.commit()
|
||||
return True
|
||||
|
||||
async def list_all(self, skip: int = 0, limit: int = 100) -> list[User]:
|
||||
"""Получить список всех пользователей"""
|
||||
result = await self.session.execute(
|
||||
select(UserModel).offset(skip).limit(limit)
|
||||
)
|
||||
db_users = result.scalars().all()
|
||||
return [self._to_entity(db_user) for db_user in db_users]
|
||||
|
||||
def _to_entity(self, db_user: UserModel | None) -> User | None:
|
||||
"""Преобразовать модель БД в доменную сущность"""
|
||||
if not db_user:
|
||||
return None
|
||||
return User(
|
||||
user_id=db_user.user_id,
|
||||
telegram_id=db_user.telegram_id,
|
||||
role=UserRole(db_user.role),
|
||||
created_at=db_user.created_at
|
||||
)
|
||||
|
||||
"""
|
||||
Реализация репозитория пользователей для PostgreSQL
|
||||
"""
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from src.domain.entities.user import User, UserRole
|
||||
from src.domain.repositories.user_repository import IUserRepository
|
||||
from src.infrastructure.database.models import UserModel
|
||||
from src.shared.exceptions import NotFoundError
|
||||
|
||||
|
||||
class PostgreSQLUserRepository(IUserRepository):
|
||||
"""PostgreSQL реализация репозитория пользователей"""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def create(self, user: User) -> User:
|
||||
"""Создать пользователя"""
|
||||
db_user = UserModel(
|
||||
user_id=user.user_id,
|
||||
telegram_id=user.telegram_id,
|
||||
role=user.role.value,
|
||||
created_at=user.created_at
|
||||
)
|
||||
self.session.add(db_user)
|
||||
await self.session.commit()
|
||||
await self.session.refresh(db_user)
|
||||
return self._to_entity(db_user)
|
||||
|
||||
async def get_by_id(self, user_id: UUID) -> Optional[User]:
|
||||
"""Получить пользователя по ID"""
|
||||
result = await self.session.execute(
|
||||
select(UserModel).where(UserModel.user_id == user_id)
|
||||
)
|
||||
db_user = result.scalar_one_or_none()
|
||||
return self._to_entity(db_user) if db_user else None
|
||||
|
||||
async def get_by_telegram_id(self, telegram_id: str) -> Optional[User]:
|
||||
"""Получить пользователя по Telegram ID"""
|
||||
result = await self.session.execute(
|
||||
select(UserModel).where(UserModel.telegram_id == telegram_id)
|
||||
)
|
||||
db_user = result.scalar_one_or_none()
|
||||
return self._to_entity(db_user) if db_user else None
|
||||
|
||||
async def update(self, user: User) -> User:
|
||||
"""Обновить пользователя"""
|
||||
result = await self.session.execute(
|
||||
select(UserModel).where(UserModel.user_id == user.user_id)
|
||||
)
|
||||
db_user = result.scalar_one_or_none()
|
||||
if not db_user:
|
||||
raise NotFoundError(f"Пользователь {user.user_id} не найден")
|
||||
|
||||
db_user.telegram_id = user.telegram_id
|
||||
db_user.role = user.role.value
|
||||
await self.session.commit()
|
||||
await self.session.refresh(db_user)
|
||||
return self._to_entity(db_user)
|
||||
|
||||
async def delete(self, user_id: UUID) -> bool:
|
||||
"""Удалить пользователя"""
|
||||
result = await self.session.execute(
|
||||
select(UserModel).where(UserModel.user_id == user_id)
|
||||
)
|
||||
db_user = result.scalar_one_or_none()
|
||||
if not db_user:
|
||||
return False
|
||||
|
||||
await self.session.delete(db_user)
|
||||
await self.session.commit()
|
||||
return True
|
||||
|
||||
async def list_all(self, skip: int = 0, limit: int = 100) -> list[User]:
|
||||
"""Получить список всех пользователей"""
|
||||
result = await self.session.execute(
|
||||
select(UserModel).offset(skip).limit(limit)
|
||||
)
|
||||
db_users = result.scalars().all()
|
||||
return [self._to_entity(db_user) for db_user in db_users]
|
||||
|
||||
def _to_entity(self, db_user: UserModel | None) -> User | None:
|
||||
"""Преобразовать модель БД в доменную сущность"""
|
||||
if not db_user:
|
||||
return None
|
||||
return User(
|
||||
user_id=db_user.user_id,
|
||||
telegram_id=db_user.telegram_id,
|
||||
role=UserRole(db_user.role),
|
||||
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 typing import Optional
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Настройки (загружаются из .env автоматически)"""
|
||||
|
||||
POSTGRES_HOST: str = "localhost"
|
||||
POSTGRES_PORT: int = 5432
|
||||
POSTGRES_USER: str = "postgres"
|
||||
POSTGRES_PASSWORD: str = "postgres"
|
||||
POSTGRES_DB: str = "lawyer_ai"
|
||||
|
||||
QDRANT_HOST: str = "localhost"
|
||||
QDRANT_PORT: int = 6333
|
||||
|
||||
REDIS_HOST: str = "localhost"
|
||||
REDIS_PORT: int = 6379
|
||||
|
||||
TELEGRAM_BOT_TOKEN: Optional[str] = None
|
||||
YANDEX_OCR_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"
|
||||
DEEPSEEK_API_URL: str = "https://api.deepseek.com/v1/chat/completions"
|
||||
|
||||
APP_NAME: str = "ИИ-юрист"
|
||||
DEBUG: bool = False
|
||||
SECRET_KEY: str = "your-secret-key-change-in-production"
|
||||
CORS_ORIGINS: list[str] = ["*"]
|
||||
|
||||
@property
|
||||
def database_url(self) -> str:
|
||||
"""Вычисляемый URL подключения"""
|
||||
return f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = True
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
|
||||
"""
|
||||
Конфигурация приложения
|
||||
|
||||
"""
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Настройки (загружаются из .env автоматически)"""
|
||||
|
||||
POSTGRES_HOST: str = "localhost"
|
||||
POSTGRES_PORT: int = 5432
|
||||
POSTGRES_USER: str = "postgres"
|
||||
POSTGRES_PASSWORD: str = "postgres"
|
||||
POSTGRES_DB: str = "lawyer_ai"
|
||||
|
||||
QDRANT_HOST: str = "localhost"
|
||||
QDRANT_PORT: int = 6333
|
||||
|
||||
REDIS_HOST: str = "localhost"
|
||||
REDIS_PORT: int = 6379
|
||||
|
||||
TELEGRAM_BOT_TOKEN: Optional[str] = None
|
||||
YANDEX_OCR_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"
|
||||
DEEPSEEK_API_URL: str = "https://api.deepseek.com/v1/chat/completions"
|
||||
|
||||
APP_NAME: str = "ИИ-юрист"
|
||||
DEBUG: bool = False
|
||||
SECRET_KEY: str = "your-secret-key-change-in-production"
|
||||
CORS_ORIGINS: list[str] = ["*"]
|
||||
|
||||
@property
|
||||
def database_url(self) -> str:
|
||||
"""Вычисляемый URL подключения"""
|
||||
return f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = True
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
|
||||
|
||||
@ -1,35 +1,35 @@
|
||||
"""
|
||||
Кастомные исключения приложения
|
||||
"""
|
||||
|
||||
|
||||
class LawyerAIException(Exception):
|
||||
"""Базовое исключение приложения"""
|
||||
pass
|
||||
|
||||
|
||||
class NotFoundError(LawyerAIException):
|
||||
"""Ресурс не найден"""
|
||||
pass
|
||||
|
||||
|
||||
class UnauthorizedError(LawyerAIException):
|
||||
"""Пользователь не авторизован"""
|
||||
pass
|
||||
|
||||
|
||||
class ForbiddenError(LawyerAIException):
|
||||
"""Доступ запрещен"""
|
||||
pass
|
||||
|
||||
|
||||
class ValidationError(LawyerAIException):
|
||||
"""Ошибка валидации данных"""
|
||||
pass
|
||||
|
||||
|
||||
class DatabaseError(LawyerAIException):
|
||||
"""Ошибка базы данных"""
|
||||
pass
|
||||
|
||||
|
||||
"""
|
||||
Кастомные исключения приложения
|
||||
"""
|
||||
|
||||
|
||||
class LawyerAIException(Exception):
|
||||
"""Базовое исключение приложения"""
|
||||
pass
|
||||
|
||||
|
||||
class NotFoundError(LawyerAIException):
|
||||
"""Ресурс не найден"""
|
||||
pass
|
||||
|
||||
|
||||
class UnauthorizedError(LawyerAIException):
|
||||
"""Пользователь не авторизован"""
|
||||
pass
|
||||
|
||||
|
||||
class ForbiddenError(LawyerAIException):
|
||||
"""Доступ запрещен"""
|
||||
pass
|
||||
|
||||
|
||||
class ValidationError(LawyerAIException):
|
||||
"""Ошибка валидации данных"""
|
||||
pass
|
||||
|
||||
|
||||
class DatabaseError(LawyerAIException):
|
||||
"""Ошибка базы данных"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user