All checks were successful
CI / changes (pull_request) Successful in 5s
CI / backend-tests (push) Successful in 6m25s
CI / backend-tests (pull_request) Successful in 6m20s
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m38s
Deploy / build-worker (push) Successful in 2m42s
Deploy / deploy (push) Successful in 1m13s
CI / changes (push) Successful in 5s
CI / frontend-tests (push) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
Optional external-OpenAI layer over the deterministic forecasting engine. Gated by llm_enabled (default False) so prod makes no network calls until deliberately enabled. Allowlist-first SafePayload contract + is_confidential hard-block + RU-PII regex scrub (mandatory on the external path). Abstract LLMProvider seam (is_external) for a future RU-hosted provider. Sync httpx core (Celery-friendly); tool/function-calling pass-through; timeout + bounded 429/5xx retry + per-request call cap, all degrading to fallback. Raw httpx (no openai SDK -> no pyproject/lock drift). 47 tests, ruff + mypy clean. Refs #960
76 lines
3.2 KiB
Python
76 lines
3.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=(
|
||
"Ты ассистент в системе анализа недвижимости. Отвечай кратко и по делу. "
|
||
"Не выдумывай факты — если данных недостаточно, скажи об этом."
|
||
),
|
||
)
|
||
|
||
_TEMPLATES: dict[str, PromptTemplate] = {
|
||
_SYSTEM_BASE.name: _SYSTEM_BASE,
|
||
}
|
||
|
||
|
||
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"]
|