SilverElixir commited on
Commit
cc503f9
·
unverified ·
1 Parent(s): 3585679

Add files via upload

Browse files
bot.py CHANGED
@@ -4643,6 +4643,7 @@ async def _webhook_startup() -> None:
4643
 
4644
  commands = [
4645
  BotCommand(command="start", description="О боте и список команд"),
 
4646
  BotCommand(command="draw", description="Нарисовать изображение по описанию"),
4647
  BotCommand(command="tts", description="Озвучить текст"),
4648
  BotCommand(command="imgmodel", description="Выбрать модель для генерации изображений"),
 
4643
 
4644
  commands = [
4645
  BotCommand(command="start", description="О боте и список команд"),
4646
+ BotCommand(command="reset", description="Очистить историю диалога"),
4647
  BotCommand(command="draw", description="Нарисовать изображение по описанию"),
4648
  BotCommand(command="tts", description="Озвучить текст"),
4649
  BotCommand(command="imgmodel", description="Выбрать модель для генерации изображений"),
lumen_formatting.py CHANGED
@@ -159,6 +159,38 @@ _BULLET_MARKER_RE = re.compile(r"^([ \t]*)[-*][ \t]+", re.MULTILINE)
159
  def _normalize_bullet_markers(text: str) -> str:
160
  return _BULLET_MARKER_RE.sub(lambda m: m.group(1) + "• ", text)
161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  # ── Markdown-цитаты "> текст" → Telegram <blockquote> ───────────────────────
163
  # Тот же принцип, что и у _normalize_bullet_markers выше: "> " — обычный
164
  # GFM-синтаксис цитаты, модели он известен без единого слова в system_prompt.py
@@ -229,6 +261,10 @@ def _md_to_html(text: str) -> str:
229
  1.4. Маркеры списков "-"/"* " → "•" (_normalize_bullet_markers) — ДО таблиц,
230
  чтобы строка-разделитель таблицы ("|---|---|") успела обработаться первой
231
  и не была принята за маркер списка.
 
 
 
 
232
  1.45. Markdown-цитаты "> " → сентинелы \x00BQS\x00/\x00BQE\x00 (_convert_
233
  blockquotes) — сентинелы, не сразу <blockquote>, т.к. Phase 2 экранировал
234
  бы буквальный тег; настоящий тег подставляется после Phase 3 (см. ниже).
@@ -306,6 +342,12 @@ def _md_to_html(text: str) -> str:
306
  # чтобы строка-разделитель таблицы ("|---|---|") успела обработаться первой.
307
  text = _normalize_bullet_markers(text)
308
 
 
 
 
 
 
 
309
  # ── Phase 1.45: markdown-цитаты "> " → сентинелы blockquote (см.
310
  # _convert_blockquotes выше) — ДО HTML-экранирования, т.к. решение "это
311
  # строка цитаты" принимается по буквальному "> " в начале строки; сама
 
159
  def _normalize_bullet_markers(text: str) -> str:
160
  return _BULLET_MARKER_RE.sub(lambda m: m.group(1) + "• ", text)
161
 
162
+ # ── Markdown-заголовки "#"/"##"/"###" → **жирный текст** ────────────────────
163
+ # РЕГРЕССИЯ, найденная при живом тестировании (18 августа 2026, реальные скриншоты
164
+ # из Telegram): system_prompt.py прямо запрещает markdown-заголовки ("Также никогда
165
+ # не используй markdown-заголовки (#, ##, ### и т.п.)... Для выделения структуры
166
+ # используй жирный текст (**...**) вместо заголовков"), но модели (особенно
167
+ # бесплатные модели OpenRouter) регулярно их всё равно пишут — то же самое
168
+ # несоблюдение инструкции моделью, которое уже потребовало защитных сеток для
169
+ # таблиц (_convert_markdown_tables_to_lists) и маркеров списков
170
+ # (_normalize_bullet_markers) выше. Без скрубера здесь "### Как она выводится?"
171
+ # уходит в Telegram буквально с "###" в начале строки — реальный найденный баг,
172
+ # видимый на скриншотах. ATX-заголовок распознаётся по стандартному правилу
173
+ # CommonMark: 1-6 "#" в начале строки, затем обязательно пробел/таб — это
174
+ # исключает "C#"/"#tag" и подобные легитимные "#" не в начале строки (те тут
175
+ # не попадают под шаблон вообще, т.к. якорь "^" требует начала строки).
176
+ _HEADER_MARKER_RE = re.compile(r"^[ \t]*#{1,6}[ \t]+(.*)$", re.MULTILINE)
177
+
178
+ def _normalize_headers(text: str) -> str:
179
+ def _repl(m: re.Match) -> str:
180
+ content = m.group(1).rstrip()
181
+ if not content:
182
+ # Голая строка из одних "#" без текста — нечего выделять жирным,
183
+ # просто убираем маркер целиком, а не оставляем пустую "****".
184
+ return ""
185
+ if content.startswith("**") and content.endswith("**") and len(content) > 4:
186
+ # Модель сама уже обернула текст заголовка в **bold** (нередкий
187
+ # случай — "### **Важно**") — оборачивать ЕЩЁ раз дало бы "****Важно****"
188
+ # и сломало бы парность звёздочек в Phase 3 ниже. Раз обёртка уже
189
+ # есть, только снимаем сам маркер "#", остальное не трогаем.
190
+ return content
191
+ return f"**{content}**"
192
+ return _HEADER_MARKER_RE.sub(_repl, text)
193
+
194
  # ── Markdown-цитаты "> текст" → Telegram <blockquote> ───────────────────────
195
  # Тот же принцип, что и у _normalize_bullet_markers выше: "> " — обычный
196
  # GFM-синтаксис цитаты, модели он известен без единого слова в system_prompt.py
 
261
  1.4. Маркеры списков "-"/"* " → "•" (_normalize_bullet_markers) — ДО таблиц,
262
  чтобы строка-разделитель таблицы ("|---|---|") успела обработаться первой
263
  и не была принята за маркер списка.
264
+ 1.41. Markdown-заголовки "#"/"##"/"###" → **жирный текст** (_normalize_headers) —
265
+ после списков (не пересекаются по синтаксису), до Phase 3, чтобы
266
+ получившиеся "**...**" были обработаны обычным bold-регэкспом ниже
267
+ на общих основаниях, а не отдельной веткой.
268
  1.45. Markdown-цитаты "> " → сентинелы \x00BQS\x00/\x00BQE\x00 (_convert_
269
  blockquotes) — сентинелы, не сразу <blockquote>, т.к. Phase 2 экранировал
270
  бы буквальный тег; настоящий тег подставляется после Phase 3 (см. ниже).
 
342
  # чтобы строка-разделитель таблицы ("|---|---|") успела обработаться первой.
343
  text = _normalize_bullet_markers(text)
344
 
345
+ # ── Phase 1.41: markdown-заголовки "#"/"##"/"###" → **жирный текст** (см.
346
+ # _normalize_headers выше) — после списков, до HTML-экранирования и до Phase 3
347
+ # (получившийся "**...**" обрабатывается обычным bold-регэкспом на общих
348
+ # основаниях, отдельная ветка не нужна).
349
+ text = _normalize_headers(text)
350
+
351
  # ── Phase 1.45: markdown-цитаты "> " → сентинелы blockquote (см.
352
  # _convert_blockquotes выше) — ДО HTML-экранирования, т.к. решение "это
353
  # строка цитаты" принимается по буквальному "> " в начале строки; сама
lumen_router_config.py CHANGED
@@ -333,6 +333,30 @@ _OR_MODEL_HEALTH: dict[str, _ModelHealthNote] = {
333
  "уверенность и в оставшихся двух (вторая — dolphin-mistral, уже была исключена по "
334
  "другой причине выше)."
335
  ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
  }
337
 
338
  # Вычисляется ИЗ _OR_MODEL_HEALTH выше — единственное место, где решается, какие
@@ -387,11 +411,13 @@ def _gemini_route(models: list[str]) -> list[tuple[str, str]]:
387
  # мелкие модели ниже в цепочке дадут похожие инциденты, тогда стоит
388
  # рассмотреть полное исключение, а не просто понижение приоритета.
389
  #
390
- # Модели, убранные из списка целиком (провайдер снял с бесплатного тира или
391
- # слаг подтверждённо не обслуживается полные причины и даты см. в
392
- # _OR_MODEL_HEALTH выше): meta-llama/llama-3.3-70b-instruct, qwen/qwen3-next-
393
- # 80b-a3b-instruct, z-ai/glm-4.5-air, meta-llama/llama-3.2-3b-instruct,
394
- # liquid/lfm-2.5-1.2b-instruct.
 
 
395
  #
396
  # nemotron-3.5-lightning ДОБАВЛЕНА (аудит моделей, 17 августа 2026, по
397
  # актуальному "Top Weekly free" каталогу OpenRouter): 30B MoE (3B активных),
@@ -400,8 +426,14 @@ def _gemini_route(models: list[str]) -> list[tuple[str, str]]:
400
  # Вышла всего неделю назад (11 августа 2026) — калибровочного сравнения с
401
  # Claude Sonnet ещё не было ни разу, поэтому поставлена последней перед
402
  # generic-резервом, а не выше уже проверенных калибровкой моделей.
 
 
 
 
 
 
 
403
  _OR_LIGHT_ORDER: list[str] = [
404
- "nvidia/nemotron-nano-9b-v2:free",
405
  "inclusionai/ling-3.0-flash:free",
406
  "nvidia/nemotron-3-nano-30b-a3b:free",
407
  "openai/gpt-oss-20b:free",
 
333
  "уверенность и в оставшихся двух (вторая — dolphin-mistral, уже была исключена по "
334
  "другой причине выше)."
335
  ),
336
+ # ── Найдено при калибровочном прогоне против Claude Sonnet 5 (18 августа 2026,
337
+ # реальные продакшен-логи + скриншоты Telegram) ──
338
+ "nvidia/nemotron-nano-9b-v2:free": _ModelHealthNote(
339
+ reason="ПОДТВЕРЖДЕНО ПО РЕАЛЬНЫМ ЛОГАМ ПРОДА И СКРИНШОТАМ (18 августа 2026): стояла первой в "
340
+ "_OR_LIGHT_ORDER и реально ответила на 37 из 45 (82%) успешных лёгких текстовых запросов "
341
+ "за сессию — но КАЖДЫЙ развёрнутый ответ (2+ предложения) содержал грубую порчу текста: "
342
+ "случайные фрагменты слов из десятка не относящихся к разговору языков, вклиненные "
343
+ "прямо ВНУТРЬ русских слов и предложений (подтверждённые примеры из одной сессии: "
344
+ "испанское 'modelo' вместо 'модель', португальское 'mesmo'/'ambiental', французское "
345
+ "'sommet'/'fermer'/'pente'/'Série', арабское 'زراعة' и 'دان', корейское '택' и '종료', "
346
+ "китайское '欧洲' и '諜', японская катакана 'キュ', деванагари 'कंपनी', вьетнамское "
347
+ "'tiền', итальянское 'militarizzazione', полностью вымышленное имя сооснователя "
348
+ "Anthropic 'lagoo Isaacs' вместо реальных Дарио/Дэниэлы Амодей, и медицинский термин "
349
+ "'carcinogenesis', случайно подставленный вместо слова 'убийство' в разборе причин "
350
+ "Первой мировой войны). Модель НЕ мертва технически (не 404, отвечает и тратит "
351
+ "заметную часть бесплатного дневного лимита), но выдаёт продакшен-трафику постоянно "
352
+ "испорченный на нечитаемость текст — тот самый класс проблемы, ради которого этот "
353
+ "реестр уже исключает uncensored/некачественные модели (см. dolphin-mistral выше), а "
354
+ "не только официально снятые с тарифа. Дополнительно (не решающий, но подтверждающий "
355
+ "аргумент): вопреки размеру 'nano'/9B и позиции головы 'лёгкой' цепочки, реальные "
356
+ "длительности стриминга по логам — от 12 до 92 секунд на ответ, то есть заметно "
357
+ "медленнее, чем у более крупных моделей дальше по списку — таблоид-эффект 'самая "
358
+ "быстрая и лёгкая' на практике не подтвердился ни разу за сессию."
359
+ ),
360
  }
361
 
362
  # Вычисляется ИЗ _OR_MODEL_HEALTH выше — единственное место, где решается, какие
 
411
  # мелкие модели ниже в цепочке дадут похожие инциденты, тогда стоит
412
  # рассмотреть полное исключение, а не просто понижение приоритета.
413
  #
414
+ # Модели, убранные из списка целиком (провайдер снял с бесплатного тира, слаг
415
+ # подтверждённо не обслуживается, или подтверждена грубая порча текста — полные
416
+ # причины и даты см. в _OR_MODEL_HEALTH выше): meta-llama/llama-3.3-70b-instruct,
417
+ # qwen/qwen3-next-80b-a3b-instruct, z-ai/glm-4.5-air, meta-llama/llama-3.2-3b-
418
+ # instruct, liquid/lfm-2.5-1.2b-instruct, nvidia/nemotron-nano-9b-v2 (стояла
419
+ # первой здесь — см. подробный разбор порчи текста на реальном трафике в записи
420
+ # реестра выше, калибровка 18 августа 2026).
421
  #
422
  # nemotron-3.5-lightning ДОБАВЛЕНА (аудит моделей, 17 августа 2026, по
423
  # актуальному "Top Weekly free" каталогу OpenRouter): 30B MoE (3B активных),
 
426
  # Вышла всего неделю назад (11 августа 2026) — калибровочного сравнения с
427
  # Claude Sonnet ещё не было ни разу, поэтому поставлена последней перед
428
  # generic-резервом, а не выше уже проверенных калибровкой моделей.
429
+ #
430
+ # ПОСЛЕ ИСКЛЮЧЕНИЯ nemotron-nano-9b-v2 (см. выше) ling-3.0-flash становится
431
+ # НОВОЙ головой списка — ни разу не поймана в порче текста в той же сессии
432
+ # логов, но и калибровочного сравнения с Claude Sonnet у неё тоже пока не было
433
+ # (см. её же комментарий в докстринге секции ниже про 1.49T токенов/неделю).
434
+ # Стоит присмотреться к первым реальным ответам именно в этой новой роли головы
435
+ # списка — если порча обнаружится и там, кандидат на аналогичное исключение.
436
  _OR_LIGHT_ORDER: list[str] = [
 
437
  "inclusionai/ling-3.0-flash:free",
438
  "nvidia/nemotron-3-nano-30b-a3b:free",
439
  "openai/gpt-oss-20b:free",
test_lumen_formatting.py CHANGED
@@ -256,6 +256,59 @@ def test_md_to_html_full_pipeline_converts_bullet_list_with_bold():
256
  assert "*" not in result.replace("</b>", "").replace("<b>", "")
257
 
258
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  # ─────────────────── spoiler-тег: защитная сетка (тот же принцип, что <u>) ───────────────────
260
 
261
  def test_md_to_html_strips_literal_spoiler_tag():
 
256
  assert "*" not in result.replace("</b>", "").replace("<b>", "")
257
 
258
 
259
+ # ─────────────── markdown-заголовки "#"/"##"/"###" → **жирный текст** ───────────────
260
+ # РЕГРЕССИЯ, найденная на реальных скриншотах Telegram (18 августа 2026):
261
+ # system_prompt.py запрещает markdown-заголовки, но модели (особенно бесплатные
262
+ # модели OpenRouter) регулярно их всё равно пишут — раньше "###" уходило в
263
+ # Telegram буквально, без единой защитной сетки (в отличие от таблиц/списков).
264
+
265
+ def test_normalize_headers_converts_h3_to_bold():
266
+ assert lumen_formatting._normalize_headers("### Как она выводится?") == "**Как она выводится?**"
267
+
268
+
269
+ def test_normalize_headers_converts_h1_and_h2():
270
+ assert lumen_formatting._normalize_headers("# Заголовок") == "**Заголовок**"
271
+ assert lumen_formatting._normalize_headers("## Подзаголовок") == "**Подзаголовок**"
272
+
273
+
274
+ def test_normalize_headers_does_not_touch_hash_mid_line():
275
+ # "C#" / "#tag" не в начале строки — ATX-заголовок ТОЛЬКО в начале строки.
276
+ text = "Язык C# отличается от C++.\nПодробнее: #tag"
277
+ assert lumen_formatting._normalize_headers(text) == text
278
+
279
+
280
+ def test_normalize_headers_requires_space_after_hashes():
281
+ # "#без_пробела" — не заголовок по правилам CommonMark ATX, не трогаем.
282
+ assert lumen_formatting._normalize_headers("#без_пробела текст") == "#без_пробела текст"
283
+
284
+
285
+ def test_normalize_headers_bare_hashes_with_no_text_removed():
286
+ assert lumen_formatting._normalize_headers("### \nследующая строка") == "\nследующая строка"
287
+
288
+
289
+ def test_normalize_headers_does_not_double_wrap_already_bold_content():
290
+ # "### **Важно**" — модель сама уже обернула текст в bold; повторная обёртка
291
+ # дала бы "****Важно****" и сломала бы парность "**" в Phase 3.
292
+ assert lumen_formatting._normalize_headers("### **Важно**") == "**Важно**"
293
+
294
+
295
+ def test_md_to_html_full_pipeline_strips_stray_header_markers():
296
+ # Регрессия на реальный найденный баг: пользователь видел буквальные "###" в
297
+ # сообщении бота вместо жирного текста — полный конвейер должен это исправлять.
298
+ result = lumen_formatting._md_to_html("### Как она выводится?\nобычный текст")
299
+ assert "###" not in result
300
+ assert result.startswith("<b>Как она выводится?</b>")
301
+
302
+
303
+ def test_md_to_html_header_hash_inside_code_block_untouched():
304
+ # "#" внутри блока кода (например, комментарий Python) не должен считаться
305
+ # заголовком — код уже вынесен плейсхолдером до этой фазы.
306
+ text = "```python\n# обычный комментарий\nprint(1)\n```"
307
+ result = lumen_formatting._md_to_html(text)
308
+ assert "# обычный комментарий" in result
309
+ assert "<b>" not in result
310
+
311
+
312
  # ─────────────────── spoiler-тег: защитная сетка (тот же принцип, что <u>) ───────────────────
313
 
314
  def test_md_to_html_strips_literal_spoiler_tag():
test_lumen_router_config.py CHANGED
@@ -232,6 +232,19 @@ def test_or_light_order_no_longer_starts_with_dead_or_worst_offender_models():
232
  assert lumen_router_config._OR_LIGHT_ORDER[0] not in {"openai/gpt-oss-20b:free", "nvidia/nemotron-3-nano-30b-a3b:free"}
233
 
234
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  # ─────────────────── единый реестр "нездоровых" моделей OpenRouter (аудит техдолга) ───────────────────
236
  # Раньше "эта модель сейчас плохая" отслеживалось тремя независимыми механизмами
237
  # (_TEMPORARY_FREE_MODELS/_ROUTER_EXCLUDED_OR_MODELS/точечные вычёркивания из
 
232
  assert lumen_router_config._OR_LIGHT_ORDER[0] not in {"openai/gpt-oss-20b:free", "nvidia/nemotron-3-nano-30b-a3b:free"}
233
 
234
 
235
+ def test_nemotron_nano_9b_v2_excluded_after_calibration_run():
236
+ # РЕГРЕССИЯ (калибровочный прогон против Claude Sonnet 5, 18 августа 2026):
237
+ # раньше стояла ПЕРВОЙ в _OR_LIGHT_ORDER и реально отвечала на 82% лёгких
238
+ # текстовых запросов сессии — каждый развёрнутый ответ содержал грубую порчу
239
+ # текста (случайные фрагменты слов на десятке не относящихся к разговору
240
+ # языков внутри русских предложений, см. полный разбор в _OR_MODEL_HEALTH).
241
+ assert "nvidia/nemotron-nano-9b-v2:free" in lumen_router_config._ROUTER_EXCLUDED_OR_MODELS
242
+ assert "nvidia/nemotron-nano-9b-v2:free" not in lumen_router_config._OR_LIGHT_ORDER
243
+ assert lumen_router_config._OR_MODEL_HEALTH["nvidia/nemotron-nano-9b-v2:free"].reason
244
+ route = lumen_router_config._or_route(["nvidia/nemotron-nano-9b-v2:free", "inclusionai/ling-3.0-flash:free"])
245
+ assert [m for _, m in route] == ["inclusionai/ling-3.0-flash:free"]
246
+
247
+
248
  # ─────────────────── единый реестр "нездоровых" моделей OpenRouter (аудит техдолга) ───────────────────
249
  # Раньше "эта модель сейчас плохая" отслеживалось тремя независимыми механизмами
250
  # (_TEMPORARY_FREE_MODELS/_ROUTER_EXCLUDED_OR_MODELS/точечные вычёркивания из