From dfc188e179f378910491ab95fdf61a0fbfcd1d5b Mon Sep 17 00:00:00 2001 From: bokho Date: Wed, 24 Dec 2025 03:14:37 +0300 Subject: [PATCH] fixed DI --- backend/requirements.txt | 2 +- .../src/application/services/cache_service.py | 2 +- backend/src/presentation/api/v1/admin.py | 122 ++++---- .../src/presentation/api/v1/collections.py | 261 ++++++++++-------- .../src/presentation/api/v1/conversations.py | 154 ++++++----- backend/src/presentation/api/v1/documents.py | 260 ++++++++--------- backend/src/presentation/api/v1/messages.py | 189 +++++++------ backend/src/presentation/api/v1/rag.py | 23 +- backend/src/presentation/api/v1/users.py | 31 ++- backend/src/presentation/main.py | 17 +- .../schemas/collection_schemas.py | 154 +++++------ .../schemas/conversation_schemas.py | 70 ++--- .../presentation/schemas/document_schemas.py | 104 +++---- .../presentation/schemas/message_schemas.py | 104 +++---- .../src/presentation/schemas/user_schemas.py | 104 +++---- backend/src/shared/config.py | 22 +- backend/src/shared/di_container.py | 29 +- 17 files changed, 859 insertions(+), 789 deletions(-) diff --git a/backend/requirements.txt b/backend/requirements.txt index d495c20..21388be 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -9,7 +9,7 @@ python-multipart==0.0.6 httpx==0.25.2 PyMuPDF==1.23.8 Pillow==10.2.0 -dishka==1.7.2 +dishka==0.7.0 numpy==1.26.4 sentence-transformers==2.7.0 qdrant-client==1.9.0 diff --git a/backend/src/application/services/cache_service.py b/backend/src/application/services/cache_service.py index 5d3c561..3cf4a77 100644 --- a/backend/src/application/services/cache_service.py +++ b/backend/src/application/services/cache_service.py @@ -26,7 +26,7 @@ class CacheService: "question": question, "answer": answer } - await self.reids_client.set_json(key, value, ttl or self.default_ttl) + await self.redis_client.set_json(key, value, ttl or self.default_ttl) async def invalidate_collection_cache(self, collection_id: UUID): pattern = f"rag:answer:{collection_id}:*" diff --git a/backend/src/presentation/api/v1/admin.py b/backend/src/presentation/api/v1/admin.py index 89ac153..d9c995a 100644 --- a/backend/src/presentation/api/v1/admin.py +++ b/backend/src/presentation/api/v1/admin.py @@ -1,58 +1,64 @@ -""" -Админ-панель - упрощенная версия через API эндпоинты -В будущем можно интегрировать полноценную админ-панель -""" -from __future__ import annotations - -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: User = FromDishka(), - use_cases: 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: User = FromDishka(), - use_cases: 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] - +""" +Админ-панель - упрощенная версия через API эндпоинты +В будущем можно интегрировать полноценную админ-панель +""" +from fastapi import APIRouter, HTTPException, Request +from typing import List, Annotated +from uuid import UUID +from dishka.integrations.fastapi import FromDishka, inject +from src.domain.repositories.user_repository import IUserRepository +from src.presentation.middleware.auth_middleware import get_current_user +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]) +@inject +async def admin_list_users( + request: Request, + user_repo: Annotated[IUserRepository, FromDishka()], + use_cases: Annotated[UserUseCases, FromDishka()], + skip: int = 0, + limit: int = 100 +): + """Получить список всех пользователей (только для админов)""" + current_user = await get_current_user(request, user_repo) + 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]) +@inject +async def admin_list_collections( + request: Request, + user_repo: Annotated[IUserRepository, FromDishka()], + use_cases: Annotated[CollectionUseCases, FromDishka()], + skip: int = 0, + limit: int = 100 +): + """Получить список всех коллекций (только для админов)""" + current_user = await get_current_user(request, user_repo) + 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] + diff --git a/backend/src/presentation/api/v1/collections.py b/backend/src/presentation/api/v1/collections.py index 91d756a..0346e3a 100644 --- a/backend/src/presentation/api/v1/collections.py +++ b/backend/src/presentation/api/v1/collections.py @@ -1,121 +1,140 @@ -""" -API роутеры для работы с коллекциями -""" -from __future__ import annotations - -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 - -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: 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: 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: 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: 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: 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: 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: 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) - +""" +API роутеры для работы с коллекциями +""" +from uuid import UUID +from fastapi import APIRouter, status, Depends, Request +from fastapi.responses import JSONResponse +from typing import List, Annotated +from dishka.integrations.fastapi import FromDishka, inject +from src.domain.repositories.user_repository import IUserRepository +from src.presentation.middleware.auth_middleware import get_current_user +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 + +router = APIRouter(prefix="/collections", tags=["collections"]) + + +@router.post("", response_model=CollectionResponse, status_code=status.HTTP_201_CREATED) +@inject +async def create_collection( + collection_data: CollectionCreate, + request: Request, + user_repo: Annotated[IUserRepository, FromDishka()], + use_cases: Annotated[CollectionUseCases, FromDishka()] +): + """Создать коллекцию""" + current_user = await get_current_user(request, user_repo) + 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) +@inject +async def get_collection( + collection_id: UUID, + use_cases: Annotated[CollectionUseCases, FromDishka()] +): + """Получить коллекцию по ID""" + collection = await use_cases.get_collection(collection_id) + return CollectionResponse.from_entity(collection) + + +@router.put("/{collection_id}", response_model=CollectionResponse) +@inject +async def update_collection( + collection_id: UUID, + collection_data: CollectionUpdate, + request: Request, + user_repo: Annotated[IUserRepository, FromDishka()], + use_cases: Annotated[CollectionUseCases, FromDishka()] +): + """Обновить коллекцию""" + current_user = await get_current_user(request, user_repo) + 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) +@inject +async def delete_collection( + collection_id: UUID, + request: Request, + user_repo: Annotated[IUserRepository, FromDishka()], + use_cases: Annotated[CollectionUseCases, FromDishka()] +): + """Удалить коллекцию""" + current_user = await get_current_user(request, user_repo) + 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]) +@inject +async def list_collections( + request: Request, + user_repo: Annotated[IUserRepository, FromDishka()], + use_cases: Annotated[CollectionUseCases, FromDishka()], + skip: int = 0, + limit: int = 100 +): + """Получить список коллекций, доступных пользователю""" + current_user = await get_current_user(request, user_repo) + 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) +@inject +async def grant_access( + collection_id: UUID, + access_data: CollectionAccessGrant, + request: Request, + user_repo: Annotated[IUserRepository, FromDishka()], + use_cases: Annotated[CollectionUseCases, FromDishka()] +): + """Предоставить доступ пользователю к коллекции""" + current_user = await get_current_user(request, user_repo) + 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) +@inject +async def revoke_access( + collection_id: UUID, + user_id: UUID, + request: Request, + user_repo: Annotated[IUserRepository, FromDishka()], + use_cases: Annotated[CollectionUseCases, FromDishka()] +): + """Отозвать доступ пользователя к коллекции""" + current_user = await get_current_user(request, user_repo) + await use_cases.revoke_access(collection_id, user_id, current_user.user_id) + return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None) + diff --git a/backend/src/presentation/api/v1/conversations.py b/backend/src/presentation/api/v1/conversations.py index 75b1792..80207be 100644 --- a/backend/src/presentation/api/v1/conversations.py +++ b/backend/src/presentation/api/v1/conversations.py @@ -1,71 +1,83 @@ -""" -API роутеры для работы с беседами -""" -from __future__ import annotations - -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: User = FromDishka(), - use_cases: 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: User = FromDishka(), - use_cases: 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: User = FromDishka(), - use_cases: 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: User = FromDishka(), - use_cases: 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] - +""" +API роутеры для работы с беседами +""" +from uuid import UUID +from fastapi import APIRouter, status, Depends, Request +from fastapi.responses import JSONResponse +from typing import List, Annotated +from dishka.integrations.fastapi import FromDishka, inject +from src.domain.repositories.user_repository import IUserRepository +from src.presentation.middleware.auth_middleware import get_current_user +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) +@inject +async def create_conversation( + conversation_data: ConversationCreate, + request: Request, + user_repo: Annotated[IUserRepository, FromDishka()], + use_cases: Annotated[ConversationUseCases, FromDishka()] +): + """Создать беседу""" + current_user = await get_current_user(request, user_repo) + 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) +@inject +async def get_conversation( + conversation_id: UUID, + request: Request, + user_repo: Annotated[IUserRepository, FromDishka()], + use_cases: Annotated[ConversationUseCases, FromDishka()] +): + """Получить беседу по ID""" + current_user = await get_current_user(request, user_repo) + 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) +@inject +async def delete_conversation( + conversation_id: UUID, + request: Request, + user_repo: Annotated[IUserRepository, FromDishka()], + use_cases: Annotated[ConversationUseCases, FromDishka()] +): + """Удалить беседу""" + current_user = await get_current_user(request, user_repo) + 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]) +@inject +async def list_conversations( + request: Request, + user_repo: Annotated[IUserRepository, FromDishka()], + use_cases: Annotated[ConversationUseCases, FromDishka()], + skip: int = 0, + limit: int = 100 +): + """Получить список бесед пользователя""" + current_user = await get_current_user(request, user_repo) + 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] + diff --git a/backend/src/presentation/api/v1/documents.py b/backend/src/presentation/api/v1/documents.py index 80d991e..d342ee4 100644 --- a/backend/src/presentation/api/v1/documents.py +++ b/backend/src/presentation/api/v1/documents.py @@ -1,123 +1,137 @@ -""" -API роутеры для работы с документами -""" -from __future__ import annotations - -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: User = FromDishka(), - use_cases: 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: User = FromDishka(), - use_cases: 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: 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: User = FromDishka(), - use_cases: 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: User = FromDishka(), - use_cases: 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: 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] - +""" +API роутеры для работы с документами +""" +from uuid import UUID +from fastapi import APIRouter, status, UploadFile, File, Depends, Request +from fastapi.responses import JSONResponse +from typing import List, Annotated +from dishka.integrations.fastapi import FromDishka, inject +from src.domain.repositories.user_repository import IUserRepository +from src.presentation.middleware.auth_middleware import get_current_user +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) +@inject +async def create_document( + document_data: DocumentCreate, + request: Request, + user_repo: Annotated[IUserRepository, FromDishka()], + use_cases: Annotated[DocumentUseCases, FromDishka()] +): + """Создать документ""" + current_user = await get_current_user(request, user_repo) + 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) +@inject +async def upload_document( + collection_id: UUID, + request: Request, + user_repo: Annotated[IUserRepository, FromDishka()], + use_cases: Annotated[DocumentUseCases, FromDishka()], + file: UploadFile = File(...) +): + """Загрузить и распарсить PDF документ или изображение""" + current_user = await get_current_user(request, user_repo) + 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) +@inject +async def get_document( + document_id: UUID, + use_cases: Annotated[DocumentUseCases, FromDishka()] +): + """Получить документ по ID""" + document = await use_cases.get_document(document_id) + return DocumentResponse.from_entity(document) + + +@router.put("/{document_id}", response_model=DocumentResponse) +@inject +async def update_document( + document_id: UUID, + document_data: DocumentUpdate, + request: Request, + user_repo: Annotated[IUserRepository, FromDishka()], + use_cases: Annotated[DocumentUseCases, FromDishka()] +): + """Обновить документ""" + current_user = await get_current_user(request, user_repo) + 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) +@inject +async def delete_document( + document_id: UUID, + request: Request, + user_repo: Annotated[IUserRepository, FromDishka()], + use_cases: Annotated[DocumentUseCases, FromDishka()] +): + """Удалить документ""" + current_user = await get_current_user(request, user_repo) + 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]) +@inject +async def list_collection_documents( + collection_id: UUID, + use_cases: Annotated[DocumentUseCases, FromDishka()], + skip: int = 0, + limit: int = 100 +): + """Получить документы коллекции""" + documents = await use_cases.list_collection_documents( + collection_id=collection_id, + skip=skip, + limit=limit + ) + return [DocumentResponse.from_entity(d) for d in documents] + diff --git a/backend/src/presentation/api/v1/messages.py b/backend/src/presentation/api/v1/messages.py index 84f5f02..b5bfc41 100644 --- a/backend/src/presentation/api/v1/messages.py +++ b/backend/src/presentation/api/v1/messages.py @@ -1,90 +1,99 @@ -""" -API роутеры для работы с сообщениями -""" -from __future__ import annotations - -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: User = FromDishka(), - use_cases: 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: 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: 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: 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: User = FromDishka(), - use_cases: 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] - +""" +API роутеры для работы с сообщениями +""" +from uuid import UUID +from fastapi import APIRouter, status, Depends, Request +from fastapi.responses import JSONResponse +from typing import List, Annotated +from dishka.integrations.fastapi import FromDishka, inject +from src.domain.repositories.user_repository import IUserRepository +from src.presentation.middleware.auth_middleware import get_current_user +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) +@inject +async def create_message( + message_data: MessageCreate, + request: Request, + user_repo: Annotated[IUserRepository, FromDishka()], + use_cases: Annotated[MessageUseCases, FromDishka()] +): + """Создать сообщение""" + current_user = await get_current_user(request, user_repo) + 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) +@inject +async def get_message( + message_id: UUID, + use_cases: Annotated[MessageUseCases, FromDishka()] +): + """Получить сообщение по ID""" + message = await use_cases.get_message(message_id) + return MessageResponse.from_entity(message) + + +@router.put("/{message_id}", response_model=MessageResponse) +@inject +async def update_message( + message_id: UUID, + message_data: MessageUpdate, + use_cases: Annotated[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) +@inject +async def delete_message( + message_id: UUID, + use_cases: Annotated[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]) +@inject +async def list_conversation_messages( + conversation_id: UUID, + request: Request, + user_repo: Annotated[IUserRepository, FromDishka()], + use_cases: Annotated[MessageUseCases, FromDishka()], + skip: int = 0, + limit: int = 100 +): + """Получить сообщения беседы""" + current_user = await get_current_user(request, user_repo) + 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] + diff --git a/backend/src/presentation/api/v1/rag.py b/backend/src/presentation/api/v1/rag.py index c0e7371..43595b0 100644 --- a/backend/src/presentation/api/v1/rag.py +++ b/backend/src/presentation/api/v1/rag.py @@ -1,10 +1,11 @@ """ API для RAG: индексация документов и ответы на вопросы """ -from __future__ import annotations - -from fastapi import APIRouter, status -from dishka.integrations.fastapi import FromDishka +from fastapi import APIRouter, status, Request +from typing import Annotated +from dishka.integrations.fastapi import FromDishka, inject +from src.domain.repositories.user_repository import IUserRepository +from src.presentation.middleware.auth_middleware import get_current_user from src.presentation.schemas.rag_schemas import ( QuestionRequest, RAGAnswer, @@ -19,23 +20,29 @@ router = APIRouter(prefix="/rag", tags=["rag"]) @router.post("/index", response_model=IndexDocumentResponse, status_code=status.HTTP_200_OK) +@inject async def index_document( body: IndexDocumentRequest, - use_cases: RAGUseCases = FromDishka(), - current_user: User = FromDishka(), + request: Request, + user_repo: Annotated[IUserRepository, FromDishka()], + use_cases: Annotated[RAGUseCases, FromDishka()], ): """Индексирование идет через чанкирование, далее эмбеддинг и загрузка в векторную бд""" + current_user = await get_current_user(request, user_repo) result = await use_cases.index_document(body.document_id) return IndexDocumentResponse(**result) @router.post("/question", response_model=RAGAnswer, status_code=status.HTTP_200_OK) +@inject async def ask_question( body: QuestionRequest, - use_cases: RAGUseCases = FromDishka(), - current_user: User = FromDishka(), + request: Request, + user_repo: Annotated[IUserRepository, FromDishka()], + use_cases: Annotated[RAGUseCases, FromDishka()], ): """Отвечает на вопрос, используя RAG в рамках беседы""" + current_user = await get_current_user(request, user_repo) result = await use_cases.ask_question( conversation_id=body.conversation_id, user_id=current_user.user_id, diff --git a/backend/src/presentation/api/v1/users.py b/backend/src/presentation/api/v1/users.py index 7895e7a..e95e921 100644 --- a/backend/src/presentation/api/v1/users.py +++ b/backend/src/presentation/api/v1/users.py @@ -1,13 +1,13 @@ """ API роутеры для работы с пользователями """ -from __future__ import annotations - from uuid import UUID -from fastapi import APIRouter, status, Depends +from fastapi import APIRouter, status, Depends, Request from fastapi.responses import JSONResponse from typing import List, Annotated from dishka.integrations.fastapi import FromDishka, inject +from src.domain.repositories.user_repository import IUserRepository +from src.presentation.middleware.auth_middleware import get_current_user 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 @@ -19,8 +19,8 @@ router = APIRouter(prefix="/users", tags=["users"]) @inject async def create_user( user_data: UserCreate, - use_cases: UserUseCases = FromDishka() -) -> UserResponse: + use_cases: Annotated[UserUseCases, FromDishka()] +): """Создать пользователя""" user = await use_cases.create_user( telegram_id=user_data.telegram_id, @@ -32,9 +32,11 @@ async def create_user( @router.get("/me", response_model=UserResponse) @inject async def get_current_user_info( - current_user: User = FromDishka() + request, + user_repo: Annotated[IUserRepository, FromDishka()] ): """Получить информацию о текущем пользователе""" + current_user = await get_current_user(request, user_repo) return UserResponse.from_entity(current_user) @@ -42,7 +44,7 @@ async def get_current_user_info( @inject async def get_user_by_telegram_id( telegram_id: str, - use_cases: UserUseCases = FromDishka() + use_cases: Annotated[UserUseCases, FromDishka()] ): """Получить пользователя по Telegram ID""" user = await use_cases.get_user_by_telegram_id(telegram_id) @@ -56,7 +58,7 @@ async def get_user_by_telegram_id( @inject async def increment_questions( telegram_id: str, - use_cases: UserUseCases = FromDishka() + use_cases: Annotated[UserUseCases, FromDishka()] ): """Увеличить счетчик использованных вопросов""" user = await use_cases.increment_questions_used(telegram_id) @@ -66,9 +68,10 @@ async def increment_questions( @router.post("/telegram/{telegram_id}/activate-premium", response_model=UserResponse) @inject async def activate_premium( + + use_cases: Annotated[UserUseCases, FromDishka()], telegram_id: str, days: int = 30, - use_cases: UserUseCases = FromDishka() ): """Активировать premium статус""" user = await use_cases.activate_premium(telegram_id, days=days) @@ -79,7 +82,7 @@ async def activate_premium( @inject async def get_user( user_id: UUID, - use_cases: UserUseCases = FromDishka() + use_cases: Annotated[UserUseCases, FromDishka()] ): """Получить пользователя по ID""" user = await use_cases.get_user(user_id) @@ -91,7 +94,7 @@ async def get_user( async def update_user( user_id: UUID, user_data: UserUpdate, - use_cases: UserUseCases = FromDishka() + use_cases: Annotated[UserUseCases, FromDishka()] ): """Обновить пользователя""" user = await use_cases.update_user( @@ -106,7 +109,7 @@ async def update_user( @inject async def delete_user( user_id: UUID, - use_cases: UserUseCases = FromDishka() + use_cases: Annotated[UserUseCases, FromDishka()] ): """Удалить пользователя""" await use_cases.delete_user(user_id) @@ -116,9 +119,9 @@ async def delete_user( @router.get("", response_model=List[UserResponse]) @inject async def list_users( + use_cases: Annotated[UserUseCases, FromDishka()], skip: int = 0, - limit: int = 100, - use_cases: UserUseCases = FromDishka() + limit: int = 100 ): """Получить список пользователей""" users = await use_cases.list_users(skip=skip, limit=limit) diff --git a/backend/src/presentation/main.py b/backend/src/presentation/main.py index 8f58e66..0666d4d 100644 --- a/backend/src/presentation/main.py +++ b/backend/src/presentation/main.py @@ -1,7 +1,6 @@ -from __future__ import annotations - import sys import os +import asyncio from pathlib import Path backend_dir = Path(__file__).parent.parent.parent @@ -24,15 +23,18 @@ from src.infrastructure.database.base import engine, Base @asynccontextmanager async def lifespan(app: FastAPI): """Управление жизненным циклом приложения""" - container = create_container() - setup_dishka(container, app) try: async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) except Exception as e: print(f"Примечание при создании таблиц: {e}") yield - await container.close() + # Cleanup container if needed + if hasattr(app.state, 'container') and hasattr(app.state.container, 'close'): + if asyncio.iscoroutinefunction(app.state.container.close): + await app.state.container.close() + else: + app.state.container.close() await engine.dispose() @@ -43,6 +45,11 @@ app = FastAPI( lifespan=lifespan ) +# Настройка Dishka ДО добавления middleware +container = create_container() +setup_dishka(container, app) +app.state.container = container + app.add_middleware( CORSMiddleware, allow_origins=settings.CORS_ORIGINS, diff --git a/backend/src/presentation/schemas/collection_schemas.py b/backend/src/presentation/schemas/collection_schemas.py index 8848fd2..08d75ec 100644 --- a/backend/src/presentation/schemas/collection_schemas.py +++ b/backend/src/presentation/schemas/collection_schemas.py @@ -1,77 +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 - +""" +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 + diff --git a/backend/src/presentation/schemas/conversation_schemas.py b/backend/src/presentation/schemas/conversation_schemas.py index 22aeb83..ba593c3 100644 --- a/backend/src/presentation/schemas/conversation_schemas.py +++ b/backend/src/presentation/schemas/conversation_schemas.py @@ -1,35 +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 - +""" +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 + diff --git a/backend/src/presentation/schemas/document_schemas.py b/backend/src/presentation/schemas/document_schemas.py index 8660e5c..6041fe0 100644 --- a/backend/src/presentation/schemas/document_schemas.py +++ b/backend/src/presentation/schemas/document_schemas.py @@ -1,52 +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 - +""" +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 + diff --git a/backend/src/presentation/schemas/message_schemas.py b/backend/src/presentation/schemas/message_schemas.py index 2f346e8..17cba5a 100644 --- a/backend/src/presentation/schemas/message_schemas.py +++ b/backend/src/presentation/schemas/message_schemas.py @@ -1,52 +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 - +""" +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 + diff --git a/backend/src/presentation/schemas/user_schemas.py b/backend/src/presentation/schemas/user_schemas.py index 3cd9e07..2b2f65f 100644 --- a/backend/src/presentation/schemas/user_schemas.py +++ b/backend/src/presentation/schemas/user_schemas.py @@ -1,52 +1,52 @@ -""" -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 - is_premium: bool = False - premium_until: datetime | None = None - questions_used: int = 0 - - @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, - is_premium=user.is_premium, - premium_until=user.premium_until, - questions_used=user.questions_used - ) - - class Config: - from_attributes = True - +""" +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 + is_premium: bool = False + premium_until: datetime | None = None + questions_used: int = 0 + + @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, + is_premium=user.is_premium, + premium_until=user.premium_until, + questions_used=user.questions_used + ) + + class Config: + from_attributes = True + diff --git a/backend/src/shared/config.py b/backend/src/shared/config.py index 6ddc937..f60b4e7 100644 --- a/backend/src/shared/config.py +++ b/backend/src/shared/config.py @@ -7,18 +7,19 @@ from typing import Optional class Settings(BaseSettings): + """Настройки (загружаются из .env автоматически)""" - POSTGRES_HOST: str - POSTGRES_PORT: int - POSTGRES_USER: str - POSTGRES_PASSWORD: str - POSTGRES_DB: str + POSTGRES_HOST: str = "localhost" + POSTGRES_PORT: int = 5432 + POSTGRES_USER: str = "postgres" + POSTGRES_PASSWORD: str = "postgres" + POSTGRES_DB: str = "lawyer_ai" - QDRANT_HOST: str - QDRANT_PORT: int + QDRANT_HOST: str = "localhost" + QDRANT_PORT: int = 6333 - REDIS_HOST: str - REDIS_PORT: int + REDIS_HOST: str = "localhost" + REDIS_PORT: int = 6379 TELEGRAM_BOT_TOKEN: Optional[str] = None YANDEX_OCR_API_KEY: Optional[str] = None @@ -29,11 +30,12 @@ class Settings(BaseSettings): APP_NAME: str = "ИИ-юрист" DEBUG: bool = False - SECRET_KEY: str + 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: diff --git a/backend/src/shared/di_container.py b/backend/src/shared/di_container.py index eeaaa56..4908389 100644 --- a/backend/src/shared/di_container.py +++ b/backend/src/shared/di_container.py @@ -1,7 +1,7 @@ -from dishka import Container, Provider, Scope, provide +from dishka import Container, Provider, Scope, provide, make_async_container from fastapi import Request from sqlalchemy.ext.asyncio import AsyncSession -from collections.abc import AsyncIterator +from contextlib import asynccontextmanager from src.infrastructure.database.base import AsyncSessionLocal from src.infrastructure.repositories.postgresql.user_repository import PostgreSQLUserRepository @@ -39,7 +39,8 @@ from src.application.use_cases.rag_use_cases import RAGUseCases class DatabaseProvider(Provider): @provide(scope=Scope.REQUEST) - async def get_db(self) -> AsyncIterator[AsyncSession]: + @asynccontextmanager + async def get_db(self) -> AsyncSession: async with AsyncSessionLocal() as session: try: yield session @@ -94,8 +95,6 @@ class ServiceProvider(Provider): def get_parser_service(self, ocr_service: YandexOCRService) -> DocumentParserService: return DocumentParserService(ocr_service) - -class VectorServiceProvider(Provider): @provide(scope=Scope.APP) def get_qdrant_client(self) -> QdrantClient: return QdrantClient(host=settings.QDRANT_HOST, port=settings.QDRANT_PORT) @@ -133,12 +132,6 @@ class VectorServiceProvider(Provider): splitter=text_splitter, ) -class AuthProvider(Provider): - @provide(scope=Scope.REQUEST) - async def get_current_user(self, request: Request, user_repo: IUserRepository) -> User: - from src.presentation.middleware.auth_middleware import get_current_user - return await get_current_user(request, user_repo) - class UseCaseProvider(Provider): @provide(scope=Scope.REQUEST) @@ -196,12 +189,10 @@ class UseCaseProvider(Provider): def create_container() -> Container: - container = Container() - container.add_provider(DatabaseProvider()) - container.add_provider(RepositoryProvider()) - container.add_provider(ServiceProvider()) - container.add_provider(AuthProvider()) - container.add_provider(UseCaseProvider()) - container.add_provider(VectorServiceProvider()) - return container + return make_async_container( + DatabaseProvider(), + RepositoryProvider(), + ServiceProvider(), + UseCaseProvider() + )