ایجاد کارگزاران خیریه قابل اعتماد با Google ADK و AP2

۱. ایجاد اعتماد برای ایجاد سخاوت

بنر

لحظه الهام

تلفن شما زنگ می‌خورد. خبری در مورد یک برنامه سوادآموزی موفق که به کودکان در جوامع محروم در یادگیری خواندن کمک می‌کند، می‌بینید. اشتیاق شدیدی برای مشارکت در شما ایجاد می‌شود. مرورگر خود را باز می‌کنید و عبارت "کمک‌های مالی به برنامه سوادآموزی کودکان " را جستجو می‌کنید.

جستجوی گوگل

صدها نتیجه ظاهر می‌شود.

روی اولین لینک کلیک می‌کنید. وب‌سایت حرفه‌ای به نظر می‌رسد. به پایین صفحه و بخش مالی آن می‌آیید. «هزینه‌های اداری: ۲۸٪». مکث می‌کنید. فقط ۷۲ سنت از هر دلاری که اهدا می‌کنید، واقعاً برای این برنامه هزینه می‌شود. آیا این خوب است؟ مطمئن نیستید.

شما یک سازمان دیگر را امتحان می‌کنید. شما هرگز نام آنها را نشنیده‌اید. آیا آنها قانونی هستند؟ یک جستجوی سریع شما را به یک معمای پیچیده هدایت می‌کند. شما یک تاپیک در ردیت مربوط به دو سال پیش پیدا می‌کنید که در آن یک کاربر ادعا می‌کند: "این یک کلاهبرداری است، کمک مالی من هرگز به جایی نرسیده است." دیگری با شور و شوق از آنها دفاع می‌کند: "آنها در حال انجام کار واقعی هستند!" ابهام فلج کننده است.

سی دقیقه بعد ، شما در انبوهی از نظرات متناقض، رتبه‌بندی‌های بهره‌وری و سوابق IRS گیر افتاده‌اید و هنوز کمکی نکرده‌اید. جرقه اولیه سخاوت جای خود را به اصطکاک تحقیق داده است. تب برای چند روز باز می‌ماند، یادآوری کوچکی از یک نیت خوب، تا اینکه در نهایت آن را می‌بندید.

این یک شکست شخصی نیست؛ این یک شکست سیستمی است

این تجربه جهانی است. میل به بخشش فراوان است، اما این فرآیند پر از موانعی است که باعث تردید و دودلی می‌شوند:

  • اصطکاک تحقیقاتی: هر خیریه‌ای به تحقیقات خاص خود نیاز دارد.
  • تأیید اعتماد: تشخیص سازمان‌های بسیار مؤثر از سازمان‌های ناکارآمد یا حتی کلاهبرداران آشکار دشوار است.
  • فلج تحلیل: تعداد زیاد گزینه‌ها منجر به خستگی تصمیم‌گیری می‌شود.
  • از دست دادن انگیزه: با افزایش بار تدارکاتی، انگیزه عاطفی برای بخشش کمرنگ می‌شود.

این اختلاف، هزینه‌ای سرسام‌آور و واقعی دارد. کمک‌های مالی فردی در ایالات متحده بسیار زیاد است - طبق گزارش Giving USA 2024 ، هر فرد اهداکننده تقریباً در سال 2023، 374 میلیارد دلار کمک کرده است. با این حال، تحقیقات نشان می‌دهد که موانع کمک مالی - از جمله هزینه‌های جستجو، اختلاف روانی و محدودیت‌های زمانی - به طور قابل توجهی میزان کمک‌های مالی به اهداف خیریه را کاهش می‌دهد. مطالعات انجام شده روی میلیون‌ها اهداکننده نشان داده است که حتی مقادیر کمی اختلاف در فرآیند کمک‌های مالی آنلاین، مانع از تحقق نیات خیرخواهانه افراد می‌شود.

این نشان دهنده میلیاردها دلار کمک مالی از پیش تعیین شده است که هرگز به دست نیازمندان نمی‌رسد.

