Как я автоматизировала еженедельную презентацию и научила AI-бота делать правки прямо в треде

—

от автора

Расскажу, как я автоматизировала сборку еженедельной презентации: данные берутся из Redash, собирается .pptx, и раз в неделю ссылка на неё приходит в мессенджер. А потом я сделала бота, которому можно написать правку к слайду прямо в треде под презентацией.

Каждую неделю у департамента проходит встреча, и к ней нужна презентация на несколько десятков слайдов с метриками в разных разрезах. Ранее 20+ менеджеров собирали презенатцию руками: открывали Redash, прокликивали фильтры, делали скриншоты, выгружали данные в иксель и делали промежуточные расчеты, вставляли это все на слайды, подписывали цифры, делали ручное форматирование и т.д. Каждую неделю на это уходило несколько часов.

Я опишу основные шаги и как всё устроено. Код в статье упрощённый, под свои задачи его придётся дорабатывать.

Что в итоге получилось

Раз в неделю в 9 утра в канал приходит пост со ссылкой на свежую презентацию.

В самой презентации:

  • графики собраны на свежих данных, в одном стиле, фокусная неделя выделена цветом

  • на каждом слайде есть KPI-карточки: факт, таргет и значение с начала года

  • ручные слайды, которые заполняют коллеги, переносятся из прошлой недели

  • в заметках к слайду лежат ссылки на запросы в Redash

Если кому-то нужно что-то поменять, он пишет в тред под постом, например «на слайде 12 подпиши значения на всех барах», и через пару минут получает исправленный слайд.

Часть 1. Как собирается презентация

Общая схема:

Redash API → pandas → matplotlib (только графики) → python-pptx (всё остальное) → Nextcloud → Mattermost

Шаг 1. Забираем данные из Redash

Подробно про это я писала в прошлой статье. Если коротко, берём query_id, передаём параметры и ждём результат. Если результат не готов сразу, Redash вернёт job, и его надо опрашивать, пока запрос не досчитается.

def fetch_query(session, query_id, params, poll_seconds=2.0):    r = session.post(        f"{BASE_URL}/api/queries/{query_id}/results",        json={"parameters": params, "max_age": 0},    ).json()    while "job" in r:        job = r["job"]        if job["status"] == 3:  # success            rid = job["query_result_id"]            return session.get(f"{BASE_URL}/api/query_results/{rid}").json()        if job["status"] == 4:  # failure            raise RuntimeError(job["error"])        time.sleep(poll_seconds)        r = session.get(f"{BASE_URL}/api/jobs/{job['id']}").json()    return r

На всю презентацию у меня больше 40 запросов, и полная сборка идёт 10 минут.

Шаг 2. Каждый слайд собирает своя функция

На каждый слайд у меня отдельная функция. Она сама берёт данные для своего слайда, рисует его и возвращает заголовок и ссылки на запросы.

def build_metric_slide(prs, ctx):    df = fetch_df(METRIC_QUERY_ID, {"weeks": ctx.weeks})    slide = prs.slides.add_slide(prs.slide_layouts[6])  # пустой layout    title(slide, "Metric A", focus_week=ctx.focus)    kpi_card(slide, fact=df.value.iloc[-1], target=METRIC_TARGET)    add_chart(slide, bars_png(df, highlight="last"))    return {"slide": "Metric A", "query_url": redash_link(METRIC_QUERY_ID)}

Так можно пересобрать один слайд, а не всю презентацию: python deck.py --only metric_a. Если правка касается одного графика, ждать полную сборку ради неё нет смысла. Ещё если одна функция упала, остальные слайды всё равно соберутся, и каждый слайд можно отлаживать отдельно.

Шаг 3. Редактируемые элементы вместо картинок

Для удобства правок самим человеком картинкой вставляется только сам график, а заголовки, KPI-карточки, подписи и таблицы я делаю обычными элементами PowerPoint через python-pptx, и их можно поправить руками.

def kpi_card(slide, x, y, fact, target, scope):    card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, x, y, w, h)    card.fill.solid()    card.fill.fore_color.rgb = LAVENDER    tf = card.text_frame    run(tf.paragraphs[0], scope, size=11, color=INK_SOFT)    run(tf.add_paragraph(), fmt_pct(fact), size=28, bold=True,        color=GOOD if fact >= target else BAD)

Заголовок графика тоже текстом на слайде, внутри картинки его нет.

демонстрационный вариант с демо-стилей и демо-данными

демонстрационный вариант с демо-стилей и демо-данными

Шаг 4. Стиль

Цвета, шрифты и размеры я вынесла в константы в начале файла: светло-красный для баров ниже таргета, ярко-красный для плохой динамики, свои цвета для шапки таблиц. Фокусная неделя на графике выделена, единый стиль цифр и цветовая гамма.

