gendesign/backend/app/services/llm/prompts.py
Light1YT 645b3c14a3
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
fix(chat): clean квартирография rendering in chat_system prompt (v2, #957)
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
2026-06-09 15:07:14 +05:00

106 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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"]