چشم‌انداز

یک تجربه متفاوت را تصور کنید. به جای یک جلسه تحقیق ۳۰ دقیقه‌ای، شما به سادگی می‌گویید:

«می‌خواهم ۵۰ دلار به یک برنامه سوادآموزی برای کودکان اهدا کنم. یک موسسه خیریه با رتبه بالا، کارآمد و تأیید شده برای من پیدا کنید.»

و در عرض چند ثانیه، پاسخی دریافت می‌کنید که اعتماد به نفس شما را افزایش می‌دهد:

کارت نتایج خیریه

این وعده‌ی یک عامل بخشنده‌ی هوش مصنوعی است. اما برای تحقق این چشم‌انداز، باید یک چالش اساسی را حل کنیم: وقتی یک عامل هوش مصنوعی مستقل با پول سروکار دارد، اعتماد اختیاری نیست؛ بلکه کل پایه و اساس است.

  • چگونه می‌توانیم ثابت کنیم که یک کاربر چه چیزی را مجاز دانسته است؟
  • اگر اشتباهی رخ دهد چه کسی پاسخگو است؟
  • چگونه به اهداکنندگان، خیریه‌ها و شبکه‌های پرداخت اعتماد لازم برای مشارکت را می‌دهیم؟

ماموریت امروز شما

در این کارگاه، شما با ترکیب دو فناوری قدرتمند، آن عامل قابل اعتماد را خواهید ساخت:

کیت توسعه عامل گوگل (ADK)

پروتکل پرداخت‌های عامل (AP2)

نقش

کارخانه‌ای برای ساخت عوامل هوش مصنوعی در سطح تولید

طرح معماری برای اعتماد در تراکنش‌های هوش مصنوعی

آنچه ارائه می‌دهد

• چارچوبی برای هماهنگی چندعاملی
• ویژگی‌های امنیتی داخلی مانند تأیید ابزار
• قابلیت مشاهده و ردیابی آماده برای تولید
• رابط کاربری ساده پایتون برای رفتارهای پیچیده عامل

• مرزهای امنیتی مبتنی بر نقش
• اعتبارنامه‌های دیجیتال قابل تأیید (دستورالعمل‌ها)
• اثبات رمزنگاری رضایت
• مسیرهای حسابرسی کامل برای پاسخگویی

بیشتر بدانید

مستندات ADK

پروتکل AP2

آنچه خواهید ساخت

معماری

در پایان این کارگاه، شما موارد زیر را ایجاد خواهید کرد:

یک سیستم چندعامله با نقش‌های تخصصی:

  • یک نماینده خرید که خیریه‌های تأیید شده را پیدا می‌کند
  • یک نماینده تجاری که پیشنهادهای اهدایی الزام‌آور ایجاد می‌کند
  • ارائه‌دهنده‌ی اعتبارنامه‌ها که پرداخت‌ها را به طور ایمن پردازش می‌کند
  • یک هماهنگ‌کننده که کل جریان را هماهنگ می‌کند

سه نوع اعتبارنامه قابل تأیید :

  • IntentMandate: «یک موسسه خیریه آموزشی برای من پیدا کنید»
  • CartMandate: «50 دلار برای اتاق مطالعه، امضا شده توسط فروشنده»
  • PaymentMandate: "فرآیند از طریق پرداخت شبیه‌سازی‌شده"

امنیت در هر لایه :

  • مرزهای اعتماد مبتنی بر نقش
  • رضایت صریح کاربر

یک دنباله حسابرسی کامل :

  • هر تصمیمی قابل ردیابی است
  • هر رضایت ثبت شده
  • هر جابجایی قابل مشاهده

🔒 مهم: این یک محیط یادگیری امن است

آماده برای ایجاد اعتماد؟

در ماژول بعدی، محیط توسعه شما را راه‌اندازی کرده و اولین عامل هوش مصنوعی شما را خواهیم ساخت. شما به سرعت متوجه خواهید شد که چرا عامل‌های ساده قابل اعتماد نیستند - و سپس بقیه کارگاه را صرف یادگیری نحوه رفع این مشکل خواهید کرد.

