Игорь Градов
Игорь Градов
6 мин
ai

Gradio получает экран входа за 30 минут: трейсы Langfuse разделяются по пользователям

Gradio за полчаса получает экран входа, а Langfuse начинает разделять трейсы по пользователям, и всё это без переписывания интерфейса, буквально двумя параметрами в методе запуска.

Почему это важно

Как только локальным MCP-агентом пользуется больше одного человека, история, чекпоинты и трассировки превращаются в кашу без привязки к конкретному пользователю. Аутентификация в Gradio решает проблему на уровне интерфейса, а связка с Langfuse и LangGraph разносит данные автоматически.

Разработчик Владимир опубликовал на Хабре практическое продолжение цикла про локального MCP-агента (программу, которая сама вызывает внешние инструменты по заданному сценарию). В предыдущих частях он собрал агента на LangGraph с Gradio-интерфейсом, чекпоинтами и трассировкой. Проблема возникла, когда агентом стали пользоваться коллеги: контейнер в связке Windows-WSL-Docker показывал один и тот же IP-адрес, и отличить пользователей по сети не получилось. Решение: прикрутить к Gradio аутентификацию и прокинуть имя пользователя в трейсы Langfuse и чекпоинты LangGraph.

Что понадобится?

  • Работающий Gradio-интерфейс (версия с поддержкой параметра auth в demo.launch)
  • Настроенный LangGraph-агент с трассировкой через Langfuse
  • Docker Desktop на Windows с WSL (или любая среда, где запускается контейнер)
  • Текстовый файл для хранения логинов и паролей (формат логин:пароль, по одной паре на строку)
  • Примерно 30 минут на интеграцию

Как устроено хранение паролей?

В полноценных системах пароль не хранится в открытом виде. Вместо него сохраняют хеш (результат необратимого преобразования), часто с «солью» (случайной добавкой). При входе система хеширует введённый пароль тем же способом и сравнивает хеши, а не сами пароли. Даже при утечке базы злоумышленник не получает готовый список.

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

# Формат: логин:пароль (по одному на строку)
admin:admin

Пошаговая инструкция

1. Создайте файл с учётными записями.

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

2. Напишите функцию парсинга и проверки.

Функция get_gradio_auth читает файл при старте, собирает словарь «логин: пароль» и возвращает замыкание для проверки:

import secrets
from pathlib import Path
from core import settings

def get_gradio_auth():
    if not settings.service.auth_enabled:
        return None

    path = Path(settings.service.auth_users_file)
    if not path.is_file():
        raise RuntimeError(f'Файл учётных записей не найден: {path}')

    credentials = {}
    for line in path.read_text(encoding='utf-8').splitlines():
        if not line or line.startswith('#') or ':' not in line:
            continue
        user, psw = line.split(':', 1)
        if (user := user.strip()) and (psw := psw.strip()):
            credentials[user] = psw

    if not credentials:
        raise RuntimeError(
            f'Файл учётных записей пуст или некорректен: {path}'
        )

    def checker(username: str, password: str) -> bool:
        stored = credentials.get(username)
        return stored is not None and secrets.compare_digest(stored, password)

    return checker

Обратите внимание: для сравнения пароля используется secrets.compare_digest, а не обычный оператор ==. Обычное сравнение прерывается при первом несовпавшем символе, и по разнице во времени ответа теоретически можно подбирать пароль посимвольно (timing attack, атака по времени). secrets.compare_digest тратит одинаковое время независимо от того, где именно строки расходятся. Для файла с тремя паролями это избыточная мера, но привычка полезная.

3. Передайте функцию в demo.launch.

В классе вашего приложения добавьте два параметра при запуске Gradio:

self.demo.launch(
    server_name=settings.service.host,
    server_port=settings.service.port,
    show_error=True,
    auth=get_gradio_auth(),
    auth_message='<p>Введите выданный Вам логин и пароль</p>'
)

Параметр auth принимает либо функцию проверки (как здесь), либо кортеж «логин, пароль» напрямую. Параметр auth_message задаёт текст на экране входа.

4. Прокиньте имя пользователя в методы графа.

Теперь Gradio знает, кто вошёл. Осталось передать request.username в вызовы LangGraph, чтобы чекпоинты и трейсы разделились по пользователям:

async def _continue(self, history: list, request_id: str,
                    request: gr.Request):
    phase = await self.agent.get_phase(
        request_id, username=request.username
    )
    if phase is None or phase == 'done':
        history.append({
            'role': 'assistant',
            'content': 'Нет активных задач для продолжения.'
        })
        return history, request_id

    result = await self.agent.resume(
        user_messages='Продолжить',
        request_id=request_id,
        username=request.username
    )

5. Сформируйте thread_id с привязкой к пользователю.

