feat: add submitted collection, /similar and /submitted endpoints (Stage 4)
Made-with: Cursor
This commit is contained in:
@@ -24,14 +24,14 @@ async def verify_api_key(
|
||||
) -> bool:
|
||||
"""
|
||||
Проверяет API ключ из заголовка запроса.
|
||||
|
||||
|
||||
Args:
|
||||
api_key: Ключ из заголовка X-API-Key
|
||||
settings: Настройки приложения
|
||||
|
||||
|
||||
Returns:
|
||||
True если авторизация успешна
|
||||
|
||||
|
||||
Raises:
|
||||
HTTPException: Если ключ неверный или отсутствует
|
||||
"""
|
||||
@@ -47,7 +47,7 @@ async def verify_api_key(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="API ключ не настроен на сервере",
|
||||
)
|
||||
|
||||
|
||||
# Проверяем ключ
|
||||
if api_key is None:
|
||||
logger.warning("Запрос без API ключа")
|
||||
@@ -56,14 +56,14 @@ async def verify_api_key(
|
||||
detail="API ключ не предоставлен. Используйте заголовок X-API-Key",
|
||||
headers={"WWW-Authenticate": "ApiKey"},
|
||||
)
|
||||
|
||||
|
||||
if api_key != settings.api_key:
|
||||
logger.warning("Неверный API ключ")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Неверный API ключ",
|
||||
)
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -24,7 +24,12 @@ from app.schemas import (
|
||||
ScoreRequest,
|
||||
ScoreResponse,
|
||||
ScoringParamsResponse,
|
||||
SimilarPostItem,
|
||||
SimilarRequest,
|
||||
SimilarResponse,
|
||||
StatsResponse,
|
||||
SubmittedRequest,
|
||||
SubmittedResponse,
|
||||
UpdateScoringParamsRequest,
|
||||
VectorStoreStats,
|
||||
WarmupResponse,
|
||||
@@ -49,6 +54,7 @@ RAGServiceDep = Annotated[RAGService, Depends(get_service)]
|
||||
# Health Check
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.get(
|
||||
"/health",
|
||||
response_model=HealthResponse,
|
||||
@@ -58,7 +64,7 @@ RAGServiceDep = Annotated[RAGService, Depends(get_service)]
|
||||
async def health_check(service: RAGServiceDep, _auth: AuthDep) -> HealthResponse:
|
||||
"""
|
||||
Проверяет состояние сервиса.
|
||||
|
||||
|
||||
Returns:
|
||||
HealthResponse: Статус сервиса
|
||||
"""
|
||||
@@ -73,6 +79,7 @@ async def health_check(service: RAGServiceDep, _auth: AuthDep) -> HealthResponse
|
||||
# Scoring
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.post(
|
||||
"/score",
|
||||
response_model=ScoreResponse,
|
||||
@@ -86,55 +93,55 @@ async def health_check(service: RAGServiceDep, _auth: AuthDep) -> HealthResponse
|
||||
tags=["scoring"],
|
||||
)
|
||||
async def calculate_score(
|
||||
request: ScoreRequest,
|
||||
request: ScoreRequest,
|
||||
service: RAGServiceDep,
|
||||
_auth: AuthDep,
|
||||
) -> ScoreResponse:
|
||||
"""
|
||||
Рассчитывает скор для текста поста.
|
||||
|
||||
|
||||
Args:
|
||||
request: Запрос с текстом
|
||||
service: RAG сервис
|
||||
|
||||
|
||||
Returns:
|
||||
ScoreResponse: Результат скоринга
|
||||
|
||||
|
||||
Raises:
|
||||
HTTPException: При ошибке расчета
|
||||
"""
|
||||
try:
|
||||
result = await service.calculate_score(request.text)
|
||||
response_dict = result.to_dict()
|
||||
|
||||
|
||||
return ScoreResponse(
|
||||
rag_score=response_dict["rag_score"],
|
||||
rag_confidence=response_dict["rag_confidence"],
|
||||
rag_score_pos_only=response_dict["rag_score_pos_only"],
|
||||
meta=ScoreMetadata(**response_dict["meta"]),
|
||||
)
|
||||
|
||||
|
||||
except TextTooShortError as e:
|
||||
logger.warning(f"Текст слишком короткий: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={"detail": str(e), "error_type": "TextTooShortError"},
|
||||
)
|
||||
|
||||
|
||||
except InsufficientExamplesError as e:
|
||||
logger.warning(f"Недостаточно примеров: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={"detail": str(e), "error_type": "InsufficientExamplesError"},
|
||||
)
|
||||
|
||||
|
||||
except ModelNotLoadedError as e:
|
||||
logger.error(f"Модель не загружена: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail={"detail": str(e), "error_type": "ModelNotLoadedError"},
|
||||
)
|
||||
|
||||
|
||||
except ScoringError as e:
|
||||
logger.error(f"Ошибка скоринга: {e}")
|
||||
raise HTTPException(
|
||||
@@ -147,6 +154,7 @@ async def calculate_score(
|
||||
# Examples
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.post(
|
||||
"/examples/positive",
|
||||
response_model=ExampleResponse,
|
||||
@@ -159,27 +167,27 @@ async def calculate_score(
|
||||
tags=["examples"],
|
||||
)
|
||||
async def add_positive_example(
|
||||
request: ExampleRequest,
|
||||
request: ExampleRequest,
|
||||
service: RAGServiceDep,
|
||||
_auth: AuthDep,
|
||||
x_test_mode: str | None = Header(default=None, alias="X-Test-Mode"),
|
||||
) -> ExampleResponse:
|
||||
"""
|
||||
Добавляет текст как положительный пример (опубликованный пост).
|
||||
|
||||
|
||||
При наличии заголовка X-Test-Mode: true пример НЕ сохраняется (тестовый режим).
|
||||
|
||||
|
||||
Args:
|
||||
request: Запрос с текстом
|
||||
service: RAG сервис
|
||||
x_test_mode: Заголовок тестового режима
|
||||
|
||||
|
||||
Returns:
|
||||
ExampleResponse: Результат добавления
|
||||
"""
|
||||
# Тестовый режим — не сохраняем примеры
|
||||
is_test = x_test_mode and x_test_mode.lower() == "true"
|
||||
|
||||
|
||||
if is_test:
|
||||
logger.info("Тестовый режим: положительный пример НЕ сохранён")
|
||||
return ExampleResponse(
|
||||
@@ -188,22 +196,22 @@ async def add_positive_example(
|
||||
positive_count=service.vector_store.positive_count,
|
||||
negative_count=service.vector_store.negative_count,
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
added = await service.add_positive_example(request.text)
|
||||
|
||||
|
||||
if added:
|
||||
message = "Положительный пример добавлен"
|
||||
else:
|
||||
message = "Пример не добавлен (дубликат или слишком короткий текст)"
|
||||
|
||||
|
||||
return ExampleResponse(
|
||||
success=added,
|
||||
message=message,
|
||||
positive_count=service.vector_store.positive_count,
|
||||
negative_count=service.vector_store.negative_count,
|
||||
)
|
||||
|
||||
|
||||
except ModelNotLoadedError as e:
|
||||
logger.error(f"Модель не загружена: {e}")
|
||||
raise HTTPException(
|
||||
@@ -224,27 +232,27 @@ async def add_positive_example(
|
||||
tags=["examples"],
|
||||
)
|
||||
async def add_negative_example(
|
||||
request: ExampleRequest,
|
||||
request: ExampleRequest,
|
||||
service: RAGServiceDep,
|
||||
_auth: AuthDep,
|
||||
x_test_mode: str | None = Header(default=None, alias="X-Test-Mode"),
|
||||
) -> ExampleResponse:
|
||||
"""
|
||||
Добавляет текст как отрицательный пример (отклоненный пост).
|
||||
|
||||
|
||||
При наличии заголовка X-Test-Mode: true пример НЕ сохраняется (тестовый режим).
|
||||
|
||||
|
||||
Args:
|
||||
request: Запрос с текстом
|
||||
service: RAG сервис
|
||||
x_test_mode: Заголовок тестового режима
|
||||
|
||||
|
||||
Returns:
|
||||
ExampleResponse: Результат добавления
|
||||
"""
|
||||
# Тестовый режим — не сохраняем примеры
|
||||
is_test = x_test_mode and x_test_mode.lower() == "true"
|
||||
|
||||
|
||||
if is_test:
|
||||
logger.info("Тестовый режим: отрицательный пример НЕ сохранён")
|
||||
return ExampleResponse(
|
||||
@@ -253,22 +261,128 @@ async def add_negative_example(
|
||||
positive_count=service.vector_store.positive_count,
|
||||
negative_count=service.vector_store.negative_count,
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
added = await service.add_negative_example(request.text)
|
||||
|
||||
|
||||
if added:
|
||||
message = "Отрицательный пример добавлен"
|
||||
else:
|
||||
message = "Пример не добавлен (дубликат или слишком короткий текст)"
|
||||
|
||||
|
||||
return ExampleResponse(
|
||||
success=added,
|
||||
message=message,
|
||||
positive_count=service.vector_store.positive_count,
|
||||
negative_count=service.vector_store.negative_count,
|
||||
)
|
||||
|
||||
|
||||
except ModelNotLoadedError as e:
|
||||
logger.error(f"Модель не загружена: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail={"detail": str(e), "error_type": "ModelNotLoadedError"},
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Similar & Submitted
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.post(
|
||||
"/similar",
|
||||
response_model=SimilarResponse,
|
||||
responses={
|
||||
400: {"model": ErrorResponse, "description": "Ошибка в запросе"},
|
||||
401: {"model": ErrorResponse, "description": "Не авторизован"},
|
||||
403: {"model": ErrorResponse, "description": "Доступ запрещён"},
|
||||
503: {"model": ErrorResponse, "description": "Сервис недоступен"},
|
||||
},
|
||||
summary="Поиск похожих постов",
|
||||
tags=["similar"],
|
||||
)
|
||||
async def find_similar_posts(
|
||||
request: SimilarRequest,
|
||||
service: RAGServiceDep,
|
||||
_auth: AuthDep,
|
||||
) -> SimilarResponse:
|
||||
"""
|
||||
Ищет похожие submitted-посты за последние N часов.
|
||||
|
||||
Args:
|
||||
request: Запрос с текстом, threshold и hours
|
||||
service: RAG сервис
|
||||
|
||||
Returns:
|
||||
SimilarResponse: Список похожих постов
|
||||
"""
|
||||
try:
|
||||
similar = await service.find_similar_posts(
|
||||
text=request.text,
|
||||
threshold=request.threshold,
|
||||
hours=request.hours,
|
||||
)
|
||||
return SimilarResponse(
|
||||
similar_count=len(similar),
|
||||
similar_posts=[SimilarPostItem(**item) for item in similar],
|
||||
)
|
||||
except TextTooShortError as e:
|
||||
logger.warning(f"Текст слишком короткий: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={"detail": str(e), "error_type": "TextTooShortError"},
|
||||
)
|
||||
except ModelNotLoadedError as e:
|
||||
logger.error(f"Модель не загружена: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail={"detail": str(e), "error_type": "ModelNotLoadedError"},
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/submitted",
|
||||
response_model=SubmittedResponse,
|
||||
responses={
|
||||
400: {"model": ErrorResponse, "description": "Ошибка в запросе"},
|
||||
401: {"model": ErrorResponse, "description": "Не авторизован"},
|
||||
403: {"model": ErrorResponse, "description": "Доступ запрещён"},
|
||||
503: {"model": ErrorResponse, "description": "Сервис недоступен"},
|
||||
},
|
||||
summary="Добавить submitted-пост",
|
||||
tags=["submitted"],
|
||||
)
|
||||
async def add_submitted_post(
|
||||
request: SubmittedRequest,
|
||||
service: RAGServiceDep,
|
||||
_auth: AuthDep,
|
||||
) -> SubmittedResponse:
|
||||
"""
|
||||
Добавляет submitted-пост в коллекцию для индексации ботом.
|
||||
|
||||
Args:
|
||||
request: Запрос с текстом, post_id и rag_score
|
||||
service: RAG сервис
|
||||
|
||||
Returns:
|
||||
SubmittedResponse: Результат добавления
|
||||
"""
|
||||
try:
|
||||
added = await service.add_submitted_post(
|
||||
text=request.text,
|
||||
post_id=request.post_id,
|
||||
rag_score=request.rag_score,
|
||||
)
|
||||
if added:
|
||||
message = "Submitted-пост добавлен"
|
||||
else:
|
||||
message = "Пост не добавлен (дубликат или слишком короткий текст)"
|
||||
return SubmittedResponse(
|
||||
success=added,
|
||||
message=message,
|
||||
submitted_count=service.vector_store.submitted_count,
|
||||
)
|
||||
except ModelNotLoadedError as e:
|
||||
logger.error(f"Модель не загружена: {e}")
|
||||
raise HTTPException(
|
||||
@@ -281,6 +395,7 @@ async def add_negative_example(
|
||||
# Stats & Warmup
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.get(
|
||||
"/stats",
|
||||
response_model=StatsResponse,
|
||||
@@ -294,15 +409,15 @@ async def add_negative_example(
|
||||
async def get_stats(service: RAGServiceDep, _auth: AuthDep) -> StatsResponse:
|
||||
"""
|
||||
Возвращает статистику сервиса.
|
||||
|
||||
|
||||
Args:
|
||||
service: RAG сервис
|
||||
|
||||
|
||||
Returns:
|
||||
StatsResponse: Статистика
|
||||
"""
|
||||
stats = service.get_stats()
|
||||
|
||||
|
||||
return StatsResponse(
|
||||
model_name=stats["model_name"],
|
||||
model_loaded=stats["model_loaded"],
|
||||
@@ -325,15 +440,15 @@ async def get_stats(service: RAGServiceDep, _auth: AuthDep) -> StatsResponse:
|
||||
async def warmup(service: RAGServiceDep, _auth: AuthDep) -> WarmupResponse:
|
||||
"""
|
||||
Прогревает модель (загружает если не загружена).
|
||||
|
||||
|
||||
Args:
|
||||
service: RAG сервис
|
||||
|
||||
|
||||
Returns:
|
||||
WarmupResponse: Результат прогрева
|
||||
"""
|
||||
success = await service.warmup()
|
||||
|
||||
|
||||
if success:
|
||||
message = "Модель успешно загружена"
|
||||
else:
|
||||
@@ -342,7 +457,7 @@ async def warmup(service: RAGServiceDep, _auth: AuthDep) -> WarmupResponse:
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail={"detail": message, "error_type": "ModelNotLoadedError"},
|
||||
)
|
||||
|
||||
|
||||
return WarmupResponse(
|
||||
success=success,
|
||||
model_loaded=service.is_model_loaded,
|
||||
@@ -354,6 +469,7 @@ async def warmup(service: RAGServiceDep, _auth: AuthDep) -> WarmupResponse:
|
||||
# Scoring Parameters
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.get(
|
||||
"/scoring/params",
|
||||
response_model=ScoringParamsResponse,
|
||||
@@ -370,10 +486,10 @@ async def get_scoring_params(
|
||||
) -> ScoringParamsResponse:
|
||||
"""
|
||||
Возвращает текущие параметры формулы расчета score.
|
||||
|
||||
|
||||
Args:
|
||||
service: RAG сервис
|
||||
|
||||
|
||||
Returns:
|
||||
ScoringParamsResponse: Текущие параметры формулы
|
||||
"""
|
||||
@@ -399,17 +515,17 @@ async def update_scoring_params(
|
||||
) -> ScoringParamsResponse:
|
||||
"""
|
||||
Обновляет параметры формулы расчета score.
|
||||
|
||||
|
||||
Можно обновить один или несколько параметров одновременно.
|
||||
Параметры, которые не указаны, остаются без изменений.
|
||||
|
||||
|
||||
Args:
|
||||
request: Запрос с новыми параметрами
|
||||
service: RAG сервис
|
||||
|
||||
|
||||
Returns:
|
||||
ScoringParamsResponse: Обновленные параметры формулы
|
||||
|
||||
|
||||
Raises:
|
||||
HTTPException: При невалидных значениях параметров
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user