Spaces:
Running
Running
SilverElixir commited on
Commit ·
9d7cca0
1
Parent(s): 339b914
Instant dots, buying-advice routing, br handling, plain-fallback cleanup, live-log model updates
Browse files- bot.py +12 -10
- docs/PROMPT_EVAL.md +15 -0
- lumen_formatting.py +33 -0
- lumen_router_config.py +13 -1
- test_lumen_formatting.py +22 -0
- test_lumen_router_config.py +17 -0
bot.py
CHANGED
|
@@ -401,12 +401,12 @@ STREAM_TYPING_MAX_CATCHUP_TICKS = int(os.getenv("STREAM_TYPING_MAX_CATCHUP_TICKS
|
|
| 401 |
# куска — всегда минимум из двух.
|
| 402 |
FIRST_CHUNK_TIMEOUT_SEC = float(os.getenv("FIRST_CHUNK_TIMEOUT_SEC", "12"))
|
| 403 |
# Анимация ожидания ("бегущие точки") в плейсхолдере, пока не пришёл первый
|
| 404 |
-
# кусок стрима: перв
|
| 405 |
-
# ответ
|
| 406 |
-
# _DOTS_TICK_SEC. Интервалы подобраны под лимит Telegram (не чаще
|
| 407 |
-
# секунду) с запасом; правки идут только показывать нечего (до
|
| 408 |
-
# поэтому с показом текста не конфликтуют.
|
| 409 |
-
_DOTS_START_AFTER_SEC =
|
| 410 |
_DOTS_TICK_SEC = 1.2
|
| 411 |
_DOTS_FRAMES = (".", "..", "…")
|
| 412 |
# RICH_MESSAGES_ENABLED — отправка финальных ответов через sendRichMessage /
|
|
@@ -628,7 +628,7 @@ async def _close_sessions() -> None:
|
|
| 628 |
# именно ради старых тестов на `bot.X`, но с переездом тестов на прямой импорт
|
| 629 |
# модуля этот ре-экспорт стал мёртвым (см. аудит техдолга, 26 августа 2026) и
|
| 630 |
# убран вместе с соответствующим `__all__`.
|
| 631 |
-
from lumen_formatting import _md_to_html, _md_to_rich_html, _split_text_chunks
|
| 632 |
|
| 633 |
_PRUNE_SENTINEL = object()
|
| 634 |
|
|
@@ -869,7 +869,7 @@ async def _send_text(message: Message, text: str, parse_html: bool = True, **kwa
|
|
| 869 |
)
|
| 870 |
if res is None and parse_html:
|
| 871 |
res = await _tg_call(
|
| 872 |
-
message.reply, chunk,
|
| 873 |
call_timeout=TELEGRAM_REQUEST_TIMEOUT,
|
| 874 |
parse_mode=None,
|
| 875 |
**chunk_kwargs,
|
|
@@ -885,7 +885,7 @@ async def _send_text(message: Message, text: str, parse_html: bool = True, **kwa
|
|
| 885 |
if res is None and parse_html:
|
| 886 |
await _tg_call(
|
| 887 |
bot.send_message,
|
| 888 |
-
chat_id=message.chat.id, text=chunk,
|
| 889 |
call_timeout=TELEGRAM_REQUEST_TIMEOUT,
|
| 890 |
parse_mode=None,
|
| 891 |
**chunk_kwargs,
|
|
@@ -914,7 +914,9 @@ async def _edit_message_quietly(msg: Message | None, text: str, **kwargs: Any) -
|
|
| 914 |
if res is not None:
|
| 915 |
return True
|
| 916 |
kwargs["parse_mode"] = None
|
| 917 |
-
|
|
|
|
|
|
|
| 918 |
return res is not None
|
| 919 |
except Exception:
|
| 920 |
return False
|
|
|
|
| 401 |
# куска — всегда минимум из двух.
|
| 402 |
FIRST_CHUNK_TIMEOUT_SEC = float(os.getenv("FIRST_CHUNK_TIMEOUT_SEC", "12"))
|
| 403 |
# Анимация ожидания ("бегущие точки") в плейсхолдере, пока не пришёл первый
|
| 404 |
+
# кусок стрима: первые полсекунды висит статичное "…" (дешевле, чем дёргать
|
| 405 |
+
# API ради мгновенных ответов — их анимация вообще не касается), дальше —
|
| 406 |
+
# кадр каждые _DOTS_TICK_SEC. Интервалы подобраны под лимит Telegram (не чаще
|
| 407 |
+
# правки в секунду) с запасом; правки идут только показывать нечего (до
|
| 408 |
+
# первого куска), поэтому с показом текста не конфликтуют.
|
| 409 |
+
_DOTS_START_AFTER_SEC = 0.5
|
| 410 |
_DOTS_TICK_SEC = 1.2
|
| 411 |
_DOTS_FRAMES = (".", "..", "…")
|
| 412 |
# RICH_MESSAGES_ENABLED — отправка финальных ответов через sendRichMessage /
|
|
|
|
| 628 |
# именно ради старых тестов на `bot.X`, но с переездом тестов на прямой импорт
|
| 629 |
# модуля этот ре-экспорт стал мёртвым (см. аудит техдолга, 26 августа 2026) и
|
| 630 |
# убран вместе с соответствующим `__all__`.
|
| 631 |
+
from lumen_formatting import _md_to_html, _md_to_rich_html, _split_text_chunks, _strip_markdown
|
| 632 |
|
| 633 |
_PRUNE_SENTINEL = object()
|
| 634 |
|
|
|
|
| 869 |
)
|
| 870 |
if res is None and parse_html:
|
| 871 |
res = await _tg_call(
|
| 872 |
+
message.reply, _strip_markdown(chunk),
|
| 873 |
call_timeout=TELEGRAM_REQUEST_TIMEOUT,
|
| 874 |
parse_mode=None,
|
| 875 |
**chunk_kwargs,
|
|
|
|
| 885 |
if res is None and parse_html:
|
| 886 |
await _tg_call(
|
| 887 |
bot.send_message,
|
| 888 |
+
chat_id=message.chat.id, text=_strip_markdown(chunk),
|
| 889 |
call_timeout=TELEGRAM_REQUEST_TIMEOUT,
|
| 890 |
parse_mode=None,
|
| 891 |
**chunk_kwargs,
|
|
|
|
| 914 |
if res is not None:
|
| 915 |
return True
|
| 916 |
kwargs["parse_mode"] = None
|
| 917 |
+
# Последний рубеж — голый текст БЕЗ markdown-синтаксиса (см.
|
| 918 |
+
# _strip_markdown): сырые `**` в чате хуже потери жирности.
|
| 919 |
+
res = await _tg_call(msg.edit_text, _strip_markdown(text), **kwargs)
|
| 920 |
return res is not None
|
| 921 |
except Exception:
|
| 922 |
return False
|
docs/PROMPT_EVAL.md
CHANGED
|
@@ -5,6 +5,21 @@
|
|
| 5 |
шкале ниже. Цель: понять, где бот далёк от персоны, и править `system_prompt.py`
|
| 6 |
точечно, а не вслепую.
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
## Как гонять
|
| 9 |
|
| 10 |
1. Каждый вопрос — отдельным сообщением, по порядку. Контекст между вопросами
|
|
|
|
| 5 |
шкале ниже. Цель: понять, где бот далёк от персоны, и править `system_prompt.py`
|
| 6 |
точечно, а не вслепую.
|
| 7 |
|
| 8 |
+
Файл живёт в проекте постоянно (не удаляется после калибровки): каждый новый
|
| 9 |
+
прогон дописывается в «Журнал прогонов» ниже — так видно прогресс цифрой и
|
| 10 |
+
заметен откат, если правка промпта что-то сломала.
|
| 11 |
+
|
| 12 |
+
Читать удобнее всего в браузере на GitHub (открыть файл в репозитории —
|
| 13 |
+
разметка отрисуется красиво). В Блокноте Windows будет сырой текст с `#` —
|
| 14 |
+
это нормально, содержание то же.
|
| 15 |
+
|
| 16 |
+
## Журнал прогонов
|
| 17 |
+
|
| 18 |
+
| Дата | Версия промпта | Бот (из 50) | Claude (из 50) | Заметки |
|
| 19 |
+
|---|---|---|---|---|
|
| 20 |
+
| 2026-09-17 | до правок | 42 | ~50 (частичный прогон) | Провалы: Q1 каша, Q8 список, Q17 без уточнения, Q21 выдумка фото, Q22 без оговорки |
|
| 21 |
+
| | | | | |
|
| 22 |
+
|
| 23 |
## Как гонять
|
| 24 |
|
| 25 |
1. Каждый вопрос — отдельным сообщением, по порядку. Контекст между вопросами
|
lumen_formatting.py
CHANGED
|
@@ -291,6 +291,11 @@ def _md_to_html(text: str) -> str:
|
|
| 291 |
# баг). Сначала убираем заведомо битые self-closing варианты (напр. "<b/>"),
|
| 292 |
# затем конвертируем корректные парные теги в markdown-эквивалент — дальше
|
| 293 |
# они идут по тому же (уже проверенному) конвейеру, что и обычный markdown.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 294 |
text = re.sub(r"</?(?:b|strong|i|em|u|s|code|pre)\s*/>", "", text, flags=re.IGNORECASE)
|
| 295 |
text = re.sub(r"<(?:b|strong)>(.*?)</(?:b|strong)>", r"**\1**", text, flags=re.IGNORECASE | re.DOTALL)
|
| 296 |
text = re.sub(r"<(?:i|em)>(.*?)</(?:i|em)>", r"*\1*", text, flags=re.IGNORECASE | re.DOTALL)
|
|
@@ -396,6 +401,12 @@ def _md_to_html(text: str) -> str:
|
|
| 396 |
replacement = f"<code>{inner}</code>"
|
| 397 |
text = text.replace(key, replacement)
|
| 398 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
return text
|
| 400 |
|
| 401 |
|
|
@@ -440,6 +451,9 @@ def _md_to_rich_html(text: str) -> str:
|
|
| 440 |
if not text:
|
| 441 |
return ""
|
| 442 |
|
|
|
|
|
|
|
|
|
|
| 443 |
text = re.sub(r"</?(?:b|strong|i|em|u|s|code|pre)\s*/>", "", text, flags=re.IGNORECASE)
|
| 444 |
text = re.sub(r"<(?:b|strong)>(.*?)</(?:b|strong)>", r"**\1**", text, flags=re.IGNORECASE | re.DOTALL)
|
| 445 |
text = re.sub(r"<(?:i|em)>(.*?)</(?:i|em)>", r"*\1*", text, flags=re.IGNORECASE | re.DOTALL)
|
|
@@ -544,6 +558,25 @@ def _md_to_rich_html(text: str) -> str:
|
|
| 544 |
replacement = f"<code>{inner}</code>"
|
| 545 |
text = text.replace(key, replacement)
|
| 546 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 547 |
return text
|
| 548 |
|
| 549 |
|
|
|
|
| 291 |
# баг). Сначала убираем заведомо битые self-closing варианты (напр. "<b/>"),
|
| 292 |
# затем конвертируем корректные парные теги в markdown-эквивалент — дальше
|
| 293 |
# они идут по тому же (уже проверенному) конвейеру, что и обычный markdown.
|
| 294 |
+
# <br> — в сентинел (прод-кейс 17.09.2026: модель пишет "<br>" внутри ячеек
|
| 295 |
+
# таблиц, без обработки долетает до escape и светится буквально). Сентинел
|
| 296 |
+
# невидим для escape и раскрывается в "\n" в самом конце (см. финал ниже);
|
| 297 |
+
# строгий паттерн (<br>, <br/>, <br />) намеренно НЕ ест "<blockquote>".
|
| 298 |
+
text = re.sub(r"<br\s*/?>", "\x00BR\x00", text, flags=re.IGNORECASE)
|
| 299 |
text = re.sub(r"</?(?:b|strong|i|em|u|s|code|pre)\s*/>", "", text, flags=re.IGNORECASE)
|
| 300 |
text = re.sub(r"<(?:b|strong)>(.*?)</(?:b|strong)>", r"**\1**", text, flags=re.IGNORECASE | re.DOTALL)
|
| 301 |
text = re.sub(r"<(?:i|em)>(.*?)</(?:i|em)>", r"*\1*", text, flags=re.IGNORECASE | re.DOTALL)
|
|
|
|
| 401 |
replacement = f"<code>{inner}</code>"
|
| 402 |
text = text.replace(key, replacement)
|
| 403 |
|
| 404 |
+
# Сентинел <br> (см. Phase 0): в обычном HTML-пути — перенос строки.
|
| 405 |
+
# Оговорка: <br> ВНУТРИ код-блоков тоже превратится в перенос (Phase 0
|
| 406 |
+
# идёт до экстракции кода) — тот же класс компромисса, что уже есть у
|
| 407 |
+
# <b>/<i> в коде выше; код-примеры с буквальным <br> редки.
|
| 408 |
+
text = text.replace("\x00BR\x00", "\n")
|
| 409 |
+
|
| 410 |
return text
|
| 411 |
|
| 412 |
|
|
|
|
| 451 |
if not text:
|
| 452 |
return ""
|
| 453 |
|
| 454 |
+
# Тот же сентинел <br>, что в _md_to_html выше, но раскрывается в <br/>
|
| 455 |
+
# (валидный рич-тег переноса, в т.ч. внутри ячеек <td>).
|
| 456 |
+
text = re.sub(r"<br\s*/?>", "\x00BR\x00", text, flags=re.IGNORECASE)
|
| 457 |
text = re.sub(r"</?(?:b|strong|i|em|u|s|code|pre)\s*/>", "", text, flags=re.IGNORECASE)
|
| 458 |
text = re.sub(r"<(?:b|strong)>(.*?)</(?:b|strong)>", r"**\1**", text, flags=re.IGNORECASE | re.DOTALL)
|
| 459 |
text = re.sub(r"<(?:i|em)>(.*?)</(?:i|em)>", r"*\1*", text, flags=re.IGNORECASE | re.DOTALL)
|
|
|
|
| 558 |
replacement = f"<code>{inner}</code>"
|
| 559 |
text = text.replace(key, replacement)
|
| 560 |
|
| 561 |
+
text = text.replace("\x00BR\x00", "<br/>")
|
| 562 |
+
|
| 563 |
+
return text
|
| 564 |
+
|
| 565 |
+
|
| 566 |
+
def _strip_markdown(text: str) -> str:
|
| 567 |
+
"""Сносит markdown-разметку в голый текст для последнего рубежа отправки
|
| 568 |
+
(plain-text без parse_mode): иначе при отказе HTML/рич в чат уходят сырые
|
| 569 |
+
`**`, backtick-и и `[label](url)` (прод-кейс 17.09.2026 — ответ с `**`
|
| 570 |
+
прилетел как есть). Чистит только синтаксис, слова не трогает."""
|
| 571 |
+
if not text:
|
| 572 |
+
return ""
|
| 573 |
+
text = re.sub(r"```[a-zA-Z0-9]*\n(.*?)\n```", r"\1", text, flags=re.DOTALL)
|
| 574 |
+
text = re.sub(r"\[([^\[\]]+)\]\((https?://[^\s()]+)\)", r"\1", text)
|
| 575 |
+
text = re.sub(r"(\*\*|__)(.*?)\1", r"\2", text, flags=re.DOTALL)
|
| 576 |
+
text = re.sub(r"(\*|_)(.*?)\1", r"\2", text)
|
| 577 |
+
text = re.sub(r"~~(.*?)~~", r"\1", text)
|
| 578 |
+
text = re.sub(r"`([^`\n]+)`", r"\1", text)
|
| 579 |
+
text = re.sub(r"^#{1,6}\s+", "", text, flags=re.MULTILINE)
|
| 580 |
return text
|
| 581 |
|
| 582 |
|
lumen_router_config.py
CHANGED
|
@@ -547,6 +547,11 @@ def _gemini_route(models: list[str]) -> list[tuple[str, str]]:
|
|
| 547 |
# ОБНОВЛЕНО вечером 17.09.2026 по первому же прод-трафику: inkling-small
|
| 548 |
# исключён (только agent harnesses, см. _OR_MODEL_HEALTH), nex-n2.5-mini
|
| 549 |
# подтверждён живьём (успешный ответ в проде) и повышен на его место.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 550 |
_OR_LIGHT_ORDER: list[str] = [
|
| 551 |
"nvidia/nemotron-3.5-lightning:free",
|
| 552 |
"nex-agi/nex-n2.5-mini:free",
|
|
@@ -692,7 +697,9 @@ _HEAVY_QUERY_RE = re.compile(
|
|
| 692 |
r"|найди\s+(ошибк\w*|баг\w*)|объясни\s+(код|ошибк\w*)"
|
| 693 |
r"|напиши\s+(эссе|статью|доклад|реферат|сочинение|резюме|cv)\b"
|
| 694 |
r"|проанализируй\w*|разбер(и|ём)\s+подробно|объясни\s+подробно"
|
| 695 |
-
|
|
|
|
|
|
|
| 696 |
r"|реши\s+(задач\w*|уравнени\w*|систем\w*)"
|
| 697 |
r"|составь\s+(план|таблиц\w*|список\s+из)"
|
| 698 |
r"|многошагов\w*|пошагов\w*\s+(инструкц\w*|план\w*)"
|
|
@@ -720,6 +727,11 @@ _FRESHNESS_QUERY_RE = re.compile(
|
|
| 720 |
r"|новост\w*|курс\s+(валют|доллара|евро|рубл\w*)|погод\w*|прогноз\w*"
|
| 721 |
r"|расписани\w*|афиш\w*"
|
| 722 |
r"|цена\w*|стоимост\w*|сколько\s+стоит"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 723 |
r"|кто\s+(сейчас|является|президент|премьер|глава|ceo|мэр)"
|
| 724 |
r"|результат\w*\s+(матч\w*|игр\w*|выбор\w*)"
|
| 725 |
r"|в\s+эт(ом|ой)\s+(году|месяце|неделе)"
|
|
|
|
| 547 |
# ОБНОВЛЕНО вечером 17.09.2026 по первому же прод-трафику: inkling-small
|
| 548 |
# исключён (только agent harnesses, см. _OR_MODEL_HEALTH), nex-n2.5-mini
|
| 549 |
# подтверждён живьём (успешный ответ в проде) и повышен на его место.
|
| 550 |
+
# НАБЛЮДЕНИЕ (там же): один ответ nex-mini содержал вкрапления чужих языков
|
| 551 |
+
# ("сопプロцессоры", испанское "cuello", "Обе.platforms") — детектор
|
| 552 |
+
# [mush-suspect] поймал только явное смешение внутри слов, замены целых слов
|
| 553 |
+
# не ловит никто. Один инцидент — не хроника nano-9b: наблюдаем, при повторе —
|
| 554 |
+
# понижать (см. прецедент super-120b ниже).
|
| 555 |
_OR_LIGHT_ORDER: list[str] = [
|
| 556 |
"nvidia/nemotron-3.5-lightning:free",
|
| 557 |
"nex-agi/nex-n2.5-mini:free",
|
|
|
|
| 697 |
r"|найди\s+(ошибк\w*|баг\w*)|объясни\s+(код|ошибк\w*)"
|
| 698 |
r"|напиши\s+(эссе|статью|доклад|реферат|сочинение|резюме|cv)\b"
|
| 699 |
r"|проанализируй\w*|разбер(и|ём)\s+подробно|объясни\s+подробно"
|
| 700 |
+
# Голое "сравни( конкурентов)" — тоже сравнение (прод-кейс 17.09.2026),
|
| 701 |
+
# вариант с "и/с" выше оставлен для явных пар.
|
| 702 |
+
r"|сравни\b|докажи\b|доказательство"
|
| 703 |
r"|реши\s+(задач\w*|уравнени\w*|систем\w*)"
|
| 704 |
r"|составь\s+(план|таблиц\w*|список\s+из)"
|
| 705 |
r"|многошагов\w*|пошагов\w*\s+(инструкц\w*|план\w*)"
|
|
|
|
| 727 |
r"|новост\w*|курс\s+(валют|доллара|евро|рубл\w*)|погод\w*|прогноз\w*"
|
| 728 |
r"|расписани\w*|афиш\w*"
|
| 729 |
r"|цена\w*|стоимост\w*|сколько\s+стоит"
|
| 730 |
+
# Советы по покупке (прод-кейс 17.09.2026: "какой проц лучше всего брать" —
|
| 731 |
+
# цены, наличие и новинки меняются постоянно, ответ из памяти протухает).
|
| 732 |
+
# Ложные срабатывания дешёвые (Gemini и так отвечает на всё подряд).
|
| 733 |
+
r"|лучше\s+(всего\s+)?(брать|взять|купить|выбрать)"
|
| 734 |
+
r"|(брать|взять|купить|выбрать)\s+лучше"
|
| 735 |
r"|кто\s+(сейчас|является|президент|премьер|глава|ceo|мэр)"
|
| 736 |
r"|результат\w*\s+(матч\w*|игр\w*|выбор\w*)"
|
| 737 |
r"|в\s+эт(ом|ой)\s+(году|месяце|неделе)"
|
test_lumen_formatting.py
CHANGED
|
@@ -449,6 +449,28 @@ def test_rich_headings_tolerate_indent_and_deep_levels():
|
|
| 449 |
assert lumen_formatting._md_to_rich_html("#### Глубокий") == "<h4>Глубокий</h4>"
|
| 450 |
|
| 451 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
def test_rich_prices_are_not_treated_as_math():
|
| 453 |
# "$50 до $100" — контент с пробелом у границы, не формула. Одиночный знак
|
| 454 |
# без пары — тоже текст.
|
|
|
|
| 449 |
assert lumen_formatting._md_to_rich_html("#### Глубокий") == "<h4>Глубокий</h4>"
|
| 450 |
|
| 451 |
|
| 452 |
+
def test_br_tag_becomes_line_break_in_both_paths():
|
| 453 |
+
# Прод-кейс 17.09.2026: модель пишет "<br>" внутри ячеек таблиц — без
|
| 454 |
+
# обработки долетает до escape и светится буквально ("...сгор.<br>Pro Max").
|
| 455 |
+
assert lumen_formatting._md_to_html("a<br>b") == "a\nb"
|
| 456 |
+
assert lumen_formatting._md_to_html("a<br/>b") == "a\nb"
|
| 457 |
+
assert lumen_formatting._md_to_rich_html("| H | K |\n|---|---|\n| a<br>b | 2 |") == (
|
| 458 |
+
"<table bordered><tr><th>H</th><th>K</th></tr><tr><td>a<br/>b</td><td>2</td></tr></table>"
|
| 459 |
+
)
|
| 460 |
+
# "<blockquote>" не должен съедаться строгим паттерном <br>.
|
| 461 |
+
assert "blockquote" in lumen_formatting._md_to_html("<blockquote>ц</blockquote>")
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
def test_strip_markdown_removes_syntax_keeping_words():
|
| 465 |
+
assert lumen_formatting._strip_markdown("**жирный** и *курсив*") == "жирный и курсив"
|
| 466 |
+
assert lumen_formatting._strip_markdown("код `x=1` тут") == "код x=1 тут"
|
| 467 |
+
assert lumen_formatting._strip_markdown("[текст](https://example.com)") == "текст"
|
| 468 |
+
assert lumen_formatting._strip_markdown("## Заголовок") == "Заголовок"
|
| 469 |
+
assert lumen_formatting._strip_markdown("~~чист~~") == "чист"
|
| 470 |
+
assert lumen_formatting._strip_markdown("") == ""
|
| 471 |
+
assert lumen_formatting._strip_markdown("обычный текст 5 < 10") == "обычный текст 5 < 10"
|
| 472 |
+
|
| 473 |
+
|
| 474 |
def test_rich_prices_are_not_treated_as_math():
|
| 475 |
# "$50 до $100" — контент с пробелом у границы, не формула. Одиночный знак
|
| 476 |
# без пары — тоже текст.
|
test_lumen_router_config.py
CHANGED
|
@@ -75,6 +75,23 @@ def test_looks_like_freshness_query_detects_schedule_and_forecast():
|
|
| 75 |
assert lumen_router_config._looks_like_freshness_query("любимый цвет") is False
|
| 76 |
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
def test_build_route_youtube_link_forces_gemini_only():
|
| 79 |
route = lumen_router_config._build_route(needs_youtube=True, needs_website=False, media_mime=None, is_heavy=False, needs_freshness=False)
|
| 80 |
assert all(p == "gemini" for p, _ in route)
|
|
|
|
| 75 |
assert lumen_router_config._looks_like_freshness_query("любимый цвет") is False
|
| 76 |
|
| 77 |
|
| 78 |
+
def test_looks_like_freshness_query_detects_buying_advice():
|
| 79 |
+
# Прод-кейс 17.09.2026: "какой проц лучше всего брать под ртх 5060" ушёл
|
| 80 |
+
# лёгким маршрутом в слабую модель — та ответила протухшими знаниями
|
| 81 |
+
# (RTX 5060 "ещё не объявлена") вместо поиска. Советы по покупке всегда
|
| 82 |
+
# про цены/наличие/новинки — им нужен живой поиск.
|
| 83 |
+
assert lumen_router_config._looks_like_freshness_query("какой проц лучше всего брать под ртх 5060") is True
|
| 84 |
+
assert lumen_router_config._looks_like_freshness_query("что лучше купить для игр") is True
|
| 85 |
+
assert lumen_router_config._looks_like_freshness_query("какой телефон лучше взять") is True
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def test_looks_like_heavy_query_detects_bare_compare():
|
| 89 |
+
# Прод-кейс 17.09.2026: "сравни конкурентов" без "и/с" между объектами
|
| 90 |
+
# пролетал мимо тяжёлой ветки — сравнение ехало на слабую модель.
|
| 91 |
+
assert lumen_router_config._looks_like_heavy_query("сравни конкурентов") is True
|
| 92 |
+
assert lumen_router_config._looks_like_heavy_query("сравни iphone и samsung") is True
|
| 93 |
+
|
| 94 |
+
|
| 95 |
def test_build_route_youtube_link_forces_gemini_only():
|
| 96 |
route = lumen_router_config._build_route(needs_youtube=True, needs_website=False, media_mime=None, is_heavy=False, needs_freshness=False)
|
| 97 |
assert all(p == "gemini" for p, _ in route)
|