Spaces:
Running
Running
SilverElixir commited on
Commit ·
c019423
1
Parent(s): c0d4d78
Throttle streaming catch-up edits and pace chunk sends
Browse files- lumen_rich.py +3 -0
- lumen_streaming.py +5 -1
- tests/test_bot_streaming.py +71 -0
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 |
-
|
|
|
|
|
|
|
|
|
|
| 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)
|