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:
+11
-9
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user