Spaces:
Running
Running
SilverElixir commited on
Commit ·
693519a
1
Parent(s): 217e497
Formatting like Claude: one list item per line, prompt rule plus post-fix
Browse files- lumen_formatting.py +26 -3
- system_prompt.py +1 -0
- tests/test_lumen_formatting.py +24 -0
lumen_formatting.py
CHANGED
|
@@ -122,6 +122,24 @@ _BULLET_MARKER_RE = re.compile(r"^([ \t]*)[-*][ \t]+", re.MULTILINE)
|
|
| 122 |
def _normalize_bullet_markers(text: str) -> str:
|
| 123 |
return _BULLET_MARKER_RE.sub(lambda m: m.group(1) + "• ", text)
|
| 124 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
# ── Markdown-заголовки "#"/"##"/"###" → **жирный текст** ────────────────────
|
| 126 |
# Регрессия 18.08.2026: модели пишут ### вопреки промпту. Режем ATX по CommonMark (решётки + пробел), C#/#tag не трогаем.
|
| 127 |
_HEADER_MARKER_RE = re.compile(r"^[ \t]*#{1,6}[ \t]+(.*)$", re.MULTILINE)
|
|
@@ -196,9 +214,10 @@ def _md_to_html(text: str) -> str:
|
|
| 196 |
ДО LaTeX/таблиц/списков/markdown, иначе обратные слэши и "|"/"-" внутри
|
| 197 |
реального кода (regex, пути Windows, побитовое ИЛИ) были бы испорчены.
|
| 198 |
1.3. LaTeX → юникод (_scrub_latex) — код уже вынесен шагом 1.
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
|
|
|
| 202 |
1.45. Markdown-цитаты "> " → сентинелы \x00BQS\x00/\x00BQE\x00 (_convert_
|
| 203 |
blockquotes) — сентинелы, не сразу <blockquote>, т.к. Phase 2 экранировал
|
| 204 |
бы буквальный тег; настоящий тег подставляется после Phase 3 (см. ниже).
|
|
@@ -260,6 +279,10 @@ def _md_to_html(text: str) -> str:
|
|
| 260 |
# чтобы строка-разделитель таблицы ("|---|---|") успела обработаться первой.
|
| 261 |
text = _normalize_bullet_markers(text)
|
| 262 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
# ── Phase 1.41: markdown-заголовки "#"/"##"/"###" → **жирный текст** (см.
|
| 264 |
# _normalize_headers выше) — после списков, до HTML-экранирования и до Phase 3
|
| 265 |
# (получившийся "**...**" обрабатывается обычным bold-регэкспом на общих
|
|
|
|
| 122 |
def _normalize_bullet_markers(text: str) -> str:
|
| 123 |
return _BULLET_MARKER_RE.sub(lambda m: m.group(1) + "• ", text)
|
| 124 |
|
| 125 |
+
# Слипшиеся в один абзац буллеты ("• A ... • B ... • C", прод 22.09.2026: модель
|
| 126 |
+
# написала весь список сравнения в одну строку) — разносим по строкам. Только
|
| 127 |
+
# длинные абзацы с 3+ разделителями: короткие "чай • кофе" — обычная проза, не трогаем.
|
| 128 |
+
_INLINE_BULLETS_MIN_SEPS = 3
|
| 129 |
+
_INLINE_BULLETS_MIN_LEN = 200
|
| 130 |
+
|
| 131 |
+
def _split_inline_bullets(text: str) -> str:
|
| 132 |
+
out = []
|
| 133 |
+
for line in text.split("\n"):
|
| 134 |
+
if len(line) >= _INLINE_BULLETS_MIN_LEN and line.count(" • ") >= _INLINE_BULLETS_MIN_SEPS:
|
| 135 |
+
head, *items = line.split(" • ")
|
| 136 |
+
# Первый кусок — вводная фраза ("Вот моменты:"), дальше — пункты.
|
| 137 |
+
out.append(head.rstrip())
|
| 138 |
+
out.extend("• " + item.strip() for item in items if item.strip())
|
| 139 |
+
else:
|
| 140 |
+
out.append(line)
|
| 141 |
+
return "\n".join(out)
|
| 142 |
+
|
| 143 |
# ── Markdown-заголовки "#"/"##"/"###" → **жирный текст** ────────────────────
|
| 144 |
# Регрессия 18.08.2026: модели пишут ### вопреки промпту. Режем ATX по CommonMark (решётки + пробел), C#/#tag не трогаем.
|
| 145 |
_HEADER_MARKER_RE = re.compile(r"^[ \t]*#{1,6}[ \t]+(.*)$", re.MULTILINE)
|
|
|
|
| 214 |
ДО LaTeX/таблиц/списков/markdown, иначе обратные слэши и "|"/"-" внутри
|
| 215 |
реального кода (regex, пути Windows, побитовое ИЛИ) были бы испорчены.
|
| 216 |
1.3. LaTeX → юникод (_scrub_latex) — код уже вынесен шагом 1.
|
| 217 |
+
1.4. Маркеры списков "-"/"* " → "•" (_normalize_bullet_markers) — ДО таблиц,
|
| 218 |
+
чтобы строка-разделитель таблицы ("|---|---|") успела обработаться первой
|
| 219 |
+
и не была принята за маркер списка.
|
| 220 |
+
1.405. Слипшиеся "• A • B" в одном абзаце → по строкам (_split_inline_bullets).
|
| 221 |
1.45. Markdown-цитаты "> " → сентинелы \x00BQS\x00/\x00BQE\x00 (_convert_
|
| 222 |
blockquotes) — сентинелы, не сразу <blockquote>, т.к. Phase 2 экранировал
|
| 223 |
бы буквальный тег; настоящий тег подставляется после Phase 3 (см. ниже).
|
|
|
|
| 279 |
# чтобы строка-разделитель таблицы ("|---|---|") успела обработаться первой.
|
| 280 |
text = _normalize_bullet_markers(text)
|
| 281 |
|
| 282 |
+
# ── Phase 1.405: слипшиеся "• A • B" в одном абзаце → по строкам (см.
|
| 283 |
+
# _split_inline_bullets выше) — после нормализации маркеров, до таблиц/escape.
|
| 284 |
+
text = _split_inline_bullets(text)
|
| 285 |
+
|
| 286 |
# ── Phase 1.41: markdown-заголовки "#"/"##"/"###" → **жирный текст** (см.
|
| 287 |
# _normalize_headers выше) — после списков, до HTML-экранирования и до Phase 3
|
| 288 |
# (получившийся "**...**" обрабатывается обычным bold-регэкспом на общих
|
system_prompt.py
CHANGED
|
@@ -129,6 +129,7 @@ SYSTEM_PROMPT = (
|
|
| 129 |
"writing HTML tags yourself will not render them; they will show to the user as visible garbage text like '<b>' right in the message. "
|
| 130 |
"Write math in LaTeX markup: inline formulas as $...$ (dollar sign tight against the formula on both sides, with NO spaces right after the opening and before the closing one: '$E=mc^2$', not '$ E $'), block ones as $$...$$ on a new line. Do not format prices that way: '$80 000' or 'from $50 to $100' is plain text, not formulas. "
|
| 131 |
"Mark structure with markdown headings (##, ###) — they render. "
|
|
|
|
| 132 |
"For comparisons and structured data (device specs, pros/cons, timelines, etc.) use real markdown tables: a header row, a dash separator row (|---|---|), data rows: cells are short text, **bold** and `code` work inside cells.\n\n"
|
| 133 |
"PROSE VS LISTS:\n"
|
| 134 |
"For extended explanations, analyses, stories about what is happening, write connected prose in paragraphs; do not break it into bulleted/numbered points — a solid list of points instead of a connected explanation looks like a templated AI answer, not live speech. Format as a list only what the user really asked to enumerate or rank; comparisons across several parameters go into a real markdown table (see the FORMATTING section above). Inside ordinary text, enumerating several things is a plain phrase with commas ('among them are X, Y and Z'), not markers on a new line under each point.\n\n"
|
|
|
|
| 129 |
"writing HTML tags yourself will not render them; they will show to the user as visible garbage text like '<b>' right in the message. "
|
| 130 |
"Write math in LaTeX markup: inline formulas as $...$ (dollar sign tight against the formula on both sides, with NO spaces right after the opening and before the closing one: '$E=mc^2$', not '$ E $'), block ones as $$...$$ on a new line. Do not format prices that way: '$80 000' or 'from $50 to $100' is plain text, not formulas. "
|
| 131 |
"Mark structure with markdown headings (##, ###) — they render. "
|
| 132 |
+
"Lists: one item per line, each starting with '- ' — never join several bullets into a single paragraph. "
|
| 133 |
"For comparisons and structured data (device specs, pros/cons, timelines, etc.) use real markdown tables: a header row, a dash separator row (|---|---|), data rows: cells are short text, **bold** and `code` work inside cells.\n\n"
|
| 134 |
"PROSE VS LISTS:\n"
|
| 135 |
"For extended explanations, analyses, stories about what is happening, write connected prose in paragraphs; do not break it into bulleted/numbered points — a solid list of points instead of a connected explanation looks like a templated AI answer, not live speech. Format as a list only what the user really asked to enumerate or rank; comparisons across several parameters go into a real markdown table (see the FORMATTING section above). Inside ordinary text, enumerating several things is a plain phrase with commas ('among them are X, Y and Z'), not markers on a new line under each point.\n\n"
|
tests/test_lumen_formatting.py
CHANGED
|
@@ -262,6 +262,30 @@ def test_normalize_bullet_markers_does_not_touch_table_separator_row():
|
|
| 262 |
assert lumen_formatting._normalize_bullet_markers(text) == text
|
| 263 |
|
| 264 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
def test_md_to_html_full_pipeline_converts_bullet_list_with_bold():
|
| 266 |
text = "* **Возмездие:** аргумент про справедливость\n* **Сдерживание:** снижает преступность"
|
| 267 |
result = lumen_formatting._md_to_html(text)
|
|
|
|
| 262 |
assert lumen_formatting._normalize_bullet_markers(text) == text
|
| 263 |
|
| 264 |
|
| 265 |
+
def test_split_inline_bullets_splits_long_run_into_lines():
|
| 266 |
+
# Прод 22.09.2026: модель написала весь список сравнения в один абзац через "•".
|
| 267 |
+
items = ["пункт %d с развёрнутым текстом для набора длины абзаца" % i for i in range(5)]
|
| 268 |
+
text = "Вот моменты сравнения характеристик: " + " • ".join(items)
|
| 269 |
+
assert len(text) >= 200
|
| 270 |
+
result = lumen_formatting._split_inline_bullets(text)
|
| 271 |
+
lines = result.split("\n")
|
| 272 |
+
assert lines[0] == "Вот моменты сравнения характеристик: " + items[0]
|
| 273 |
+
assert lines[1:] == ["• " + item for item in items[1:]]
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
def test_split_inline_bullets_ignores_short_prose():
|
| 277 |
+
# Короткие "чай • кофе" и два разделителя — обычная проза, не список.
|
| 278 |
+
assert lumen_formatting._split_inline_bullets("На выбор чай • кофе • сок.") == "На выбор чай • кофе • сок."
|
| 279 |
+
assert lumen_formatting._split_inline_bullets("Плюсы • минусы") == "Плюсы • минусы"
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
def test_md_to_html_splits_inline_bullets_end_to_end():
|
| 283 |
+
items = ["тезис номер %d с подробным раскрытием мысли" % i for i in range(4)]
|
| 284 |
+
text = "Итоги сравнения моделей: " + " • ".join(items)
|
| 285 |
+
result = lumen_formatting._md_to_html(text)
|
| 286 |
+
assert result.count("\n• ") == 3
|
| 287 |
+
|
| 288 |
+
|
| 289 |
def test_md_to_html_full_pipeline_converts_bullet_list_with_bold():
|
| 290 |
text = "* **Возмездие:** аргумент про справедливость\n* **Сдерживание:** снижает преступность"
|
| 291 |
result = lumen_formatting._md_to_html(text)
|