بیایید با درک مستقیم مشکل شروع کنیم.

۲. آماده‌سازی فضای کاری

بنیاد نمایندگان قابل اعتماد

قبل از اینکه بتوانیم عامل هوش مصنوعی خود را بسازیم، باید یک محیط توسعه تمیز، سازگار و به درستی پیکربندی شده آماده کنیم. این ماژول یک گام متمرکز برای اطمینان از وجود تمام ابزارها و سرویس‌های لازم است.

تکمیل موفقیت‌آمیز این تنظیمات به این معنی است که می‌توانید بدون نگرانی در مورد مشکلات پیکربندی، کاملاً روی کار هیجان‌انگیز ساخت منطق عامل در ماژول‌های بعدی تمرکز کنید.

دسترسی به پوسته ابری

ابتدا، Cloud Shell را باز می‌کنیم که یک ترمینال مبتنی بر مرورگر است و Google Cloud SDK و سایر ابزارهای ضروری از پیش نصب شده روی آن قرار دارند.

به اعتبار ابری گوگل نیاز دارید؟

روی «فعال کردن پوسته ابری» در بالای کنسول گوگل کلود کلیک کنید (این نماد ترمینال در نوار ناوبری بالا سمت راست است).

پوسته ابری

شناسه پروژه گوگل کلود خود را پیدا کنید:

  • کنسول گوگل کلود را باز کنید: 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

این ممکن است ۱-۲ دقیقه طول بکشد. خواهید دید:

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 - فایل متغیرهای محیطی

تنظیم محیط پایتون

حالا یک محیط پایتون ایزوله برای پروژه‌مان ایجاد خواهیم کرد.

ایجاد و فعال‌سازی محیط مجازی

# 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 : چارچوب کیت توسعه عامل
  • google-cloud-aiplatform : ادغام Vertex AI و Gemini
  • ap2 : کیت توسعه نرم‌افزار پروتکل پرداخت‌های عامل (از گیت‌هاب)
  • مدیریت متغیرهای محیطی در پایتون-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.
======================================================================

عیب‌یابی

قدم بعدی چیست؟

محیط شما اکنون کاملاً آماده است! شما موارد زیر را دارید:

  • ✅ پروژه گوگل کلود پیکربندی شد
  • ✅ API های مورد نیاز فعال هستند
  • ✅ کتابخانه‌های ADK و AP2 نصب شده‌اند
  • ✅ کد قالب آماده برای تغییر

در ماژول بعدی، اولین عامل هوش مصنوعی خود را در چند خط کد خواهید ساخت و کشف خواهید کرد که چرا عامل‌های ساده هنگام انجام تراکنش‌های مالی قابل اعتماد نیستند.

۳. اولین نماینده شما و کشف شکاف اعتماد

بنر

از ایده تا تعامل

در ماژول قبلی، محیط توسعه خود را آماده کردیم. اکنون، کار هیجان‌انگیز آغاز می‌شود. ما اولین عامل خود را خواهیم ساخت و اجرا خواهیم کرد، اولین قابلیت آن را به آن می‌دهیم و با انجام این کار، چالش‌های اساسی را که باید برای قابل اعتماد کردن واقعی آن حل کنیم، کشف خواهیم کرد .

این ماژول تصویر «قبل» شماست - لحظه‌ای که نشان می‌دهد چرا ایجاد نمایندگان قابل اعتماد به چیزی بیش از دسترسی به ابزارها برای یک 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=[]
)

توجه داشته باشید که کامنت‌های placeholder از یک الگو پیروی می‌کنند: MODULE_3_STEP_X_DESCRIPTION . ما این نشانگرها را جایگزین خواهیم کرد تا به تدریج عامل خود را بسازیم.

مرحله ۲: وارد کردن اجزای مورد نیاز

