This commit is contained in:
bokho 2025-12-24 03:14:37 +03:00
parent 493c385cb1
commit dfc188e179
17 changed files with 859 additions and 789 deletions

View File

@ -9,7 +9,7 @@ python-multipart==0.0.6
httpx==0.25.2 httpx==0.25.2
PyMuPDF==1.23.8 PyMuPDF==1.23.8
Pillow==10.2.0 Pillow==10.2.0
dishka==1.7.2 dishka==0.7.0
numpy==1.26.4 numpy==1.26.4
sentence-transformers==2.7.0 sentence-transformers==2.7.0
qdrant-client==1.9.0 qdrant-client==1.9.0

View File

@ -26,7 +26,7 @@ class CacheService:
"question": question, "question": question,
"answer": answer "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): async def invalidate_collection_cache(self, collection_id: UUID):
pattern = f"rag:answer:{collection_id}:*" pattern = f"rag:answer:{collection_id}:*"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,11 @@
""" """
API для RAG: индексация документов и ответы на вопросы API для RAG: индексация документов и ответы на вопросы
""" """
from __future__ import annotations from fastapi import APIRouter, status, Request
from typing import Annotated
from fastapi import APIRouter, status from dishka.integrations.fastapi import FromDishka, inject
from dishka.integrations.fastapi import FromDishka 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 ( from src.presentation.schemas.rag_schemas import (
QuestionRequest, QuestionRequest,
RAGAnswer, RAGAnswer,
@ -19,23 +20,29 @@ router = APIRouter(prefix="/rag", tags=["rag"])
@router.post("/index", response_model=IndexDocumentResponse, status_code=status.HTTP_200_OK) @router.post("/index", response_model=IndexDocumentResponse, status_code=status.HTTP_200_OK)
@inject
async def index_document( async def index_document(
body: IndexDocumentRequest, body: IndexDocumentRequest,
use_cases: RAGUseCases = FromDishka(), request: Request,
current_user: User = FromDishka(), 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) result = await use_cases.index_document(body.document_id)
return IndexDocumentResponse(**result) return IndexDocumentResponse(**result)
@router.post("/question", response_model=RAGAnswer, status_code=status.HTTP_200_OK) @router.post("/question", response_model=RAGAnswer, status_code=status.HTTP_200_OK)
@inject
async def ask_question( async def ask_question(
body: QuestionRequest, body: QuestionRequest,
use_cases: RAGUseCases = FromDishka(), request: Request,
current_user: User = FromDishka(), user_repo: Annotated[IUserRepository, FromDishka()],
use_cases: Annotated[RAGUseCases, FromDishka()],
): ):
"""Отвечает на вопрос, используя RAG в рамках беседы""" """Отвечает на вопрос, используя RAG в рамках беседы"""
current_user = await get_current_user(request, user_repo)
result = await use_cases.ask_question( result = await use_cases.ask_question(
conversation_id=body.conversation_id, conversation_id=body.conversation_id,
user_id=current_user.user_id, user_id=current_user.user_id,

View File

@ -1,13 +1,13 @@
""" """
API роутеры для работы с пользователями API роутеры для работы с пользователями
""" """
from __future__ import annotations
from uuid import UUID from uuid import UUID
from fastapi import APIRouter, status, Depends from fastapi import APIRouter, status, Depends, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from typing import List, Annotated from typing import List, Annotated
from dishka.integrations.fastapi import FromDishka, inject 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.presentation.schemas.user_schemas import UserCreate, UserUpdate, UserResponse
from src.application.use_cases.user_use_cases import UserUseCases from src.application.use_cases.user_use_cases import UserUseCases
from src.domain.entities.user import User from src.domain.entities.user import User
@ -19,8 +19,8 @@ router = APIRouter(prefix="/users", tags=["users"])
@inject @inject
async def create_user( async def create_user(
user_data: UserCreate, user_data: UserCreate,
use_cases: UserUseCases = FromDishka() use_cases: Annotated[UserUseCases, FromDishka()]
) -> UserResponse: ):
"""Создать пользователя""" """Создать пользователя"""
user = await use_cases.create_user( user = await use_cases.create_user(
telegram_id=user_data.telegram_id, telegram_id=user_data.telegram_id,
@ -32,9 +32,11 @@ async def create_user(
@router.get("/me", response_model=UserResponse) @router.get("/me", response_model=UserResponse)
@inject @inject
async def get_current_user_info( 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) return UserResponse.from_entity(current_user)
@ -42,7 +44,7 @@ async def get_current_user_info(
@inject @inject
async def get_user_by_telegram_id( async def get_user_by_telegram_id(
telegram_id: str, telegram_id: str,
use_cases: UserUseCases = FromDishka() use_cases: Annotated[UserUseCases, FromDishka()]
): ):
"""Получить пользователя по Telegram ID""" """Получить пользователя по Telegram ID"""
user = await use_cases.get_user_by_telegram_id(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 @inject
async def increment_questions( async def increment_questions(
telegram_id: str, telegram_id: str,
use_cases: UserUseCases = FromDishka() use_cases: Annotated[UserUseCases, FromDishka()]
): ):
"""Увеличить счетчик использованных вопросов""" """Увеличить счетчик использованных вопросов"""
user = await use_cases.increment_questions_used(telegram_id) 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) @router.post("/telegram/{telegram_id}/activate-premium", response_model=UserResponse)
@inject @inject
async def activate_premium( async def activate_premium(
use_cases: Annotated[UserUseCases, FromDishka()],
telegram_id: str, telegram_id: str,
days: int = 30, days: int = 30,
use_cases: UserUseCases = FromDishka()
): ):
"""Активировать premium статус""" """Активировать premium статус"""
user = await use_cases.activate_premium(telegram_id, days=days) user = await use_cases.activate_premium(telegram_id, days=days)
@ -79,7 +82,7 @@ async def activate_premium(
@inject @inject
async def get_user( async def get_user(
user_id: UUID, user_id: UUID,
use_cases: UserUseCases = FromDishka() use_cases: Annotated[UserUseCases, FromDishka()]
): ):
"""Получить пользователя по ID""" """Получить пользователя по ID"""
user = await use_cases.get_user(user_id) user = await use_cases.get_user(user_id)
@ -91,7 +94,7 @@ async def get_user(
async def update_user( async def update_user(
user_id: UUID, user_id: UUID,
user_data: UserUpdate, user_data: UserUpdate,
use_cases: UserUseCases = FromDishka() use_cases: Annotated[UserUseCases, FromDishka()]
): ):
"""Обновить пользователя""" """Обновить пользователя"""
user = await use_cases.update_user( user = await use_cases.update_user(
@ -106,7 +109,7 @@ async def update_user(
@inject @inject
async def delete_user( async def delete_user(
user_id: UUID, user_id: UUID,
use_cases: UserUseCases = FromDishka() use_cases: Annotated[UserUseCases, FromDishka()]
): ):
"""Удалить пользователя""" """Удалить пользователя"""
await use_cases.delete_user(user_id) await use_cases.delete_user(user_id)
@ -116,9 +119,9 @@ async def delete_user(
@router.get("", response_model=List[UserResponse]) @router.get("", response_model=List[UserResponse])
@inject @inject
async def list_users( async def list_users(
use_cases: Annotated[UserUseCases, FromDishka()],
skip: int = 0, skip: int = 0,
limit: int = 100, limit: int = 100
use_cases: UserUseCases = FromDishka()
): ):
"""Получить список пользователей""" """Получить список пользователей"""
users = await use_cases.list_users(skip=skip, limit=limit) users = await use_cases.list_users(skip=skip, limit=limit)

View File

@ -1,7 +1,6 @@
from __future__ import annotations
import sys import sys
import os import os
import asyncio
from pathlib import Path from pathlib import Path
backend_dir = Path(__file__).parent.parent.parent backend_dir = Path(__file__).parent.parent.parent
@ -24,15 +23,18 @@ from src.infrastructure.database.base import engine, Base
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
"""Управление жизненным циклом приложения""" """Управление жизненным циклом приложения"""
container = create_container()
setup_dishka(container, app)
try: try:
async with engine.begin() as conn: async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all) await conn.run_sync(Base.metadata.create_all)
except Exception as e: except Exception as e:
print(f"Примечание при создании таблиц: {e}") print(f"Примечание при создании таблиц: {e}")
yield 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() await engine.dispose()
@ -43,6 +45,11 @@ app = FastAPI(
lifespan=lifespan lifespan=lifespan
) )
# Настройка Dishka ДО добавления middleware
container = create_container()
setup_dishka(container, app)
app.state.container = container
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=settings.CORS_ORIGINS, allow_origins=settings.CORS_ORIGINS,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,18 +7,19 @@ from typing import Optional
class Settings(BaseSettings): class Settings(BaseSettings):
"""Настройки (загружаются из .env автоматически)"""
POSTGRES_HOST: str POSTGRES_HOST: str = "localhost"
POSTGRES_PORT: int POSTGRES_PORT: int = 5432
POSTGRES_USER: str POSTGRES_USER: str = "postgres"
POSTGRES_PASSWORD: str POSTGRES_PASSWORD: str = "postgres"
POSTGRES_DB: str POSTGRES_DB: str = "lawyer_ai"
QDRANT_HOST: str QDRANT_HOST: str = "localhost"
QDRANT_PORT: int QDRANT_PORT: int = 6333
REDIS_HOST: str REDIS_HOST: str = "localhost"
REDIS_PORT: int REDIS_PORT: int = 6379
TELEGRAM_BOT_TOKEN: Optional[str] = None TELEGRAM_BOT_TOKEN: Optional[str] = None
YANDEX_OCR_API_KEY: Optional[str] = None YANDEX_OCR_API_KEY: Optional[str] = None
@ -29,11 +30,12 @@ class Settings(BaseSettings):
APP_NAME: str = "ИИ-юрист" APP_NAME: str = "ИИ-юрист"
DEBUG: bool = False DEBUG: bool = False
SECRET_KEY: str SECRET_KEY: str = "your-secret-key-change-in-production"
CORS_ORIGINS: list[str] = ["*"] CORS_ORIGINS: list[str] = ["*"]
@property @property
def database_url(self) -> str: def database_url(self) -> str:
"""Вычисляемый URL подключения"""
return f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}" return f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
class Config: class Config:

View File

@ -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 fastapi import Request
from sqlalchemy.ext.asyncio import AsyncSession 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.database.base import AsyncSessionLocal
from src.infrastructure.repositories.postgresql.user_repository import PostgreSQLUserRepository 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): class DatabaseProvider(Provider):
@provide(scope=Scope.REQUEST) @provide(scope=Scope.REQUEST)
async def get_db(self) -> AsyncIterator[AsyncSession]: @asynccontextmanager
async def get_db(self) -> AsyncSession:
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
try: try:
yield session yield session
@ -94,8 +95,6 @@ class ServiceProvider(Provider):
def get_parser_service(self, ocr_service: YandexOCRService) -> DocumentParserService: def get_parser_service(self, ocr_service: YandexOCRService) -> DocumentParserService:
return DocumentParserService(ocr_service) return DocumentParserService(ocr_service)
class VectorServiceProvider(Provider):
@provide(scope=Scope.APP) @provide(scope=Scope.APP)
def get_qdrant_client(self) -> QdrantClient: def get_qdrant_client(self) -> QdrantClient:
return QdrantClient(host=settings.QDRANT_HOST, port=settings.QDRANT_PORT) return QdrantClient(host=settings.QDRANT_HOST, port=settings.QDRANT_PORT)
@ -133,12 +132,6 @@ class VectorServiceProvider(Provider):
splitter=text_splitter, 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): class UseCaseProvider(Provider):
@provide(scope=Scope.REQUEST) @provide(scope=Scope.REQUEST)
@ -196,12 +189,10 @@ class UseCaseProvider(Provider):
def create_container() -> Container: def create_container() -> Container:
container = Container() return make_async_container(
container.add_provider(DatabaseProvider()) DatabaseProvider(),
container.add_provider(RepositoryProvider()) RepositoryProvider(),
container.add_provider(ServiceProvider()) ServiceProvider(),
container.add_provider(AuthProvider()) UseCaseProvider()
container.add_provider(UseCaseProvider()) )
container.add_provider(VectorServiceProvider())
return container