andrewbokh #6

Merged
Arxip222 merged 4 commits from andrewbokh into main 2025-12-24 10:36:03 +03:00
17 changed files with 859 additions and 789 deletions
Showing only changes of commit dfc188e179 - Show all commits

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

@ -2,12 +2,12 @@
Админ-панель - упрощенная версия через API эндпоинты Админ-панель - упрощенная версия через API эндпоинты
В будущем можно интегрировать полноценную админ-панель В будущем можно интегрировать полноценную админ-панель
""" """
from __future__ import annotations from fastapi import APIRouter, HTTPException, Request
from typing import List, Annotated
from fastapi import APIRouter, HTTPException
from typing import List
from uuid import UUID from uuid import UUID
from dishka.integrations.fastapi import FromDishka 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.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
@ -21,13 +21,16 @@ router = APIRouter(prefix="/admin", tags=["admin"])
@router.get("/users", response_model=List[UserResponse]) @router.get("/users", response_model=List[UserResponse])
@inject
async def admin_list_users( async def admin_list_users(
request: Request,
user_repo: Annotated[IUserRepository, FromDishka()],
use_cases: Annotated[UserUseCases, FromDishka()],
skip: int = 0, skip: int = 0,
limit: int = 100, limit: int = 100
current_user: User = FromDishka(),
use_cases: UserUseCases = FromDishka()
): ):
"""Получить список всех пользователей (только для админов)""" """Получить список всех пользователей (только для админов)"""
current_user = await get_current_user(request, user_repo)
if not current_user.is_admin(): if not current_user.is_admin():
raise HTTPException(status_code=403, detail="Требуются права администратора") raise HTTPException(status_code=403, detail="Требуются права администратора")
users = await use_cases.list_users(skip=skip, limit=limit) users = await use_cases.list_users(skip=skip, limit=limit)
@ -35,13 +38,16 @@ async def admin_list_users(
@router.get("/collections", response_model=List[CollectionResponse]) @router.get("/collections", response_model=List[CollectionResponse])
@inject
async def admin_list_collections( async def admin_list_collections(
request: Request,
user_repo: Annotated[IUserRepository, FromDishka()],
use_cases: Annotated[CollectionUseCases, FromDishka()],
skip: int = 0, skip: int = 0,
limit: int = 100, limit: int = 100
current_user: User = FromDishka(),
use_cases: CollectionUseCases = FromDishka()
): ):
"""Получить список всех коллекций (только для админов)""" """Получить список всех коллекций (только для админов)"""
current_user = await get_current_user(request, user_repo)
from src.infrastructure.database.base import AsyncSessionLocal from src.infrastructure.database.base import AsyncSessionLocal
from src.infrastructure.repositories.postgresql.collection_repository import PostgreSQLCollectionRepository from src.infrastructure.repositories.postgresql.collection_repository import PostgreSQLCollectionRepository
from sqlalchemy import select from sqlalchemy import select

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 from fastapi import APIRouter, status, Depends, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from typing import List from typing import List, Annotated
from dishka.integrations.fastapi import FromDishka 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 ( from src.presentation.schemas.collection_schemas import (
CollectionCreate, CollectionCreate,
CollectionUpdate, CollectionUpdate,
@ -22,12 +22,15 @@ 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)
@inject
async def create_collection( async def create_collection(
collection_data: CollectionCreate, collection_data: CollectionCreate,
current_user: User = FromDishka(), request: Request,
use_cases: CollectionUseCases = FromDishka() 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( collection = await use_cases.create_collection(
name=collection_data.name, name=collection_data.name,
owner_id=current_user.user_id, owner_id=current_user.user_id,
@ -38,9 +41,10 @@ async def create_collection(
@router.get("/{collection_id}", response_model=CollectionResponse) @router.get("/{collection_id}", response_model=CollectionResponse)
@inject
async def get_collection( async def get_collection(
collection_id: UUID, collection_id: UUID,
use_cases: CollectionUseCases = FromDishka() use_cases: Annotated[CollectionUseCases, FromDishka()]
): ):
"""Получить коллекцию по ID""" """Получить коллекцию по ID"""
collection = await use_cases.get_collection(collection_id) collection = await use_cases.get_collection(collection_id)
@ -48,13 +52,16 @@ async def get_collection(
@router.put("/{collection_id}", response_model=CollectionResponse) @router.put("/{collection_id}", response_model=CollectionResponse)
@inject
async def update_collection( async def update_collection(
collection_id: UUID, collection_id: UUID,
collection_data: CollectionUpdate, collection_data: CollectionUpdate,
current_user: User = FromDishka(), request: Request,
use_cases: CollectionUseCases = FromDishka() 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 = await use_cases.update_collection(
collection_id=collection_id, collection_id=collection_id,
user_id=current_user.user_id, user_id=current_user.user_id,
@ -66,24 +73,30 @@ async def update_collection(
@router.delete("/{collection_id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/{collection_id}", status_code=status.HTTP_204_NO_CONTENT)
@inject
async def delete_collection( async def delete_collection(
collection_id: UUID, collection_id: UUID,
current_user: User = FromDishka(), request: Request,
use_cases: CollectionUseCases = FromDishka() 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) await use_cases.delete_collection(collection_id, current_user.user_id)
return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None) return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None)
@router.get("", response_model=List[CollectionResponse]) @router.get("", response_model=List[CollectionResponse])
@inject
async def list_collections( async def list_collections(
request: Request,
user_repo: Annotated[IUserRepository, FromDishka()],
use_cases: Annotated[CollectionUseCases, FromDishka()],
skip: int = 0, skip: int = 0,
limit: int = 100, limit: int = 100
current_user: User = FromDishka(),
use_cases: CollectionUseCases = FromDishka()
): ):
"""Получить список коллекций, доступных пользователю""" """Получить список коллекций, доступных пользователю"""
current_user = await get_current_user(request, user_repo)
collections = await use_cases.list_user_collections( collections = await use_cases.list_user_collections(
user_id=current_user.user_id, user_id=current_user.user_id,
skip=skip, skip=skip,
@ -93,13 +106,16 @@ async def list_collections(
@router.post("/{collection_id}/access", response_model=CollectionAccessResponse, status_code=status.HTTP_201_CREATED) @router.post("/{collection_id}/access", response_model=CollectionAccessResponse, status_code=status.HTTP_201_CREATED)
@inject
async def grant_access( async def grant_access(
collection_id: UUID, collection_id: UUID,
access_data: CollectionAccessGrant, access_data: CollectionAccessGrant,
current_user: User = FromDishka(), request: Request,
use_cases: CollectionUseCases = FromDishka() 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( access = await use_cases.grant_access(
collection_id=collection_id, collection_id=collection_id,
user_id=access_data.user_id, user_id=access_data.user_id,
@ -109,13 +125,16 @@ async def grant_access(
@router.delete("/{collection_id}/access/{user_id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/{collection_id}/access/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
@inject
async def revoke_access( async def revoke_access(
collection_id: UUID, collection_id: UUID,
user_id: UUID, user_id: UUID,
current_user: User = FromDishka(), request: Request,
use_cases: CollectionUseCases = FromDishka() 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) await use_cases.revoke_access(collection_id, user_id, current_user.user_id)
return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None) return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None)

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 from fastapi import APIRouter, status, Depends, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from typing import List from typing import List, Annotated
from dishka.integrations.fastapi import FromDishka 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 ( from src.presentation.schemas.conversation_schemas import (
ConversationCreate, ConversationCreate,
ConversationResponse ConversationResponse
@ -19,12 +19,15 @@ 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)
@inject
async def create_conversation( async def create_conversation(
conversation_data: ConversationCreate, conversation_data: ConversationCreate,
current_user: User = FromDishka(), request: Request,
use_cases: ConversationUseCases = FromDishka() 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( conversation = await use_cases.create_conversation(
user_id=current_user.user_id, user_id=current_user.user_id,
collection_id=conversation_data.collection_id collection_id=conversation_data.collection_id
@ -33,35 +36,44 @@ async def create_conversation(
@router.get("/{conversation_id}", response_model=ConversationResponse) @router.get("/{conversation_id}", response_model=ConversationResponse)
@inject
async def get_conversation( async def get_conversation(
conversation_id: UUID, conversation_id: UUID,
current_user: User = FromDishka(), request: Request,
use_cases: ConversationUseCases = FromDishka() user_repo: Annotated[IUserRepository, FromDishka()],
use_cases: Annotated[ConversationUseCases, FromDishka()]
): ):
"""Получить беседу по ID""" """Получить беседу по ID"""
current_user = await get_current_user(request, user_repo)
conversation = await use_cases.get_conversation(conversation_id, current_user.user_id) conversation = await use_cases.get_conversation(conversation_id, current_user.user_id)
return ConversationResponse.from_entity(conversation) return ConversationResponse.from_entity(conversation)
@router.delete("/{conversation_id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/{conversation_id}", status_code=status.HTTP_204_NO_CONTENT)
@inject
async def delete_conversation( async def delete_conversation(
conversation_id: UUID, conversation_id: UUID,
current_user: User = FromDishka(), request: Request,
use_cases: ConversationUseCases = FromDishka() 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) await use_cases.delete_conversation(conversation_id, current_user.user_id)
return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None) return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None)
@router.get("", response_model=List[ConversationResponse]) @router.get("", response_model=List[ConversationResponse])
@inject
async def list_conversations( async def list_conversations(
request: Request,
user_repo: Annotated[IUserRepository, FromDishka()],
use_cases: Annotated[ConversationUseCases, FromDishka()],
skip: int = 0, skip: int = 0,
limit: int = 100, limit: int = 100
current_user: User = FromDishka(),
use_cases: ConversationUseCases = FromDishka()
): ):
"""Получить список бесед пользователя""" """Получить список бесед пользователя"""
current_user = await get_current_user(request, user_repo)
conversations = await use_cases.list_user_conversations( conversations = await use_cases.list_user_conversations(
user_id=current_user.user_id, user_id=current_user.user_id,
skip=skip, skip=skip,

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, UploadFile, File from fastapi import APIRouter, status, UploadFile, File, Depends, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from typing import List from typing import List, Annotated
from dishka.integrations.fastapi import FromDishka 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 ( from src.presentation.schemas.document_schemas import (
DocumentCreate, DocumentCreate,
DocumentUpdate, DocumentUpdate,
@ -20,12 +20,15 @@ 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)
@inject
async def create_document( async def create_document(
document_data: DocumentCreate, document_data: DocumentCreate,
current_user: User = FromDishka(), request: Request,
use_cases: DocumentUseCases = FromDishka() 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( document = await use_cases.create_document(
collection_id=document_data.collection_id, collection_id=document_data.collection_id,
title=document_data.title, title=document_data.title,
@ -36,13 +39,16 @@ async def create_document(
@router.post("/upload", response_model=DocumentResponse, status_code=status.HTTP_201_CREATED) @router.post("/upload", response_model=DocumentResponse, status_code=status.HTTP_201_CREATED)
@inject
async def upload_document( async def upload_document(
collection_id: UUID, collection_id: UUID,
file: UploadFile = File(...), request: Request,
current_user: User = FromDishka(), user_repo: Annotated[IUserRepository, FromDishka()],
use_cases: DocumentUseCases = FromDishka() use_cases: Annotated[DocumentUseCases, FromDishka()],
file: UploadFile = File(...)
): ):
"""Загрузить и распарсить PDF документ или изображение""" """Загрузить и распарсить PDF документ или изображение"""
current_user = await get_current_user(request, user_repo)
if not file.filename: if not file.filename:
raise JSONResponse( raise JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
@ -68,9 +74,10 @@ async def upload_document(
@router.get("/{document_id}", response_model=DocumentResponse) @router.get("/{document_id}", response_model=DocumentResponse)
@inject
async def get_document( async def get_document(
document_id: UUID, document_id: UUID,
use_cases: DocumentUseCases = FromDishka() use_cases: Annotated[DocumentUseCases, FromDishka()]
): ):
"""Получить документ по ID""" """Получить документ по ID"""
document = await use_cases.get_document(document_id) document = await use_cases.get_document(document_id)
@ -78,13 +85,16 @@ async def get_document(
@router.put("/{document_id}", response_model=DocumentResponse) @router.put("/{document_id}", response_model=DocumentResponse)
@inject
async def update_document( async def update_document(
document_id: UUID, document_id: UUID,
document_data: DocumentUpdate, document_data: DocumentUpdate,
current_user: User = FromDishka(), request: Request,
use_cases: DocumentUseCases = FromDishka() 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 = await use_cases.update_document(
document_id=document_id, document_id=document_id,
user_id=current_user.user_id, user_id=current_user.user_id,
@ -96,22 +106,26 @@ async def update_document(
@router.delete("/{document_id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/{document_id}", status_code=status.HTTP_204_NO_CONTENT)
@inject
async def delete_document( async def delete_document(
document_id: UUID, document_id: UUID,
current_user: User = FromDishka(), request: Request,
use_cases: DocumentUseCases = FromDishka() 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) await use_cases.delete_document(document_id, current_user.user_id)
return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None) return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None)
@router.get("/collection/{collection_id}", response_model=List[DocumentResponse]) @router.get("/collection/{collection_id}", response_model=List[DocumentResponse])
@inject
async def list_collection_documents( async def list_collection_documents(
collection_id: UUID, collection_id: UUID,
use_cases: Annotated[DocumentUseCases, FromDishka()],
skip: int = 0, skip: int = 0,
limit: int = 100, limit: int = 100
use_cases: DocumentUseCases = FromDishka()
): ):
"""Получить документы коллекции""" """Получить документы коллекции"""
documents = await use_cases.list_collection_documents( documents = await use_cases.list_collection_documents(

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 from fastapi import APIRouter, status, Depends, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from typing import List from typing import List, Annotated
from dishka.integrations.fastapi import FromDishka 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 ( from src.presentation.schemas.message_schemas import (
MessageCreate, MessageCreate,
MessageUpdate, MessageUpdate,
@ -20,12 +20,15 @@ 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)
@inject
async def create_message( async def create_message(
message_data: MessageCreate, message_data: MessageCreate,
current_user: User = FromDishka(), request: Request,
use_cases: MessageUseCases = FromDishka() 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( message = await use_cases.create_message(
conversation_id=message_data.conversation_id, conversation_id=message_data.conversation_id,
content=message_data.content, content=message_data.content,
@ -37,9 +40,10 @@ async def create_message(
@router.get("/{message_id}", response_model=MessageResponse) @router.get("/{message_id}", response_model=MessageResponse)
@inject
async def get_message( async def get_message(
message_id: UUID, message_id: UUID,
use_cases: MessageUseCases = FromDishka() use_cases: Annotated[MessageUseCases, FromDishka()]
): ):
"""Получить сообщение по ID""" """Получить сообщение по ID"""
message = await use_cases.get_message(message_id) message = await use_cases.get_message(message_id)
@ -47,10 +51,11 @@ async def get_message(
@router.put("/{message_id}", response_model=MessageResponse) @router.put("/{message_id}", response_model=MessageResponse)
@inject
async def update_message( async def update_message(
message_id: UUID, message_id: UUID,
message_data: MessageUpdate, message_data: MessageUpdate,
use_cases: MessageUseCases = FromDishka() use_cases: Annotated[MessageUseCases, FromDishka()]
): ):
"""Обновить сообщение""" """Обновить сообщение"""
message = await use_cases.update_message( message = await use_cases.update_message(
@ -62,9 +67,10 @@ async def update_message(
@router.delete("/{message_id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/{message_id}", status_code=status.HTTP_204_NO_CONTENT)
@inject
async def delete_message( async def delete_message(
message_id: UUID, message_id: UUID,
use_cases: MessageUseCases = FromDishka() use_cases: Annotated[MessageUseCases, FromDishka()]
): ):
"""Удалить сообщение""" """Удалить сообщение"""
await use_cases.delete_message(message_id) await use_cases.delete_message(message_id)
@ -72,14 +78,17 @@ async def delete_message(
@router.get("/conversation/{conversation_id}", response_model=List[MessageResponse]) @router.get("/conversation/{conversation_id}", response_model=List[MessageResponse])
@inject
async def list_conversation_messages( async def list_conversation_messages(
conversation_id: UUID, conversation_id: UUID,
request: Request,
user_repo: Annotated[IUserRepository, FromDishka()],
use_cases: Annotated[MessageUseCases, FromDishka()],
skip: int = 0, skip: int = 0,
limit: int = 100, limit: int = 100
current_user: User = FromDishka(),
use_cases: MessageUseCases = FromDishka()
): ):
"""Получить сообщения беседы""" """Получить сообщения беседы"""
current_user = await get_current_user(request, user_repo)
messages = await use_cases.list_conversation_messages( messages = await use_cases.list_conversation_messages(
conversation_id=conversation_id, conversation_id=conversation_id,
user_id=current_user.user_id, user_id=current_user.user_id,

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

@ -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