fix: 429 rate limit виден в UI, добавлена кнопка Стоп

- RetryOnRateLimitMiddleware: при 429 шлёт события rate_limit_wait/retry
  в глобальный UI-канал (set_ui_event_queue) — без него не меняет поведение
- ui.py: рендерит  rate_limit_wait и 🔄 rate_limit_retry в лог событий
- ui.py: показывает «Агент работает... Nс» если нет событий >15с
- ui.py: кнопка «Стоп» прерывает ожидание в чате и pipeline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 16:59:10 +03:00
parent ee2c8de372
commit cba8940005
2 changed files with 117 additions and 11 deletions
+38 -3
View File
@@ -2,15 +2,34 @@
from __future__ import annotations
import asyncio
import queue
import time
from typing import Any
from langchain.agents.middleware import AgentMiddleware, AgentState
from langchain_core.messages import ToolMessage
_PAUSE = 30 # секунд ожидания при 429
_TRIES = 5 # максимум попыток
# Глобальный канал событий для UI (устанавливается из ui.py перед запуском агента).
# Если None — события просто не отправляются (CLI-режим).
_ui_event_queue: queue.Queue | None = None
def set_ui_event_queue(q: queue.Queue | None) -> None:
"""Вызывается из ui.py чтобы подключить очередь событий."""
global _ui_event_queue
_ui_event_queue = q
def _emit(event: dict) -> None:
if _ui_event_queue is not None:
try:
_ui_event_queue.put_nowait(event)
except Exception:
pass
def _is_429(exc: Exception) -> bool:
msg = str(exc)
@@ -18,7 +37,9 @@ def _is_429(exc: Exception) -> bool:
class RetryOnRateLimitMiddleware(AgentMiddleware[AgentState[Any], Any]):
"""Перехватывает 429 от любого инструмента и повторяет с паузой."""
"""Перехватывает 429 от любого инструмента и повторяет с паузой.
Отправляет события rate_limit_wait / rate_limit_retry в UI-очередь.
"""
def wrap_tool_call(self, request, handler):
for attempt in range(1, _TRIES + 1):
@@ -28,8 +49,12 @@ class RetryOnRateLimitMiddleware(AgentMiddleware[AgentState[Any], Any]):
if _is_429(e) and attempt < _TRIES:
name = request.tool_call.get("name", "")
print(f"[retry-mw] {name} → 429, жду {_PAUSE}с (попытка {attempt}/{_TRIES})...")
import time
_emit({"t": "rate_limit_wait", "name": name,
"pause": _PAUSE, "attempt": attempt, "max": _TRIES,
"ts": _now()})
time.sleep(_PAUSE)
_emit({"t": "rate_limit_retry", "name": name,
"attempt": attempt + 1, "ts": _now()})
else:
raise
@@ -41,6 +66,16 @@ class RetryOnRateLimitMiddleware(AgentMiddleware[AgentState[Any], Any]):
if _is_429(e) and attempt < _TRIES:
name = request.tool_call.get("name", "")
print(f"[retry-mw] {name} → 429, жду {_PAUSE}с (попытка {attempt}/{_TRIES})...")
_emit({"t": "rate_limit_wait", "name": name,
"pause": _PAUSE, "attempt": attempt, "max": _TRIES,
"ts": _now()})
await asyncio.sleep(_PAUSE)
_emit({"t": "rate_limit_retry", "name": name,
"attempt": attempt + 1, "ts": _now()})
else:
raise
def _now() -> str:
from datetime import datetime
return datetime.now().strftime("%H:%M:%S")