Чтобы Langfuse и LangGraph хранили данные раздельно, thread_id строится из имени пользователя и идентификатора запроса:

@staticmethod
def resolve_thread_id(username: str | None, request_id: str) -> str:
    return f'{username or "anonymous"}:{request_id}'

Если аутентификация выключена, username будет None, и все данные попадут под метку anonymous. Система продолжит работать, просто без разделения.

Что получается на практике

После запуска контейнера пользователь видит окно ввода логина и пароля вместо обычного интерфейса. Вводит admin:admin, попадает в знакомый Gradio-чат. Каждый его запрос в Langfuse теперь привязан к thread_id вида admin:req_abc123. Коллега заходит под своим логином, и его трейсы попадают в отдельную ветку: colleague:req_xyz789. Фильтрация в дашборде Langfuse по имени пользователя занимает секунду.

Частые ошибки
  • Файл паролей не попал в контейнер. Если собираете Docker-образ и забыли примонтировать файл или добавить его в COPY, приложение упадёт при старте с ошибкой RuntimeError. Проверьте docker-compose.yml или Dockerfile.
  • Кодировка файла. Функция читает файл в UTF-8. Если файл сохранён в другой кодировке (часто бывает на Windows), логины с кириллицей сломаются.
  • Пароли в открытом виде в репозитории. Файл credentials.txt попадёт в Git, если не добавить его в .gitignore. Для внутреннего инструмента на трёх человек это терпимо, для чего-то серьёзного нужен переход на хеширование или внешний провайдер.
  • request.username равен None при выключенной аутентификации. Код обрабатывает этот случай через anonymous, но если вы завязали на имя пользователя другую логику (права, фильтры), проверяйте явно.

Что делать с этим прямо сейчас, по ролям

Разработчику, который уже собрал MCP-агента на LangGraph и Gradio. Добавление аутентификации занимает буквально два параметра в demo.launch и одну функцию парсинга. Если агент работает в Docker на Windows с WSL и IP-адреса не различаются, это единственный надёжный способ привязать данные к конкретному человеку.

Тимлиду или техлиду, который отдаёт агента команде. Разделение трейсов в Langfuse по пользователям позволяет понять, кто и как использует инструмент, без ручного разбора логов. Это полезно и для отладки, и для оценки, насколько агент реально помогает.

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

Мнение редакции dzen.guru

Решение нарочито простое, и автор это честно признаёт: пароли в текстовом файле, без хеширования, без регистрации. Для внутреннего инструмента на двух-трёх человек это работает. Но если вы планируете открыть доступ шире, стоит сразу заложить хеширование (библиотека bcrypt или passlib) и вынести учётные записи в базу данных. Переход будет безболезненным: менять придётся только функцию get_gradio_auth, интерфейс Gradio останется тем же. Главная ценность этого гайда не в самом коде, а в архитектурном решении: аутентификация прокидывается через request.username сразу в thread_id, и вся цепочка от интерфейса до трассировки получает привязку к пользователю без отдельной системы управления сессиями.

Попробуйте AI-ассистент dzen.guru

Тестируйте промпты и сценарии для ваших ИИ-агентов прямо в нашем инструменте

Попробовать

Весь процесс укладывается в пять шагов и полчаса работы. Файл паролей, функция проверки, два параметра в demo.launch, username в вызовах графа и составной thread_id. После этого Langfuse покажет, кто именно из команды сломал агента вчера в полночь, а LangGraph вернёт каждому его собственную историю.

Поделиться:TelegramVK
Игорь Градов
Игорь Градов

Основатель dzen.guru. Эксперт по монетизации и продвижению на Дзен. Автор курса «Старт на Дзен 2026».

Комментарии

Читайте также

ai

Ответственность искусственного интеллекта невозможна: почему за ошибку ИИ некому платить

Компания SENSE опубликовала экспертный разбор Дмитрия Фырнина, управляющего партнёра и технического директора, о том, где проходит реальная граница…

6 мин
Внедрение искусственного интеллекта буксует: Cloud.ru собирает «карту боли» российского бизнеса
ai

Внедрение искусственного интеллекта буксует: Cloud.ru собирает «карту боли» российского бизнеса

Компания Cloud.ru 11 июня 2025 года запустила на Хабре открытый опрос о барьерах внедрения искусственного интеллекта в российском бизнесе, опираясь на данные…

4 мин
Structured outputs в OpenAI API: 5 сценариев, когда гарантия формата молча ломается
ai

Structured outputs в OpenAI API: 5 сценариев, когда гарантия формата молча ломается

Компания OpenAI ещё в августе 2024 года добавила в API так называемый strict-режим для структурированных выходов (structured outputs), и за полтора года он…

6 мин