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
PyMuPDF==1.23.8
Pillow==10.2.0
dishka==1.7.2
dishka==0.7.0
numpy==1.26.4
sentence-transformers==2.7.0
qdrant-client==1.9.0

View File

@ -26,7 +26,7 @@ class CacheService:
"question": question,
"answer": answer
}
await self.reids_client.set_json(key, value, ttl or self.default_ttl)
await self.redis_client.set_json(key, value, ttl or self.default_ttl)
async def invalidate_collection_cache(self, collection_id: UUID):
pattern = f"rag:answer:{collection_id}:*"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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