All checks were successful
CI / frontend-tests (push) Has been skipped
CI / changes (push) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Successful in 6m24s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 3m5s
Deploy / deploy (push) Successful in 1m49s
CI / backend-tests (push) Successful in 6m24s
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 2m8s
Live chat rendered the recommended unit-mix as a garbled rank-hyphen-type
list ("2-1-к", "3-2-к" = "rank 2: 1-к"). Add rule 6 to chat_system@v2:
list квартирография/segments by clean TYPE (студия / 1-к / 2-к / 3-к /
80+ м²) + share %, no ordinal-number-hyphen-type concatenation. Bumped
prompt version 1->2 (versioned-prompt convention). Test locks v2 + the rule.
Refs #957
106 lines
6.2 KiB
Python
106 lines
6.2 KiB
Python
"""Minimal versioned prompt-template scaffolding (#960).
|
||
|
||
Полная библиотека промптов приедет вместе с консьюмерами (#956 граддок-extraction,
|
||
#957 chat). Здесь — только каркас: версионированные шаблоны + ``render`` с явной
|
||
подстановкой переменных. Шаблоны намеренно НЕ содержат данных — данные вставляются
|
||
вызывающим в ``SafePayload`` (см. redaction), а не зашиваются в prompt-литералы.
|
||
|
||
Версионирование: ключ шаблона включает версию (``name@vN``), чтобы изменение
|
||
формулировки было трассируемым (важно для воспроизводимости LLM-вывода).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass
|
||
|
||
|
||
class PromptNotFoundError(KeyError):
|
||
"""Запрошен неизвестный template_name."""
|
||
|
||
|
||
@dataclass(frozen=True, slots=True)
|
||
class PromptTemplate:
|
||
"""Версионированный шаблон. ``required_vars`` валидируется в render."""
|
||
|
||
name: str
|
||
version: int
|
||
template: str
|
||
required_vars: tuple[str, ...] = ()
|
||
|
||
@property
|
||
def key(self) -> str:
|
||
return f"{self.name}@v{self.version}"
|
||
|
||
|
||
# Системный шаблон-заглушка. Без данных — только инструкция-роль. Консьюмеры
|
||
# добавят свои предметные шаблоны (extraction-schema / chat-system) здесь же.
|
||
_SYSTEM_BASE = PromptTemplate(
|
||
name="system_base",
|
||
version=1,
|
||
template=(
|
||
"Ты ассистент в системе анализа недвижимости. Отвечай кратко и по делу. "
|
||
"Не выдумывай факты — если данных недостаточно, скажи об этом."
|
||
),
|
||
)
|
||
|
||
# Системный шаблон чата по §22-форсайту (#957, Step 2). Версионируется (@vN) — изменение
|
||
# формулировки трассируемо. БЕЗ данных: данные приходят секционными tool'ами в SafePayload
|
||
# (см. services/chat/{tools,safe_payload}.py), НЕ зашиваются в литерал промпта.
|
||
_CHAT_SYSTEM = PromptTemplate(
|
||
name="chat_system",
|
||
version=2,
|
||
template=(
|
||
"Ты — ассистент по инвестиционному форсайт-отчёту земельного участка (РФ). "
|
||
"Отвечай на русском языке, по-деловому, нейтрально, без маркетинга и без emoji.\n\n"
|
||
"ЖЁСТКИЕ ПРАВИЛА:\n"
|
||
"1. Отвечай ТОЛЬКО на основе данных, полученных через инструменты (секции отчёта). "
|
||
"Чтобы получить нужные числа, вызови подходящий инструмент.\n"
|
||
"2. НИКОГДА не выдумывай числа, классы, доли или выводы. Если в полученной секции "
|
||
"данных нет (или помечено available=false) — честно скажи, что этих данных в "
|
||
"отчёте нет. Не подставляй правдоподобные значения.\n"
|
||
"3. Все числа в ответе бери ВЕРБАТИМ из секций инструментов, ничего не пересчитывай.\n"
|
||
"4. Тон советующий: отчёт помогает принять решение, но НЕ является основанием для "
|
||
"инвестиционного решения. Не давай гарантий доходности.\n"
|
||
"5. Вопросы вне отчёта по участку (градостроительная документация / ПЗЗ-разрешения, "
|
||
"сравнение с другими участками, юридические заключения) — вежливо скажи, что это вне "
|
||
"области отчёта, и предложи вопросы по самому форсайту.\n"
|
||
"6. При перечислении квартирографии / сегментов указывай ТИП понятно "
|
||
"(студия, 1-к, 2-к, 3-к, евро-форматы, 80+ м²) и долю в % если она есть. "
|
||
"НЕ нумеруй порядковыми номерами и НЕ склеивай номер с типом через дефис "
|
||
"(никаких «2-1-к»); перечисляй по одному типу в строке или через запятую.\n"
|
||
"Отвечай кратко и по существу заданного вопроса."
|
||
),
|
||
)
|
||
|
||
_TEMPLATES: dict[str, PromptTemplate] = {
|
||
_SYSTEM_BASE.name: _SYSTEM_BASE,
|
||
_CHAT_SYSTEM.name: _CHAT_SYSTEM,
|
||
}
|
||
|
||
|
||
def get_template(name: str) -> PromptTemplate:
|
||
"""Вернуть шаблон по имени (последняя версия). PromptNotFoundError если нет."""
|
||
tpl = _TEMPLATES.get(name)
|
||
if tpl is None:
|
||
raise PromptNotFoundError(name)
|
||
return tpl
|
||
|
||
|
||
def render(template_name: str, **vars: object) -> str:
|
||
"""Отрендерить шаблон по имени с подстановкой переменных.
|
||
|
||
Использует ``str.format`` (не f-string/concat) — placeholder'ы вида ``{var}``.
|
||
Проверяет, что все ``required_vars`` переданы.
|
||
|
||
Raises:
|
||
PromptNotFoundError: неизвестное имя.
|
||
KeyError: отсутствует обязательная переменная или placeholder без значения.
|
||
"""
|
||
tpl = get_template(template_name)
|
||
missing = [v for v in tpl.required_vars if v not in vars]
|
||
if missing:
|
||
raise KeyError(f"prompt '{tpl.key}' missing required vars: {', '.join(missing)}")
|
||
return tpl.template.format(**vars)
|
||
|
||
|
||
__all__ = ["PromptNotFoundError", "PromptTemplate", "get_template", "render"]
|