From 4e45b8b18192e33b99730244c83fb5e0be595327 Mon Sep 17 00:00:00 2001 From: AR 15 M4 Date: Thu, 23 Apr 2026 10:53:01 +0500 Subject: [PATCH] =?UTF-8?q?feat(sprint2.5):=20=D0=BB=D0=BE=D0=B3=D0=B8,=20?= =?UTF-8?q?=D0=B2=D1=8B=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81?= =?UTF-8?q?=D0=B8=D1=81=D1=82=D0=B5=D0=BC=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=BC=D0=BF=D1=82=D0=B0,=20markdown-=D1=80=D0=B5?= =?UTF-8?q?=D0=BD=D0=B4=D0=B5=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Три подряд доработки по плану Спринта 2.5. 1) Логи. Проблема: uvicorn ставит handlers на root-logger до того, как отработает наш lifespan, поэтому logging.basicConfig там был no-op, и logger.exception ничего не писал. Переносим basicConfig на уровень импорта main.py с force=True — наш StreamHandler перебивает uvicorn-овский root, остальные логгеры (uvicorn.access, uvicorn.error, alembic, chromadb) остаются со своими форматами. В lifespan basicConfig больше не зовётся. 2) Системный промпт вынесен из services/llm_client.py в prompts/system_prompt.md. LLMClient читает файл при импорте модуля через _load_system_prompt(); если файла нет — пустая строка + warning. Это задел под Спринт 3, где промпт будет редактируемым и версионируемым — физически положить его как файл дешевле, чем держать в исходниках. 3) Markdown в ответах ассистента. Подключены marked и DOMPurify с CDN в sandbox.html. Рендер через renderMd(text): marked.parse + DOMPurify.sanitize — защищает от + @@ -398,6 +436,16 @@ function toast(msg, kind = "ok") { setTimeout(() => t.className = "toast", 2500); } +function renderMd(text) { + try { + marked.setOptions({ breaks: true, gfm: true }); + const html = marked.parse(text || ""); + return DOMPurify.sanitize(html); + } catch (e) { + return esc(text); + } +} + async function api(path, opts = {}) { const res = await fetch(API + path, opts); if (!res.ok) { @@ -491,12 +539,16 @@ function renderMessages(messages) { box.innerHTML = '
Пусто. Напишите первую реплику.
'; return; } - box.innerHTML = messages.map(m => ` -
- ${esc(m.text)} -
${esc(fmtDate(m.created_at))}
-
- `).join(""); + box.innerHTML = messages.map(m => { + const isUser = m.role === "user"; + const body = isUser ? esc(m.text) : renderMd(m.text); + return ` +
+
${body}
+
${esc(fmtDate(m.created_at))}
+
+ `; + }).join(""); box.scrollTop = box.scrollHeight; } @@ -505,8 +557,10 @@ function appendMessage(role, text, iso) { const empty = box.querySelector(".chat-empty"); if (empty) empty.remove(); const div = document.createElement("div"); - div.className = "msg " + (role === "user" ? "user" : "assistant"); - div.innerHTML = esc(text) + `
${esc(fmtDate(iso || new Date().toISOString()))}
`; + const isUser = role === "user"; + div.className = "msg " + (isUser ? "user" : "assistant"); + const body = isUser ? esc(text) : renderMd(text); + div.innerHTML = `
${body}
${esc(fmtDate(iso || new Date().toISOString()))}
`; box.appendChild(div); box.scrollTop = box.scrollHeight; return div;