gendesign/backend/app/services/llm/prompts.py
Light1YT 4af7ba5a40
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
feat(llm): foundational LLM infra package with §19 redaction + deterministic fallback (#960)
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
2026-06-08 15:44:16 +05:00

76 lines
3.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=(
"Ты ассистент в системе анализа недвижимости. Отвечай кратко и по делу. "
"Не выдумывай факты — если данных недостаточно, скажи об этом."
),
)
_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"]