Создание надежных благотворительных агентов с помощью Google ADK и AP2

1. Укрепление доверия для раскрытия щедрости

баннер

Момент вдохновения

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

поиск Google

Появляются сотни результатов.

Вы нажимаете на первую ссылку. Сайт выглядит профессионально. Вы прокручиваете страницу вниз до финансовой отчётности. «Административные расходы: 28%». Вы замираете. Только 72 цента с каждого пожертвованного вами доллара фактически пойдут на финансирование программы. Хорошо ли это? Вы не уверены.

Вы пробуете обратиться в другую организацию. Вы никогда о них не слышали. Они легальны? Быстрый поиск приводит вас в тупик. Вы находите тему на Reddit двухлетней давности, где один пользователь утверждает: «Это мошенничество, моё пожертвование никуда не делось». Другой горячо защищает их: «Они работают на местах!» Эта двусмысленность парализует.

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

Это не личный провал, это системный сбой

Этот опыт универсален. Желание отдавать велико, но этот процесс полон препятствий, которые вызывают сомнения и сомнения:

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

Это противоречие имеет ошеломляющие реальные издержки. Объем индивидуальных пожертвований в Соединенных Штатах огромен: по данным организации Giving USA 2024 , только в 2023 году отдельные доноры пожертвовали около 374 миллиардов долларов . Однако исследования показывают , что препятствия, связанные с пожертвованиями, включая затраты на поиск, психологическое давление и временные ограничения, значительно сокращают объемы пожертвований, поступающих на благотворительные цели. Исследования, в которых приняли участие миллионы доноров, показали, что даже небольшие препятствия в процессе онлайн-пожертвований мешают людям реализовать свои благотворительные намерения.

Это миллиарды долларов предполагаемых пожертвований, которые так и не доходят до тех, кто в них нуждается.

Видение

Представьте себе другой опыт. Вместо 30-минутного сеанса исследования вы просто говорите:

«Я хочу пожертвовать 50 долларов на программу обучения грамоте детей. Найдите мне благотворительную организацию с высоким рейтингом, эффективную и проверенную».

И за считанные секунды вы получите ответ, который вселит уверенность:

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

Это обещание ИИ-агента, предоставляющего помощь. Но для реализации этой идеи нам необходимо решить фундаментальную задачу: когда автономный ИИ-агент управляет деньгами, доверие не является обязательным; оно — основа всего.

  • Как мы можем доказать, что пользователь дал разрешение?
  • Кто несет ответственность в случае совершения ошибки?
  • Как мы можем вселить уверенность в доноров, благотворительные организации и платежные сети, необходимые для участия?

Ваша миссия сегодня

На этом семинаре вы создадите надежного агента, объединив две мощные технологии:

Комплект разработки Google Agent (ADK)

Протокол платежей агентов (AP2)

Роль

Фабрика по созданию промышленных ИИ-агентов

Архитектурный проект доверия к транзакциям ИИ

Что это дает

• Структура для многоагентной оркестровки
• Встроенные функции безопасности, такие как подтверждение инструмента
• Готовая к производству возможность наблюдения и отслеживания
• Простой интерфейс Python для сложного поведения агентов

• Границы безопасности на основе ролей
• Проверяемые цифровые учетные данные (мандаты)
• Криптографическое доказательство согласия
• Полные аудиторские следы для обеспечения подотчетности

Узнать больше

Документация ADK

Протокол AP2

Что вы построите

архитектура

К концу этого семинара вы создадите:

Многоагентная система со специализированными ролями:

  • Торговый агент, который находит проверенные благотворительные организации
  • Торговый агент, который создает обязывающие предложения пожертвований
  • Поставщик учетных данных, который безопасно обрабатывает платежи
  • Оркестратор, который координирует весь поток

Три типа проверяемых учетных данных :

  • IntentMandate: «Найдите мне благотворительную организацию, занимающуюся образованием»
  • CartMandate: «50 долларов в Room to Read, подписано продавцом»
  • PaymentMandate: «Обработать с помощью имитации платежа»

Безопасность на каждом уровне :

  • Границы доверия на основе ролей
  • Явное согласие пользователя

Полный аудиторский журнал :

  • Каждое решение отслеживается
  • Каждое зарегистрированное согласие
  • Видна каждая передача

🔒 Важно: это безопасная среда обучения.

Готовы ли вы строить доверие?

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

Давайте начнем с понимания проблемы из первых рук.

2. Подготовка рабочего места

Фонд надежных агентов

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

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

Доступ к Cloud Shell

Сначала мы откроем Cloud Shell — браузерный терминал с предустановленным Google Cloud SDK и другими необходимыми инструментами.

Нужны кредиты Google Cloud?

Нажмите «Активировать Cloud Shell» в верхней части Google Cloud Console (это значок терминала в правом верхнем углу панели навигации).

облачная оболочка

Найдите идентификатор вашего проекта Google Cloud:

  • Откройте консоль Google Cloud: https://console.cloud.google.com
  • Выберите проект, который вы хотите использовать для этого семинара, из раскрывающегося списка проектов в верхней части страницы.
  • Идентификатор вашего проекта отображается на карточке информации о проекте на панели инструментов. идентификатор проекта

После открытия Cloud Shell убедитесь, что вы прошли аутентификацию:

# Check that you are logged in
gcloud auth list

Ваша учетная запись должна быть отмечена как (ACTIVE) .

Настройте свой проект

Теперь давайте настроим ваш проект Google Cloud и включим необходимые API.

Установите идентификатор вашего проекта

# Set your project using the auto-detected environment variable in Cloud Shell
gcloud config set project $GOOGLE_CLOUD_PROJECT

# Verify the project has been set
echo "Your active Google Cloud project is: $(gcloud config get-value project)"

Включить требуемые API

Вашим агентам необходим доступ к нескольким сервисам Google Cloud:

gcloud services enable \
    aiplatform.googleapis.com \
    secretmanager.googleapis.com \
    cloudtrace.googleapis.com

Это может занять 1–2 минуты. Вы увидите:

Operation "operations/..." finished successfully.

Что предоставляют эти API:

  • aiplatform.googleapis.com : Доступ к моделям Gemini для рассуждений агентов
  • secretmanager.googleapis.com : безопасное хранилище ключей API (рекомендуемая практика производства)
  • cloudtrace.googleapis.com : Наблюдаемость для нашего следа ответственности

Клонировать стартовый код

Получите репозиторий семинара со всем кодом шаблона и ресурсами:

git clone https://github.com/ayoisio/adk-ap2-charity-agents
cd adk-ap2-charity-agents
git checkout codelab

Давайте проверим, что у нас есть:

ls -la

Вы должны увидеть:

  • charity_advisor/ - Где мы будем создавать наших агентов и инструменты
  • scripts/ - Вспомогательные скрипты для тестирования и проверки
  • deploy.sh — вспомогательный скрипт для развертывания
  • setup.py — Вспомогательный скрипт для установки модуля
  • .env.template — Файл переменных среды

Настройка среды Python

Теперь мы создадим изолированную среду Python для нашего проекта.

Создать и активировать виртуальную среду

# Create the virtual environment
python3 -m venv venv

# Activate it
source venv/bin/activate

Проверка : теперь в вашем приглашении должен отображаться префикс (venv) .

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

pip install -r charity_advisor/requirements.txt
pip install -e .

Это устанавливает:

  • google-adk : Фреймворк Agent Development Kit
  • google-cloud-aiplatform : интеграция Vertex AI и Gemini
  • ap2 : SDK протокола агентских платежей (из GitHub)
  • python-dotenv : Управление переменными среды

Флаг -e позволяет импортировать модули adk_ap2_charity_agents из любого места.

Файл конфигурации среды

Создайте свою конфигурацию из шаблона:

# Copy the template
cp .env.template .env

# Get your current Project ID
PROJECT_ID=$(gcloud config get-value project)

# Replace the placeholder with your actual project ID
sed -i "s/your-project-id/$PROJECT_ID/g" .env

# Verify the replacement worked
grep GOOGLE_CLOUD_PROJECT .env

Вы должны увидеть:

GOOGLE_CLOUD_PROJECT=your-actual-project-id

Проверка

Запустите скрипт проверки, чтобы убедиться, что все настроено правильно:

python scripts/verify_setup.py

Вы должны увидеть все зеленые галочки:

======================================================================
SETUP VERIFICATION
======================================================================

✓ Python version: 3.11.x
✓ google-adk: 1.17.0
✓ google-cloud-aiplatform: 1.111.0+
✓ ap2: 0.1.0
✓ python-dotenv: 1.0.0+
✓ .env file found and contains project ID
✓ Google Cloud project configured: your-project-id

✓ Mock charity database found
✓ Agent templates ready
✓ All directories present

======================================================================
✓ Setup complete! You are ready to build trustworthy agents.
======================================================================

Поиск неисправностей

Что дальше?

Ваша среда полностью готова! Вам необходимо:

  • ✅ Проект Google Cloud настроен
  • ✅ Необходимые API включены
  • ✅ Установлены библиотеки ADK и AP2
  • ✅ Код шаблона готов к изменению

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

3. Ваш первый агент и обнаружение пробела в доверии

баннер

От идеи к взаимодействию

В предыдущем модуле мы подготовили среду разработки. Теперь начинается самое интересное. Мы создадим и запустим нашего первого агента, реализуем его первые возможности и, таким образом, выясним, какие фундаментальные задачи необходимо решить, чтобы сделать его по-настоящему надёжным .

Этот модуль — ваша картина «до» — момент, который показывает, почему для создания заслуживающих доверия агентов требуется больше, чем просто предоставление LLM доступа к инструментам.

Шаг 1: Проверьте стартовый агент

Для начала давайте рассмотрим шаблон нашего первого агента. Он содержит базовую структуру с плейсхолдерами, которые мы заполним на следующих этапах.

👉 Откройте файл

charity_advisor/simple_agent/agent.py

в вашем редакторе.

Вы увидите:

"""
A simple agent that can research charities using Google Search.
"""

# MODULE_3_STEP_2_IMPORT_COMPONENTS


simple_agent = Agent(
    name="SimpleAgent",
    model="gemini-2.5-flash",
    
    # MODULE_3_STEP_3_WRITE_INSTRUCTION
    instruction="""""",
    
    # MODULE_3_STEP_4_ADD_TOOLS
    tools=[]
)

Обратите внимание, что комментарии-заполнители следуют шаблону: MODULE_3_STEP_X_DESCRIPTION . Мы заменим эти маркеры, чтобы постепенно создавать наш агент.

Шаг 2: Импорт необходимых компонентов

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

👉 Найти:

# MODULE_3_STEP_2_IMPORT_COMPONENTS

👉 Замените эту единственную строку на:

from google.adk.agents import Agent
from google.adk.tools import google_search

Теперь класс Agent и инструмент google_search доступны в нашем файле.

Шаг 3: Напишите инструкцию для агента

Инструкция — это «должностная инструкция» агента: она указывает LLM, когда и как использовать его инструменты. Давайте составим инструкцию, которая поможет нашему агенту искать информацию о благотворительных организациях.

👉 Найти:

# MODULE_3_STEP_3_WRITE_INSTRUCTION
instruction="""""",

👉 Замените эти две строки на:

instruction="""You are a helpful research assistant. When a user asks you to find information about charities,
use the google_search tool to find the most relevant and up-to-date results from the web.
Synthesize the search results into a helpful summary.""",

Шаг 4: Добавьте инструмент поиска

Агент без инструментов — всего лишь собеседник. Давайте наделим нашего агента первой возможностью: возможностью поиска в интернете.

👉 Найти:

# MODULE_3_STEP_4_ADD_TOOLS
tools=[]

👉 Замените эти две строки на:

tools=[google_search]

Шаг 5: Проверьте своего полного агента

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

👉 Ваш полный

charity_advisor/simple_agent/agent.py

Теперь файл должен выглядеть именно так:

"""
A simple agent that can research charities using Google Search.
"""

from google.adk.agents import Agent
from google.adk.tools import google_search


simple_agent = Agent(
    name="SimpleAgent",
    model="gemini-2.5-flash",
    instruction="""You are a helpful research assistant. When a user asks you to find information about charities,
use the google_search tool to find the most relevant and up-to-date results from the web.
Synthesize the search results into a helpful summary.""",
    tools=[google_search]
)

Шаг 6: Тестирование агента — выявление пробелов в доверии

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

Тест 1: Проблема открытия

👉 В терминале Cloud Shell выполните следующую команду:

adk run charity_advisor/simple_agent

Вы должны увидеть следующий вывод:

INFO:google.adk.agents:Loading agent from charity_advisor/simple_agent
INFO:google.adk.agents:Agent 'SimpleAgent' ready

[user]:

Приглашение [user]: теперь ожидает вашего ввода.

👉 В командной строке [пользователь]: введите:

Can you find me a verified, highly-rated charity for children's literacy?

👉 Нажмите Enter и наблюдайте за ответом.

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

Судя по результатам поиска в интернете, среди известных благотворительных организаций, занимающихся развитием детской грамотности, оказались Reading Is Fundamental и Room to Read . Для проверки их статуса и рейтингов часто рекомендуются такие ресурсы, как Charity Navigator и GuideStar. Я также нашёл несколько онлайн-обсуждений, в том числе на форумах вроде Reddit, где пользователи делятся личным опытом участия в различных небольших местных программах развития грамотности.

Давайте проанализируем. Решил ли агент нашу проблему?

Нет. Он идеально воспроизвел человеческий опыт, который мы описали в Модуле 1. Он успешно автоматизировал процесс «поиска в Google» и вернул нам проблему «аналитического паралича».

Это выявляет первый пробел в доверии: отсутствие авторитетных данных.

Агент выполняет поиск в открытой сети, что означает:

  • ✅ Результаты были найдены быстро (улучшен пользовательский опыт)
  • ❌ Он смешивает организации с высоким рейтингом с обсуждениями Reddit (ненадежные источники)
  • ❌ Он не может отличить проверенные благотворительные организации от потенциальных мошеннических (нет проверки)
  • ❌ Он просит нас проверить только что предоставленную информацию (перекладывая бремя ответственности)

Тест 2: Проблема исполнения

Теперь переходим ко второму важному тесту. В строке запроса [user]: попробуйте завершить пожертвование:

Okay, please donate $50 to Room to Read for me.

Агент ответит извинениями, признав свою ограниченность:

Я понимаю, что вы хотите сделать пожертвование, и это замечательно! Однако я научный сотрудник и не имею возможности заниматься финансовыми операциями или обрабатывать платежи. Чтобы сделать пожертвование, вам необходимо посетить официальный сайт Room to Read.

Это второй, не менее важный момент «Ага!».

Агенту нельзя доверять не только поиск подходящей благотворительной организации, но и совершение им самого действия по пожертвованию.

👉 Нажмите

Ctrl+C

для выхода после завершения тестирования.

Визуализация двух разрывов

проблема доверия

Что вы только что узнали

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

Освоенные ключевые концепции

Класс Агента:

  • Основной строительный блок ADK
  • Объединяет рассуждения LLM (мозг) с инструментами (руки)
  • В комплект поставки входят модель, инструкция и инструменты.

Структура на основе папок:

  • Каждый агент находится в своей собственной папке
  • ADK ищет agent_folder/agent.py
  • Запустить с помощью adk run agent_folder

Список инструментов:

  • Определяет возможности агента
  • LLM решает, когда и как использовать инструменты
  • Может содержать несколько инструментов для разных действий.

Инструкция-подсказка:

  • Руководства по поведению агента как описание работы
  • Определяет роль, триггеры, действия и формат вывода
  • Критически важно для надежного использования инструмента

Проблема доверия:

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

Что дальше?

В следующем модуле мы начнем создавать решение, реализуя ролевую архитектуру AP2 .

Давайте создадим первого агента и посмотрим на разделение ролей в действии.

4. Создание торгового агента — ролевое обнаружение

баннер

Основа доверия: разделение ролей

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

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

Принцип AP2: Разделение ролей

Проблема с агентами, которые «делают всё»

Представьте, что вы нанимаете одного человека на должность финансового консультанта, бухгалтера и инвестиционного брокера. Удобно? Да. Безопасно? Абсолютно нет. Ему бы пришлось:

  • Ваши инвестиционные цели (роль консультанта)
  • Доступ к вашим счетам (роль бухгалтера)
  • Полномочия на перемещение ваших денег (роль брокера)

Если этот человек скомпрометирован или совершит ошибку, все окажется под угрозой.

Решение AP2: один агент, одна задача

AP2 применяет принцип разделения интересов для создания границ доверия :

