Шаблоны и версии промптов¶
Шаг 1¶
В прошлом разделе мы сформулировали developer-инструкцию AI Support Desk и проверили ее на разных обращениях. Сейчас она хранится в app/support_service.py как обычная строковая константа:
Для одной неизменяемой инструкции этого достаточно, и не каждую строку с промптом нужно сразу превращать в отдельный класс или подключать к внешней системе.
Представим, что приложение должно формировать резюме на выбранном языке, например, после добавления мультиязычности, а в некоторых окружениях разработчик включает дополнительное правило. Если собирать инструкцию через конкатенацию строк в SupportService, вскоре станет трудно понять:
- какие переменные обязательны
- какие можно не передавать
- что произойдет при опечатке в названии переменной
- какая версия инструкции участвовала в запросе
- где заканчивается шаблон и начинается пользовательский текст
Для решения этой задачи выполним еще один рефакторинг кода и создадим небольшой типизированный PromptTemplate. Он будет хранить текст промпта, его идентификатор и версию (об этом подробно поговорим в следующем шаге), проверять переменные и возвращать готовую строку.
Что такое шаблон промпта?¶
Шаблон промпта — это текст с заранее определенными местами для подстановки значений. Например:
$response_language является переменной шаблона. При сборке запроса приложение заменит ее значением:
Сам шаблон принадлежит разработчику и хранится вместе с кодом. Значения его переменных также должны поступать из контролируемой части приложения: конфигурации, аргументов сервиса или другого проверенного источника.
Обращение клиента не нужно вставлять внутрь developer-инструкции. Оно по-прежнему передается отдельным сообщением user и отделяется тегами customer_request. Шаблон помогает собирать постоянные правила, а не смешивает правила с недоверенными данными. В нашем случае недоверенные данные — это текст, который приходит от пользователя или из внешнего источника и может содержать ошибки, неожиданный текст или попытку повлиять на поведение модели.
Переменные шаблона мы разделим на две группы:
| Тип переменной | Поведение | Пример |
|---|---|---|
| Обязательная | Без значения промпт нельзя собрать | response_language |
| Необязательная | При отсутствии заменяется пустой строкой | additional_rules |
Необязательная переменная должна находиться в таком месте шаблона, где пустая строка не разрушит смысл инструкции. Например, отдельная строка с дополнительными правилами может отсутствовать. А язык ответа в нашем шаблоне обязателен: молча оставлять его пустым нельзя.
В каталоге app создайте пакет prompts и пустой файл __init__.py:
ai-support-desk/
├── app/
│ ├── prompts/
│ │ ├── __init__.py
│ │ ├── template.py
│ │ └── support_summary.py
│ ├── support_service.py
│ └── ...
└── main.py
template.py будет содержать универсальный механизм подстановки. В support_summary.py поместим конкретный промпт нашей AI-функции. Такое разделение не смешивает общий инструмент с правилами службы поддержки.
Создайте app/prompts/template.py:
from dataclasses import dataclass
from string import Template
class PromptTemplateError(ValueError):
pass
@dataclass(frozen=True)
class PromptTemplate:
prompt_id: str
version: str
body: str
required_variables: frozenset[str]
optional_variables: frozenset[str] = frozenset()
def __post_init__(self) -> None:
if not self.prompt_id.strip():
raise PromptTemplateError("Идентификатор промпта не должен быть пустым.")
if not self.version.strip():
raise PromptTemplateError("Версия промпта не должна быть пустой.")
duplicated = self.required_variables & self.optional_variables
if duplicated:
names = ", ".join(sorted(duplicated))
raise PromptTemplateError(
f"Переменные одновременно обязательные "
f"и необязательные: {names}."
)
template = Template(self.body)
if not template.is_valid():
raise PromptTemplateError("Текст содержит некорректную переменную шаблона.")
placeholders = frozenset(template.get_identifiers())
declared = self.required_variables | self.optional_variables
if placeholders != declared:
expected = ", ".join(sorted(declared)) or "нет"
actual = ", ".join(sorted(placeholders)) or "нет"
raise PromptTemplateError(
"Переменные body не совпадают с объявленными. "
f"Объявлены: {expected}. Найдены: {actual}."
)
def render(self, **variables: str) -> str:
provided = frozenset(variables)
missing = self.required_variables - provided
if missing:
names = ", ".join(sorted(missing))
raise PromptTemplateError(f"Не переданы обязательные переменные: {names}.")
allowed = self.required_variables | self.optional_variables
unexpected = provided - allowed
if unexpected:
names = ", ".join(sorted(unexpected))
raise PromptTemplateError(f"Переданы неизвестные переменные: {names}.")
invalid = sorted(
name
for name, value in variables.items()
if not isinstance(value, str)
)
if invalid:
names = ", ".join(invalid)
raise PromptTemplateError(f"Значения должны быть строками: {names}.")
values = {name: "" for name in self.optional_variables}
values.update(variables)
return Template(self.body).substitute(values).strip()
Класс является неизменяемым благодаря frozen=True. После создания нельзя случайно заменить его идентификатор, версию или текст.
Метод __post_init__() выполняется сразу после создания объекта. Он проверяет:
- непустые идентификатор и версию
- отсутствие переменной сразу в двух группах
- корректный синтаксис шаблона
- совпадение переменных в тексте с объявленными переменными
Метод render() проверяет конкретный набор значений. Обязательные переменные должны присутствовать, неизвестные запрещены, а необязательные при отсутствии получают пустую строку.
При возврате значения мы используем встроенный метод Template.substitute(). В Python есть еще метод Template.safe_substitute(), и его название может показаться надежнее, но он оставляет неизвестные переменные в итоговой строке. А для нашего приложения полезнее сразу получить понятную ошибку, чем отправить модели незаполненный $response_language.
Класс Template из стандартного модуля string использует переменные вида $name:
from string import Template
template = Template(body)
rendered = template.substitute(
response_language="русском",
additional_rules="",
)
substitute() разбирает переменные, находящиеся в body, и вставляет переданные значения. Если нужного значения нет, метод завершится с ошибкой, а не отправит модели незаполненный шаблон.
Вставленное значение не обрабатывается как второй шаблон. Например:
template = Template("Правило: $additional_rules")
rendered = template.substitute(additional_rules="Цена должна оставаться $100.")
В результате символ $ перед числом останется обычной частью значения:
Template ищет переменные только в исходном body. Он не берет уже вставленную строку и не запускает подстановку внутри нее повторно.
Теперь создайте app/prompts/support_summary.py и перенесите промпт резюмирования:
from app.prompts.template import PromptTemplate
SUPPORT_SUMMARY_PROMPT = PromptTemplate(
prompt_id="support_summary",
version="1.0.0",
body="""
Ты помогаешь оператору службы поддержки кратко пересказывать
обращения клиентов.
Язык ответа: $response_language.
Цель:
Сформулируй основную проблему клиента одним предложением.
Правила:
- Используй только факты, явно указанные в обращении.
- Если они указаны, сохраняй номер заказа, дату и название товара.
- Не добавляй советы, решения, обещания и оценки.
- Если деталь отсутствует или неясна, опусти ее.
$additional_rules
Порядок работы:
1. Определи основную проблему клиента.
2. Выбери только факты, необходимые для понимания проблемы.
3. Удали приветствия, повторы и второстепенные подробности.
4. Сформулируй итоговый ответ в требуемом формате.
Формат ответа:
Одно самостоятельное предложение без заголовка, списка и вступления.
""".strip(),
required_variables=frozenset({"response_language"}),
optional_variables=frozenset({"additional_rules"}),
)
prompt_id является стабильным именем сценария, а version обозначает конкретный вариант его поведения. Подробно разберем версию в следующем шаге. Обратите внимание: в тексте есть обязательная переменная $response_language и необязательная $additional_rules. Обе перечислены в метаданных объекта, поэтому опечатка обнаружится при запуске приложения.
В заключение шага подключим шаблон к SupportService. В app/support_service.py добавьте импорт:
Удалите строковую константу DEVELOPER_INSTRUCTION и вместо нее соберите инструкцию из шаблона:
Необязательную переменную additional_rules мы не передаем, поэтому она заменится пустой строкой. Если позднее появится контролируемое правило для конкретного окружения, его можно передать явно:
DEVELOPER_INSTRUCTION = SUPPORT_SUMMARY_PROMPT.render(
response_language="русском",
additional_rules=(
"- Не раскрывай внутренние названия подразделений."
),
)
Сейчас дополнительного требования у проекта нет, поэтому оставьте первый вариант. Не добавляйте переменную только ради того, чтобы заполнить каждое доступное место.
И теперь шаблон собирает developer-инструкцию только из контролируемых значений, а обращение добавляется отдельным сообщением user.
Проверим результат работы. Запустите проект и передайте уже знакомое обращение:
Поведение функции не должно заметно измениться. Мы не поменяли назначение промпта, а только вынесли его в отдельный объект и явно передали язык ответа.
Отдельно вызывать каждую ошибку сейчас не будем. Важно зафиксировать контракт:
- без
response_languageметодrender()завершится ошибкой - переменная
languageвместоresponse_languageбудет неизвестной additional_rulesможно не передавать- обращение клиента не является переменной этого шаблона
Теперь текст промпта отделен от сервиса, а его сборка стала проверяемой. В следующем шаге мы свяжем идентификатор и версию промпта с результатом запроса, разберем хранение промптов в проекте и определим, когда обычных файлов уже недостаточно.
Шаг 2¶
В прошлом шаге мы создали SUPPORT_SUMMARY_PROMPT с идентификатором support_summary и версией 1.0.0. Пока эти метаданные находятся только внутри объекта промпта, а сервис использует его текст для сборки developer-инструкции. Если заменить текст в support_summary.py и написать version="1.1.0", старая инструкция исчезнет из рабочего проекта. В сохраненных метриках останется название 1.0.0, но получить соответствующий ему текст уже не получится.
В этом шаге мы исправим это и сохраним каждый выпущенный вариант промпта отдельно, создадим реестр и научимся явно выбирать активную или старую версию.
Для чего вообще хранить версии промпта?¶
Промпт является частью поведения AI-функции. Один и тот же пользовательский текст может получить другой ответ после изменения:
- developer-инструкции
- модели
- параметров генерации
- переданного контекста
Если мы сохраняем название модели, но не сохраняем точный вариант промпта, воспроизвести старый запрос полностью нельзя.
| Ситуация | Зачем нужна сохраненная версия |
|---|---|
| После обновления ответы стали хуже | Запустить прежний и новый промпт на одинаковых обращениях |
| В рабочем запросе найден неправильный ответ | По prompt_version получить точный текст использованной инструкции |
| Новая версия не прошла проверочные сценарии | Вернуть предыдущий вариант без восстановления текста вручную |
| Результаты двух проверок отличаются | Убедиться, что сравнивались одинаковые или разные версии промпта |
| Нужно объяснить поведение старого запуска | Увидеть действовавшие тогда правила, а не только текущую инструкцию |
Версионирование не делает ответы модели одинаковыми и не гарантирует качество. Оно делает изменения наблюдаемыми и воспроизводимыми, ведь мы можем назвать вариант, получить его текст и снова проверить.
Для нашего проекта версия состоит из трех связанных частей:
prompt_idотвечает на вопрос «какая это AI-функция?»versionотвечает на вопрос «какой вариант этой функции?»bodyхранит точные правила и переменные выбранного варианта
Для support_summary может существовать несколько версий:
У обеих одинаковое назначение — резюмировать обращение. Но текст правил отличается. Если назначение изменится полностью, например, вместо резюме мы начнем писать ответ клиенту, это будет уже другой prompt_id, а не очередная версия support_summary. После выпуска файл версии будем считать неизменяемым. Чтобы изменить поведение, создаем новый файл с новым номером, а старый оставляем доступным для повторной проверки.
Возникает практический вопрос: где хранить версии? Основные варианты выглядят так:
| Способ | Когда подходит | Особенности |
|---|---|---|
| Отдельные файлы в проекте | Промптов немного, их выпускают вместе с приложением | Просто читать, импортировать, проверять и возвращать старую версию |
| База данных или объектное хранилище | Промпты обновляются независимо от приложения | Нужны схема данных, права доступа, кэш и обработка недоступности |
| Prompt сервис | Много команд, экспериментов, согласований и вариантов | Дает интерфейс управления, но добавляет внешнюю зависимость |
Для нашего проекта AI Support Desk выберем отдельные Python-файлы. У нас один промпт резюмирования и две версии, поэтому внешнее хранилище пока не дает практической пользы.
В прошлом шаге наш промпт находился в одном файле:
Теперь удалите этот файл и вместо него создайте пакет app/prompts/support_summary:
app/
└── prompts/
├── __init__.py
├── template.py
└── support_summary/
├── __init__.py
├── registry.py
├── v1_0_0.py
└── v1_1_0.py
Название файла повторяет номер версии, только точки заменены символами подчеркивания. Каждый файл содержит один законченный PromptTemplate. А уже registry.py будет связывать номера версий с объектами и определять активный вариант.
Создайте app/prompts/support_summary/v1_0_0.py. Перенесите туда первый вариант промпта, изменив только имя переменной:
from app.prompts.template import PromptTemplate
SUPPORT_SUMMARY_PROMPT_V1_0_0 = PromptTemplate(
prompt_id="support_summary",
version="1.0.0",
body="""
Ты помогаешь оператору службы поддержки кратко пересказывать
обращения клиентов.
Язык ответа: $response_language.
Цель:
Сформулируй основную проблему клиента одним предложением.
Правила:
- Используй только факты, явно указанные в обращении.
- Если они указаны, сохраняй номер заказа, дату и название товара.
- Не добавляй советы, решения, обещания и оценки.
- Если деталь отсутствует или неясна, опусти ее.
$additional_rules
Порядок работы:
1. Определи основную проблему клиента.
2. Выбери только факты, необходимые для понимания проблемы.
3. Удали приветствия, повторы и второстепенные подробности.
4. Сформулируй итоговый ответ в требуемом формате.
Формат ответа:
Одно самостоятельное предложение без заголовка, списка и вступления.
""".strip(),
required_variables=frozenset({"response_language"}),
optional_variables=frozenset({"additional_rules"}),
)
Этот файл является снимком первой версии. Если позднее обнаружится, что правило сформулировано неудачно, не исправляйте v1_0_0.py, а лучше создайте следующую версию. Иначе старые результаты с отметкой 1.0.0 начнут ссылаться уже на другой текст.
Во время проверки инструкции мы обнаружили пограничный случай: если обращение содержит две равнозначные проблемы, модель может сохранить только одну из них.
Давайте добавим явное правило и выпустим новый вариант промпта.
Создайте app/prompts/support_summary/v1_1_0.py:
from app.prompts.template import PromptTemplate
SUPPORT_SUMMARY_PROMPT_V1_1_0 = PromptTemplate(
prompt_id="support_summary",
version="1.1.0",
body="""
Ты помогаешь оператору службы поддержки кратко пересказывать
обращения клиентов.
Язык ответа: $response_language.
Цель:
Сформулируй основную проблему клиента одним предложением.
Правила:
- Используй только факты, явно указанные в обращении.
- Если они указаны, сохраняй номер заказа, дату и название товара.
- Если проблем несколько и они равнозначны, сохрани каждую.
- Не добавляй советы, решения, обещания и оценки.
- Если деталь отсутствует или неясна, опусти ее.
$additional_rules
Порядок работы:
1. Определи основную проблему клиента.
2. Выбери только факты, необходимые для понимания проблемы.
3. Удали приветствия, повторы и второстепенные подробности.
4. Сформулируй итоговый ответ в требуемом формате.
Формат ответа:
Одно самостоятельное предложение без заголовка, списка и вступления.
""".strip(),
required_variables=frozenset({"response_language"}),
optional_variables=frozenset({"additional_rules"}),
)
Между промптами есть одно содержательное отличие:
Инструкция продублирована намеренно: каждый файл версии должен содержать полный текст и не зависеть от других файлов. Благодаря этому версию 1.0.0 можно прочитать и использовать отдельно, даже если новые варианты промпта позднее изменятся.
Теперь создадим реестр промптов. Два файла уже хранят старый и новый тексты, а реестр даст приложению единый способ получить нужный вариант по номеру. Создайте app/prompts/support_summary/registry.py:
from app.prompts.template import (
PromptTemplate,
PromptTemplateError,
)
from app.prompts.support_summary.v1_0_0 import (
SUPPORT_SUMMARY_PROMPT_V1_0_0,
)
from app.prompts.support_summary.v1_1_0 import (
SUPPORT_SUMMARY_PROMPT_V1_1_0,
)
ACTIVE_SUPPORT_SUMMARY_VERSION = "1.1.0"
_SUPPORT_SUMMARY_PROMPTS: dict[str, PromptTemplate] = {
SUPPORT_SUMMARY_PROMPT_V1_0_0.version: SUPPORT_SUMMARY_PROMPT_V1_0_0,
SUPPORT_SUMMARY_PROMPT_V1_1_0.version: SUPPORT_SUMMARY_PROMPT_V1_1_0,
}
def get_support_summary_prompt(
version: str | None = None,
) -> PromptTemplate:
selected_version = (
ACTIVE_SUPPORT_SUMMARY_VERSION
if version is None
else version.strip()
)
try:
return _SUPPORT_SUMMARY_PROMPTS[selected_version]
except KeyError as error:
available = ", ".join(_SUPPORT_SUMMARY_PROMPTS)
raise PromptTemplateError(
f"Неизвестная версия support_summary: "
f"{selected_version}. "
f"Доступны: {available}."
) from error
def list_support_summary_versions() -> tuple[str, ...]:
return tuple(_SUPPORT_SUMMARY_PROMPTS)
Реестр выполняет три задачи:
- хранит соответствие «номер версии → объект промпта»
- обозначает активную версию
- не позволяет молча выбрать несуществующий вариант
Если вызвать get_support_summary_prompt() без аргумента, функция вернет последнюю активную версию 1.1.0. Если передать "1.0.0", она вернет сохраненный промпт этой версии. Неизвестный номер приведет к понятной PromptTemplateError.
Теперь настроим публичный импорт пакета, для этого в app/prompts/support_summary/__init__.py добавьте:
from app.prompts.support_summary.registry import (
ACTIVE_SUPPORT_SUMMARY_VERSION,
get_support_summary_prompt,
list_support_summary_versions,
)
SUPPORT_SUMMARY_PROMPT = get_support_summary_prompt()
__all__ = [
"ACTIVE_SUPPORT_SUMMARY_VERSION",
"SUPPORT_SUMMARY_PROMPT",
"get_support_summary_prompt",
"list_support_summary_versions",
]
Строка:
выбирает активный вариант при запуске приложения. Благодаря публичному имени прежний импорт в app/support_service.py не меняется:
Сервис продолжает собирать инструкцию через:
Но теперь SUPPORT_SUMMARY_PROMPT это не единственный навсегда перезаписываемый объект, а выбранная запись из реестра сохраненных версий.
Теперь давайте добавим версию в результат сервиса, для этого в app/support_service.py изменим:
@dataclass(frozen=True)
class SupportSummary:
text: str
model: str
prompt_id: str
prompt_version: str
elapsed_seconds: float
prompt_tokens: int | None
completion_tokens: int | None
total_tokens: int | None
response_id: str
При создании результата добавьте значения из фактически выбранного объекта промпта:
return SupportSummary(
text=summary,
model=llm_result.model,
prompt_id=SUPPORT_SUMMARY_PROMPT.prompt_id,
prompt_version=SUPPORT_SUMMARY_PROMPT.version,
elapsed_seconds=elapsed_seconds,
prompt_tokens=llm_result.prompt_tokens,
completion_tokens=llm_result.completion_tokens,
total_tokens=llm_result.total_tokens,
response_id=llm_result.response_id,
)
Теперь откроем app/console.py, и добавим новый вывод перед выводом модели ответа, чтобы видеть, какая версия промпта использовалась:
Запустите проект. Сейчас активная версия выбирается одной константой в registry.py:
Поэтому в выводе появится:
То есть сейчас выбор выполняется одной константой в registry.py. Чтобы временно вернуть старый вариант, можно выбрать:
После перезапуска приложения SUPPORT_SUMMARY_PROMPT будет указывать на сохраненный объект 1.0.0, а метрики снова покажут эту версию.
В заключение раздела зафиксируем правила:
- Не изменять файл версии, которая уже использовалась в запросах.
- Для содержательного изменения создавать новый файл и новый номер.
- Добавлять новую версию в реестр до выбора ее активной.
- Проверять новый вариант на повторяемом наборе обращений.
- Сохранять старые версии, пока на них ссылаются результаты, тесты или отчеты.
- Не хранить в файлах промптов секреты и пользовательские обращения.
Не обязательно хранить все экспериментальные черновики бесконечно. Версии, которые никогда не выпускались и не участвовали в сохраненных проверках, можно удалять. Но выпущенный вариант нельзя незаметно заменить другим текстом под тем же номером.
Файлы и реестр хорошо работают, пока:
- промптов и версий немного
- их меняют разработчики
- новая активная версия выпускается вместе с приложением
- не нужны одновременные варианты для разных групп пользователей
- не требуется редактирование через отдельный интерфейс
Внешнее хранилище или промпт сервис становятся полезными, когда нужно менять активную версию без выпуска приложения, проводить A/B-тесты, согласовывать тексты несколькими командами или централизованно управлять большим количеством AI-функций. При переходе во внешнее хранилище основные понятия не меняются. Все равно нужны prompt_id, неизменяемая version, реестр доступных вариантов и запись выбранной версии рядом с результатом запроса.
Мы завершили тему управления промптами, и до этого момента мы рассматривали независимые запросы, где каждый вызов модели выполняется сам по себе. В следующем разделе мы перейдем к следующему этапу — работе с диалогами. Разберем состояние диалога, Session ID, хранение истории и изоляцию сообщений разных пользователей.
Шаг 3¶
Зачем выносить изменяемые части инструкции в переменные шаблона?
Правильный ответ
- ✅ Чтобы один проверенный текст можно было безопасно использовать с разными значениями
Шаг 4¶
Какие проверки нужны объекту PromptTemplate перед рендерингом?
Правильный ответ
- ✅ При рендеринге не переданы неизвестные переменные
- ✅ Все переменные из текста объявлены в контракте шаблона
- ✅ Обязательные и необязательные переменные не пересекаются
- ✅ При рендеринге переданы все обязательные переменные
Шаг 5¶
В значение переменной additional_rules передали строку «Цена должна оставаться $100». Что произойдёт при Template.substitute()?
Правильный ответ
- ✅ Строка подставится целиком, а знак $ внутри значения не будет обработан повторно
Шаг 6¶
Зачем хранить выпущенные версии промпта и записывать prompt_id@version в результат?
Правильный ответ
- ✅ Чтобы быстро вернуть прежнюю активную версию
- ✅ Чтобы связать сохранённые метрики с точным текстом инструкции
- ✅ Чтобы сравнивать новую и предыдущую версии на одинаковых сценариях
- ✅ Чтобы воспроизвести инструкцию, использованную в старом запуске
Шаг 7¶
Рендеринг шаблона инструкции¶
Текст промпта уже подготовлен в константе PROMPT_BODY. Подставьте язык ответа и необязательное дополнительное правило.
Задача¶
Реализуйте render_support_prompt(response_language, additional_rules="").
Требования¶
response_languageиadditional_rulesдолжны быть строками. Для другого типа выбросьтеTypeError.- Очистите оба значения через
strip(). - Пустой
response_languageдолжен приводить кValueError. additional_rulesможет быть пустым.- Создайте
Template(PROMPT_BODY)и вызовитеsubstitute()с обеими переменными. - Верните итоговую инструкцию после
strip(). - Не изменяйте
PROMPT_BODY.
Пример результата¶
render_support_prompt(" русском ")
# "Язык ответа: русском.\n\nПравила:"
render_support_prompt("русском", "- Не раскрывай внутренние названия.")
# дополнительное правило появится после строки "Правила:"
Импорт Template, константа PROMPT_BODY и заготовка функции уже находятся в редакторе. Не изменяйте значение PROMPT_BODY.
Правильный ответ
from string import Template
PROMPT_BODY = (
"\nЯзык ответа: $response_language.\n\n"
"Правила:\n"
"$additional_rules\n"
)
def render_support_prompt(
response_language: str,
additional_rules: str = "",
) -> str:
if not isinstance(response_language, str) or not isinstance(additional_rules, str):
raise TypeError
response_language = response_language.strip()
additional_rules = additional_rules.strip()
if not response_language:
raise ValueError
template = Template(PROMPT_BODY)
return template.substitute(
response_language=response_language,
additional_rules=additional_rules,
).strip()
Шаг 8¶
Проверка контракта переменных шаблона¶
Перед использованием промпта нужно убедиться, что переменные в его тексте совпадают с объявленным контрактом. В этой задаче вы отработаете предсказуемую работу с шаблонами и версиями промптов.
Задача¶
Реализуйте validate_template_variables(body, required, optional). Функция должна проверить шаблон и вернуть найденные имена переменных.
Требования¶
bodyдолжен быть строкой. Для другого типа выбросьтеTypeError.requiredиoptional— множества имён переменных.- Если одно имя находится в обоих множествах, выбросьте
PromptTemplateError. - Создайте
Template(body). Еслиis_valid()возвращаетFalse, выбросьтеPromptTemplateError. - Получите переменные через
get_identifiers(). - Найденные имена должны точно совпадать с объединением
required | optional. Иначе выбросьтеPromptTemplateError. - Верните найденные имена как
frozenset[str].
Пример результата¶
validate_template_variables(
"Язык: $language.\n$rules",
{"language"},
{"rules"},
)
# frozenset({"language", "rules"})
validate_template_variables("Язык: $language", set(), set())
# PromptTemplateError
Импорт Template и класс PromptTemplateError уже находятся в заготовке. Не удаляйте и не переименовывайте этот класс.
Правильный ответ
from string import Template
class PromptTemplateError(ValueError):
pass
def validate_template_variables(
body: str,
required: set[str],
optional: set[str],
) -> frozenset[str]:
if not isinstance(body, str):
raise TypeError
if required & optional:
raise PromptTemplateError
template = Template(body)
if not template.is_valid():
raise PromptTemplateError
identifiers = frozenset(template.get_identifiers())
if identifiers != required | optional:
raise PromptTemplateError
return identifiers
Шаг 9¶
Выбор версии промпта из реестра¶
Реестр хранит выпущенные версии промпта. Если версия не указана, приложение использует активную.
Задача¶
Реализуйте select_prompt(registry, active_version, requested_version=None).
Требования¶
- Если
requested_version is None, выберитеactive_version. - Если
requested_versionпередан явно, он должен быть строкой. Для другого типа выбросьтеTypeError. - Удалите внешние пробелы из явно переданной версии. Для пустого результата выбросьте
PromptVersionError. - Верните объект из
registryс выбранным ключом. - Если выбранной версии нет в
registry, выбросьтеPromptVersionError. - Не изменяйте реестр.
Пример результата¶
registry = {"1.0.0": old_prompt, "1.1.0": new_prompt}
select_prompt(registry, "1.1.0")
# new_prompt
select_prompt(registry, "1.1.0", " 1.0.0 ")
# old_prompt
Класс PromptVersionError, импорт Any и заготовка функции уже находятся в редакторе. Не удаляйте и не переименовывайте предоставленный класс ошибки.
Правильный ответ
from typing import Any
class PromptVersionError(ValueError):
pass
def select_prompt(
registry: dict[str, Any],
active_version: str,
requested_version: str | None = None,
) -> Any:
if requested_version is None:
version = active_version
else:
if not isinstance(requested_version, str):
raise TypeError
version = requested_version.strip()
if not version:
raise PromptVersionError
try:
return registry[version]
except KeyError:
raise PromptVersionError