From 145dc62e0b6dc94e2c4089c17f70bad0531512b1 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Wed, 17 Jun 2026 21:23:13 +0300 Subject: [PATCH] fix(admin-scrape): ge=1 on count param + enforce LLM finish_reason (#review-followup) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - admin_scrape.py /failures limit: ge=0 → ge=1 (returning 0 rows is nonsensical for a list endpoint) - orchestrator.py: check result.finish_reason after ok=True; 'length'/'content_filter' and any other non-stop reason triggers deterministic fallback with reason=finish_reason: (LLMResult docstring mandated this but no consumer enforced it) --- backend/app/api/v1/admin_scrape.py | 6 ++---- backend/app/services/chat/orchestrator.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/backend/app/api/v1/admin_scrape.py b/backend/app/api/v1/admin_scrape.py index c8a1293e..75677466 100644 --- a/backend/app/api/v1/admin_scrape.py +++ b/backend/app/api/v1/admin_scrape.py @@ -268,7 +268,7 @@ def revoke_task( def list_failures( db: Annotated[Session, Depends(get_db)], run_id: int | None = None, - limit: int = Query(default=50, ge=0), + limit: int = Query(default=50, ge=1), ) -> list[dict[str, Any]]: """Per-request failure log for manual browser verification.""" where = "" @@ -1284,9 +1284,7 @@ _FRESHNESS_SOURCES: list[FreshnessSource] = [ ] -def _classify_freshness( - age_days: float | None, fresh_days: float, stale_days: float -) -> str: +def _classify_freshness(age_days: float | None, fresh_days: float, stale_days: float) -> str: """fresh / stale / critical / unknown по возрасту последнего успешного прогона.""" if age_days is None: return "unknown" diff --git a/backend/app/services/chat/orchestrator.py b/backend/app/services/chat/orchestrator.py index f1f5b768..147c6418 100644 --- a/backend/app/services/chat/orchestrator.py +++ b/backend/app/services/chat/orchestrator.py @@ -165,6 +165,18 @@ def orchestrate_chat( continue # Нет tool_calls → финальная проза. + # Проверяем finish_reason: 'length' / 'content_filter' означают обрезанный + # или отфильтрованный ответ — неполная проза клиенту не отдаётся (LLMResult + # docstring §64: «консьюмер ОБЯЗАН проверять даже при ok=True»). + if result.finish_reason not in (None, "stop", "end_turn", "tool_calls"): + logger.warning( + "chat: non-stop finish_reason=%r for cad=%s run=%s → deterministic fallback", + result.finish_reason, + cad_num, + run_id, + ) + return _deterministic(report_dict, message, f"finish_reason:{result.finish_reason}") + answer = (result.content or "").strip() if not answer: # Модель вернула пустоту — пустой ответ клиенту не отдаём, деградируем.