архитектура

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

  • Ограниченный радиус атаки : если Shopping Agent скомпрометирован, злоумышленник не сможет получить доступ к платежным данным.
  • Конфиденциальность : поставщик учетных данных никогда не видит вашу переписку о покупках.
  • Соответствие требованиям : легче соблюдать требования PCI-DSS, когда платежные данные изолированы
  • Подотчетность : четкая ответственность за каждый шаг

Как общаются агенты: штат как общий блокнот

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

# Shopping Agent writes:
state["intent_mandate"] = {
    "natural_language_description": "Donate $50 to Room to Read",
    "merchants": ["Room to Read"],
    "intent_expiry": "2024-11-07T15:32:16Z",
    "amount": 50.0
}

# Merchant Agent reads:
intent = state["intent_mandate"]
charity_name = intent["merchants"][0]
amount = intent["amount"]
# Creates CartMandate based on IntentMandate...

# Credentials Provider reads:
cart_mandate = state["cart_mandate"]
# Processes payment...

Таким образом мы сохраняем границы доверия, одновременно способствуя сотрудничеству.

Наш первый агент: торговый агент

Обязанности торгового агента просты и конкретны:

  1. Используйте инструмент find_charities для запроса информации из нашей надежной базы данных.
  2. Предоставить пользователю опции
  3. Используйте инструмент save_user_choice для создания IntentMandate и сохранения его в состоянии
  4. Передать следующему агенту (Торговцу)

Вот и всё. Никакой обработки платежей, никакого создания корзины — только поиск и передача.

Давайте создадим его шаг за шагом.

Шаг 1: Добавьте помощника по проверке входных данных

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

👉 Открыто

charity_advisor/tools/charity_tools.py

Функция find_charities (уже готовая) будет вверху. Прокрутите вниз, чтобы найти:

# MODULE_4_STEP_1_ADD_VALIDATION_HELPER

👉 Замените эту единственную строку на:

def _validate_charity_data(charity_name: str, charity_ein: str, amount: float) -> tuple[bool, str]:
    """
    Validates charity selection data before saving to state.
    
    This helper function performs basic validation to ensure data quality
    before it gets passed to other agents in the pipeline.
    
    Args:
        charity_name: Name of the selected charity
        charity_ein: Employer Identification Number (should be format: XX-XXXXXXX)
        amount: Donation amount in USD
        
    Returns:
        (is_valid, error_message): Tuple where is_valid is True if all checks pass,
                                    and error_message contains details if validation fails
    """
    # Validate charity name
    if not charity_name or not charity_name.strip():
        return False, "Charity name cannot be empty"
    
    # Validate EIN format (should be XX-XXXXXXX)
    if not charity_ein or len(charity_ein) != 10 or charity_ein[2] != '-':
        return False, f"Invalid EIN format: {charity_ein}. Expected format: XX-XXXXXXX"
    
    # Validate amount
    if amount <= 0:
        return False, f"Donation amount must be positive, got: ${amount}"
    
    if amount > 1_000_000:
        return False, f"Donation amount exceeds maximum of $1,000,000: ${amount}"
    
    # All checks passed
    return True, ""

Шаг 2: Добавьте помощника по созданию IntentMandate

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

👉 В этом же файле найдите:

# MODULE_4_STEP_2_ADD_INTENTMANDATE_CREATION_HELPER

👉 Замените эту единственную строку на:

def _create_intent_mandate(charity_name: str, charity_ein: str, amount: float) -> dict:
    """
    Creates an IntentMandate - AP2's verifiable credential for user intent.
    
    This function uses the official Pydantic model from the `ap2` package
    to create a validated IntentMandate object before converting it to a dictionary.
    
    Args:
        charity_name: Name of the selected charity
        charity_ein: Employer Identification Number
        amount: Donation amount in USD
        
    Returns:
        Dictionary containing the IntentMandate structure per AP2 specification
    """
    from datetime import datetime, timedelta, timezone
    from ap2.types.mandate import IntentMandate
    
    # Set the expiry for the intent
    expiry = datetime.now(timezone.utc) + timedelta(hours=1)
    
    # Step 1: Instantiate the Pydantic model with official AP2 fields
    intent_mandate_model = IntentMandate(
        user_cart_confirmation_required=True,
        natural_language_description=f"Donate ${amount:.2f} to {charity_name}",
        merchants=[charity_name],
        skus=None,
        requires_refundability=False,
        intent_expiry=expiry.isoformat()
    )
    
    # Step 2: Convert the validated model to a dictionary for state storage
    intent_mandate_dict = intent_mandate_model.model_dump()
    
    # Step 3: Add the codelab's custom fields to the dictionary
    timestamp = datetime.now(timezone.utc)
    intent_mandate_dict.update({
        "timestamp": timestamp.isoformat(),
        "intent_id": f"intent_{charity_ein.replace('-', '')}_{int(timestamp.timestamp())}",
        "charity_ein": charity_ein,
        "amount": amount,
        "currency": "USD"
    })
    
    return intent_mandate_dict

Шаг 3: Создание инструмента передачи состояния с помощью IntentMandate

Теперь давайте создадим инструмент, который создает IntentMandate и сохраняет его в состоянии.

👉 В том же файле прокрутите вниз до

save_user_choice

Функция. Найти:

# MODULE_4_STEP_3_COMPLETE_SAVE_TOOL

👉 Замените эту единственную строку на:

    # Validate inputs before creating IntentMandate
    is_valid, error_message = _validate_charity_data(charity_name, charity_ein, amount)
    if not is_valid:
        logger.error(f"Validation failed: {error_message}")
        return {"status": "error", "message": error_message}
    
    # Create AP2 IntentMandate using our updated helper function
    intent_mandate = _create_intent_mandate(charity_name, charity_ein, amount)
    
    # Write the IntentMandate to shared state for the next agent
    tool_context.state["intent_mandate"] = intent_mandate
    
    logger.info(f"Successfully created IntentMandate and saved to state")
    logger.info(f"Intent ID: {intent_mandate['intent_id']}")
    logger.info(f"Intent expires: {intent_mandate['intent_expiry']}")
    
    # Return success confirmation
    return {
        "status": "success",
        "message": f"Created IntentMandate: ${amount:.2f} donation to {charity_name} (EIN: {charity_ein})",
        "intent_id": intent_mandate["intent_id"],
        "expiry": intent_mandate["intent_expiry"]
    }

Шаг 4: Добавьте помощника по форматированию отображения

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

👉 Прокрутите, чтобы найти:

# MODULE_4_STEP_4_ADD_FORMATTING_HELPER

👉 Замените эту единственную строку на:

def _format_charity_display(charity: dict) -> str:
    """
    Formats a charity dictionary into a user-friendly display string.
    
    This helper function demonstrates how to transform structured data
    into readable text for the user.
    
    Args:
        charity: Dictionary containing charity data (name, ein, mission, rating, efficiency)
        
    Returns:
        Formatted string suitable for display to the user
    """
    name = charity.get('name', 'Unknown')
    ein = charity.get('ein', 'N/A')
    mission = charity.get('mission', 'No mission statement available')
    rating = charity.get('rating', 0.0)
    efficiency = charity.get('efficiency', 0.0)
    
    # Format efficiency as percentage
    efficiency_pct = int(efficiency * 100)
    
    # Build formatted string
    display = f"""
**{name}** (EIN: {ein})
⭐ Rating: {rating}/5.0
💰 Efficiency: {efficiency_pct}% of funds go to programs
📋 Mission: {mission}
    """.strip()
    
    return display

Шаг 5: Создание торгового агента — импорт компонентов

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

👉 Открыто

charity_advisor/shopping_agent/agent.py

Вы увидите шаблон с комментариями-заполнителями. Давайте создадим его шаг за шагом.

👉 Найти:

# MODULE_4_STEP_5_IMPORT_COMPONENTS

👉 Замените эту единственную строку на:

from google.adk.agents import Agent
from google.adk.tools import FunctionTool
from charity_advisor.tools.charity_tools import find_charities, save_user_choice

Шаг 6: Напишите инструкцию для агента

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

👉 Найти:

# MODULE_4_STEP_6_WRITE_INSTRUCTION
instruction="""""",

👉 Замените эти две строки на:

    instruction="""You are a research specialist helping users find verified charities.

Your workflow:

1. When the user describes what cause they want to support (e.g., "education", "health", "environment"),
   use the find_charities tool to search our vetted database.

2. Present the results clearly. The tool returns formatted charity information that you should
   show to the user.

3. When the user selects a charity and specifies an amount, use the save_user_choice tool
   to create an IntentMandate and record their decision. You MUST call save_user_choice with:
   - charity_name: The exact name of the chosen charity
   - charity_ein: The EIN of the chosen charity  
   - amount: The donation amount in dollars (as a number, not a string)

4. After successfully saving, inform the user:
   - That you've created an IntentMandate (mention the intent ID if provided)
   - When the intent expires
   - That you're passing their request to the secure payment processor

IMPORTANT BOUNDARIES:
- Your ONLY job is discovery and creating the IntentMandate
- You do NOT process payments
- You do NOT see the user's payment methods
- You do NOT create cart offers (that's the Merchant Agent's job)
- After calling save_user_choice, your work is done

WHAT IS AN INTENTMANDATE:
An IntentMandate is a structured record of what the user wants to do. It includes:
- Natural language description ("Donate $50 to Room to Read")
- Which merchants can fulfill it
- When the intent expires
- Whether user confirmation is required

This is the first of three verifiable credentials in our secure payment system.

If the user asks you to do anything related to payment processing, politely explain that
you don't have that capability and that their request will be handled by the appropriate
specialist agent.""",

Шаг 7: Добавьте инструменты в агент

Теперь предоставим агенту доступ к обоим инструментам.

👉 Найти:

# MODULE_4_STEP_7_ADD_TOOLS

👉 Замените эти две строки на:

    tools=[
        FunctionTool(func=find_charities),
        FunctionTool(func=save_user_choice)
    ]

Шаг 8: Проверьте своего полного агента

Давайте проверим, что все подключено правильно.

👉 Ваш полный

charity_advisor/shopping_agent/agent.py

Теперь должно выглядеть так:

"""
Shopping Agent - Finds charities from a trusted database and saves the user's choice.
This agent acts as our specialized "Research Analyst."
"""

from google.adk.agents import Agent
from google.adk.tools import FunctionTool
from charity_advisor.tools.charity_tools import find_charities, save_user_choice


shopping_agent = Agent(
    name="ShoppingAgent",
    model="gemini-2.5-pro",
    description="Finds and recommends vetted charities from a trusted database, then creates an IntentMandate capturing the user's donation intent.",
    instruction="""You are a research specialist helping users find verified charities.

Your workflow:

1. When the user describes what cause they want to support (e.g., "education", "health", "environment"),
   use the find_charities tool to search our vetted database.

2. Present the results clearly. The tool returns formatted charity information that you should
   show to the user.

3. When the user selects a charity and specifies an amount, use the save_user_choice tool
   to create an IntentMandate and record their decision. You MUST call save_user_choice with:
   - charity_name: The exact name of the chosen charity
   - charity_ein: The EIN of the chosen charity  
   - amount: The donation amount in dollars (as a number, not a string)

4. After successfully saving, inform the user:
   - That you've created an IntentMandate (mention the intent ID if provided)
   - When the intent expires
   - That you're passing their request to the secure payment processor

IMPORTANT BOUNDARIES:
- Your ONLY job is discovery and creating the IntentMandate
- You do NOT process payments
- You do NOT see the user's payment methods
- You do NOT create cart offers (that's the Merchant Agent's job)
- After calling save_user_choice, your work is done

WHAT IS AN INTENTMANDATE:
An IntentMandate is a structured record of what the user wants to do. It includes:
- Natural language description ("Donate $50 to Room to Read")
- Which merchants can fulfill it
- When the intent expires
- Whether user confirmation is required

This is the first of three verifiable credentials in our secure payment system.

If the user asks you to do anything related to payment processing, politely explain that
you don't have that capability and that their request will be handled by the appropriate
specialist agent.""",
    tools=[
        FunctionTool(func=find_charities),
        FunctionTool(func=save_user_choice)
    ]
)

Отлично! Вы создали агент промышленного качества, соответствующий стандарту AP2, с:

  • Проверка входных данных
  • Правильное создание IntentMandate с использованием моделей AP2 Pydantic
  • Форматированный вывод
  • Четкие границы ролей
  • Подробные инструкции
  • Обработка ошибок

Шаг 9: Тестирование торгового агента

Давайте проверим, что наш агент работает правильно, создает правильные IntentMandates и соблюдает свои границы.

👉 В терминале Cloud Shell выполните:

adk run charity_advisor/shopping_agent

Появится приглашение [user]: :.

Тест 1: Обнаружение с помощью доверенной базы данных

👉 Тип:

I want to donate to an education charity. What are my options?

Через мгновение агент предоставит ответ. Благодаря нашему помощнику _format_charity_display результаты будут красиво отформатированы:

В нашей базе данных я нашел 3 проверенные благотворительные организации, занимающиеся образованием:

Комната для чтения (EIN: 77-0479905)
⭐ Рейтинг: 4.9/5.0
💰 Эффективность: 88% средств идут на программы
📋 Миссия: изменить жизнь миллионов детей из малообеспеченных семей, уделяя особое внимание грамотности и гендерному равенству в образовании.

Teach For America (EIN: 13-3541913)
⭐ Рейтинг: 4.7/5.0
💰 Эффективность: 81% средств идет на программы
📋 Миссия: Работа по расширению образовательных возможностей для детей, сталкивающихся с трудностями.

Альянс технического образования (EIN: 45-2345678)
⭐ Рейтинг: 4.8/5.0
💰 Эффективность: 92% средств идут на программы
📋 Миссия: предоставить образование в области информатики школам с недостаточным уровнем обеспеченности.

Какую благотворительную организацию вы хотели бы поддержать и какую сумму вы готовы пожертвовать?

Сравните это с простым агентом из Модуля 3 , который выдал вам непроверенные результаты Google. В этом и заключается разница между ненадёжным и надёжным поиском.

Тест 2: запись выбора пользователя и создание IntentMandate

👉 Тип:

I'll donate $50 to Room to Read.

Вы должны увидеть, как агент вызывает инструмент save_user_choice с правильными параметрами. Благодаря созданию IntentMandate с использованием моделей AP2 вы увидите более структурированный ответ:

Отлично! Я создал IntentMandate для вашего пожертвования:

Подробности намерения:

  • Идентификатор намерения: intent_774795905_1730927536
  • Сумма: 50,00 долларов США в Room to Read (EIN: 77-0479905)
  • Истекает: 2024-11-07T15:32:16Z (через 1 час)

Этот IntentMandate фиксирует ваше намерение сделать пожертвование и включает ограничения, обеспечивающие безопасную обработку транзакции. Сейчас я передаю его нашему безопасному платежному процессору для формирования официального предложения и завершения вашего пожертвования.

За кулисами только что произошло несколько событий:

  1. _validate_charity_data() проверила входные данные
  2. _create_intent_mandate() использовала модель AP2 Pydantic для построения структуры:
   # Created and validated via Pydantic
   intent_mandate_model = IntentMandate(
       user_cart_confirmation_required=True,
       natural_language_description="Donate $50.00 to Room to Read",
       merchants=["Room to Read"],
       skus=None,
       requires_refundability=False,
       intent_expiry="2024-11-07T15:32:16Z"
   )
   
   # Converted to dict and extended with custom fields
   intent_mandate_dict = intent_mandate_model.model_dump()
   intent_mandate_dict.update({
       "charity_ein": "77-0479905",
       "amount": 50.0,
       "currency": "USD"
   })
  1. Инструмент записал ТОЛЬКО IntentMandate в общее состояние:
   state["intent_mandate"] = intent_mandate_dict
  1. Сообщение об успешном завершении с подробностями намерения было возвращено LLM.
  2. Магистр права составил окончательное сообщение для пользователя.

Этот IntentMandate теперь готов к получению агентом продавца (модуль 5). Продавец извлечёт из IntentMandate название благотворительной организации и сумму и проверит, не истёк ли срок его действия.

Тест 3: Проверка в действии

Let's verify our validation helper catches bad input.

👉 Type:

I'll donate -$25 to Room to Read.

The agent should catch the invalid amount:

I encountered an issue: Donation amount must be positive, got: $-25.0

Please specify a positive donation amount and I'll create the IntentMandate for you.

This is defensive programming in action. Our _validate_charity_data helper prevented invalid data from entering the system and creating a malformed IntentMandate.

Test 4: Verify Trust Boundary

👉 Try asking the agent to process payment:

Now process my credit card payment.

The agent should refuse, respecting its role boundary:

I don't have the capability to process payments—that's not part of my role. My job is to help you find verified charities and create the IntentMandate that captures your donation intent.

Your IntentMandate has already been created and passed to our secure payment processor. The Merchant Agent will create a formal offer (CartMandate), and then the Credentials Provider will handle the actual payment with your explicit consent.

