MODULE_03 · УРОК 3.2

Динамический промпт

Секции из прошлого урока сейчас - здоровенная f-строка в main.py. Будет другая папка, другой набор tools, ветка git - и вы снова лезете в середину многострочника с ножницами. Давайте починим это, вынесем сборку в функцию: на входе факты о запуске, на выходе текст промпта.

ЧТО ПОЛУЧИТСЯ Файл prompts.py с build_system_prompt(ctx). В main.py вместо простыни будет instructions=build_system_prompt({...}) - вызов функции с контекстом.

Слова по делу

  • Контекст промпта - обычный словарь/TypedDict: cwd, тип песочницы, имена tools, опционально ветка и текст из AGENTS.md.
  • Чистая функция - одни и те же входные данные -> один и тот же текст. Без чтения диска внутри (чтение AGENTS.md сделаем в main.py на уроке 3.4).

Быстрый путь

  1. Создать prompts.py.
  2. Перенести туда Agency / Guardrails из main.py.
  3. В main.py вызвать build_system_prompt(...) - вызов функции с контекстом.
  4. Один раз print готового текста и посмотреть глазами, как выглядит промпт.

Какие файлы трогаем

Файл Что делать
prompts.py Создать. PromptContext + build_system_prompt.
main.py Править. Убрать длинный f-string; вызвать билдер.
остальное Не трогать.

Шаг 1. Создайте prompts.py

prompts.py (целиком)

from typing import TypedDict, NotRequired


class PromptContext(TypedDict):
    working_directory: str
    sandbox_type: str
    tool_names: list[str]
    git_branch: NotRequired[str]
    project_context: NotRequired[str]


def build_system_prompt(ctx: PromptContext) -> str:
    parts: list[str] = []

    parts.append(f"Ты coding-агент. Рабочая директория: {ctx['working_directory']}")
    parts.append(f"Песочница (sandbox): {ctx['sandbox_type']}")

    tools = ", ".join(ctx["tool_names"])
    parts.append(f"""
# Agency
- ПОЛЬЗУЙСЯ инструментами: читай, ищи, запускай — потом отвечай.
- НЕ объясняй, что собираешься сделать. Сделай, потом коротко отчитайся.
- Доступные инструменты: {tools}
- Для поиска — grep, для файла — read; bash — только если без него никак.
""")

    if branch := ctx.get("git_branch"):
        parts.append(f"- Текущая ветка git: {branch}")

    parts.append("""
# Guardrails
- Меняй по минимуму.
- Сначала поищи существующий паттерн в проекте.
- Новые зависимости — только по явной просьбе пользователя.
""")

    if project := ctx.get("project_context"):
        parts.append(f"""
# Инструкции проекта (из AGENTS.md)
{project}
""")

    return "\n".join(parts)

Никакого Jinja и «фреймворка промптов». Список строк + join. Условные куски — обычный if. Скучно? Отлично: так и должно быть.

Шаг 2. Подключите в main.py

Удалите здоровенный instructions = f"""...""" из прошлого урока. Вместо него:

main.py — фрагмент

from prompts import build_system_prompt

# имена так, как вы реально регистрируете tools
tool_names = ["read", "grep", "bash"]

instructions = build_system_prompt({
    "working_directory": str(cwd),
    "sandbox_type": "local",
    "tool_names": tool_names,
    # позже: "git_branch": "...", "project_context": "..."
})

# временно — убедиться, что секции на месте:
# print(instructions)

agent = Agent(
    "deepseek:deepseek-chat",
    deps_type=Path,
    tools=[Tool(bash_fn, takes_ctx=False)],
    instructions=instructions,
)
ЗАЧЕМ ФУНКЦИЯ, А НЕ СТРОКА Строку неудобно тестировать и резать для субагента. Функцию можно вызвать в тесте: «с веткой main в тексте есть Current branch», «без ветки — строки нет». Один файл — окупается на третий раз, когда добавите секцию.

Проверьте

uv run python main.py . "Найди все TODO-комментарии в проекте"

Поведение как в 3.1 (текст промпта тот же по смыслу). Раскомментируйте print(instructions): должны быть cwd, sandbox local, Agency, Guardrails и список tools.

Бонус: передайте "git_branch": "main" и снова print — строка про ветку есть. Уберите ключ — строки нет.

Готово, если:

  • Есть файл prompts.py с build_system_prompt
  • В main.py instructions собирается через эту функцию, а не гигантским f-string
  • git_branch и project_context опциональны и появляются только если переданы
  • В распечатке промпта видны Agency, Guardrails и имена tools