SilverElixir commited on
Commit
c019423
·
1 Parent(s): c0d4d78

Throttle streaming catch-up edits and pace chunk sends

Browse files
lumen_rich.py CHANGED
@@ -105,6 +105,9 @@ async def _send_text(message: Message, text: str, parse_html: bool = True, **kwa
105
  chunks = _split_text_chunks(text, bot.TG_MAX_LEN)
106
  for i, chunk in enumerate(chunks):
107
  chunk_kwargs = kwargs if i == len(chunks) - 1 else {k: v for k, v in kwargs.items() if k != "reply_markup"}
 
 
 
108
  if parse_html:
109
  # Сначала рич (таблицы/заголовки/математика), при любом неуспехе —
110
  # обычный HTML-путь ниже. Reply threading — только у первого чанка.
 
105
  chunks = _split_text_chunks(text, bot.TG_MAX_LEN)
106
  for i, chunk in enumerate(chunks):
107
  chunk_kwargs = kwargs if i == len(chunks) - 1 else {k: v for k, v in kwargs.items() if k != "reply_markup"}
108
+ if i > 0:
109
+ # Пауза между кусками — против флуд-контроля за серию подряд.
110
+ await bot._typing_sleep(bot.STREAM_TYPING_TICK_SEC)
111
  if parse_html:
112
  # Сначала рич (таблицы/заголовки/математика), при любом неуспехе —
113
  # обычный HTML-путь ниже. Reply threading — только у первого чанка.
lumen_streaming.py CHANGED
@@ -366,9 +366,13 @@ async def _run_streaming_reply(
366
  for step_len in _typing_catchup_steps(remaining_len, typing_speed, bot.STREAM_TYPING_TICK_SEC, bot.STREAM_TYPING_MAX_CATCHUP_TICKS):
367
  await bot._typing_sleep(bot.STREAM_TYPING_TICK_SEC)
368
  current_chunk_text = target_full[:already_shown_len + step_len]
369
- if current_chunk_text != last_edited_plain:
 
 
 
370
  await bot._tg_call(sent_messages[-1].edit_text, current_chunk_text, parse_mode=None, call_timeout=15.0)
371
  last_edited_plain = current_chunk_text
 
372
 
373
  # Финал — с полной HTML-конвертацией (во время стрима голый текст: частичный markdown дал бы несбалансированные теги).
374
  final_text = final_chunks[-1]
 
366
  for step_len in _typing_catchup_steps(remaining_len, typing_speed, bot.STREAM_TYPING_TICK_SEC, bot.STREAM_TYPING_MAX_CATCHUP_TICKS):
367
  await bot._typing_sleep(bot.STREAM_TYPING_TICK_SEC)
368
  current_chunk_text = target_full[:already_shown_len + step_len]
369
+ # Довывод правится тем же троттлингом, что основной цикл: тик
370
+ # короче интервала правок, иначе серия быстро ловти 429.
371
+ now = time.monotonic()
372
+ if current_chunk_text != last_edited_plain and now - last_edit_ts >= bot.STREAM_EDIT_MIN_INTERVAL_SEC:
373
  await bot._tg_call(sent_messages[-1].edit_text, current_chunk_text, parse_mode=None, call_timeout=15.0)
374
  last_edited_plain = current_chunk_text
375
+ last_edit_ts = now
376
 
377
  # Финал — с полной HTML-конвертацией (во время стрима голый текст: частичный markdown дал бы несбалансированные теги).
378
  final_text = final_chunks[-1]
tests/test_bot_streaming.py CHANGED
@@ -849,6 +849,77 @@ def test_run_streaming_reply_catchup_never_exceeds_max_ticks_even_for_long_slow_
849
  lumen_typing_pace._speed_ema.pop(pace_key, None)
850
 
851
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
852
  def test_rich_send_used_for_final_answer_with_table():
853
  chat_id = 999401
854
  incoming = _FakeIncomingMessage(chat_id)
 
849
  lumen_typing_pace._speed_ema.pop(pace_key, None)
850
 
851
 
852
+ def test_run_streaming_reply_catchup_edits_respect_min_interval(monkeypatch):
853
+ # Довывод правится тем же троттлингом, что основной цикл: тик короче
854
+ # интервала правок, серия быстро ловила бы 429.
855
+ import lumen_typing_pace
856
+ import time
857
+ chat_id = 999113
858
+ pace_key = lumen_typing_pace.speed_key("gemini", bot.DEFAULT_GEMINI_MODEL)
859
+ original_ema = lumen_typing_pace._speed_ema.pop(pace_key, None)
860
+ lumen_typing_pace._speed_ema[pace_key] = lumen_typing_pace.MIN_CHARS_PER_SEC
861
+ monkeypatch.setattr(bot, "STREAM_EDIT_MIN_INTERVAL_SEC", 0.4)
862
+ monkeypatch.setattr(bot, "STREAM_TYPING_TICK_SEC", 0.3)
863
+ monkeypatch.setattr(bot, "_typing_sleep", asyncio.sleep)
864
+ long_text = "Буква " * 500
865
+ fake_stream = _fake_gemini_stream([long_text])
866
+ fake_client = MagicMock()
867
+ fake_client.aio.models.generate_content_stream = fake_stream
868
+ incoming = _FakeIncomingMessage(chat_id)
869
+ edit_ts = []
870
+ orig_tg_call = bot._tg_call
871
+
872
+ async def rec_tg_call(method, *args, **kwargs):
873
+ if getattr(method, "__name__", "") == "edit_text":
874
+ edit_ts.append(time.monotonic())
875
+ return await orig_tg_call(method, *args, **kwargs)
876
+
877
+ monkeypatch.setattr(bot, "_tg_call", rec_tg_call)
878
+ original_client = bot.client
879
+ bot.client = fake_client
880
+ try:
881
+ answer, _ = asyncio.run(bot._try_gemini_streaming(chat_id, "Привет!", incoming, bot.DEFAULT_GEMINI_MODEL))
882
+ assert answer == long_text.strip()
883
+ # Последняя правка финальная терминальная (один вызов, вне троттлинга
884
+ # по дизайну) — меряем промежутки прогрессивных правок.
885
+ gaps = [b - a for a, b in zip(edit_ts, edit_ts[1:-1])]
886
+ assert gaps, "expected progressive edits during streaming"
887
+ assert min(gaps) >= 0.35
888
+ finally:
889
+ bot.client = original_client
890
+ bot.chat_state.pop(chat_id, None)
891
+ if original_ema is not None:
892
+ lumen_typing_pace._speed_ema[pace_key] = original_ema
893
+ else:
894
+ lumen_typing_pace._speed_ema.pop(pace_key, None)
895
+
896
+
897
+ def test_send_text_pauses_between_chunks(monkeypatch):
898
+ # Куски уходят с паузой против флуд-контроля за серию подряд.
899
+ from types import SimpleNamespace
900
+ sleeps = []
901
+
902
+ async def rec_sleep(seconds):
903
+ sleeps.append(seconds)
904
+
905
+ monkeypatch.setattr(bot, "_typing_sleep", rec_sleep)
906
+
907
+ class _Bot:
908
+ async def send_message(self, **kwargs):
909
+ return SimpleNamespace()
910
+
911
+ monkeypatch.setattr(bot, "bot", _Bot())
912
+ incoming = _FakeIncomingMessage(999114)
913
+ text = "слово " * 800
914
+ chunks = lumen_streaming._split_text_chunks(text, bot.TG_MAX_LEN)
915
+ assert len(chunks) > 1
916
+ try:
917
+ asyncio.run(bot._send_text(incoming, text))
918
+ assert sleeps == [bot.STREAM_TYPING_TICK_SEC] * (len(chunks) - 1)
919
+ finally:
920
+ bot.chat_state.pop(999114, None)
921
+
922
+
923
  def test_rich_send_used_for_final_answer_with_table():
924
  chat_id = 999401
925
  incoming = _FakeIncomingMessage(chat_id)