feat(sprint2.5): логи, вынесение системного промпта, markdown-рендер

Три подряд доработки по плану Спринта 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 — защищает от <script> на случай, если LLM вернёт
сырой HTML. Реплики пациента остаются plain text (esc). Добавлены
стили для p/ul/ol/code/pre/a/h1-h3/blockquote внутри .msg.assistant,
чтобы всё выглядело уместно в пузыре. Обёртка msg-body введена,
чтобы разделить контент и msg-meta.

План в SPRINTS.md уточнён по переиндексации — будет отдельный
endpoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
AR 15 M4
2026-04-23 10:53:01 +05:00
parent 835c0b3cc3
commit 4e45b8b181
5 changed files with 104 additions and 23 deletions
+11 -9
View File
@@ -1,4 +1,5 @@
import logging
from pathlib import Path
import httpx
@@ -6,17 +7,18 @@ from config import settings
logger = logging.getLogger(__name__)
DEFAULT_SYSTEM_PROMPT = """Ты — виртуальный ассистент клиники, который первым отвечает пациентам в чате.
SYSTEM_PROMPT_PATH = Path(__file__).resolve().parent.parent / "prompts" / "system_prompt.md"
Твоя задача — помочь пациенту по бытовым и организационным вопросам: запись, расписание врачей, подготовка к приёму, как проехать, документы, оплата, ДМС, детский приём и т. п.
Правила:
- Отвечай коротко, дружелюбно, на «вы», простым русским языком без медицинской латыни.
- Опирайся ТОЛЬКО на предоставленные выдержки из базы знаний. Если ответа в них нет — честно скажи, что уточнишь у оператора, и предложи подключить оператора.
- Не ставь диагнозы и не назначай лечение. Если вопрос про симптомы, лекарства, дозировки или «что со мной» — мягко предложи записаться к врачу и подключить оператора, если нужно.
- Не выдумывай телефоны, адреса, цены, имена врачей, расписание. Только из источников.
- Если пациент просит оператора — коротко подтверди, что сейчас его подключишь.
- Источники указывать не нужно: пациент их не видит. Ответ — обычный текст, как в чате."""
def _load_system_prompt() -> str:
try:
return SYSTEM_PROMPT_PATH.read_text(encoding="utf-8").strip()
except FileNotFoundError:
logger.warning("System prompt file not found at %s — using empty prompt", SYSTEM_PROMPT_PATH)
return ""
DEFAULT_SYSTEM_PROMPT = _load_system_prompt()
DEFAULT_USER_TEMPLATE = """Вопрос пациента:
{question}