This is the trust boundary in action. The agent knows it's not authorized to handle payment data, and its instruction explicitly guides it to explain this to users while also teaching them about the IntentMandate concept.

👉 Press

Ctrl+C

to exit when finished testing.

What You Just Built

You've successfully implemented the first piece of AP2's architecture with proper IntentMandate creation using AP2 Pydantic models.

Key Concepts Mastered

Role-Based Architecture:

  • Each agent has one clearly defined job
  • Agents communicate through shared state, not direct access
  • Trust boundaries limit the impact of compromise

IntentMandate (AP2 Credential #1):

  • Created using official AP2 Pydantic models for validation
  • Structured capture of user intent
  • Includes expiry for security (prevents replay attacks)
  • Specifies constraints (merchants, refundability, confirmation)
  • Natural language description for humans
  • Machine-readable for agents
  • Model validated before conversion to dictionary

State as Shared Memory:

  • tool_context.state is the "notepad" all agents can access
  • Writing to state = making verifiable credentials available
  • Reading from state = consuming and validating credentials
  • Downstream agents extract what they need from credentials

FunctionTool:

  • Converts Python functions into LLM-callable tools
  • Relies on docstrings and type hints for LLM understanding
  • Handles invocation automatically
  • Tool composability: small focused tools > monolithic ones

Agent Instructions:

  • Step-by-step workflow guidance
  • Explicit boundaries ("do NOT...")
  • Parameter specifications to prevent errors
  • Technical definitions (what is IntentMandate)
  • Edge case handling (what to say when...)

Что дальше?

In the next module, we'll build the Merchant Agent to receive the IntentMandate and create the second verifiable credential: CartMandate .

The Shopping Agent has created an IntentMandate capturing the user's intent with expiry. Now we need an agent to read that credential, validate it hasn't expired, and create a formal, signed offer that says: "I, the merchant, will honor this price and deliver these goods."

Let's build the Merchant Agent and see the second AP2 credential in action.

5. Building the Merchant Agent - Binding Offers & CartMandate

баннер

From Discovery to Commitment

In the previous module, you built the Shopping Agent—a specialist that finds verified charities and creates an IntentMandate capturing the user's intent. Now we need an agent to receive that IntentMandate and create a formal, binding offer.

This is where AP2's second key principle comes into play: verifiable credentials through CartMandate .

AP2 Principle: CartMandate & Binding Offers

Why We Need a Merchant Role

In Module 4, the Shopping Agent created an IntentMandate and saved it to state:

state["intent_mandate"] = {
    "natural_language_description": "Donate $50 to Room to Read",
    "merchants": ["Room to Read"],
    "amount": 50.0,
    "intent_expiry": "2024-11-07T15:32:16Z"
}

But this is just user intent. Before any payment can be processed, we need:

  • A formal offer structure that payment systems understand
  • Proof that the merchant will honor this price
  • A binding commitment that can't be altered mid-transaction
  • Validation that the intent hasn't expired

This is the Merchant Agent's job.

What is a CartMandate?

A CartMandate is AP2's term for a "digital shopping cart" that serves as a binding offer. It's structured according to the W3C PaymentRequest standard, which means:

  • Payment processors worldwide recognize the format
  • It contains all transaction details in a standardized way
  • It can be cryptographically signed to prove authenticity

Think of it like a written quote from a contractor:

  • ❌ Verbal: "Yeah, I can do that job for about fifty bucks"
  • ✅ Written quote: Itemized costs, total, signature, date

The written quote is binding. The CartMandate is the digital equivalent.

intent to cart

The Structure of a CartMandate

A CartMandate in AP2 has a specific nested structure:

cart_mandate = {
    "contents": {  # ← AP2 wrapper
        "id": "cart_xyz123",
        "cart_expiry": "2024-11-07T15:47:16Z",
        "merchant_name": "Room to Read",
        "user_cart_confirmation_required": False,
        
        "payment_request": {  # ← W3C PaymentRequest nested inside
            "method_data": [...],
            "details": {...},
            "options": {...}
        }
    },
    "merchant_authorization": "SIG_a3f7b2c8"  # ← Merchant signature
}

Three main components:

1. contents - The cart wrapper containing:

  • Cart ID and expiry
  • Имя продавца
  • The W3C PaymentRequest

2. payment_request (inside contents) - What's being purchased:

  • method_data: Payment types accepted
  • details: Items and total
  • options: Shipping, payer info requirements

3. merchant_authorization - Cryptographic signature

Merchant Signatures: Proof of Commitment

The merchant signature is critical. It proves:

  • This offer came from an authorized merchant
  • The merchant commits to honor this exact price
  • The offer hasn't been tampered with since creation

In production, this would be a cryptographic signature using PKI (Public Key Infrastructure) or JWT (JSON Web Tokens). For our educational workshop, we'll simulate this with a SHA-256 hash.

# Production (real signature):
signature = sign_with_private_key(cart_data, merchant_private_key)

# Workshop (simulated signature):
cart_hash = hashlib.sha256(cart_json.encode()).hexdigest()
signature = f"SIG_{cart_hash[:16]}"

Our Mission: Build the Merchant Agent

The Merchant Agent will:

  1. Read the IntentMandate from state (what Shopping Agent wrote)
  2. Validate that the intent hasn't expired
  3. Extract the charity name, amount, and other details
  4. Create a W3C-compliant PaymentRequest structure using AP2 Pydantic models
  5. Wrap it in AP2's CartMandate with expiry
  6. Add a simulated merchant signature
  7. Write the CartMandate to state for the Credentials Provider (next module)

Давайте создадим его шаг за шагом.

Step 1: Add Expiry Validation Helper

First, let's set up the merchant-related tools file and add a helper to validate IntentMandate expiry.

👉 Open

charity_advisor/tools/merchant_tools.py

Let's add the expiry validation:

👉 Find:

# MODULE_5_STEP_1_ADD_EXPIRY_VALIDATION_HELPER

👉 Replace that single line with:

def _validate_intent_expiry(intent_expiry_str: str) -> tuple[bool, str]:
    """
    Validates that the IntentMandate hasn't expired.
    
    This is a critical security check - expired intents should not be processed.
    
    Args:
        intent_expiry_str: The ISO 8601 timestamp string from the IntentMandate.
        
    Returns:
        (is_valid, error_message): Tuple indicating if intent is still valid.
    """
    try:
        # The .replace('Z', '+00:00') is for compatibility with older Python versions
        expiry_time = datetime.fromisoformat(intent_expiry_str.replace('Z', '+00:00'))
        now = datetime.now(timezone.utc)
        
        if expiry_time < now:
            return False, f"IntentMandate expired at {intent_expiry_str}"
        
        time_remaining = expiry_time - now
        logger.info(f"IntentMandate valid. Expires in {time_remaining.total_seconds():.0f} seconds")
        
        return True, ""
        
    except (ValueError, TypeError) as e:
        return False, f"Invalid intent_expiry format: {e}"

Step 2: Add Signature Generation Helper

Now let's create a helper that generates the simulated merchant signature.

👉 Find:

# MODULE_5_STEP_2_ADD_SIGNATURE_HELPER

👉 Replace that single line with:

def _generate_merchant_signature(cart_contents: CartContents) -> str:
    """
    Generates a simulated merchant signature for the CartMandate contents.
    
    In production, this would use PKI or JWT with the merchant's private key.
    For this codelab, we use a SHA-256 hash of the sorted JSON representation.
    
    Args:
        cart_contents: The Pydantic model of the cart contents to sign.
        
    Returns:
        Simulated signature string (format: "SIG_" + first 16 chars of hash).
    """
    # Step 1: Dump the Pydantic model to a dictionary. The `mode='json'` argument
    # ensures that complex types like datetimes are serialized correctly.
    cart_contents_dict = cart_contents.model_dump(mode='json')
    
    # Step 2: Use the standard json library to create a stable, sorted JSON string.
    # separators=(',', ':') removes whitespace for a compact and canonical representation.
    cart_json = json.dumps(cart_contents_dict, sort_keys=True, separators=(',', ':'))
    
    # Step 3: Generate SHA-256 hash.
    cart_hash = hashlib.sha256(cart_json.encode('utf-8')).hexdigest()
    
    # Step 4: Create signature in a recognizable format.
    signature = f"SIG_{cart_hash[:16]}"
    
    logger.info(f"Generated merchant signature: {signature}")
    return signature

Step 3A: Create the Tool Signature and Setup

Now let's start building the main tool. We'll create it incrementally across four substeps. First, the function signature and initial setup.

👉 Find:

# MODULE_5_STEP_3A_CREATE_TOOL_SIGNATURE

👉 Replace that single line with:

async def create_cart_mandate(tool_context: Any) -> Dict[str, Any]:
    """
    Creates a W3C PaymentRequest-compliant CartMandate from the IntentMandate.
    
    This tool reads the IntentMandate from shared state, validates it, and
    creates a formal, signed offer using the official AP2 Pydantic models.
    
    Returns:
        Dictionary containing status and the created CartMandate.
    """
    logger.info("Tool called: Creating CartMandate from IntentMandate")
    
    # MODULE_5_STEP_3B_ADD_VALIDATION_LOGIC

Step 3B: Add Validation Logic

Now let's add the logic to read and validate the IntentMandate using AP2 Pydantic models, and extract the data we need.

👉 Find:

# MODULE_5_STEP_3B_ADD_VALIDATION_LOGIC

👉 Replace that single line with:

    # 1. Read IntentMandate dictionary from state
    intent_mandate_dict = tool_context.state.get("intent_mandate")
    if not intent_mandate_dict:
        logger.error("No IntentMandate found in state")
        return {
            "status": "error",
            "message": "No IntentMandate found. Shopping Agent must create intent first."
        }
    
    # 2. Parse dictionary into a validated Pydantic model
    try:
        intent_mandate_model = IntentMandate.model_validate(intent_mandate_dict)
    except Exception as e:
        logger.error(f"Could not validate IntentMandate structure: {e}")
        return {"status": "error", "message": f"Invalid IntentMandate structure: {e}"}
    
    # 3. Validate that the intent hasn't expired (CRITICAL security check)
    is_valid, error_message = _validate_intent_expiry(intent_mandate_model.intent_expiry)
    if not is_valid:
        logger.error(f"IntentMandate validation failed: {error_message}")
        return {"status": "error", "message": error_message}
    
    # 4. Extract data. Safely access standard fields from the model, and
    # custom fields (like 'amount') from the original dictionary.
    charity_name = intent_mandate_model.merchants[0] if intent_mandate_model.merchants else "Unknown Charity"
    amount = intent_mandate_dict.get("amount", 0.0)
    
    # MODULE_5_STEP_3C_CREATE_CARTMANDATE_STRUCTURE

Step 3C: Create CartMandate Structure

Now let's build the W3C-compliant PaymentRequest structure and wrap it in the AP2 CartMandate using Pydantic models.

👉 Find:

# MODULE_5_STEP_3C_CREATE_CARTMANDATE_STRUCTURE

👉 Replace that single line with:

    # 5. Build the nested Pydantic models for the CartMandate
    timestamp = datetime.now(timezone.utc)
    cart_id = f"cart_{hashlib.sha256(f'{charity_name}{timestamp.isoformat()}'.encode()).hexdigest()[:12]}"
    cart_expiry = timestamp + timedelta(minutes=15)
    
    payment_request_model = PaymentRequest(
        method_data=[PaymentMethodData(
            supported_methods="CARD",
            data={"supported_networks": ["visa", "mastercard", "amex"], "supported_types": ["debit", "credit"]}
        )],
        details=PaymentDetailsInit(
            id=f"order_{cart_id}",
            display_items=[PaymentItem(
                label=f"Donation to {charity_name}",
                amount=PaymentCurrencyAmount(currency="USD", value=amount)  # Pydantic v2 handles float -> str conversion
            )],
            total=PaymentItem(
                label="Total Donation",
                amount=PaymentCurrencyAmount(currency="USD", value=amount)
            )
        ),
        options=PaymentOptions(request_shipping=False)
    )
    
    cart_contents_model = CartContents(
        id=cart_id,
        cart_expiry=cart_expiry.isoformat(),
        merchant_name=charity_name,
        user_cart_confirmation_required=False,
        payment_request=payment_request_model
    )
    
    # MODULE_5_STEP_3D_ADD_SIGNATURE_AND_SAVE

Step 3D: Add Signature and Save to State

Finally, let's sign the CartMandate using our Pydantic model and save it to state for the next agent.

👉 Find:

# MODULE_5_STEP_3D_ADD_SIGNATURE_AND_SAVE

👉 Replace that single line with:

    # 6. Generate signature from the validated Pydantic model
    signature = _generate_merchant_signature(cart_contents_model)
    
    # 7. Create the final CartMandate model, now including the signature
    cart_mandate_model = CartMandate(
        contents=cart_contents_model,
        merchant_authorization=signature
    )
    
    # 8. Convert the final model to a dictionary for state storage and add the custom timestamp
    cart_mandate_dict = cart_mandate_model.model_dump(mode='json')
    cart_mandate_dict["timestamp"] = timestamp.isoformat()
    
    # 9. Write the final dictionary to state
    tool_context.state["cart_mandate"] = cart_mandate_dict
    
    logger.info(f"CartMandate created successfully: {cart_id}")
    
    return {
        "status": "success",
        "message": f"Created signed CartMandate {cart_id} for ${amount:.2f} donation to {charity_name}",
        "cart_id": cart_id,
        "cart_expiry": cart_expiry.isoformat(),
        "signature": signature
    }

Step 4: Build the Merchant Agent - Import Components

Now let's create the agent that will use this tool.

👉 Open

charity_advisor/merchant_agent/agent.py

You'll see a template with placeholder markers. Let's start by importing what we need.

👉 Find:

# MODULE_5_STEP_4_IMPORT_COMPONENTS

👉 Replace that single line with:

from google.adk.agents import Agent
from google.adk.tools import FunctionTool
from charity_advisor.tools.merchant_tools import create_cart_mandate

Step 5: Write the Merchant Agent Instruction

Now let's write the instruction that tells the agent when and how to use its tool.

👉 Find:

# MODULE_5_STEP_5_WRITE_INSTRUCTION
instruction="""""",

👉 Replace those two lines with:

    instruction="""You are a merchant specialist responsible for creating formal, signed offers (CartMandates).

Your workflow:

1. Read the IntentMandate from shared state.
   The IntentMandate was created by the Shopping Agent and contains:
   - merchants: List of merchant names
   - amount: Donation amount
   - charity_ein: Tax ID
   - intent_expiry: When the intent expires

2. Use the create_cart_mandate tool to create a W3C PaymentRequest-compliant CartMandate.
   This tool will:
   - Validate the IntentMandate hasn't expired (CRITICAL security check)
   - Extract the charity name and amount from the IntentMandate
   - Create a structured offer with payment methods, transaction details, and merchant info
   - Generate a merchant signature to prove authenticity
   - Save the CartMandate to state for the payment processor

3. After creating the CartMandate, inform the user:
   - That you've created a formal, signed offer
   - The cart ID
   - When the cart expires (15 minutes)
   - That you're passing it to the secure payment processor

IMPORTANT BOUNDARIES:
- Your ONLY job is creating signed CartMandates from valid IntentMandates
- You do NOT process payments
- You do NOT see the user's payment methods or credentials
- You do NOT interact with payment networks
- You MUST validate that the IntentMandate hasn't expired before creating a cart
- After calling create_cart_mandate, your work is done

WHAT IS A CARTMANDATE:
A CartMandate is a binding commitment that says:
"I, the merchant, commit to accepting $X for this charity donation, and I prove it with my signature."

This commitment is structured using the W3C PaymentRequest standard and includes:
- Payment methods accepted (card, bank transfer)
- Transaction details (amount, charity name)
- Cart expiry (15 minutes from creation)
- Merchant signature (proof of commitment)

This is the second of three verifiable credentials in our secure payment system.""",

Step 6: Add Tools to the Merchant Agent

👉 Find:

# MODULE_5_STEP_6_ADD_TOOLS
tools=[],

👉 Replace those two lines with:

    tools=[
        FunctionTool(func=create_cart_mandate)
    ],

Step 7: Verify the Complete Merchant Agent

Let's confirm everything is wired correctly.

👉 Your complete

charity_advisor/merchant_agent/agent.py

should now look like this:

"""
Merchant Agent - Creates W3C-compliant CartMandates with merchant signatures.
This agent acts as our "Contract Creator."
"""

from google.adk.agents import Agent
from google.adk.tools import FunctionTool
from charity_advisor.tools.merchant_tools import create_cart_mandate


merchant_agent = Agent(
    name="MerchantAgent",
    model="gemini-2.5-flash",
    description="Creates formal, signed CartMandates for charity donations following W3C PaymentRequest standards.",
    tools=[
        FunctionTool(func=create_cart_mandate)
    ],
    instruction="""You are a merchant specialist responsible for creating formal, signed offers (CartMandates).

Your workflow:

1. Read the IntentMandate from shared state.
   The IntentMandate was created by the Shopping Agent and contains:
   - merchants: List of merchant names
   - amount: Donation amount
   - charity_ein: Tax ID
   - intent_expiry: When the intent expires

2. Use the create_cart_mandate tool to create a W3C PaymentRequest-compliant CartMandate.
   This tool will:
   - Validate the IntentMandate hasn't expired (CRITICAL security check)
   - Extract the charity name and amount from the IntentMandate
   - Create a structured offer with payment methods, transaction details, and merchant info
   - Generate a merchant signature to prove authenticity
   - Save the CartMandate to state for the payment processor

3. After creating the CartMandate, inform the user:
   - That you've created a formal, signed offer
   - The cart ID
   - When the cart expires (15 minutes)
   - That you're passing it to the secure payment processor

IMPORTANT BOUNDARIES:
- Your ONLY job is creating signed CartMandates from valid IntentMandates
- You do NOT process payments
- You do NOT see the user's payment methods or credentials
- You do NOT interact with payment networks
- You MUST validate that the IntentMandate hasn't expired before creating a cart
- After calling create_cart_mandate, your work is done

WHAT IS A CARTMANDATE:
A CartMandate is a binding commitment that says:
"I, the merchant, commit to accepting $X for this charity donation, and I prove it with my signature."

This commitment is structured using the W3C PaymentRequest standard and includes:
- Payment methods accepted (card, bank transfer)
- Transaction details (amount, charity name)
- Cart expiry (15 minutes from creation)
- Merchant signature (proof of commitment)

This is the second of three verifiable credentials in our secure payment system."""
)

Checkpoint : You now have a complete Merchant Agent with proper AP2 CartMandate creation using Pydantic models.

Step 8: Test the Merchant Agent

Now let's verify that our agent correctly creates CartMandates with signatures and validates expiry.

Test Setup: Run the Test Script

👉 In your Cloud Shell terminal, run:

python scripts/test_merchant.py

Ожидаемый результат:

======================================================================
MERCHANT AGENT TEST
======================================================================

Simulated IntentMandate from Shopping Agent:
  charity: Room to Read
  amount: $50.00
  expiry: 2024-11-07T16:32:16Z

----------------------------------------------------------------------
Merchant Agent Response:
----------------------------------------------------------------------
Perfect! I've received your IntentMandate and created a formal, signed offer (CartMandate) for your donation.

**CartMandate Details:**
- **Cart ID**: cart_3b4c5d6e7f8a
- **Donation Amount**: $50.00 to Room to Read
- **Payment Methods Accepted**: Credit/debit cards (Visa, Mastercard, Amex) or bank transfer
- **Cart Expires**: 2024-11-07T15:47:16Z (in 15 minutes)
- **Merchant Signature**: SIG_a3f7b2c8d9e1f4a2

This signed CartMandate proves my commitment to accept this donation amount. I'm now passing this to the secure payment processor to complete your transaction.

======================================================================
CARTMANDATE CREATED:
======================================================================
  ID: cart_3b4c5d6e7f8a
  Amount: 50.00
  Merchant: Room to Read
  Expires: 2024-11-07T15:47:16Z
  Signature: SIG_a3f7b2c8d9e1f4a2
======================================================================

Test 2: Verify W3C Compliance

Let's validate that our CartMandate structure is fully compliant with both AP2 and W3C PaymentRequest standards.

👉 Run the validation script:

python scripts/validate_cartmandate.py

Ожидаемый результат:

======================================================================
AP2 & W3C PAYMENTREQUEST VALIDATION
======================================================================
✅ CartMandate is AP2 and W3C PaymentRequest compliant

Structure validation passed:
  ✓ AP2 'contents' wrapper present
  ✓ AP2 'merchant_authorization' signature present
  ✓ cart_expiry present
  ✓ payment_request nested inside contents
  ✓ method_data present and valid
  ✓ details.total.amount present with currency and value
  ✓ All required W3C PaymentRequest fields present
======================================================================

What You Just Built

You've successfully implemented AP2's CartMandate using Pydantic models for proper structure, expiry validation, and merchant signatures.

Key Concepts Mastered

CartMandate (AP2 Credential #2):

  • Created using official AP2 Pydantic models
  • AP2 structure with contents wrapper
  • W3C PaymentRequest nested inside
  • Cart expiry (shorter than intent)
  • Merchant signature for binding commitment
  • Model validation ensures spec compliance

Expiry Validation:

  • Reading IntentMandate from state
  • Validating structure with IntentMandate.model_validate()
  • Parsing ISO 8601 timestamps
  • Comparing to current time
  • Security feature preventing stale processing

Merchant Signature:

  • Proves authenticity and commitment
  • Generated from validated Pydantic model
  • Uses model_dump(mode='json') for canonical representation
  • Simulated with SHA-256 for education
  • Production uses PKI/JWT
  • Signs the contents model, not dictionaries

W3C PaymentRequest:

  • Built using AP2's PaymentRequest Pydantic model
  • Industry standard for payment data
  • Nested inside AP2 structure
  • Contains method_data, details, options
  • Enables interoperability

Credential Chain with Models:

  • Shopping → IntentMandate (validated)
  • Merchant reads IntentMandate → CartMandate (both models validated)
  • Credentials Provider will read CartMandate → PaymentMandate
  • Each step validates previous credential using Pydantic

Model-Driven Development:

  • Input validation via model_validate()
  • Type-safe construction
  • Automatic serialization via model_dump()
  • Production-ready patterns

Что дальше?

In the next module, we'll build the Credentials Provider to process payments securely.

The Merchant Agent has created a binding offer with expiry using AP2 models. Now we need an agent to read that CartMandate, get user consent, and execute the payment.

Let's build the Credentials Provider and complete the AP2 credential chain.

6. Building the Credentials Provider - Secure Payment Execution

баннер

From Binding Offer to Payment Execution

In Module 5, you built the Merchant Agent—a specialist that reads IntentMandates, validates they haven't expired, and creates binding CartMandates with merchant signatures. Now we need an agent to receive that CartMandate and execute the actual payment.

This is where AP2's third and final principle comes into play: secure payment execution through PaymentMandate .

AP2 Principle: PaymentMandate & Payment Execution

Why We Need a Credentials Provider Role

In Module 5, the Merchant Agent created a CartMandate and saved it to state:

state["cart_mandate"] = {
    "contents": {
        "id": "cart_abc123",
        "cart_expiry": "2025-11-07:15:47:16Z",
        "payment_request": {
            "details": {
                "total": {
                    "amount": {"currency": "USD", "value": "50.00"}
                }
            }
        }
    },
    "merchant_authorization": "SIG_a3f7b2c8"
}

But this is just a binding offer. Before payment can be executed, we need:

  • Validation that the cart hasn't expired
  • User consent to proceed with payment
  • A credential that authorizes payment execution
  • Actual payment processing (or simulation for our workshop)

This is the Credentials Provider's job.

What is a PaymentMandate?

A PaymentMandate is AP2's term for the final authorization that allows payment to be executed. It's the third and final verifiable credential in the AP2 chain.

Think of the three credentials like a contract signing process:

  • IntentMandate : "I'm interested in buying this" (Letter of intent)
  • CartMandate : "I, the merchant, offer to sell at this price" (Written quote)
  • PaymentMandate : "I authorize you to charge my payment method" (Signed contract)

Only after all three credentials exist can payment be executed.

complete credential chain

The Structure of a PaymentMandate

A PaymentMandate in AP2 has a specific structure:

payment_mandate = {
    "payment_mandate_contents": {  # ← AP2 wrapper
        "payment_mandate_id": "payment_xyz123",
        "payment_details_id": "cart_abc123",  # Links to CartMandate
        "user_consent": True,
        "consent_timestamp": "2025-11-07T15:48:00Z",
        "amount": {
            "currency": "USD",
            "value": "50.00"
        },
        "merchant_name": "Room to Read"
    },
    "agent_present": True,  # Human-in-the-loop flow
    "timestamp": "2025-11-07T15:48:00Z"
}

Ключевые компоненты:

1. payment_mandate_contents - The authorization wrapper containing:

  • payment_mandate_id: Unique identifier
  • payment_details_id: Links back to CartMandate
  • user_consent: Whether user approved
  • amount: Payment amount (extracted from CartMandate)

2. agent_present - Whether this is a human-in-the-loop flow

3. timestamp - When authorization was created

Our Mission: Build the Credentials Provider

The Credentials Provider will:

  1. Read the CartMandate from state (what Merchant Agent wrote)
  2. Validate that the cart hasn't expired using AP2 Pydantic models
  3. Extract payment details from the nested structure
  4. Create a PaymentMandate with user consent using AP2 models
  5. Simulate payment processing (in production, would call real payment API)
  6. Write the PaymentMandate and payment result to state

Давайте создадим его шаг за шагом.

Step 1: Add Cart Expiry Validation Helper

First, let's create a helper that validates the CartMandate hasn't expired—just like we validated IntentMandate expiry in Module 5.

👉 Open

charity_advisor/tools/payment_tools.py

Let's add the expiry validation:

👉 Find:

# MODULE_6_STEP_1_ADD_CART_EXPIRY_VALIDATION_HELPER

👉 Replace that single line with:

def _validate_cart_expiry(cart: CartMandate) -> tuple[bool, str]:
    """
    Validates that the CartMandate hasn't expired.
    
    This is a critical security check - expired carts should not be processed.
    
    Args:
        cart: The Pydantic CartMandate model to validate.
        
    Returns:
        (is_valid, error_message): Tuple indicating if cart is still valid.
    """
    try:
        expiry_str = cart.contents.cart_expiry
        expiry_time = datetime.fromisoformat(expiry_str.replace('Z', '+00:00'))
        now = datetime.now(timezone.utc)
        
        if expiry_time < now:
            return False, f"CartMandate expired at {expiry_str}"
        
        time_remaining = expiry_time - now
        logger.info(f"CartMandate valid. Expires in {time_remaining.total_seconds():.0f} seconds")
        
        return True, ""
        
    except (ValueError, TypeError, AttributeError) as e:
        return False, f"Invalid cart_expiry format or structure: {e}"

Step 2: Add PaymentMandate Creation Helper

Now let's create a helper that builds the PaymentMandate structure using official AP2 Pydantic models.

👉 Find:

# MODULE_6_STEP_2_ADD_PAYMENT_MANDATE_CREATION_HELPER

👉 Replace that single line with:

def _create_payment_mandate(cart: CartMandate, consent_granted: bool) -> dict:
    """
    Creates a PaymentMandate using the official AP2 Pydantic models.
    
    It links to the CartMandate and includes user consent status.
    
    Args:
        cart: The validated Pydantic CartMandate model being processed.
        consent_granted: Whether the user has consented to the payment.
        
    Returns:
        A dictionary representation of the final, validated PaymentMandate.
    """
    timestamp = datetime.now(timezone.utc)
    
    # Safely extract details from the validated CartMandate model
    cart_id = cart.contents.id
    merchant_name = cart.contents.merchant_name
    total_item = cart.contents.payment_request.details.total
    
    # Create the nested PaymentResponse model for the mandate
    payment_response_model = PaymentResponse(
        request_id=cart_id,
        method_name="CARD",  # As per the simulated flow
        details={"token": "simulated_payment_token_12345"}
    )
    
    # Create the PaymentMandateContents model
    payment_mandate_contents_model = PaymentMandateContents(
        payment_mandate_id=f"payment_{hashlib.sha256(f'{cart_id}{timestamp.isoformat()}'.encode()).hexdigest()[:12]}",
        payment_details_id=cart_id,
        payment_details_total=total_item,
        payment_response=payment_response_model,
        merchant_agent=merchant_name,
        timestamp=timestamp.isoformat()
    )
    
    # Create the top-level PaymentMandate model
    # In a real system, a user signature would be added to this model
    payment_mandate_model = PaymentMandate(
        payment_mandate_contents=payment_mandate_contents_model
    )
    
    # Convert the final Pydantic model to a dictionary for state storage
    final_dict = payment_mandate_model.model_dump(mode='json')
    
    # Add any custom/non-standard fields required by the codelab's logic to the dictionary
    # The spec does not have these fields, but your original code did. We add them
    # back to ensure compatibility with later steps.
    final_dict['payment_mandate_contents']['user_consent'] = consent_granted
    final_dict['payment_mandate_contents']['consent_timestamp'] = timestamp.isoformat() if consent_granted else None
    final_dict['agent_present'] = True
    
    return final_dict

Step 3A: Create the Tool Signature and Setup

Now let's start building the main tool incrementally. First, the function signature and initial setup.

👉 Find:

# MODULE_6_STEP_3A_CREATE_TOOL_SIGNATURE

👉 Replace that single line with:

async def create_payment_mandate(tool_context: Any) -> Dict[str, Any]:
    """
    Creates a PaymentMandate and simulates payment processing using Pydantic models.
    
    This tool now reads the CartMandate from state, parses it into a validated model,
    and creates a spec-compliant PaymentMandate.
    """
    logger.info("Tool called: Creating PaymentMandate and processing payment")
    
    # MODULE_6_STEP_3B_VALIDATE_CARTMANDATE

Step 3B: Validate CartMandate

Now let's add the logic to read, validate the CartMandate using AP2 Pydantic models, and check expiry.

👉 Find:

# MODULE_6_STEP_3B_VALIDATE_CARTMANDATE

👉 Replace that single line with:

    # 1. Read CartMandate dictionary from state
    cart_mandate_dict = tool_context.state.get("cart_mandate")
    if not cart_mandate_dict:
        logger.error("No CartMandate found in state")
        return { "status": "error", "message": "No CartMandate found. Merchant Agent must create cart first." }
    
    # 2. Parse dictionary into a validated Pydantic model
    try:
        cart_model = CartMandate.model_validate(cart_mandate_dict)
    except Exception as e:
        logger.error(f"Could not validate CartMandate structure: {e}")
        return {"status": "error", "message": f"Invalid CartMandate structure: {e}"}
    
    # 3. Validate that the cart hasn't expired using the Pydantic model
    is_valid, error_message = _validate_cart_expiry(cart_model)
    if not is_valid:
        logger.error(f"CartMandate validation failed: {error_message}")
        return {"status": "error", "message": error_message}
    
    # MODULE_6_STEP_3C_EXTRACT_PAYMENT_DETAILS

Step 3C: Extract Payment Details from Nested Structure

Now let's navigate the validated CartMandate model to extract the payment details we need.

👉 Find:

# MODULE_6_STEP_3C_EXTRACT_PAYMENT_DETAILS

👉 Replace that single line with:

    # 4. Safely extract data from the validated model
    cart_id = cart_model.contents.id
    merchant_name = cart_model.contents.merchant_name
    amount_value = cart_model.contents.payment_request.details.total.amount.value
    currency = cart_model.contents.payment_request.details.total.amount.currency
    consent_granted = True  # Assume consent for this codelab flow
    
    # MODULE_6_STEP_3D_CREATE_PAYMENTMANDATE_AND_SIMULATE

Step 3D: Create PaymentMandate and Simulate Payment

Finally, let's create the PaymentMandate using our Pydantic-based helper, simulate payment processing, and save everything to state.

👉 Find:

# MODULE_6_STEP_3D_CREATE_PAYMENTMANDATE_AND_SIMULATE

👉 Replace that single line with:

    # 5. Create the spec-compliant PaymentMandate using the validated CartMandate model
    payment_mandate_dict = _create_payment_mandate(cart_model, consent_granted)
    
    # 6. Simulate payment processing
    transaction_id = f"txn_{hashlib.sha256(f'{cart_id}{datetime.now(timezone.utc).isoformat()}'.encode()).hexdigest()[:16]}"
    payment_result = {
        "transaction_id": transaction_id,
        "status": "completed",
        "amount": amount_value,
        "currency": currency,
        "merchant": merchant_name,
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "simulation": True
    }
    
    # 7. Write the compliant PaymentMandate dictionary and result to state
    tool_context.state["payment_mandate"] = payment_mandate_dict
    tool_context.state["payment_result"] = payment_result
    
    logger.info(f"Payment processed successfully: {transaction_id}")
    
    return {
        "status": "success",
        "message": f"Payment of {currency} {amount_value:.2f} to {merchant_name} processed successfully",
        "transaction_id": transaction_id,
        "payment_mandate_id": payment_mandate_dict["payment_mandate_contents"]["payment_mandate_id"]
    }

Step 4: Build the Credentials Provider Agent - Import Components

Now let's create the agent that uses this tool.

👉 Open

charity_advisor/credentials_provider/agent.py

You'll see a template with placeholder markers. Let's start by importing what we need.

👉 Find:

# MODULE_6_STEP_4_IMPORT_COMPONENTS

👉 Replace that single line with:

from google.adk.agents import Agent
from google.adk.tools import FunctionTool
from charity_advisor.tools.payment_tools import create_payment_mandate

Step 5: Write the Credentials Provider Instruction

Now let's write the instruction that guides the agent.

👉 Find:

# MODULE_6_STEP_5_WRITE_INSTRUCTION
instruction="""""",

👉 Replace those two lines with:

    instruction="""You are a payment specialist responsible for securely processing payments with user consent.

Your workflow:

1. Read the CartMandate from shared state.
   The CartMandate was created by the Merchant Agent and has this structure:
   - contents: AP2 wrapper containing:
     - id: Cart identifier
     - cart_expiry: When the cart expires
     - merchant_name: Who is receiving payment
     - payment_request: W3C PaymentRequest with transaction details
   - merchant_authorization: Merchant's signature

2. Extract payment details from the nested structure:
   - Navigate: cart_mandate["contents"]["payment_request"]["details"]["total"]["amount"]
   - This gives you the currency and value

3. **IMPORTANT - Two-Turn Conversational Confirmation Pattern:**
   Before calling create_payment_mandate, you MUST:
   - Present the payment details clearly to the user
   - Ask explicitly: "I'm ready to process a payment of $X to [Charity Name]. Do you want to proceed with this donation?"
   - WAIT for the user's explicit confirmation (e.g., "yes", "proceed", "confirm")
   - ONLY call create_payment_mandate AFTER receiving explicit confirmation
   - If user says "no" or "cancel", DO NOT call the tool

4. After user confirms, use the create_payment_mandate tool to:
   - Validate the CartMandate hasn't expired (CRITICAL security check)
   - Create a PaymentMandate (the third AP2 credential)
   - Simulate payment processing
   - Record the transaction result

5. After processing, inform the user:
   - That payment was processed successfully (this is a simulation)
   - The transaction ID
   - The amount and merchant
   - That this completes the three-agent AP2 credential chain

IMPORTANT BOUNDARIES:
- Your ONLY job is creating PaymentMandates and processing payments
- You do NOT discover charities (that's Shopping Agent's job)
- You do NOT create offers (that's Merchant Agent's job)
- You MUST validate that the CartMandate hasn't expired before processing
- You MUST get explicit user confirmation before calling create_payment_mandate
- In production, this consent mechanism would be even more robust

WHAT IS A PAYMENTMANDATE:
A PaymentMandate is the final credential that authorizes payment execution. It:
- Links to the CartMandate (proving the merchant's offer)
- Records user consent
- Contains payment details extracted from the CartMandate
- Enables the actual payment transaction

This is the third and final verifiable credential in our secure payment system.

THE COMPLETE AP2 CREDENTIAL CHAIN:
1. Shopping Agent creates IntentMandate (user's intent)
2. Merchant Agent reads IntentMandate, creates CartMandate (merchant's binding offer)
3. You read CartMandate, get user confirmation, create PaymentMandate (authorized payment execution)

Each credential:
- Has an expiry time (security feature)
- Links to the previous credential
- Is validated before the next step
- Creates an auditable chain of trust""",

Step 6: Add Tools to the Credentials Provider

👉 Find:

# MODULE_6_STEP_6_ADD_TOOLS
tools=[],

👉 Replace those two lines with:

    tools=[
        FunctionTool(func=create_payment_mandate)
    ],

Step 7: Verify the Complete Credentials Provider

Let's confirm everything is wired correctly.

👉 Your complete

charity_advisor/credentials_provider/agent.py

should now look like this:

"""
Credentials Provider Agent - Handles payment processing with user consent.
This agent acts as our "Payment Processor."
"""

from google.adk.agents import Agent
from google.adk.tools import FunctionTool
from charity_advisor.tools.payment_tools import create_payment_mandate


credentials_provider = Agent(
    name="CredentialsProvider",
    model="gemini-2.5-flash",
    description="Securely processes payments by creating PaymentMandates and executing transactions with user consent.",
    tools=[
        FunctionTool(func=create_payment_mandate)
    ],
    instruction="""You are a payment specialist responsible for securely processing payments with user consent.

Your workflow:

1. Read the CartMandate from shared state.
   The CartMandate was created by the Merchant Agent and has this structure:
   - contents: AP2 wrapper containing:
     - id: Cart identifier
     - cart_expiry: When the cart expires
     - merchant_name: Who is receiving payment
     - payment_request: W3C PaymentRequest with transaction details
   - merchant_authorization: Merchant's signature

2. Extract payment details from the nested structure:
   - Navigate: cart_mandate["contents"]["payment_request"]["details"]["total"]["amount"]
   - This gives you the currency and value

3. **IMPORTANT - Two-Turn Conversational Confirmation Pattern:**
   Before calling create_payment_mandate, you MUST:
   - Present the payment details clearly to the user
   - Ask explicitly: "I'm ready to process a payment of $X to [Charity Name]. Do you want to proceed with this donation?"
   - WAIT for the user's explicit confirmation (e.g., "yes", "proceed", "confirm")
   - ONLY call create_payment_mandate AFTER receiving explicit confirmation
   - If user says "no" or "cancel", DO NOT call the tool

4. After user confirms, use the create_payment_mandate tool to:
   - Validate the CartMandate hasn't expired (CRITICAL security check)
   - Create a PaymentMandate (the third AP2 credential)
   - Simulate payment processing
   - Record the transaction result

5. After processing, inform the user:
   - That payment was processed successfully (this is a simulation)
   - The transaction ID
   - The amount and merchant
   - That this completes the three-agent AP2 credential chain

IMPORTANT BOUNDARIES:
- Your ONLY job is creating PaymentMandates and processing payments
- You do NOT discover charities (that's Shopping Agent's job)
- You do NOT create offers (that's Merchant Agent's job)
- You MUST validate that the CartMandate hasn't expired before processing
- You MUST get explicit user confirmation before calling create_payment_mandate
- In production, this consent mechanism would be even more robust

WHAT IS A PAYMENTMANDATE:
A PaymentMandate is the final credential that authorizes payment execution. It:
- Links to the CartMandate (proving the merchant's offer)
- Records user consent
- Contains payment details extracted from the CartMandate
- Enables the actual payment transaction

This is the third and final verifiable credential in our secure payment system.

THE COMPLETE AP2 CREDENTIAL CHAIN:
1. Shopping Agent creates IntentMandate (user's intent)
2. Merchant Agent reads IntentMandate, creates CartMandate (merchant's binding offer)
3. You read CartMandate, get user confirmation, create PaymentMandate (authorized payment execution)

Each credential:
- Has an expiry time (security feature)
- Links to the previous credential
- Is validated before the next step
- Creates an auditable chain of trust"""
)

Checkpoint : You now have a complete Credentials Provider with proper CartMandate reading and PaymentMandate creation using AP2 Pydantic models.

Step 8: Test the Credentials Provider

Now let's verify that our agent correctly processes payments and completes the credential chain.

👉 In your Cloud Shell terminal, run:

python scripts/test_credentials_provider.py

Ожидаемый результат:

======================================================================
CREDENTIALS PROVIDER TEST (MOCK - NO CONFIRMATION)
======================================================================

Simulated CartMandate from Merchant Agent:
  - Cart ID: cart_test123
  - Merchant: Room to Read
  - Amount: $50.00
  - Expires: 2025-11-07T15:47:16Z
  - Signature: SIG_test_signature

Calling Credentials Provider to process payment...
======================================================================
INFO:charity_advisor.tools.payment_tools:Tool called: Creating PaymentMandate and processing payment
INFO:charity_advisor.tools.payment_tools:CartMandate valid. Expires in 900 seconds
INFO:charity_advisor.tools.payment_tools:Payment processed successfully: txn_a3f7b2c8d9e1f4a2

======================================================================
CREDENTIALS PROVIDER RESPONSE:
======================================================================
I've successfully processed your payment. Here are the details:

**Payment Completed** (Simulated)
- Transaction ID: txn_a3f7b2c8d9e1f4a2
- Amount: USD 50.00
- Merchant: Room to Read
- Status: Completed

This completes the three-agent AP2 credential chain:
1.  Shopping Agent created IntentMandate (your intent)
2.  Merchant Agent created CartMandate (binding offer)
3.  Credentials Provider created PaymentMandate (payment authorization)

Your donation has been processed securely through our verifiable credential system.

======================================================================
PAYMENTMANDATE CREATED:
======================================================================
  Payment Mandate ID: payment_3b4c5d6e7f8a
  Linked to Cart: cart_test123
  User Consent: True
  Amount: USD 50.00
  Merchant: Room to Read
  Agent Present: True
======================================================================

======================================================================
PAYMENT RESULT:
======================================================================
  Transaction ID: txn_a3f7b2c8d9e1f4a2
  Status: completed
  Amount: USD 50.00
  Merchant: Room to Read
  Simulation: True
======================================================================

Step 9: Test the Complete Three-Agent Pipeline

Now let's test all three agents working together!

👉 Run the full pipeline test:

python scripts/test_full_pipeline.py

Ожидаемый результат:

======================================================================
THREE-AGENT PIPELINE TEST (AP2 CREDENTIAL CHAIN)
======================================================================

[1/3] SHOPPING AGENT - Finding charity and creating IntentMandate...
----------------------------------------------------------------------
✓ IntentMandate created
  - Intent ID: intent_774799058_1730927536
  - Description: Donate $75.00 to Room to Read
  - Merchant: Room to Read
  - Amount: $75.0
  - Expires: 2025-11-07T16:32:16Z

[2/3] MERCHANT AGENT - Reading IntentMandate and creating CartMandate...
----------------------------------------------------------------------
✓ CartMandate created
  - ID: cart_3b4c5d6e7f8a
  - Expires: 2025-11-07T15:47:16Z
  - Signature: SIG_a3f7b2c8d9e1f4a2

[3/3] CREDENTIALS PROVIDER - Creating PaymentMandate and processing...
----------------------------------------------------------------------
NOTE: In the web UI, this would show a confirmation dialog
      For this test, consent is automatically granted
✓ Payment processed (SIMULATED)
  - Transaction ID: txn_a3f7b2c8d9e1f4a2
  - Amount: $75.0
  - Status: completed

======================================================================
COMPLETE AP2 CREDENTIAL CHAIN
======================================================================

✓ Credential 1: IntentMandate (User's Intent)
  - Intent ID: intent_774799058_1730927536
  - Description: Donate $75.00 to Room to Read
  - Expiry: 2025-11-07T16:32:16Z

✓ Credential 2: CartMandate (Merchant's Offer)
  - Cart ID: cart_3b4c5d6e7f8a
  - Cart Expiry: 2025-11-07T15:47:16Z
  - Merchant Signature: SIG_a3f7b2c8d9e1f4a2

✓ Credential 3: PaymentMandate (Payment Execution)
  - Payment Mandate ID: payment_3b4c5d6e7f8a
  - Linked to Cart: cart_3b4c5d6e7f8a
  - Agent Present: True

✓ Transaction Result:
  - Transaction ID: txn_a3f7b2c8d9e1f4a2
  - Simulation: True

======================================================================
✅ COMPLETE PIPELINE TEST PASSED
======================================================================

This is the complete AP2 credential chain in action!

Each agent:

  1. Reads a credential from state
  2. Validates it using Pydantic models (structure + expiry check)
  3. Creates the next credential using AP2 models
  4. Writes to state for the next agent

What You Just Built

You've successfully completed the AP2 three-agent credential chain with proper structure validation using Pydantic models and payment simulation.

Key Concepts Mastered

PaymentMandate (AP2 Credential #3):

  • Created using official AP2 Pydantic models
  • Final credential authorizing payment execution
  • Links to CartMandate via payment_details_id
  • Records user consent and timestamp
  • Contains payment amount extracted from CartMandate
  • Includes agent_present flag for human-in-the-loop
  • Model validation ensures spec compliance

Reading from CartMandate:

  • Validate structure with CartMandate.model_validate()
  • Type-safe attribute access: cart_model.contents.payment_request.details.total.amount
  • Understand AP2 wrapper vs W3C standard separation
  • Extract merchant_name, amount, currency safely from model
  • Pydantic catches structure errors automatically

Cart Expiry Validation:

  • Accepts validated CartMandate Pydantic model
  • Reads from cart.contents.cart_expiry (attribute access)
  • Security feature preventing stale cart processing
  • Shorter duration (15 min) than intent (1 hour)

Payment Simulation:

  • Educational mock of real payment processor
  • Generates transaction ID
  • Records payment_result in state
  • Clearly marked as simulation (simulation: True flag)

Complete AP2 Chain with Models:

  • Three agents, three credentials, three Pydantic validations
  • Each agent validates previous credential's structure using models
  • Each credential links to previous for audit trail
  • State-based handoffs maintain role separation
  • Type safety throughout the chain

Model-Driven Development:

  • Input validation via model_validate()
  • Type-safe construction with nested models
  • Automatic serialization via model_dump(mode='json')
  • Production-ready patterns from the start

Что дальше?

In the next module, we'll build the Orchestrator Agent that coordinates all three specialist agents.

You've built three powerful specialist agents using AP2 Pydantic models. Now let's build the conductor that orchestrates them into a seamless donation experience.

Let's build the Orchestrator and see the complete system in action.

7. Orchestration - Bringing It All Together

sequential pipeline

From Specialists to Seamless Experience

In the previous modules, you built three specialized agents:

  • Shopping Agent : Finds charities, creates IntentMandate
  • Merchant Agent : Creates CartMandate from IntentMandate
  • Credentials Provider : Creates PaymentMandate, processes payment

These agents naturally fall into two phases:

  • Phase 1 (Shopping) : Multi-turn conversation to find and select charity
  • Phase 2 (Processing) : Atomic execution of offer creation and payment

But right now, you'd have to manually orchestrate these phases yourself.

This is where ADK's orchestration patterns shine.

AP2 Principle: Orchestration Enforces Trust Boundaries

Why Orchestration Matters for Security

Orchestration isn't just about convenience—it's about enforcing trust boundaries through architecture.

Without orchestration:

# User could accidentally skip steps or reorder them
shopping_agent.run("Find charity")
# Oops, forgot to create CartMandate!
credentials_provider.run("Process payment")  # No offer to validate!

With orchestration:

# Pipeline enforces correct order
donation_processing_pipeline = SequentialAgent(
    sub_agents=[
        merchant_agent,      # Must run first
        credentials_provider # Must run second
    ]
)
# Steps ALWAYS run in order, no skipping allowed

The sequential pipeline guarantees:

  • ✅ IntentMandate created before CartMandate
  • ✅ CartMandate created before payment processing
  • ✅ Each agent runs in its isolated context
  • ✅ State flows forward through the credential chain

Our Mission: Build the Complete System

We'll build two layers:

Layer 1: The Processing Pipeline (SequentialAgent)

  • Wires together Merchant → Credentials
  • Runs automatically in sequence after charity is selected
  • Atomic execution of offer and payment

Layer 2: The Root Orchestrator (user-facing Agent)

  • Friendly personality
  • Delegates to shopping_agent for charity selection
  • Delegates to processing pipeline after IntentMandate is created
  • Handles conversation and phase transitions

This two-layer approach matches the natural flow:

  • Shopping Phase : Multi-turn conversation (user browses, asks questions, decides)
  • Processing Phase : Atomic execution (offer → payment)

Let's build both.

Step 1: Import Orchestration Components

First, let's set up the orchestration file with the necessary imports.

👉 Open

charity_advisor/agent.py

Начнем с импорта:

👉 Find:

# MODULE_7_STEP_1_IMPORT_COMPONENTS

👉 Replace that single line with:

from google.adk.agents import Agent, SequentialAgent
from charity_advisor.shopping_agent.agent import shopping_agent
from charity_advisor.merchant_agent.agent import merchant_agent
from charity_advisor.credentials_provider.agent import credentials_provider

Step 2: Create the Processing Pipeline

Now let's create the pipeline that runs offer creation and payment processing atomically.

👉 Find:

# MODULE_7_STEP_2_CREATE_SEQUENTIAL_PIPELINE

👉 Replace those two lines with:

# Create the donation processing pipeline
# This runs Merchant → Credentials in sequence AFTER charity is selected
donation_processing_pipeline = SequentialAgent(
    name="DonationProcessingPipeline",
    description="Creates signed offer and processes payment after charity is selected",
    sub_agents=[
        merchant_agent,
        credentials_provider
    ]
)

Step 3A: Create Root Agent Setup

Now let's create the user-facing agent that orchestrates both phases. We'll build this in three parts: setup (3A), instruction (3B), and sub-agents (3C).

👉 Find:

# MODULE_7_STEP_3A_CREATE_ROOT_AGENT_SETUP

👉 Replace that single line with:

# Create the root orchestrator agent
# This is what users interact with directly
root_agent = Agent(
    name="CharityAdvisor",
    model="gemini-2.5-pro",
    description="A friendly charity giving assistant that helps users donate to verified organizations.",
    # MODULE_7_STEP_3B_WRITE_ROOT_AGENT_INSTRUCTION

Step 3B: Write the Root Agent Instruction

Now let's add the instruction that guides the charity advisor's behavior across both phases.

👉 Find:

# MODULE_7_STEP_3B_WRITE_ROOT_AGENT_INSTRUCTION

👉 Replace that single line with:

    instruction="""You are a helpful and friendly charity giving advisor.

Your workflow has TWO distinct phases:

PHASE 1: CHARITY SELECTION (delegate to shopping_agent)
When a user expresses interest in donating:
1. Delegate to shopping_agent immediately
2. The shopping_agent will:
   - Search for charities matching their cause
   - Present verified options with ratings
   - Engage in conversation (user may ask questions, change their mind)
   - Wait for user to select a specific charity and amount
   - Create an IntentMandate when user decides
3. Wait for shopping_agent to complete

You'll know Phase 1 is complete when shopping_agent's response includes:
- "IntentMandate created" or "Intent ID: intent_xxx" 
- Charity name and donation amount

PHASE 2: PAYMENT PROCESSING (delegate to DonationProcessingPipeline)
After shopping_agent completes:
1. Acknowledge the user's selection naturally:
   "Perfect! Let me process your $X donation to [Charity]..."
2. Delegate to DonationProcessingPipeline
3. The pipeline will automatically:
   - Create signed cart offer (MerchantAgent)
   - Get consent and process payment (CredentialsProvider)
4. After pipeline completes, summarize the transaction

CRITICAL RULES:
- Phase 1 may take multiple conversation turns (this is normal)
- Only proceed to Phase 2 after IntentMandate exists
- Don't rush the user during charity selection
- Don't ask user to "proceed" between phases - transition automatically

EXAMPLE FLOW:
User: "I want to donate to education"
You: [delegate to shopping_agent]
Shopping: "Here are 3 education charities..." [waits]
User: "Tell me more about the first one"
Shopping: "Room to Read focuses on..." [waits]
User: "Great, I'll donate $50 to Room to Read"
Shopping: "IntentMandate created (ID: intent_123)..."
You: "Perfect! Processing your $50 donation to Room to Read..." [delegate to DonationProcessingPipeline]
Pipeline: [creates offer, gets consent, processes payment]
You: "Done! Your donation was processed successfully. Transaction ID: txn_456"

Your personality:
- Warm and encouraging
- Patient during charity selection
- Clear about educational nature
- Smooth transitions between phases""",
# MODULE_7_STEP_3C_ADD_ROOT_AGENT_SUBAGENTS

Step 3C: Add the Sub-Agents

Finally, let's give the charity advisor access to both the shopping agent and the processing pipeline, and close the Agent definition.

👉 Find:

# MODULE_7_STEP_3C_ADD_ROOT_AGENT_SUBAGENTS

👉 Replace that single line with:

    sub_agents=[
        shopping_agent,
        donation_processing_pipeline
    ]
)

Step 4: Verify the Complete System

Let's confirm the orchestration is wired correctly.

👉 Your complete

charity_advisor/agent.py

should now look like this:

"""
Main orchestration: The donation processing pipeline and root orchestrator agent.
"""

from google.adk.agents import Agent, SequentialAgent
from charity_advisor.shopping_agent.agent import shopping_agent
from charity_advisor.merchant_agent.agent import merchant_agent
from charity_advisor.credentials_provider.agent import credentials_provider

# Create the donation processing pipeline
# This runs Merchant → Credentials in sequence AFTER charity is selected
donation_processing_pipeline = SequentialAgent(
    name="DonationProcessingPipeline",
    description="Creates signed offer and processes payment after charity is selected",
    sub_agents=[
        merchant_agent,
        credentials_provider
    ]
)

# Create the root orchestrator agent
# This is what users interact with directly
root_agent = Agent(
    name="CharityAdvisor",
    model="gemini-2.5-flash",
    description="A friendly charity giving assistant that helps users donate to verified organizations.",
    instruction="""You are a helpful and friendly charity giving advisor.

Your workflow has TWO distinct phases:

PHASE 1: CHARITY SELECTION (delegate to shopping_agent)
When a user expresses interest in donating:
1. Delegate to shopping_agent immediately
2. The shopping_agent will:
   - Search for charities matching their cause
   - Present verified options with ratings
   - Engage in conversation (user may ask questions, change their mind)
   - Wait for user to select a specific charity and amount
   - Create an IntentMandate when user decides
3. Wait for shopping_agent to complete

You'll know Phase 1 is complete when shopping_agent's response includes:
- "IntentMandate created" or "Intent ID: intent_xxx" 
- Charity name and donation amount

PHASE 2: PAYMENT PROCESSING (delegate to DonationProcessingPipeline)
After shopping_agent completes:
1. Acknowledge the user's selection naturally:
   "Perfect! Let me process your $X donation to [Charity]..."
2. Delegate to DonationProcessingPipeline
3. The pipeline will automatically:
   - Create signed cart offer (MerchantAgent)
   - Get consent and process payment (CredentialsProvider)
4. After pipeline completes, summarize the transaction

CRITICAL RULES:
- Phase 1 may take multiple conversation turns (this is normal)
- Only proceed to Phase 2 after IntentMandate exists
- Don't rush the user during charity selection
- Don't ask user to "proceed" between phases - transition automatically

EXAMPLE FLOW:
User: "I want to donate to education"
You: [delegate to shopping_agent]
Shopping: "Here are 3 education charities..." [waits]
User: "Tell me more about the first one"
Shopping: "Room to Read focuses on..." [waits]
User: "Great, I'll donate $50 to Room to Read"
Shopping: "IntentMandate created (ID: intent_123)..."
You: "Perfect! Processing your $50 donation to Room to Read..." [delegate to DonationProcessingPipeline]
Pipeline: [creates offer, gets consent, processes payment]
You: "Done! Your donation was processed successfully. Transaction ID: txn_456"

Your personality:
- Warm and encouraging
- Patient during charity selection
- Clear about educational nature
- Smooth transitions between phases""",
    sub_agents=[
        shopping_agent,
        donation_processing_pipeline
    ]
)

Step 5: Harden with Validation Callbacks (Optional Skip to Step 7)

обратные вызовы

The SequentialAgent guarantees execution order , but what if:

  • Shopping Agent fails silently (IntentMandate never created)
  • An hour passes between Shopping and Merchant (intent expires)
  • State gets corrupted or cleared
  • Someone tries to call Merchant directly, bypassing Shopping

Callbacks add architectural enforcement - they validate prerequisites before an agent even starts its LLM call. This is defense in depth: tools validate during execution, callbacks validate before execution.

Let's add validation callbacks to our Merchant and Credentials Provider agents.

Step 5A: Add Merchant Validation - Import Callback Types

First, let's add the imports needed for callbacks.

👉 Open

charity_advisor/merchant_agent/agent.py

At the top of the file, after the existing imports, add:

from typing import Optional
from datetime import datetime, timezone
from google.adk.agents.callback_context import CallbackContext
from google.genai.types import Content, Part
import logging

logger = logging.getLogger(__name__)

Step 5B: Build the Intent Validation Function

Now let's create a callback function that validates the IntentMandate before Merchant Agent runs.

👉 В

charity_advisor/merchant_agent/agent.py

, add this function BEFORE the

merchant_agent = Agent(...)

определение:

def validate_intent_before_merchant(
    callback_context: CallbackContext,
) -> Optional[Content]:
    """
    Validates IntentMandate exists and hasn't expired before Merchant runs.
    
    This callback enforces that the Shopping Agent completed successfully
    before the Merchant Agent attempts to create a CartMandate.
    
    Returns:
        None: Allow Merchant Agent to proceed normally
        Content: Skip Merchant Agent and return error to user
    """
    state = callback_context.state
    
    # Check credential exists
    if "intent_mandate" not in state:
        logger.error("❌ IntentMandate missing - Shopping Agent may have failed")
        return Content(parts=[Part(text=(
            "Error: Cannot create cart. User intent was not properly recorded. "
            "Please restart the donation process."
        ))])
    
    intent_mandate = state["intent_mandate"]
    
    # Validate expiry (critical security check)
    try:
        expiry_time = datetime.fromisoformat(
            intent_mandate["intent_expiry"].replace('Z', '+00:00')
        )
        now = datetime.now(timezone.utc)
        
        if expiry_time < now:
            logger.error(f"❌ IntentMandate expired at {intent_mandate['intent_expiry']}")
            return Content(parts=[Part(text=(
                "Error: Your donation intent has expired. "
                "Please select a charity again to restart."
            ))])
        
        time_remaining = expiry_time - now
        logger.info(f"✓ IntentMandate validated. Expires in {time_remaining.total_seconds():.0f}s")
        
    except (KeyError, ValueError) as e:
        logger.error(f"❌ Invalid IntentMandate structure: {e}")
        return Content(parts=[Part(text=(
            "Error: Invalid intent data. Please restart the donation."
        ))])
    
    # All checks passed - allow Merchant Agent to proceed
    logger.info(f"✓ Prerequisites met for Merchant Agent: {intent_mandate['intent_id']}")
    return None

Step 5C: Attach Callback to Merchant Agent

Now let's connect the callback to the agent.

👉 В

charity_advisor/merchant_agent/agent.py

, modify the

merchant_agent = Agent(...)

определение:

Find this line in the Agent definition:

merchant_agent = Agent(
    name="MerchantAgent",
    model="gemini-2.5-flash",
    description="Creates formal, signed CartMandates for charity donations following W3C PaymentRequest standards.",

Add this line right after the

description

линия:

    before_agent_callback=validate_intent_before_merchant,

Your agent definition should now look like:

merchant_agent = Agent(
    name="MerchantAgent",
    model="gemini-2.5-flash",
    description="Creates formal, signed CartMandates for charity donations following W3C PaymentRequest standards.",
    before_agent_callback=validate_intent_before_merchant,
    tools=[
        FunctionTool(func=create_cart_mandate)
    ],
    instruction="""..."""
)

Step 6: Add Credentials Provider Validation (Optional Skip to Step 7)

Same pattern - let's add validation for the payment step.

Step 6A: Import Callback Types

👉 Open

charity_advisor/credentials_provider/agent.py

At the top of the file, after the existing imports, add:

from typing import Optional
from datetime import datetime, timezone
from google.adk.agents.callback_context import CallbackContext
from google.genai.types import Content, Part
import logging

logger = logging.getLogger(__name__)

Step 6B: Build Cart Validation Function

👉 В

charity_advisor/credentials_provider/agent.py

, add this function BEFORE the

credentials_provider = Agent(...)

определение:

def validate_cart_before_payment(
    callback_context: CallbackContext,
) -> Optional[Content]:
    """
    Validates CartMandate exists and hasn't expired before payment processing.
    
    This callback enforces that the Merchant Agent completed successfully
    before the Credentials Provider attempts to process payment.
    
    Returns:
        None: Allow Credentials Provider to proceed
        Content: Skip payment processing and return error
    """
    state = callback_context.state
    
    # Check credential exists
    if "cart_mandate" not in state:
        logger.error("❌ CartMandate missing - Merchant Agent may have failed")
        return Content(parts=[Part(text=(
            "Error: Cannot process payment. Cart was not properly created. "
            "Please restart the donation process."
        ))])
    
    cart_mandate = state["cart_mandate"]
    
    # Validate AP2 structure
    if "contents" not in cart_mandate:
        logger.error("❌ CartMandate missing AP2 contents wrapper")
        return Content(parts=[Part(text=(
            "Error: Invalid cart structure. Please restart."
        ))])
    
    # Validate expiry
    try:
        contents = cart_mandate["contents"]
        expiry_time = datetime.fromisoformat(
            contents["cart_expiry"].replace('Z', '+00:00')
        )
        now = datetime.now(timezone.utc)
        
        if expiry_time < now:
            logger.error(f"❌ CartMandate expired at {contents['cart_expiry']}")
            return Content(parts=[Part(text=(
                "Error: Your cart has expired (15 minute limit). "
                "Please restart the donation to get a fresh offer."
            ))])
        
        time_remaining = expiry_time - now
        logger.info(f"✓ CartMandate validated. Expires in {time_remaining.total_seconds():.0f}s")
        
    except (KeyError, ValueError) as e:
        logger.error(f"❌ Invalid CartMandate structure: {e}")
        return Content(parts=[Part(text=(
            "Error: Invalid cart data. Please restart the donation."
        ))])
    
    # All checks passed - allow payment processing
    logger.info(f"✓ Prerequisites met for Credentials Provider: {contents['id']}")
    return None

Step 6C: Attach Callback to Credentials Provider

👉 В

charity_advisor/credentials_provider/agent.py

, modify the

credentials_provider = Agent(...)

определение:

Find this line in the Agent definition:

credentials_provider = Agent(
    name="CredentialsProvider",
    model="gemini-2.5-flash",
    description="Securely processes payments by creating PaymentMandates and executing transactions with user consent.",

Add this line right after the

description

линия:

    before_agent_callback=validate_cart_before_payment,

Your agent definition should now look like:

credentials_provider = Agent(
    name="CredentialsProvider",
    model="gemini-2.5-flash",
    description="Securely processes payments by creating PaymentMandates and executing transactions with user consent.",
    before_agent_callback=validate_cart_before_payment,
    tools=[
        FunctionTool(func=create_payment_mandate)
    ],
    instruction="""..."""
)

Step 7: Test with ADK Web UI

Now let's test the complete hardened system with validation callbacks active.

👉 In your Cloud Shell terminal, run:

adk web

You should see output like:

+-----------------------------------------------------------------------------+
| ADK Web Server started                                                      |
|                                                                             |
| For local testing, access at http://localhost:8000.                         |
+-----------------------------------------------------------------------------+

INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

👉 Next, to access the ADK Web UI from your browser:

From the Web preview icon (looks like an eye or a square with an arrow) in the Cloud Shell toolbar (usually top right), select Change port . In the pop-up window, set the port to 8000 and click "Change and Preview" . Cloud Shell will then open a new browser tab displaying the ADK Web UI.

webpreview

👉 Select your agent from the dropdown:

In the ADK Web UI, you'll see a dropdown menu at the top. Select charity_advisor from the list.

agent-select

You'll see the ADK web interface with:

  • Chat panel : Left side, for conversation
  • Trace panel : Right side, for observability (we'll use this in Module 9)

Test 1: Complete Donation Flow (Normal Case)

👉 In the chat interface, type:

I want to donate to an education charity

Watch the complete flow unfold:

adk web shopping agent

adk web donation processing pipeline

What's happening (visible in the trace panel on the right):

1. Advisor delegates to ShoppingAgent:

  • ShoppingAgent searches for education charities
  • Shows you 3 verified options with details

2. You interact with ShoppingAgent (may take multiple turns):

User: "Tell me more about Room to Read"
Shopping: [explains mission and impact]
User: "I'll donate $50 to Room to Read"

3. ShoppingAgent creates IntentMandate:

  • Creates and signs the intent
  • Returns confirmation with Intent ID

4. Advisor transitions to processing phase:

Perfect! Processing your $50 donation to Room to Read...

5. DonationProcessingPipeline activates:

  • Merchant callback validates IntentMandate (✓ passed) ← NEW!
  • Merchant Agent creates CartMandate with signature
  • Credentials callback validates CartMandate (✓ passed) ← NEW!
  • Credentials Provider prepares payment

6. Payment processes:

  • Credentials Provider creates PaymentMandate
  • Simulates payment processing
  • Returns transaction ID

7. Advisor summarizes:

Perfect! Your donation has been processed successfully! 🎉

Подробности:

  • Сумма: 50,00 долларов США
  • Charity: Room to Read (EIN: 77-0479905)
  • Transaction ID: txn_a3f7b2c8d9e1f4a2

Test 2: Verify Callbacks Catch Failures (Optional Advanced Test)

Want to see the callbacks in action catching errors? You'd need to manually corrupt state (advanced debugging), but in production, callbacks would catch:

  • Shopping Agent tool fails → Merchant callback blocks: "Error: Cannot create cart..."
  • 2 hours pass → Merchant callback blocks: "Error: Intent expired..."
  • Cart expires → Credentials callback blocks: "Error: Cart expired (15 min limit)..."

These edge cases are now architecturally enforced by your validation callbacks.

What You Just Built

You've successfully orchestrated three specialized agents into a seamless, trustworthy system with architectural validation.

Что дальше?

You've now completed the technical core of building trustworthy agents:

You've built a complete trustworthy system locally enforcing the credential chain. Now let's make it accessible to real users through production deployment—and enable the accountability trail that makes Module 9 possible.

Let's deploy your hardened agent to Google Cloud.

8. Развертывание

баннер

Your trustworthy donation system is now complete with three specialized agents working locally:

But it only runs on your development machine. To make this system useful to real users—and to capture the accountability trails that prove trustworthiness—you need to deploy it to production.

This module walks you through deploying your agent to Google Cloud with observability enabled from day one . The --trace_to_cloud flag you'll use during deployment is what makes the accountability trail in Module 9 possible.

Understanding Deployment Options

The ADK supports multiple deployment targets. Each has different characteristics for complexity, session management, scaling, and cost:

Фактор

Local ( adk web )

Агент Движок

Cloud Run

Сложность

Минимальный

Низкий

Середина

Session Persistence

In-memory only (lost on restart)

Vertex AI managed (automatic)

Cloud SQL (PostgreSQL) or in-memory

Инфраструктура

None (dev machine only)

Полностью управляемый

Container + optional database

Холодный старт

Н/Д

100-500мс

100-2000ms

Масштабирование

Единичный экземпляр

Автоматический

Automatic (to zero)

Cost Model

Free (local compute)

Compute-based

Request-based + free tier

Поддержка пользовательского интерфейса

Да (встроенный)

No (API only)

Yes (via --with_ui flag)

Observability Setup

Local trace viewer

Automatic with --trace_to_cloud

Requires --trace_to_cloud flag

Лучше всего подходит для

Development & testing

Production agents

Production agents

Recommendation: For this trustworthy donation system, we recommend Agent Engine as your primary production deployment because it provides:

  • Fully managed infrastructure (no containers to manage)
  • Built-in session persistence via VertexAiSessionService
  • Automatic scaling without cold starts
  • Simplified deployment (no Docker knowledge required)
  • Cloud Trace integration out of the box

Additional Option: Google Kubernetes Engine (GKE)

For advanced users requiring Kubernetes-level control, custom networking, or multi-service orchestration, GKE deployment is available. This option provides maximum flexibility but requires more operational expertise (cluster management, manifests, service accounts).

GKE deployment is not covered in this codelab but is fully documented in the ADK GKE Deployment Guide .

Предпосылки

1. Google Cloud Project Setup

You need a Google Cloud project with billing enabled. If you don't have one:

  1. Create a project: Google Cloud Console
  2. Enable billing: Enable Billing
  3. Note your Project ID (not the project name or number)

2. Re-Authentication (Optional)

Authenticate with Google Cloud:

gcloud auth application-default login
gcloud config set project YOUR_PROJECT_ID

Replace YOUR_PROJECT_ID with your actual Google Cloud project ID.

Verify your authentication:

gcloud config get-value project
# Should output: YOUR_PROJECT_ID

3. Переменные среды

Use these commands to auto-populate your .env file:

# Get your current Project ID
PROJECT_ID=$(gcloud config get-value project)
STAGING_BUCKET_VALUE="gs://${PROJECT_ID}-staging"
ENV_FILE=".env"

# Check if STAGING_BUCKET is already set in the .env file
if grep -q "^STAGING_BUCKET=" "${ENV_FILE}"; then
  # If it exists, replace the line
  # The sed command finds the line starting with STAGING_BUCKET= and replaces the entire line.
  # Using | as a delimiter to avoid issues with slashes in the bucket name.
  sed -i "s|^STAGING_BUCKET=.*|STAGING_BUCKET=${STAGING_BUCKET_VALUE}|" "${ENV_FILE}"
  echo "Updated STAGING_BUCKET in ${ENV_FILE}"
else
  # If it doesn't exist, add it to the end of the file
  echo "STAGING_BUCKET=${STAGING_BUCKET_VALUE}" >> "${ENV_FILE}"
  echo "Added STAGING_BUCKET to ${ENV_FILE}"
fi

# Verify it was added or updated correctly
echo "Current STAGING_BUCKET setting:"
grep "^STAGING_BUCKET=" "${ENV_FILE}"

Вы должны увидеть:

STAGING_BUCKET=gs://your-actual-project-id-staging

Важные примечания:

  • Replace YOUR_PROJECT_ID with your actual project ID (or use the commands above)
  • For GOOGLE_CLOUD_LOCATION , use a supported region
  • The staging bucket will be created automatically if it doesn't exist when you run the deployment script

4. Enable Required APIs

The deployment process needs several Google Cloud APIs enabled. Run this command to enable them:

gcloud services enable \
    aiplatform.googleapis.com \
    storage.googleapis.com \
    cloudbuild.googleapis.com \
    cloudtrace.googleapis.com \
    compute.googleapis.com

This command enables:

  • AI Platform API - For Agent Engine and Vertex AI models
  • Cloud Storage API - For staging bucket
  • Cloud Build API - For container building (Cloud Run)
  • Cloud Trace API - For observability and accountability trails
  • Compute Engine API - For service account management

Step 1: Understand the Deployment Infrastructure

Your project includes a unified deployment script ( deploy.sh ) that handles all deployment modes.

👉 Review the deployment script (optional):

cat deploy.sh

The script provides three deployment modes:

  • ./deploy.sh local - Run locally with in-memory storage
  • ./deploy.sh agent-engine - Deploy to Vertex AI Agent Engine (recommended)
  • ./deploy.sh cloud-run - Deploy to Cloud Run with optional UI

How it works under the hood:

For Agent Engine deployment, the script executes:

adk deploy agent_engine \
  --project=$GOOGLE_CLOUD_PROJECT \
  --region=$GOOGLE_CLOUD_LOCATION \
  --staging_bucket=$STAGING_BUCKET \
  --display_name="Charity Advisor" \
  --trace_to_cloud \
  charity_advisor

For Cloud Run deployment, it executes:

adk deploy cloud_run \
  --project=$GOOGLE_CLOUD_PROJECT \
  --region=$GOOGLE_CLOUD_LOCATION \
  --service_name="charity-advisor" \
  --app_name="charity_advisor" \
  --with_ui \
  --trace_to_cloud \
  charity_advisor

The --trace_to_cloud flag is critical for both deployment types—it enables Cloud Trace integration for the accountability trail you'll explore in Module 9.

Step 2: Prepare the Agent Engine Wrapper

Agent Engine requires a specific entry point that wraps your agent for the managed runtime. This file has been created for you.

👉 Review

charity_advisor/agent_engine_app.py

:

"""Agent Engine application wrapper.

This file prepares the Charity Advisor agent for deployment to Vertex AI Agent Engine.
"""

from vertexai import agent_engines
from .agent import root_agent

# Wrap the agent in an AdkApp object for Agent Engine deployment
app = agent_engines.AdkApp(
    agent=root_agent,
    enable_tracing=True,  # Enables Cloud Trace integration automatically
)

Why this file is needed:

  • Agent Engine requires the agent wrapped in an AdkApp object
  • The enable_tracing=True parameter enables Cloud Trace integration automatically
  • This wrapper is referenced by the ADK CLI during deployment
  • It configures VertexAiSessionService for automatic session persistence

Agent Engine is the recommended production deployment for your trustworthy donation system because it provides fully managed infrastructure with built-in session persistence.

Run the Deployment

From your project root:

chmod +x deploy.sh
./deploy.sh agent-engine

Deployment Phases

Watch the script execute these phases:

Phase 1: API Enablement
   aiplatform.googleapis.com
   storage.googleapis.com
   cloudbuild.googleapis.com
   cloudtrace.googleapis.com
   compute.googleapis.com

Phase 2: IAM Setup
   Getting project number
   Granting Storage Object Admin
   Granting Vertex AI User
   Granting Cloud Trace Agent

Phase 3: Staging Bucket
   Creating gs://your-project-id-staging (if needed)
   Setting permissions

Phase 4: Validation
   Checking agent.py exists
   Verifying root_agent defined
   Checking agent_engine_app.py exists
   Validating requirements.txt

Phase 5: Build & Deploy
   Packaging agent code
   Uploading to staging bucket
   Creating Agent Engine instance
   Configuring session persistence
   Setting up Cloud Trace integration
   Running health checks

This process takes 5-10 minutes as it packages the agent and deploys it to Vertex AI infrastructure.

Save Your Agent Engine ID

Upon successful deployment:

✅ Agent Engine created successfully!

   Agent Engine ID: 7917477678498709504
   Resource Name: projects/123456789/locations/us-central1/reasoningEngines/7917477678498709504
   Endpoint: https://us-central1-aiplatform.googleapis.com/v1/...

   ⚠️  IMPORTANT: Save the Agent Engine ID from the output above
   Add it to your .env file as:
   AGENT_ENGINE_ID=7917477678498709504

   This ID is required for:
   - Testing the deployed agent
   - Updating the deployment later
   - Accessing logs and traces

Update your .env file immediately:

echo "AGENT_ENGINE_ID=7917477678498709504" >> .env

What Was Deployed

Your Agent Engine deployment now includes:

All three agents (Shopping, Merchant, Credentials) running in managed runtime
Complete credential chain logic (Intent → Cart → Payment mandates)
User consent mechanism with confirmation workflow
Automatic session persistence via VertexAiSessionService
Auto-scaling infrastructure managed by Google
Cloud Trace integration for complete observability

Step 4: Test Your Deployed Agent

Update Your Environment

Verify your .env includes the Agent Engine ID:

AGENT_ENGINE_ID=7917477678498709504  # From deployment output
GOOGLE_CLOUD_PROJECT=your-project-id
GOOGLE_CLOUD_LOCATION=us-central1
STAGING_BUCKET=gs://your-project-id-staging

Run the Test Script

Your project includes a test script specifically for Agent Engine deployments.

👉 Run the test:

python scripts/test_deployed_agent.py

Ожидаемый результат

Testing Agent Engine deployment...
Project: your-project-id
Location: us-central1
Agent Engine ID: 7917477678498709504
Endpoint: https://us-central1-aiplatform.googleapis.com/v1/...

Creating session...
✓ Session created: 4857885913439920384

Sending donation query...
✓ Response received:
  Event 1: I'll help you donate $50 to a children's education charity...
  Event 2: Here are some highly-rated children's education charities...
  Event 3: Which charity would you like to support?...

✅ Test completed successfully!

Session ID: 4857885913439920384

This donation generated a trace in Cloud Trace.
View it in Module 9: Observability

To view traces:
https://console.cloud.google.com/traces/list?project=your-project-id

Контрольный список проверки

After testing, verify:

✅ Agent responds to queries
✅ All three agents execute in sequence (Shopping → Merchant → Credentials)
✅ Consent mechanism activates (confirmation requested)
✅ Session persists across requests
✅ No authentication errors
✅ No connection timeouts

If you encounter errors:

  • Check your environment variables are set correctly
  • Verify APIs are enabled: gcloud services list --enabled
  • Check Agent Engine logs in Vertex AI Console
  • Verify the agent_engine_app.py file exists in your charity_advisor folder

Step 5: Deploy to Cloud Run (Optional)

While Agent Engine is recommended for streamlined production deployment, Cloud Run offers more control and supports the ADK web UI. This section is optional.

When to Use Cloud Run

Choose Cloud Run if you need:

  • The ADK web UI for user interaction
  • Full control over the container environment
  • Custom database configurations
  • Integration with existing Cloud Run services

Run the Deployment

chmod +x deploy.sh
./deploy.sh cloud-run

What's different:

The script will automatically:

  • Build a Docker container with your agent code
  • Create a Cloud SQL PostgreSQL database (if needed)
  • Configure the database connection
  • Deploy with the ADK web UI enabled

The deployment takes 10-15 minutes due to Cloud SQL provisioning.

Управление сеансом:

  • Uses DatabaseSessionService instead of VertexAiSessionService
  • Requires database credentials in .env (or auto-generated)
  • State persists in PostgreSQL database

UI Support:

  • Web UI available at: https://charity-advisor-xyz.a.run.app

Testing Cloud Run Deployment

If you deployed to Cloud Run with --with_ui , you can test directly in your browser:

  1. Navigate to your Service URL (provided in deployment output)
  2. You'll see the ADK web interface. Select your agent from the dropdown.
  3. Start a test donation:
   I want to donate $50 to a children's education charity
  1. Наблюдайте за ходом выполнения:
    • ShoppingAgent finds charities and saves your intent
    • MerchantAgent creates the cart mandate
    • CredentialsProvider creates payment mandate and requests confirmation
    • After you confirm, payment is processed
  2. Verify the response includes:
    • Charity recommendations
    • Запрос подтверждения
    • Success message after approval

Поиск неисправностей

Распространенные проблемы

Issue: ERROR: GOOGLE_CLOUD_PROJECT is not set

Solution: Ensure your .env file has the correct project ID:

GOOGLE_CLOUD_PROJECT=your-actual-project-id

Issue: Staging bucket not created automatically

Solution: The script should create the bucket automatically. If not, create it manually:

gsutil mb -p $GOOGLE_CLOUD_PROJECT -l $GOOGLE_CLOUD_LOCATION $STAGING_BUCKET

Краткое содержание

You've successfully:

✅ Understood the deployment infrastructure provided by deploy.sh
✅ Reviewed the Agent Engine wrapper configuration
✅ Deployed your trustworthy donation system to Agent Engine (recommended)
✅ Enabled Cloud Trace integration with --trace_to_cloud
✅ Verified the agent is accessible and functional
✅ Created the foundation for accountability trails in Module 9

In the next module, you'll see exactly what this flag unlocks: complete visibility into every donation, every consent moment, and every step of the credential chain.

9. Observability

баннер

graph trace

In Module 1, you learned about a fundamental problem: when an AI agent handles money, how do you prove what happened?

A user could claim:

  • "I never chose that charity!"
  • "I didn't authorize that payment!"
  • "The system charged me without my consent!"

In a traditional black-box AI system, you'd have no way to prove otherwise. But your trustworthy donation system is different. In Module 8, you deployed with the --trace_to_cloud flag, which means every donation now creates a complete, tamper-evident audit trail in Cloud Trace .

This module teaches you to read those traces and use them as evidence. You'll learn to:

  • Navigate Cloud Trace Explorer to find production traces
  • Read the waterfall view to understand execution flow
  • Find the credential chain (Intent → Cart → Payment mandates)
  • Locate consent moments with timestamp proof
  • Use traces for dispute resolution
  • Export traces for compliance and audits

This is what separates trustworthy systems from capable-but-opaque ones: the ability to prove what happened with forensic precision .

Understanding Traces and Spans

Before viewing traces in Cloud Trace, you need to understand what you're looking at.

What is a Trace?

A trace is the complete timeline of your agent handling a single request. It captures everything from when a user sends a query until the final response is delivered.

Each trace shows:

  • Total duration of the request
  • All operations that executed
  • How operations relate to each other (parent-child relationships)
  • When each operation started and ended
  • Success or failure status

For your charity agent: One trace = one complete donation flow from "I want to donate" to "Payment successful."

What is a Span?

A span represents a single unit of work within a trace. Think of spans as the building blocks of a trace.

Common span types in your donation system:

Span Type

Что это представляет

Пример

agent_run

Execution of an agent

ShoppingAgent.run , MerchantAgent.run

call_llm

Request to a language model

gemini-2.5-flash request for charity selection

execute_tool

Tool function execution

find_charities , create_payment_mandate

state_read

Reading from session memory

Retrieving intent_mandate from state

state_write

Writing to session memory

Storing cart_mandate in state

Each span contains:

  • Name: What operation this represents
  • How long it took (start time → end time)
  • Attributes: Metadata like tool inputs, model responses, token counts
  • Status: Success ( OK ) or error ( ERROR )
  • Parent-child relationships: Which operations triggered which

How Spans Form a Trace

Spans nest inside each other to show causation:

Root Span: CharityAdvisor.run (entire request)
  └─ Child: DonationPipeline.run (sequential workflow)
      ├─ Child: ShoppingAgent.run
         ├─ Grandchild: call_llm (Gemini processes charity search)
         ├─ Grandchild: execute_tool (find_charities)
         └─ Grandchild: execute_tool (save_user_choice)
      ├─ Child: MerchantAgent.run
         ├─ Grandchild: call_llm (Gemini generates cart)
         └─ Grandchild: execute_tool (create_cart_mandate)
      └─ Child: CredentialsProvider.run
          ├─ Grandchild: call_llm (Gemini processes payment)
          └─ Grandchild: execute_tool (create_payment_mandate) [CONSENT!]

This hierarchy shows exactly what happened and in what order . You can see that the payment mandate was created after the cart mandate, which was after the user selected a charity.

Step 1: Access Cloud Trace Explorer

Now let's view the actual traces from your deployed agent.

  1. Open the Google Cloud Console: console.cloud.google.com
  2. Select your project from the dropdown at the top (should be pre-selected if you've been working in it)
  3. Navigate to Cloud Trace Explorer:

На что вы смотрите

The Trace Explorer shows a list of all traces from your project:

Столбец

What It Shows

Запрос

HTTP method and endpoint (for API requests)

Время начала

When the request began

Задержка

Total duration of the request

Пролеты

Number of operations in the trace

Each row represents one complete request to your deployed agent.

Generate Test Traces (If Needed)

If you don't see any traces yet, the list might be empty because:

  • No requests have been made to your deployed agent yet
  • Traces take 1-2 minutes to appear after a request

Generate a test trace:

If you deployed to Cloud Run with UI , visit your service URL and complete a donation in the browser.

If you deployed to Agent Engine , run the test script from Module 8:

python scripts/test_deployed_agent.py

Wait 1-2 minutes , then refresh the Cloud Trace Explorer page. You should now see traces.

Filter Traces

Use the filter options at the top to find specific traces:

  • Time range: Change from "Last hour" to "Last 24 hours" if needed
  • Min latency / Max latency: Filter for slow requests
  • Request filter: Search by specific operations (eg, "DonationPipeline")

For this module, focus on traces with longer durations (>5 seconds), as these represent complete donation flows with all three agents executing.

Step 2: Examine a Complete Donation Flow

Click on any trace in the list to open the waterfall view . This is where you'll spend most of your time analyzing agent behavior.

Understanding the Waterfall View

The waterfall view is a Gantt chart showing the complete execution timeline:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
              Timeline (horizontal = time) 
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

invocation                           ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 8.2s
  agent_run: CharityAdvisor          ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 8.1s
    agent_run: DonationPipeline      ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 7.9s
      agent_run: ShoppingAgent       ▓▓▓▓▓▓ 2.1s
        call_llm: gemini-2.5-flash   ▓▓▓▓ 1.2s
        execute_tool: find_charities ▓▓ 0.5s
        execute_tool: save_user_choice  0.3s
      agent_run: MerchantAgent       ▓▓▓ 1.8s
        call_llm: gemini-2.5-flash   ▓▓ 0.9s
        execute_tool: create_cart_mandate  0.7s
      agent_run: CredentialsProvider ▓▓▓▓▓▓▓▓ 4.0s
        call_llm: gemini-2.5-flash   ▓▓ 0.8s
        execute_tool: create_payment_mandate ▓▓▓▓▓ 3.0s [CONSENT]

Reading the Chart

Each bar represents a span:

  • Horizontal position: When it started
  • Length: How long it took
  • Indentation: Shows parent-child relationships
  • Color: Typically blue for normal, red for errors

Key observations from this example trace:

Total duration: 8.2 seconds
Sequential execution: ShoppingAgent completed before MerchantAgent started
MerchantAgent completed

до

CredentialsProvider started
Consent was the longest operation: 3.0 seconds for create_payment_mandate (because it waited for user confirmation)
LLM calls are visible: Each agent made one Gemini request
Tool calls are captured: All six tools executed successfully

This visual immediately shows you where time is spent and what order operations executed in .

Click on a Span for Details

Click on the invocation span (the root span at the top). In the right panel, you'll see detailed attributes:

{
  "http.method": "POST",
  "http.status_code": 200,
  "http.url": "https://charity-advisor-xyz.a.run.app/api/run",
  "user_id": "test_user_123",
  "session_id": "4857885913439920384",
  "trace_id": "a1b2c3d4e5f6...",
  "span_id": "1234567890abcdef"
}

These attributes provide context about the entire request.

Step 3: Find the Credential Chain

Your trustworthy system uses a credential chain to prove authorization at each step:

IntentMandate (User chose charity)
    ↓
CartMandate (Merchant created cart, signed IntentMandate)
    ↓
PaymentMandate (Payment provider created payment, signed CartMandate)

Let's find each mandate in the trace.

Finding the IntentMandate

Click on the execute_tool: save_user_choice span (under ShoppingAgent).

In the attributes panel, you'll see:

{
  "tool.name": "save_user_choice",
  "tool.input.charity_name": "Save the Children",
  "tool.input.amount": 50,
  "tool.output.status": "success",
  "tool.output.intent_mandate": {
    "charity_name": "Save the Children",
    "amount": 50,
    "timestamp": "2024-11-08T15:30:12.345Z",
    "signature": "a3f7b9c1d2e4..."
  }
}

This proves:

  • ✅ User selected "Save the Children"
  • ✅ Amount was $50
  • ✅ Choice was recorded at 15:30:12 UTC
  • ✅ Signature was generated (in production, this would be cryptographic)

The IntentMandate is now in session state and available to subsequent agents.

Finding the CartMandate

Click on the execute_tool: create_cart_mandate span (under MerchantAgent).

In the attributes panel:

{
  "tool.name": "create_cart_mandate",
  "tool.input.intent_mandate": {
    "charity_name": "Save the Children",
    "amount": 50,
    "signature": "a3f7b9c1d2e4..."
  },
  "tool.output.status": "success",
  "tool.output.cart_mandate": {
    "cart_id": "cart_7893",
    "intent_signature": "a3f7b9c1d2e4...",
    "cart_signature": "e8f2a9b3c7d1...",
    "timestamp": "2024-11-08T15:30:14.789Z"
  }
}

This proves:

  • ✅ MerchantAgent received the IntentMandate (input shows it)
  • ✅ Cart was created with ID "cart_7893"
  • ✅ Cart signature references the IntentMandate signature (chain link!)
  • ✅ Created at 15:30:14 UTC (2.4 seconds after intent)

The CartMandate now references the IntentMandate, forming the chain.

Finding the PaymentMandate

Click on the execute_tool: create_payment_mandate span (under CredentialsProvider).

In the attributes panel:

{
  "tool.name": "create_payment_mandate",
  "tool.input.cart_mandate": {
    "cart_id": "cart_7893",
    "intent_signature": "a3f7b9c1d2e4...",
    "cart_signature": "e8f2a9b3c7d1..."
  },
  "tool.confirmation_required": true,
  "tool.confirmation_timestamp": "2024-11-08T15:30:17.891Z",
  "tool.user_response": "CONFIRMED",
  "tool.wait_duration_ms": 29168,
  "tool.output.status": "success",
  "tool.output.payment_mandate": {
    "payment_id": "pay_9821",
    "cart_signature": "e8f2a9b3c7d1...",
    "payment_signature": "b4c9e2a7f8d3...",
    "timestamp": "2024-11-08T15:30:47.059Z"
  }
}

This proves the complete chain:

  • ✅ CredentialsProvider received the CartMandate (input shows it)
  • ✅ Payment references the CartMandate signature (chain link!)
  • Confirmation was required ( confirmation_required: true )
  • User confirmed at 15:30:17 UTC
  • System waited 29.2 seconds for user decision
  • ✅ Payment was created after confirmation (timestamp: 15:30:47)

Visualizing the Chain

The trace proves the credential chain executed correctly:

15:30:12 UTC  IntentMandate created (signature: a3f7...)
                  
15:30:14 UTC  CartMandate created (references: a3f7...)
                  
15:30:17 UTC  User consent requested
                  
15:30:47 UTC  PaymentMandate created (references: e8f2...)

Each mandate references the signature of the previous one. This is tamper-evident - you can verify the chain by checking that signatures match.

Step 4: Analyzing Performance and Bottlenecks

Cloud Trace doesn't just prove what happened—it shows you where time is spent so you can optimize.

Identify the Critical Path

In the waterfall view, look for the longest spans in the vertical stack. These represent your performance bottlenecks.

From our example trace:

Total: 8.2 seconds

Breakdown:
  - ShoppingAgent:         2.1s (26%)
  - MerchantAgent:         1.8s (22%)
  - CredentialsProvider:   4.0s (49%)   Bottleneck
  - Other overhead:        0.3s (3%)

Critical insight: CredentialsProvider accounts for 49% of total time. Why?

Drill into the CredentialsProvider span:

CredentialsProvider: 4.0s
  - call_llm:              0.8s (20%)
  - create_payment_mandate: 3.0s (75%)   User consent wait
  - Other:                 0.2s (5%)

The 3.0-second delay is expected and good - it's the user deliberating before confirming. This is not a performance problem; it's proof of thoughtful consent.

Tracking LLM Costs

Click on any call_llm span to see token usage:

{
  "llm.model": "gemini-2.5-flash",
  "llm.usage.prompt_tokens": 487,
  "llm.usage.completion_tokens": 156,
  "llm.usage.total_tokens": 643,
  "llm.response_time_ms": 1243
}

Use this to:

  • Track cost per request (tokens × model pricing)
  • Identify unnecessarily long prompts
  • Compare model performance (Flash vs Pro)
  • Optimize for latency vs. quality

Пример расчета:

Gemini 2.5 Flash pricing (as of Nov 2024):
  Input:  $0.075 per 1M tokens
  Output: $0.30 per 1M tokens

This request:
  Input:  487 tokens × $0.075 / 1M = $0.000037
  Output: 156 tokens × $0.30 / 1M  = $0.000047
  Total:                            = $0.000084 (~$0.00008)

For 10,000 donations/month:
  10,000 × 3 agents × $0.00008 = $2.40/month in LLM costs

This granular visibility helps you make data-driven decisions about model selection.

Comparing Across Traces

Filter for multiple traces and compare durations:

Trace 1: 8.2s  (with consent wait: 3.0s)
Trace 2: 12.5s (with consent wait: 7.8s)  ← User took longer
Trace 3: 5.1s  (with consent wait: 0.2s)  ← User clicked fast
Trace 4: 6.3s  (with consent wait: 1.5s)

Insight: Most variation comes from user decision time, not system performance. The core agent execution (minus consent) is consistent at ~5 seconds.

This tells you the system is performing reliably.

For production systems, set up alerts to catch issues before users complain.

Alert on High Error Rates

Create an alert when >5% of traces contain errors:

  1. Navigate to Cloud Monitoring
  2. Click "Alerting""Create Policy"
  3. Настроить:
    Resource: Cloud Trace Span
    Metric: Span error count
    Condition: Rate > 5% over 5 minutes
    Notification: Email your-team@example.com
    

Alert on High Latency

Create an alert when p95 latency exceeds 15 seconds:

Resource: Cloud Trace
Metric: Span duration (95th percentile)
Condition: > 15000ms for 5 minutes
Notification: PagerDuty

This catches performance degradation before it impacts user experience.

Create an alert if any payment processes without confirmation:

Resource: Cloud Trace Span
Filter: tool.name="create_payment_mandate" AND tool.confirmation_required!=true
Condition: Any match
Notification: Critical alert to security team

This is a safety violation detector - if it fires, something is very wrong with your consent mechanism.

Что вы узнали

Through Cloud Trace, you now understand how to:

Navigate Cloud Trace Explorer to find production traces
Read waterfall views to see complete execution flow
Trace the credential chain through IntentMandate → CartMandate → PaymentMandate ✅ Use traces as evidence for dispute resolution
Analyze performance to identify bottlenecks
Track LLM costs at a granular level

The Difference This Makes

Compare two systems handling the same "I never authorized this!" complaint:

System Without Observability

User: "I never authorized that $50 donation!"
You:  "Our logs show the transaction completed successfully."
User: "But I didn't approve it!"
You:  "The system requires confirmation before processing."
User: "I never saw any confirmation!"
You:  "..." [no way to prove what happened]

Result: Refund issued, trust lost, user never returns.

System With Cloud Trace

User: "I never authorized that $50 donation!"
You:  "Let me pull up the trace from your session..."
      [Shows waterfall with consent span]
You:  "Here's the evidence:
       - 15:30:17 UTC: System asked for confirmation
       - Message shown: 'You are about to donate $50...'
       - 15:30:47 UTC: You clicked 'CONFIRM'
       - Wait time: 29.2 seconds
       
       The system waited almost 30 seconds for your decision.
       Here's the exact timestamp of your confirmation."
       
User: "Oh... I remember now. My mistake. Sorry!"

Result: Trust preserved, no refund needed, user continues using service.

This is the power of accountability trails. You move from "trust us" to "let us show you exactly what happened."

Что дальше?

You've now completed the technical core of building trustworthy agents:

Module 1-6: Designed a trustworthy architecture (roles, credentials, consent)
Module 7: Orchestrated complex workflows (SequentialAgent)
Module 8: Deployed with observability enabled
Module 9: Learned to read and use accountability trails

The architecture you've built—role separation, credential chains, consent mechanisms, complete observability—transfers directly to production systems handling real money, real data, and real consequences.

10. Your Journey Forward

Что вы построили

You started this workshop with a question: "How do I build AI agents I can actually trust with money?"

You now have the answer.

Where you started (Module 3):

simple_agent = Agent(
    model="gemini-2.5-flash",
    instruction="Find charities and donate",
    tools=[google_search]
)

Where you are now (Module 10):

  • ✅ Three specialized agents with role separation
  • ✅ Three verifiable credentials (Intent → Cart → Payment mandates)
  • ✅ Complete credential chain with expiry validation at each step
  • ✅ Explicit consent mechanism with timestamp proof
  • ✅ Production deployment to Agent Engine with observability
  • ✅ Complete accountability trail in Cloud Trace
  • ✅ Forensic evidence for dispute resolution

Workshop vs. Production: The Gap

Your system demonstrates the correct architecture and patterns , but uses educational simplifications that must be upgraded for real money and real users.

Here's exactly what was simplified and what production requires:

Компонент

Workshop Implementation

Требования к производству

Подписи

SHA-256 hashes ( SIG_abc123 ) for demonstration

Real cryptographic signatures using PKI or JWT with private keys

Обработка платежей

Simulated returns ( simulation: True flag)

Integration with real payment APIs (Stripe, PayPal, Square)

Аутентификация пользователя

Implicit trust (no login required)

OAuth 2.0, WebAuthn, or session management

Управление секретами

Environment variables in .env file

Google Secret Manager or Cloud KMS with encryption

Charity Database

Mock JSON file with 9 charities

Live API integration (IRS Tax Exempt Organization Search, Charity Navigator API)

Обработка ошибок

Basic try-catch with error messages

Retry logic with exponential backoff, circuit breakers, dead letter queues

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

Manual verification via scripts

Comprehensive unit/integration/E2E test suite with CI/CD

Session Persistence

In-memory (local) or automatic (Agent Engine)

Production database with backups and disaster recovery

Ограничение скорости

None (educational environment)

Rate limits per user, IP-based throttling, abuse detection

Key Architectural Patterns You Mastered

The patterns you learned in this workshop are production patterns . Don't doubt them.

Role Separation (AP2 Principle #1)

Each agent has ONE clear job and sees ONLY what it needs. If one agent is compromised, the attacker cannot access other agents' data. This limits the blast radius.

Production systems using this: Payment processing, document workflows, approval chains, multi-step forms with validation gates.

Verifiable Credentials (AP2 Principle #2)

Each credential has expiry time, references the previous credential, and requires validation before the next step. This creates a tamper-evident audit chain.

Production value: Complete proof of what happened, when, and in what order. Essential for dispute resolution and regulatory compliance.

Timestamp proof that user approved action. Cannot be disputed.

Production value: Legal requirement for financial transactions. Protects both user and company.

Sequential Orchestration (ADK Pattern)

Enforces correct execution order. Prevents skipping steps. Guarantees each agent sees previous agent's output.

Production value: Perfect for human-in-the-loop systems where users expect immediate results. This is the right pattern for donation flows, checkout processes, and approval chains.

Complete Observability (OpenTelemetry + Cloud Trace)

Every decision, tool call, consent moment, and credential handoff captured automatically.

Production value: Forensic evidence for disputes. Performance optimization data. Compliance audit trails. Debug production issues with precision.

Ресурсы для продолжения обучения

ADK Documentation:

AP2 & Related Standards:

Google Cloud Services:

Cleanup Resources

To avoid ongoing charges, delete your deployment:

Agent Engine: Follow steps in the Agent Engine docs

Cloud Run (if deployed):

gcloud run services delete charity-advisor \
    --region=$GOOGLE_CLOUD_LOCATION

Storage Buckets:

gsutil -m rm -r gs://$GOOGLE_CLOUD_PROJECT-staging
gsutil -m rm -r gs://$GOOGLE_CLOUD_PROJECT-artifacts

Ваше путешествие продолжается

You started with a simple question and built a complete answer. You've mastered the foundational patterns for trustworthy AI agents. These patterns transfer to any domain where AI agents handle sensitive operations—financial transactions, healthcare decisions, legal documents, supply chain operations.

The principles transfer. The trust model works.

Now go build something trustworthy! ❤️

баннер