قبل از اینکه بتوانیم کلاس 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 در فایل ما موجود هستند.

مرحله ۳: دستورالعمل عامل را بنویسید

این دستورالعمل، «شرح وظایف» نماینده است - به 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.""",

مرحله ۴: ابزار جستجو را اضافه کنید

یک عامل بدون ابزار فقط یک مکالمه‌گر است. بیایید اولین قابلیت را به عامل خود بدهیم: توانایی جستجو در وب.

👉 پیدا کنید:

# MODULE_3_STEP_4_ADD_TOOLS
tools=[]

👉 آن دو خط را با موارد زیر جایگزین کنید:

tools=[google_search]

مرحله ۵: نماینده خود را به طور کامل تأیید کنید

بیایید قبل از آزمایش، مطمئن شویم که همه قطعات سر جای خود هستند.

👉 کامل شما

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]
)

مرحله ۶: تست عامل - آشکار کردن شکاف‌های اعتماد

حالا که عامل ما کاملاً پیکربندی شده است، بیایید آن را آزمایش و رفتارش را تحلیل کنیم. اینجاست که متوجه می‌شویم چرا عامل‌های ساده هنگام تصمیم‌گیری‌های مالی قابل اعتماد نیستند.

آزمون ۱: مسئله‌ی کشف

👉 در ترمینال 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]: اکنون منتظر ورودی شماست.

👉 در اعلان [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، پیدا کردم که در آنها کاربران تجربیات شخصی خود را با برنامه‌های مختلف سوادآموزی کوچک‌تر و محلی به اشتراک می‌گذارند.

بیایید این را تحلیل کنیم. آیا این نماینده مشکل ما را حل کرده است؟

خیر. این سیستم، تجربه انسانی که در ماژول ۱ توضیح دادیم را به طور کامل تکرار کرده است. این سیستم با موفقیت فرآیند «جستجو در گوگل» را خودکار کرده و مشکل «فلج تحلیلی» را به خودمان برگردانده است.

این اولین شکاف اعتماد را آشکار می‌کند: فقدان داده‌های معتبر.

عامل در حال جستجو در وب باز است، که به معنی:

  • ✅ به سرعت نتایج را پیدا کرد (تجربه کاربری بهبود یافته)
  • ❌ این سایت سازمان‌های دارای رتبه بالا را با بحث‌های ردیت ترکیب می‌کند (منابع غیرقابل اعتماد)
  • ❌ نمی‌تواند بین خیریه‌های تأیید شده و کلاهبرداری‌های احتمالی تمایز قائل شود (تأیید نشده)
  • ❌ از ما می‌خواهد اطلاعاتی را که ارائه داده تأیید کنیم (و بار مسئولیت را به گردن دیگری می‌اندازد)

آزمون ۲: مشکل اجرا

حالا نوبت به آزمایش حیاتی دوم می‌رسد. در اعلان [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 ، شروع به ساخت راهکار خواهیم کرد.

بیایید اولین عامل را بسازیم و جداسازی نقش‌ها را در عمل ببینیم.

۴. ساخت عامل خرید - کشف مبتنی بر نقش

بنر

بنیاد اعتماد: تفکیک نقش‌ها

در ماژول قبلی، متوجه شدید که یک عامل ساده و همه منظوره در دو جبهه شکست می‌خورد: نمی‌تواند کشف قابل اعتمادی ارائه دهد و نمی‌تواند تراکنش‌های امن را اجرا کند. اکنون با پیاده‌سازی اولین اصل از پروتکل پرداخت‌های عامل، یعنی معماری مبتنی بر نقش ، شروع به حل این مشکلات خواهیم کرد.

قبل از اینکه هر کدی بنویسیم، بیایید بفهمیم که چرا این اصل مهم است.

اصل AP2: تفکیک نقش‌ها

مشکل مامورانی که «همه کارها را انجام می‌دهند»

تصور کنید که یک نفر را به عنوان مشاور مالی، حسابدار و کارگزار سرمایه‌گذاری خود استخدام می‌کنید. راحت؟ بله. امن؟ مطلقاً نه. آنها موارد زیر را خواهند داشت:

  • اهداف سرمایه‌گذاری شما (نقش مشاور)
  • دسترسی به حساب‌های شما (نقش حسابدار)
  • اختیار جابجایی پول شما (نقش دلال)

اگر این شخص به خطر بیفتد - یا اشتباه کند - همه چیز در معرض خطر است.

راه حل AP2: یک نماینده، یک کار

AP2 اصل جداسازی دغدغه‌ها را برای ایجاد مرزهای اعتماد اعمال می‌کند:

معماری

چرا این مهم است:

  • شعاع انفجار محدود : اگر عامل خرید به خطر بیفتد، مهاجم نمی‌تواند به اطلاعات پرداخت دسترسی پیدا کند.
  • حریم خصوصی : ارائه دهنده اعتبارنامه هرگز مکالمه خرید شما را نمی‌بیند
  • انطباق‌پذیری : برآورده کردن الزامات 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 و ذخیره آن در state استفاده کنید.
  4. به نماینده بعدی (تاجر) تحویل دهید

همین. نه پرداختی انجام می‌شود، نه سبد خریدی ایجاد می‌شود—فقط کشف و تحویل کالا انجام می‌شود.

بیایید آن را گام به گام بسازیم.

مرحله ۱: افزودن کمک‌کننده اعتبارسنجی ورودی

هنگام ساخت ابزارهای تولید، اعتبارسنجی ورودی بسیار مهم است. بیایید یک تابع کمکی ایجاد کنیم که داده‌های خیریه را قبل از ذخیره در حالت، اعتبارسنجی کند.

👉 باز است

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, ""

مرحله ۲: اضافه کردن IntentMandate Creation Helper

حالا بیایید کمکی ایجاد کنیم که ساختار AP2 IntentMandate را می‌سازد. این یکی از سه اعتبارنامه قابل تأیید در 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

مرحله ۳: ساخت ابزار انتقال وضعیت با IntentMandate

حالا بیایید ابزاری بسازیم که IntentMandate را ایجاد کرده و آن را در state ذخیره کند.

👉 در همان فایل، به پایین اسکرول کنید تا به

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"]
    }

مرحله ۴: افزودن ابزار کمکی قالب‌بندی نمایش

قبل از اینکه عامل را بسازیم، بیایید یک تابع کمکی دیگر اضافه کنیم که داده‌های خیریه را برای نمایش کاربرپسند قالب‌بندی می‌کند.

👉 برای یافتن موارد زیر اسکرول کنید:

# 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)
    ]

مرحله ۸: نماینده کامل خود را تأیید کنید

بیایید بررسی کنیم که همه چیز به درستی سیم‌کشی شده باشد.

👉 کامل شما

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
  • خروجی فرمت شده
  • مرزهای نقش را مشخص کنید
  • دستورالعمل‌های دقیق
  • مدیریت خطا

مرحله ۹: عامل خرید را آزمایش کنید

بیایید تأیید کنیم که عامل ما به درستی کار می‌کند، IntentMandate های مناسبی ایجاد می‌کند و به مرزهای آن احترام می‌گذارد.

👉 در ترمینال Cloud Shell خود، دستور زیر را اجرا کنید:

adk run charity_advisor/shopping_agent

اعلان [user]: ظاهر خواهد شد.

آزمون ۱: کشف با پایگاه داده‌ی مورد اعتماد

👉 نوع:

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

پس از لحظه‌ای، عامل پاسخی ارائه می‌دهد. به لطف کمک‌کننده‌ی _format_charity_display ما، نتایج به زیبایی قالب‌بندی شده‌اند:

من ۳ موسسه خیریه آموزشی تأیید شده را در پایگاه داده خود پیدا کردم:

اتاق مطالعه (EIN: 77-0479905)
⭐ امتیاز: ۴.۹/۵.۰
💰 کارایی: ۸۸٪ از بودجه به برنامه‌ها اختصاص می‌یابد
📋 ماموریت: با تمرکز بر سوادآموزی و برابری جنسیتی در آموزش، زندگی میلیون‌ها کودک در جوامع کم‌درآمد را متحول می‌کند.

Teach For America (EIN: 13-3541913)
⭐ امتیاز: ۴.۷/۵.۰
💰 کارایی: ۸۱٪ از بودجه به برنامه‌ها اختصاص می‌یابد
📋 ماموریت: تلاش برای گسترش فرصت‌های آموزشی برای کودکانی که با سختی‌ها روبرو هستند.

اتحادیه آموزش فناوری (EIN: 45-2345678)
⭐ امتیاز: ۴.۸/۵.۰
💰 کارایی: ۹۲٪ از بودجه به برنامه‌ها اختصاص می‌یابد
📋 ماموریت: آموزش علوم کامپیوتر را به مدارس محروم می‌آورد.

دوست دارید از کدام خیریه حمایت کنید و چقدر حاضرید کمک مالی کنید؟

این را با عامل ساده ماژول ۳ مقایسه کنید ، که نتایج گوگل بررسی نشده را به شما می‌داد. این تفاوت بین کشف غیرقابل اعتماد و قابل اعتماد است.

آزمون ۲: ثبت انتخاب کاربر و ایجاد IntentMandate

👉 نوع:

I'll donate $50 to Room to Read.

شما باید ببینید که عامل، ابزار save_user_choice را با پارامترهای صحیح فراخوانی می‌کند. به لطف ایجاد IntentMandate با استفاده از مدل‌های AP2، پاسخ ساختاریافته‌تری خواهید دید:

عالی! من یک IntentMandate برای کمک مالی شما ایجاد کرده‌ام:

جزئیات قصد:

  • شناسه هدف: intent_774795905_1730927536
  • مبلغ: ۵۰ دلار به اتاق مطالعه (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. The tool wrote ONLY the IntentMandate to shared state:
   state["intent_mandate"] = intent_mandate_dict
  1. A success message with intent details was returned to the LLM
  2. The LLM composed the final user message

This IntentMandate is now ready for the Merchant Agent (Module 5) to pick up. The Merchant will extract the charity name and amount from the IntentMandate and validate that it hasn't expired.

Test 3: Validation in Action

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)

Let's build it step by step.

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

Expected output:

======================================================================
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

Expected output:

======================================================================
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"
}

Key components:

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

Let's build it step by step.

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

Expected output:

======================================================================
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

Expected output:

======================================================================
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

Let's start with imports:

👉 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.

👉 In

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.

👉 In

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

line:

    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

👉 In

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

👉 In

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

line:

    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

شما باید خروجی مانند زیر را ببینید:

+-----------------------------------------------------------------------------+
| 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! 🎉

جزئیات:

  • Amount: $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. Deployment

بنر

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 )

Agent Engine

اجرای ابری

پیچیدگی

مینیمال

کم

متوسط

Session Persistence

In-memory only (lost on restart)

Vertex AI managed (automatic)

Cloud SQL (PostgreSQL) or in-memory

زیرساخت

None (dev machine only)

Fully managed

Container + optional database

Cold Start

ناموجود

100-500ms

100-2000ms

مقیاس‌بندی

Single instance

خودکار

Automatic (to zero)

Cost Model

Free (local compute)

Compute-based

Request-based + free tier

UI Support

Yes (built-in)

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. Environment Variables

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}"

You should see:

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

Verification Checklist

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.

Session Management:

  • 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. Observe the execution flow:
    • 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
    • Confirmation request
    • 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

What It Represents

مثال

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:

What You're Looking At

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.

مسیر بحرانی را شناسایی کنید

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

Example calculation:

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. Configure:
    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.

What You've Learned

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

What You've Built

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

Production Requirements

امضاها

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.

Resources for Continued Learning

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

Your Journey Continues

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! ❤️

بنر