Кто сказал, что разработка ИИ‑агента — это сложно, долго и только для крупных корпораций? Сегодня мы убедимся, что добавить в своё приложение умного ассистента может каждый. Встречайте: ИИ‑агент, который помогает пользователю находить оптимальные варианты и обновляет данные в реальном времени.

Используя CopilotKit, LangGraph и Google Maps API, мы создадим приложение, которое не только действует по сценариям, но и предлагает решения. Мы изучим, как реализовать human‑in‑the‑loop, чтобы пользователь мог одобрять или отклонять действия агента.

Приятного прочтения (‑:

Что такое CopilotKit?

CopilotKit — это фреймворк, который превращает создание ИИ‑ассистентов из сложного инженерного процесса в увлекательную и понятную задачу. Вам больше не нужно вручную продумывать каждую деталь интеграции ИИ в приложение — CopilotKit сделает это за вас. Хотите встраивать умных ассистентов прямо в свои приложения? Легко. Настроить чат‑бота, который понимает контекст и выполняет задачи? Несколько строк кода — и готово.

CopilotKit предлагает полный набор инструментов для создания умных ИИ‑ассистентов. Вы можете подключать готовые компоненты чат‑ботов или создавать свои с нуля, при этом поддерживается «безголовый» UI для полного контроля над дизайном. Ассистенты не только отвечают на запросы, но и выполняют действия внутри приложения — например, записывают данные, ищут информацию или запускают процессы. Платформа позволяет создавать динамические React‑компоненты прямо внутри чата, делая взаимодействие не только функциональным, но и визуально привлекательным. Ассистенты понимают контекст и текущее состояние приложения, благодаря чему они адаптируют ответы и действия к конкретной ситуации.

Всё это поддерживается интеграцией с ведущими ИИ‑платформами: OpenAI, Anthropic, Azure, Google Generative AI и другими. CopilotKit — это билет в мир умных ИИ‑ассистентов, которые меняют привычные приложения и делают их удобнее и функциональнее.

Подготавливаем файлы проекта

В этом гайде мы начнём с приложения, в котором ещё нет ИИ, затем шаг за шагом добавим функции, которые предоставляет CopilotKit. Исходный код доступен здесь.

Начальная настройка

Мы будем работать с веткой coagents-travel-tutorial-start, в ней уже содержится стартовый код для нашего приложения. Клонируем её на свой компьютер:

git clone -b coagents-travel-tutorial-start https://github.com/CopilotKit/CopilotKit.git
cd CopilotKit

Пример кода для туториала находится в директории examples/coagents-travel, которая разделена на два каталога: ui — содержит приложение на Next.js, куда мы будем интегрировать агента LangGraph, и agent — включает Python‑реализацию самого агента LangGraph. Сначала перейдём в директорию examples/coagents-travel, чтобы начать настройку:

cd examples/coagents-travel

Установка зависимостей

Начнём с настройки приложения на Next.js. Убедитесь, что у вас установлен менеджер пакетов pnpm. Заскочим в папку ui и установим все необходимые зависимости проекта:

npm install -g pnpm@latest-10
cd ui
pnpm install

Установка API-ключей

Для работы с CopilotKit вам понадобятся API‑ключи. Создадим файл .env в директории ui и добавим в него переменные окружения:

# ui/.env

OPENAI_API_KEY=<ваш ключ доступа к API OpenAI>
NEXT_PUBLIC_CPK_PUBLIC_API_KEY=<ваш ключ доступа к API CopilotKit>

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

pnpm run dev

Если всё настроено правильно, откройте в браузере http://localhost:3000 — здесь вы увидите работающее приложение‑планировщик. Оно ещё не обладает искусственным интеллектом, но это лишь вопрос времени.

Давайте теперь глубже разберёмся, как работает агент LangGraph.

Агент LangGraph

Перед тем как интегрировать агента LangGraph, давайте уделим немного времени тому, чтобы понять, как он работает. Для этого урока мы не будем создавать LangGraph‑агента с нуля — вместо этого мы воспользуемся готовой версией, расположенной в директории agent.

Как устроен агент LangGraph?

LangGraph — это, по сути, графовая структура, где каждый узел выполняет конкретную задачу: один может обрабатывать запросы, другой — сохранять данные, третий — обновлять состояние приложения. Такая архитектура позволяет агенту принимать сложные решения и обрабатывать задачи последовательно или параллельно.

Установка LangGraph Studio

LangGraph Studio — это отличный инструмент для визуализации и отладки рабочих процессов LangGraph. Настоятельно рекомендуется использовать LangGraph Studio совместно с CopilotKit, чтобы лучше понимать, как LangGraph функционирует. Установка LangGraph Studio подробно описана здесь.

Ещё одна установка API-ключей

Создадим файл .env в директории agent и внесём в него следующие переменные окружения:

# agent/.env

OPENAI_API_KEY=<ваш ключ доступа к API OpenAI>
GOOGLE_MAPS_API_KEY=<ваш ключ доступа к API Google Maps>

Если вам нужен API‑ключ для Google Maps, можно обратиться к официальному руководству от Google для его получения.

Визуализация агента LangGraph

Когда LangGraph Studio установлена, давайте откроем в ней директорию examples/coagents-travel/agent, чтобы загрузить нужного агента LangGraph и увидеть его работу в визуальном формате. Настройка может занять пару минут. Результат будет выглядеть как интерактивная схема, которая показывает взаимодействия между узлами — логику работы агента.

Граф, созданный в LangGraph Studio
Граф, созданный в LangGraph Studio

Тестирование агента LangGraph

Как проверить, что агент работает? Всё просто: добавьте сообщение в переменную состояния messages (это, по сути, запрос пользователя) и нажмите Submit. После чего агент обработает ваш ввод, ответит в чате и выполнит связанные задачи. Например:

  • Агент активирует узел search_node, чтобы выполнить поиск.

  • Получив ответ, задействует узел trips_node, чтобы обновить состояние приложения, добавив новую поездку.

Круто, правда? Всё это можно наблюдать на графе. Агент не только решает задачи, но и показывает, как именно он это делает.

Точки останова: контроль за действиями агента

Что если ИИ в какой‑то момент понадобится подсказка человека? Здесь на помощь приходит концепция human‑in‑the‑loop — пользовательский контроль над ключевыми решениями агента. Пример из жизни: представьте, что агент хочет добавить поездку в ваш список, но вы хотите проверить детали перед подтверждением. Именно для таких случаев LangGraph поддерживает точки останова.

Чтобы добавить точку останова, мы нажмём на узел trips_node и включим параметр interrupt_after. Ну а теперь пробуем создать новую поездку — агент остановится на полпути, запросит одобрения и только потом продолжит выполнение.

Выполнение приостановлено через точку останова
Выполнение приостановлено через точку останова

Оставляем LangGraph Studio запущенной

Вашему агенту нужно постоянное рабочее пространство, и пока таким местом будет LangGraph Studio. Оставьте студию запущенной локально. В нижнем левом углу интерфейса вы увидите URL‑адрес, он пригодится позже для подключения к агенту.

Локальный адрес LangGraph Studio показан в левом нижнем углу
Локальный адрес LangGraph Studio показан в левом нижнем углу

Теперь интегрируем LangGraph в наше приложение, чтобы превратить его в настоящего ассистента-агента.

Настройка CopilotKit

Сделаем наш проект по‑настоящему умным и свяжем всё воедино. У нас уже есть приложение и агент, а сейчас мы добавим CopilotKit, чтобы интеграция стала реальностью. Для этой задачи мы установим следующие зависимости:

  • @copilotkit/react-core: основная библиотека, содержащая провайдер CopilotKit и полезные хуки для управления функциональностью.

  • @copilotkit/react-ui: библиотека пользовательского интерфейса CopilotKit. Здесь находятся готовые компоненты, такие как боковая панель, всплывающее окно чата, текстовые поля и многое другое.

Сначала откроем директорию ui и установим пакеты CopilotKit:

cd ../ui
pnpm add @copilotkit/react-core @copilotkit/react-ui

Эти два пакета — всё, что нужно для интеграции CopilotKit в React‑приложение. Первый пакет отвечает за функциональность, а второй — за визуальные компоненты.

Добавление CopilotKit

Здесь у нас есть два варианта, как мы можем настроить CopilotKit: Copilot Cloud — быстрый и удобная управляемая платформа, где уже настроено всё необходимое, и самостоятельный хостинг, который подходит тем, кто хочет большего контроля.

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

Настройка Copilot Cloud

Подключение к Copilot Cloud начинается с создания аккаунта на платформе. Регистрация занимает всего минуту, после чего откроется доступ к системе.

Следующий шаг — получение API‑ключа Copilot Cloud. Для этого нужно войти в систему, ввести API‑ключ OpenAI, нажать галочку — и ваш публичный ключ будет сгенерирован автоматически.

CopilotKit Cloud UI
Интерфейс CopilotKit Cloud

И наконец, откроем файл .env в директории ui и добавим полученный API‑ключ Copilot Cloud:

# ui/.env

# ...Здесь другие переменные окружения...
NEXT_PUBLIC_CPK_PUBLIC_API_KEY=<ваш ключ доступа к API CopilotKit>

Интеграция CopilotKit

Настроив ключи, пришло время интегрировать CopilotKit в приложение. Начнём с обёртки: откроем файл ui/app/page.tsx и «обмотаем» приложение компонентом CopilotKit.

Далее займёмся пользовательскими компонентами. Платформа предоставляет готовые элементы вроде <CopilotPopup /> и <CopilotSidebar />, которые легко интегрируются и дополняют ваш интерфейс. Хотите больше кастомизации? Включите «безголовый режим» через хук useCopilotChat — это позволит создать интерфейс, адаптированный под ваш дизайн.

Для добавления боковой панели чата используем компонент <CopilotSidebar /> — обновим файл ui/app/page.tsx, добавив этот элемент. Убедимся, что импортированы необходимые стили — так панель будет выглядеть великолепно сразу после внедрения:

// ui/app/page.tsx

"use client";

// ...Здесь другие импорты...
import { TasksList } from "@/components/TasksList";
import { TasksProvider } from "@/lib/hooks/use-tasks";
import { CopilotKit } from "@copilotkit/react-core";
import { CopilotSidebar } from "@copilotkit/react-ui";
import "@copilotkit/react-ui/styles.css";

//...

export default function Home() {
  return (
    <CopilotKit
      publicApiKey={process.env.NEXT_PUBLIC_CPK_PUBLIC_API_KEY}
    >
      <CopilotSidebar
        defaultOpen={true}
        clickOutsideToClose={false}
        labels={{
          title: "Планировщик поездок",
          initial: "Привет! ? Я здесь, чтобы помочь вам спланировать путешествия. Могу с лёгкостью организовать поездки, добавить в них интересные места или вместе с вами спланировать новое приключение с нуля.",
        }}
      />
      <TooltipProvider>
        <TripsProvider>
          <main className="h-screen w-screen">
            <MapCanvas />
          </main>
        </TripsProvider>
      </TooltipProvider>
    </CopilotKit>
  );
}

При желании можно настроить заголовок и начальное сообщение от ИИ через пропс labels.

Проверяем

Запустим приложение, откроем его в браузере и посмотрим направо. А там… уже появилась новая боковая панель чата — и всё это всего за несколько строк кода.

У нас уже есть панель чата, но она пока не умеет принимать решения. Давайте добавим эту функциональность с помощью агента LangGraph, который уже настроен в директории asset. Готовы сделать ассистента по‑настоящему умным?

Делаем ассистента агентным

Краткий обзор React-состояния

Давайте рассмотрим, как работает состояние приложения. Откроем файл lib/hooks/use-trips.tsx. Там мы найдём TripsProvider, который задаёт множество полезных функций. Основное внимание уделено объекту state, определённому через тип AgentState. Состояние state доступно во всём приложении через хук useTrips, который используется компонентами вроде TripCard, TripContent и TripSelect.

Если вы уже работали с React‑приложениями, эта концепция должна быть вам знакома, ведь управление состоянием через контекст или библиотеку является стандартной практикой.

Объединение агента с состоянием

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

Настройка туннеля

Если вы используете Copilot Cloud, вы уже готовы; если же выбрали самостоятельный хостинг, понадобятся ещё несколько шагов.

Для подключения локально запущенного агента LangGraph к Copilot Cloud применим CLI CopilotKit. Номер порта возьмём тот из интерфейса LangGraph Studio, который был в левом нижнем углу.

LangGraph Studio Endpoint

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

# На отметке <port_number> нужно указать номер порта
npx @copilotkit/cli tunnel <port_number>

Готово, мы создали туннель! Вот что отобразится:

✔ Tunnel created successfully!
Tunnel Information:
Local: localhost:54209
Public URL: https://light-pandas-argue.loca.lt
Press Ctrl+C to stop the tunnel

Скопируем сгенерированный URL: это шлюз между локальным агентом LangGraph и облачной платформой CopilotKit.

Получение LangSmith API Key

Для следующего шага потребуется LangSmith API Key: здесь несколько подсказок, как его заполучить.

Теперь ваш агент готов принимать запросы и синхронизироваться с приложением.

Подключение туннеля к Copilot Cloud

Переходим на Copilot Cloud, прокручиваем вниз до раздела Remote Endpoints и жмём кнопку + Add New. После чего выбираем платформу LangGraph и добавляем публичный URL — тот, что был создан в CLI CopilotKit, — вместе с API‑ключом LangSmith. Нажимаем Create.

Отлично! Теперь конечная точка вашего агента добавлена — CopilotKit знает, куда отправлять запросы при вызове агента.

Закрепляем выбранного агента

В этом проекте мы используем только одного агента. Чтобы все запросы направлялись именно к нему, настроим провайдер <CopilotKit />, указав имя агента в параметрах — travel, как в файле agents/langgraph.json:

// ui/app/page.tsx

//...

<CopilotKit
  //...
  agent="travel"
>
    {/* ... */}
</CopilotKit>

А это памятка на случай, если вас заинтересует работа с многоагентными потоками:
https://docs.copilotkit.ai/coagents/concepts/multi‑agent‑flows.

Всё, теперь ассистент стал по‑настоящему агентным: он может не только вести диалоги, но и выполнять действия. Теперь он точно знает, что делать, — даже если мы сами не знаем (‑:

Коннектим состояния агента и приложения

Теперь наша цель — настроить двухстороннее соединение между состоянием агента LangGraph и состоянием приложения: тогда станут доступны динамические взаимодействия в режиме реального времени.

Агенты LangGraph имеют собственное состояние, которое показано в интерфейсе LangGraph Studio (в нижнем левом углу). Чтобы синхронизировать состояния агента и приложения, применим хук useCoAgent из CopilotKit. Для этого откроем файл ui/lib/hooks/use-trips.tsx и добавим следующие строки кода. С этим хуком двухсторонняя синхронизация состояний готова.

// ui/lib/hooks/use-trips.tsx

// ...Здесь другие импорты...
import { AgentState, defaultTrips} from "@/lib/trips"; 
import { useCoAgent } from "@copilotkit/react-core"; 

export const TripsProvider = ({ children }: { children: ReactNode }) => {
  const { state, setState } = useCoAgent<AgentState>({
    name: "travel",
    initialState: {
      trips: defaultTrips,
      selected_trip_id: defaultTrips[0].id,
    },
  });
  //...

Разбираемся в новом коде:

  • Хук useCoAgent — этот универсальный хук позволяет указать тип данных, соответствующий состоянию агента LangGraph. Мы используем тип AgentState для согласованности и избегаем приведения типов к any, что считается плохой практикой.

  • Параметр name связывает наше приложение с именем графа из файла agent/langgraph.json. Убедитесь, что имя указано правильно, чтобы агент и приложение всегда были синхронизированы.

  • Параметр initialState. Укажем значение defaultTrips, определённое в @/lib/types.ts: благодаря этому начальному состоянию можно проверить функциональность сразу после запуска.

Начальное состояние defaultTrips имеет следующую структуру:

// ui/lib/types.ts

export const defaultTrips: Trip[] = [
  {
    id: "1",
    name: "Деловая поездка в Нью-Йорк",
    center_latitude: 40.7484,
    center_longitude: -73.9857,
    places: [
      {
        id: "1",
        name: "Центральный парк",
        address: "Нью-Йорк, NY 10024",
        description: "Знаменитый нью-йоркский парк.",
        latitude: 40.785091,
        longitude: -73.968285,
        rating: 4.7,
      },
      {
        id: "3",
        name: "Таймс-сквер",
        address: "Таймс-сквер, Нью-Йорк, NY 10036",
        description: "Известная площадь в Нью-Йорке.",
        latitude: 40.755499,
        longitude: -73.985701,
        rating: 4.6,
      },
    ],
    zoom_level: 14,
  },
  //...
];

Тестируем!

Запустим приложение и зададим ассистенту вопрос о своих поездках. Например:

Сколько у меня поездок?

Агент извлечёт данные из состояния приложения. Чудеса? Нет, просто ИИ.

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


Стриминг ответа

Теперь, когда ваш агент умеет взаимодействовать с данными и обновлять состояние приложения, пора поднять юзер‑экспириенс на новый уровень. Как насчёт функции потоковой передачи текста? Она будет показывать деятельность агента в реальном времени, создавая эффект живого общения — как в популярных ИИ‑системах вроде ChatGPT.

На этом этапе мы задействуем функцию copilotkit_emit_state в SDK CopilotKit, она позволит вашему агенту на ходу отправлять обновления о статусе выполнения задачи.

Установка SDK CopilotKit

Сначала установим SDK CopilotKit. Поскольку мы работаем с Python‑агентом и управляем зависимостями через poetry, начнём с установки соответствующего SDK. Загляните сюда, если poetry ещё не установлен.

poetry add copilotkit==0.1.31a4

Добавление функции передачи состояния

Итак, как это работает? По умолчанию состояние агента обновляется, когда он переходит между узлами графа, но иногда нужно показать прогресс в процессе выполнения действия — например, пока агент ищет данные или обрабатывает запрос. Хорошая новость: это можно сделать вручную с помощью copilotkit_emit_state.

Давайте обновим узел search_node, чтобы передавать промежуточное состояние. Для этого откроем файл agent/travel/search.py и добавим следующую конфигурацию:

# agent/travel/search.py

# ...Здесь другие импорты...
from copilotkit.langchain import copilotkit_emit_state, copilotkit_customize_config 

async def search_node(state: AgentState, config: RunnableConfig):
    """
    Узел поиска осуществляет поиск географических точек.
    """
    ai_message = cast(AIMessage, state["messages"][-1])
    config = copilotkit_customize_config(
        config,
        emit_intermediate_state=[{
            "state_key": "search_progress",
            "tool": "search_for_places",
            "tool_argument": "search_progress",
        }],
    )
    //...

Передача промежуточного состояния

Мы подключили copilotkit_emit_state, чтобы передавать состояние вручную на каждом этапе поиска, и теперь агент сможет сообщать обновления в режиме реального времени, позволяя видеть прогресс для каждого запроса. Откроем файл agent/travel/search.py и внесём следующие изменения, чтобы агент передавал состояние как в начале, так и на промежуточных этапах поиска:

# agent/travel/search.py

#...

async def search_node(state: AgentState, config: RunnableConfig):
    """
    Узел поиска осуществляет поиск географических точек.
    """
    ai_message = cast(AIMessage, state["messages"][-1])
    config = copilotkit_customize_config(
        config,
        emit_intermediate_state=[{
            "state_key": "search_progress",
            "tool": "search_for_places",
            "tool_argument": "search_progress",
        }],
    )

    #...

    state["search_progress"] = state.get("search_progress", [])
    queries = ai_message.tool_calls[0]["args"]["queries"]

    for query in queries:
        state["search_progress"].append({
            "query": query,
            "results": [],
            "done": False
        })

    await copilotkit_emit_state(config, state) 

    #...

Обновление и передача прогресса
Покажем результаты в реальном времени и по завершении поиска снова обновим прогресс. Вот какие строки кода agent/travel/search.py нужны для этого:

# agent/travel/search.py

#...

async def search_node(state: AgentState, config: RunnableConfig):
    """
    Узел поиска осуществляет поиск географических точек.
    """
    ai_message = cast(AIMessage, state["messages"][-1])
    config = copilotkit_customize_config(
        config,
        emit_intermediate_state=[{
            "state_key": "search_progress",
            "tool": "search_for_places",
            "tool_argument": "search_progress",
        }],
    )
    state["search_progress"] = state.get("search_progress", [])
    queries = ai_message.tool_calls[0]["args"]["queries"]

    for query in queries:
        state["search_progress"].append({
            "query": query,
            "results": [],
            "done": False
        })

    await copilotkit_emit_state(config, state) 

    #...

    places = []

    for i, query in enumerate(queries):
        response = gmaps.places(query)
        for result in response.get("results", []):
            place = {
                "id": result.get("place_id", f"{result.get('name', '')}-{i}"),
                "name": result.get("name", ""),
                "address": result.get("formatted_address", ""),
                "latitude": result.get("geometry", {}).get("location", {}).get("lat", 0),
                "longitude": result.get("geometry", {}).get("location", {}).get("lng", 0),
                "rating": result.get("rating", 0),
            }
            places.append(place)
        state["search_progress"][i]["done"] = True
        await copilotkit_emit_state(config, state) 

    state["search_progress"] = []
    await copilotkit_emit_state(config, state) 

    #...

Отображение прогресса в интерфейсе

Чтобы показать прогресс в пользовательском интерфейсе, воспользуемся хуком useCoAgentStateRender, который будет рендерить состояние search_progress. Откроем файл ui/lib/hooks/use-trips.tsx и добавим код для отображения прогресса поиска:

// ui/lib/hooks/use-trips.tsx

//...Здесь другие импорты...
import { useCoAgent, useCoAgentStateRender } from "@copilotkit/react-core"; 
import { SearchProgress } from "@/components/SearchProgress"; 

export const TripsProvider = ({ children }: { children: ReactNode }) => {
  
  //...
  
  useCoAgentStateRender<AgentState>({
    name: "travel",
    render: ({ state }) => {
      if (state.search_progress) {
        return <SearchProgress progress={state.search_progress} />
      }
      return null;
    },
  });

  //...

}

Компонент <SearchProgress /> уже настроен (его реализация описана в ui/components/SearchProgress.tsx). Ключ состояния search_progress уже определён в типе AgentState в ui/lib/types.ts, так что вам не нужно создавать его с нуля.

Попробуем написать для агента запрос, требующий поиска, например:

Добавь пять интересных поездок по Волгограду.

Наблюдаем, как прогресс подгружается в реальном времени:

Добавление функции human-in-the-loop

Что делать, если агент захочет принять решение, но вы с ним несогласны? Вот тут‑то и вступает в игру функция human‑in‑the‑loop: она позволяет вам вмешаться, одобрить, отклонить или даже изменить действия агента. На этом этапе мы настроим точку останова в потоке агента, чтобы в определённый момент приостановить его выполнение и дождаться нашего решения перед продолжением.

Когда агент достигает точки останова, мы отправляем её на фронтенд, где пользователь может одобрить или отклонить действие; затем агент продолжает работать в зависимости от решения пользователя. В общем и целом процесс выглядит следующим образом:

Coagents HITL Infographic

Добавление точки останова

Добавить функционал human‑in‑the‑loop в LangGraph — дело простое. Узел trips_node будет играть роль посредника, передавая действия узлу perform_trips_node. Настроим точку останова в trips_node: для этого откроем файл agent/travel/agent.py и укажем через функцию compile, где должен происходить «перерыв».

# agent/travel/agent.py

#...

graph = graph_builder.compile(
    checkpointer=MemorySaver(),
    # Приостанавливаемся прямо здесь и ожидаем пользовательского ответа.
    interrupt_after=["trips_node"], 
)

Сразу после узла trips_node агент поставит выполнение на паузу и будет дожидаться разрешения пользователя, вместо того чтобы сразу предпринимать действия.

Обработка решения пользователя

Когда пользователь принимает решение, агент должен корректно обработать его выбор: если пользователь нажимает «Отмена» — выполнение действия прекращается, если одобряет действие — агент продолжает его выполнение. Для реализации этой логики добавим обработку в узле perform_trips_node:

# agent/travel/trips.py

#...

async def perform_trips_node(state: AgentState, config: RunnableConfig):
    """Обработка поездки"""
    ai_message = cast(AIMessage, state["messages"][-2]) 
    tool_message = cast(ToolMessage, state["messages"][-1]) 
    #...

И вот условие проверяет, что выбрал пользователь, и реагирует соответствующим образом: если пользователь нажимает «Отмена», мы прекращаем выполнение и возвращаем пользовательское сообщение, а для любого другого ответа работа продолжается.

# agent/travel/trips.py

#...

async def perform_trips_node(state: AgentState, config: RunnableConfig):
    """Обработка поездки"""
    ai_message = cast(AIMessage, state["messages"][-2])
    tool_message = cast(ToolMessage, state["messages"][-1])

    if tool_message.content == "CANCEL":
      return {
        "messages": AIMessage(content="Обработка поездки отменена."),
      }

    # Обрабатываем маловероятный случай, когда ai_message не является AIMessage
    # или не содержит вызовов инструментов, — такого происходить не должно.
    if not isinstance(ai_message, AIMessage) or not ai_message.tool_calls:
        return state

    #...

Отображение интерфейса решения

Теперь нам нужно обновить фронтенд, чтобы визуализировать запросы от агента и дать пользователю возможность принимать решения. Мы используем хуки useCopilotAction с опцией renderAndWait. Откроем файл ui/lib/hooks/use-trips.tsx и добавим следующий код:

// ui/lib/hooks/use-trips.tsx

//...Здесь другие импорты...
import { AddTrips, EditTrips, DeleteTrips } from "@/components/humanInTheLoop"; 
import { useCoAgent, useCoAgentStateRender, useCopilotAction } from "@copilotkit/react-core"; 

//...

export const TripsProvider = ({ children }: { children: ReactNode }) => {

  //...

  useCoAgentStateRender<AgentState>({
    name: "travel",
    render: ({ state }) => {
      return <SearchProgress progress={state.search_progress} />
    },
  });

  useCopilotAction({ 
    name: "add_trips",
    description: "Добавить поездки",
    parameters: [
      {
        name: "trips",
        type: "object[]",
        description: "Добавляемые поездки",
        required: true,
      },
    ],
    renderAndWait: AddTrips,
  });

  useCopilotAction({
    name: "update_trips",
    description: "Изменить поездки",
    parameters: [
      {
        name: "trips",
        type: "object[]",
        description: "Изменяемые поездки",
        required: true,
      },
    ],
    renderAndWait: EditTrips,
  });

  useCopilotAction({
    name: "delete_trips",
    description: "Удалить поездки",
    parameters: [
      {
        name: "trip_ids",
        type: "string[]",
        description: "Идентификаторы удаляемых поездок",
        required: true,
      },
    ],
    renderAndWait: (props) => DeleteTrips({ ...props, trips: state.trips }),
  });

  //...

С этими настройками фронтенд готов отображать запросы от агента и фиксировать решения пользователя. Осталась одна важная деталь: как именно обрабатывать ввод пользователя и отправлять его обратно агенту.

Давайте разберёмся, как логика реализуется на фронтенде. В качестве примера рассмотрим компонент DeleteTrips, но точно такие же принципы применимы к компонентам AddTrips и EditTrips.

// ui/lib/components/humanInTheLoop/DeleteTrips.tsx

import { Trip } from "@/lib/types";
import { PlaceCard } from "@/components/PlaceCard";
import { X, Trash } from "lucide-react";
import { ActionButtons } from "./ActionButtons"; 
import { RenderFunctionStatus } from "@copilotkit/react-core";

export type DeleteTripsProps = {
  args: any;
  status: RenderFunctionStatus;
  handler: any;
  trips: Trip[];
};

export const DeleteTrips = ({ args, status, handler, trips }: DeleteTripsProps) => {
  const tripsToDelete = trips.filter((trip: Trip) => args?.trip_ids?.includes(trip.id));
  return (
    <div className="space-y-4 w-full bg-secondary p-6 rounded-lg">
    <h1 className="text-sm">Будут удалены эти поездки:</h1>
      {status !== "complete" && tripsToDelete?.map((trip: Trip) => (
        <div key={trip.id} className="flex flex-col gap-4">
          <>
            <hr className="my-2" />
            <div className="flex flex-col gap-4">
            <h2 className="text-lg font-bold">{trip.name}</h2>
            {trip.places?.map((place) => (
              <PlaceCard key={place.id} place={place} />
            ))}
            </div>
          </>
        </div>
      ))}
      { status !== "complete" && (
        <ActionButtons
          status={status} 
          handler={handler} 
          approve={<><Trash className="w-4 h-4 mr-2" /> Delete</>} 
          reject={<><X className="w-4 h-4 mr-2" /> Cancel</>} 
        />
      )}
    </div>
  );
};

Ключевая часть здесь — компонент ActionButtons, он позволяет пользователю выбирать — одобрить или отклонить действие:

// ui/lib/components/humanInTheLoop/ActionButtons.tsx

import { RenderFunctionStatus } from "@copilotkit/react-core";
import { Button } from "../ui/button";

export type ActionButtonsProps = {
    status: RenderFunctionStatus;
    handler: any;
    approve: React.ReactNode;
    reject: React.ReactNode;
}

export const ActionButtons = ({ status, handler, approve, reject }: ActionButtonsProps) => (
  <div className="flex gap-4 justify-between">
    <Button 
      className="w-full"
      variant="outline"
      disabled={status === "complete" || status === "inProgress"} 
      onClick={() => handler?.("CANCEL")} 
    >
      {reject}
    </Button>
    <Button 
      className="w-full"
      disabled={status === "complete" || status === "inProgress"} 
      onClick={() => handler?.("SEND")} 
    >
      {approve}
    </Button>
  </div>
);

Кнопки вызывают handler?.("CANCEL"), если пользователь нажимает Отмена, и handler?.("SEND"), если Удалить. При помощи обработчиков onClick решение пользователя — «CANCEL» или "SEND" — отправляется обратно агенту.

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

Теперь ваш агент обладает функционалом human‑in‑the‑loop: он способен запрашивать подтверждение перед выполнением действия, реагировать на пользовательские решения, а затем продолжать работать в зависимости от выбора.


Всё готово!

Мы подробно выяснили, как добавить ассистента-агента в приложение с помощью CopilotKit, LangGraph и Google Maps API, научились синхронизировать состояние агента и приложения в реальном времени и реализовали концепцию human‑in‑the‑loop, чтобы внедрить интерактивность. Вы можете протестировать получившийся планировщик путешествий в онлайне (или посмотреть видеодемонстрацию).

Ещё немного полезных ссылок:

Спасибо, что прочитали! Жду вас в комментариях (‑:

Комментарии (0)