Отдельно расскажу про шрифт. Я использую Manrope, и графики почему-то выглядели сжатыми. Оказалось, что в скачанных TTF-файлах шрифт внутри назывался Manrope ExtraLight. matplotlib не находил Manrope и без всяких предупреждений рисовал DejaVu. Я переименовала шрифты внутри файлов через fontTools и положила их прямо в скрипт, чтобы сборка не зависела от того, что стоит на машине.

Так что если график выглядит как-то странно, проверьте, каким шрифтом matplotlib его на самом деле нарисовал.

Шаг 5. Фокусная неделя

Данные доезжают с задержкой, и у разных метрик она разная. По последней неделе часть событий ещё не закончилась, а какие-то появятся только через несколько недель. Если показать последнюю неделю как есть, кажется, что метрики резко упали.

Поэтому у каждой метрики своя «фокусная неделя»:

  • если данные доезжают быстро, это позапрошлая неделя

  • если медленно, неделя, которая была 4 недели назад

  • значение с начала года считается до воскресенья фокусной недели

Под заголовком каждого слайда написано, за какую неделю цифры, например focus week 05.10-11.10.

Шаг 6. Ручные слайды

Некоторые слайды из данных не собрать: статусы инициатив, договорённости с партнёрами, комментарии. Их заполняют коллеги.

Перед сборкой скрипт скачивает прошлую презентацию, находит эти слайды по заголовку и копирует как есть. Коллеги правят их в файле в течение недели, и в новую презентацию попадает уже их версия.

PASSTHROUGH_TITLES = {    "Manual slide A",    "Manual slide B",    "Manual slide C",}prev_map = {heading(s): s for s in Presentation(prev_path).slides}...if title in PASSTHROUGH_TITLES and title in prev_map:    copy_slide(prev_map[title], into=prs)

Шаг 7. Если слайд не собрался

Запросов много, и иногда какой-то падает: таблица не обновилась или кто-то поменял параметры запроса. Не хотелось, чтобы из-за одного слайда люди остались без всей презентации.

Поэтому каждая функция обёрнута в try/except. Вместо упавшего слайда появляется заглушка с текстом ошибки, а в посте написано, какие слайды не собрались.

try:    summary = build(prs, ctx)except Exception as e:    drop_half_built_slide(prs)    placeholder(prs, f"[не собрался] {slide_title(name)}", f"{type(e).__name__}: {e}")    failed.append(name)

Шаг 8. Ссылки на запросы в заметках

В заметки к каждому слайду скрипт пишет ссылки на запросы Redash и подписывает, к какому графику какая ссылка.

Шаг 9. Публикация

Скрипт запускается по крону утром в день встречи и делает следующее:

  1. проверяет, не отправлена ли уже презентация за эту неделю, чтобы не прислать её дважды

  2. скачивает из Nextcloud прошлую презентацию, чтобы взять из неё ручные слайды

  3. собирает новую

  4. загружает её в Nextcloud как Weekly_<воскресенье>.pptx и получает ссылку на редактирование

  5. отправляет пост со ссылкой в Mattermost через webhook

Часть 2. Бот для правок

На первом же встрече с новой автоматической презентацией выяснилось, что до встречи в презентацию успевает прийти с десяток правок: добавить подписи, переименовать заголовок, проверить странную цифру, показать какой-то разрез отдельно.

Сначала все правки делала я сама: меняла код, пересобирала слайд, отправляла обратно. Утром перед встречей я только этим и занималась, поэтому сделала бота, которому правки можно писать прямо в тред под постом.

Как это выглядит для коллег

  1. Человек пишет в треде: @deck-bot на слайде 12 подпиши значения на всех барах

  2. Через 1–3 минуты бот отвечает:

@anna.k Готово: слайд 12 с правкамиЧто поменялось: подписи значений на всех барах, раньше были только на фокусной неделеВставьте его в основную презентацию. Правка будет внесена в код проекта,и начиная с ближайшей встречи вы получите обновлённый слайд.
  1. Человек вставляет исправленный слайд в презентацию. Потом я в течение недели захожу в тред со всеми правками, просматриваю их и отбираю те, которые нужно добавить в код сборки презентации и тогда ерез неделю слайды уже соберётся в новом виде с нужными правками.

Что агенту можно делать

Можно было дать LLM-агенту доступ к репозиторию, Redash и Nextcloud и пусть сам разбирается. Мне такой вариант не понравился: сложно предсказать, что он сделает. Поэтому напрямую агент ничего не трогает. У него есть несколько инструментов (через MCP), и все они реализованы в самом сервисе презентации:

Инструмент

Что делает

deck_slides

список слайдов в опубликованной презентации: номер и первые строки текста

list_slides

короткие имена функций-слайдов

search_code, read_code

поиск и чтение кода

rebuild_slides

пересобрать слайды на свежих данных, код не меняется

preview_fix

применить правку, собрать слайд, сохранить черновик

list_drafts, commit_drafts

собрать черновики за неделю в один коммит

get_job

статус долгой задачи

Агент выбирает, что сделать, а проверки и сборка живут в коде сервиса, и на них можно написать обычные тесты.

Шаг 1. Найти слайд

Коллеги называют слайды по-своему: «слайд 12», «первый слайд», «где конверсия». А в коде у слайдов короткие технические имена.

Поэтому есть deck_slides. Он открывает ту же презентацию, которая пришла людям, и отдаёт слайды с номерами и первыми строками текста. Агент находит нужный слайд, берёт заголовок и через search_code ищет функцию, которая его собирает.

Если слайд ручной и функции у него нет, агент так и отвечает: этот слайд правится руками. Если непонятно, о каком слайде речь, он задаёт один уточняющий вопрос.

Шаг 2. Данные или оформление

Просьбы бывают двух видов:

  • «цифра странная», «данные не доехали»: тут агент код не трогает, а пересобирает слайд на свежих данных. Если цифра осталась той же, он пишет, что она совпадает с источником, и даёт ссылку на запрос

  • «покажи по-другому»: агент читает код функции и делает минимальную правку

Для ошибок Redash я добавила отдельное правило. Если Redash ругается на параметры, скорее всего кто-то поменял сам запрос. В этом случае агент не гадает, а пишет, что запрос изменился, и тегает меня.

Шаг 3. Сначала собрать на копии

Правка применяется к копии кода во временной папке, и на этой копии в отдельном процессе собираются нужные слайды. Если какой-то слайд не собрался, ничего не сохраняется, а ошибка уходит агенту.

Если всё собралось, исправленный слайд загружается в Nextcloud отдельным файлом, а правка сохраняется как черновик.

Основную презентацию бот не перезаписывает никогда. Коллеги весь день правят её руками, и при перезаписи их изменения бы пропали. Поэтому бот присылает отдельный файл, и слайд из него вставляют сами.

Шаг 4. Черновики вместо коммитов

В первой версии бот сразу коммитил каждую правку в ветку и добавлял её в PR. В первый же день два человека попросили правки одновременно, и коммиты начали конфликтовать (409 Conflict).

Я переделала это на черновики. Черновик хранится как JSON: что заменить, кто попросил, текст просьбы и ссылка на тред. Новые правки применяются поверх уже сохранённых черновиков, поэтому агент работает с актуальной версией кода.

Когда я пишу в треде «собери правки в PR», агент применяет все черновики за неделю, собирает все слайды, которые они затронули, и делает один коммит и один PR. Если черновик уже не применяется, например его перекрыла более поздняя правка, он попадает в список «Не вошли» с причиной.

PR я смотрю и мержу сама также в треде через агента, без меня в код ничего не попадает.

Шаг 5. Долгие задачи

Слайд собирается 1–3 минуты, а у шага агента таймаут по умолчанию минута. Поэтому rebuild_slides, preview_fix и commit_drafts сразу отдают job_id, а агент спрашивает get_job, пока задача не закончится.

На чём я спотыкалась

  • Пересобирать надо только то, о чём попросили. Когда ради одного слайда несколько минут пересобирается вся презентация, это очень раздражает, так что флаг --only лучше сделать сразу.

  • Когда два человека писали в тред одновременно, очистка временных папок одного запроса удаляла папки другого. Помогло удалять папки по возрасту, а не все, кроме текущей.

  • «Слайд 12» для человека это номер в его файле, а не в коде. Пока не было deck_slides, агент иногда правил не тот слайд.

Как я работала с AI

Код я писала вместе с Claude Code. На мне были постановка задачи, правила (что считать фокусной неделей, когда красить в красный, что вообще показывать на слайде) и проверка каждого слайда. Агент писал и переписывал код, искал ошибки и делал мелкие правки, которых в такой задаче очень много.

Что мне помогало:

  • давать референс: старую презентацию или PDF со стилем, и просить сделать так же

  • после каждого изменения просить картинку слайда и смотреть на неё, а не верить на слово

Заключение

Раньше на презентацию каждую неделю уходило несколько человеко-часов, теперь она собирается сама. Правки, которые раньше копились у меня, бот делает в треде за пару минут. Но в код они попадают только после того, как я посмотрю PR.

Если будете делать что-то похожее, я бы советовала не давать агенту прямой доступ ко всему, а сделать для него несколько узких инструментов и всю логику держать в коде.

Об авторе

Меня зовут Валерия Смирнова, я уже более 6 лет работаю в продуктовой и BI-аналитике. Веду канал про BI и визуализацию данных.

ссылка на оригинал статьи https://habr.com/ru/articles/1102162/