AP2 ve UCP ile güvenli aracı ticareti

1. Genel Bakış

Bu codelab'de, iki açık kaynaklı ticaret protokolünü kullanarak birden fazla sinema satıcısından sinema bileti ayırtan bir ADK aracısı çalıştıracaksınız:

  • UCP (Evrensel Ticaret Protokolü): Ajanların satıcıları keşfetmesi, kataloglarda arama yapması ve ödeme akışlarını yönetmesi için kullanılan bir standarttır.
  • AP2 (Agent Payments Protocol): Kriptografik olarak imzalanmış yetkiler kullanılarak güvenli ve doğrulanabilir ödeme yetkilendirmesi için kullanılan bir protokol.

CineAgent adlı demo uygulaması, farklı özelliklere (koltuk seçimi, özel formatlar ve ödeme yöntemleri) sahip iki sahte sinema satıcısına bağlanır ve aramadan ödemeye kadar tüm rezervasyon akışını yönetir.

Neler öğreneceksiniz?

  • UCP satıcı keşfinin /.well-known/ucp profilleri üzerinden işleyiş şekli
  • ADK aracısı, kataloglarda arama yapmak ve ödeme oluşturmak için UCP'yi nasıl kullanır?
  • AP2 zorunlulukları (CartMandate, PaymentMandate) işlemleri nasıl güvence altına alır?
  • Uçtan uca UCP ve AP2 protokolleri, ajan tabanlı e-ticaretin güvenliğini nasıl sağlar?

İhtiyacınız olanlar

  • Faturalandırmanın etkin olduğu bir Google Cloud projesi
  • Chrome gibi bir web tarayıcısı
  • Python 3.11 veya sonraki sürümler

Bu codelab, Python ve Google Cloud hakkında bilgi sahibi olan orta düzey geliştiriciler içindir. Bu codelab'in tamamlanması yaklaşık 15 dakika sürer.

Bu kod laboratuvarında oluşturulan kaynakların maliyeti 5 ABD dolarından az olmalıdır.

2. UCP ve AP2 protokollerini anlama

Aracı oluşturmaya başlamadan önce, bu güvenli aracı tabanlı ticareti mümkün kılan iki protokolü anlayalım.

Evrensel Ticaret Protokolü (UCP)

UCP, yapay zeka ajanlarının satıcılarla etkileşim kurma şeklini standart hâle getirir. Standartlaştırılmış bir kaynak modeli sunarak temsilcilerin her mağaza için özel API'leri öğrenmesi sorununu çözer.

İşleyiş şekli:

  1. Bulma: UCP'ye uygun her satıcı, standart bir konumda profil gösterir: /.well-known/ucp. Örnek: Everlane'in UCP uç noktası.Bir temsilci bu profili okuduğunda şunları arar:
    • Özellikler: İşletmenin desteklediği bağımsız temel özellikler (ör. katalogda arama veya ödeme).
    • Hizmetler: Veri alışverişi için kullanılan alt düzey iletişim katmanları. Örnekler: REST API, MCP (Model Context Protocol), A2A (Agent2Agent Protocol).
    • Uzantılar: Bir satıcının özel davranışa ihtiyacı varsa bu profilde özel uzantılar tanımlayabilir.
  2. İşlemler: Ajan, keşfedildikten sonra işlemleri gerçekleştirmek için sağlanan hizmet uç noktasını kullanır. Bu codelab'de hizmet aktarımı olarak Model Context Protocol (MCP) kullanılır. Aracı, keşfedilen özellikleri (ürün arama, ödeme oluşturma ve satın alma işlemlerini tamamlama) çağırmak için bu uç noktaya JSON-RPC 2.0 çağrıları yapar.

UCP İş Akışı

Agent Payments Protocol (AP2)

AP2, ödemelerin kullanıcılar adına aracılar tarafından nasıl yetkilendirileceğini standartlaştırır. Hassas ödeme bilgilerini işleyen aracıların güvenlik sorununu çözer.

İşleyiş şekli:

  1. Sepet Yetkisi: Bir aracı, UCP Protokolü'nü kullanarak ödeme oluşturduğunda satıcı CartMandate döndürür. Bu, alışveriş sepeti ayrıntılarını ve satıcının kriptografik imzasını içeren bir JSON nesnesidir. Fiyat sabitleme garantisi görevi görür. Satıcı, bu yetkiyi verdikten sonra fiyatı değiştiremez.
  2. Ödeme Talimatı: Kullanıcı (veya kullanıcı adına hareket eden temsilci), ödeme yetkisi vermek için alışveriş sepetinin içeriğini doğruladıktan sonra PaymentMandate oluşturur. Bu PaymentMandate, CartMandate'e referans verir ve kullanıcının kriptografik imzasını (veya yetkilendirme jetonunu) içerir.
  3. Çift İmza Doğrulaması: Satıcı, her iki yetkiyi de alır. CartMandate üzerinde kendi imzalarını, PaymentMandate üzerinde ise kullanıcının imzasını doğrularlar. Her ikisi de geçerliyse işlem devam eder.

Bu "çift kilit" sistemi, satıcıların fazla ücret almasını ve aracıların yetkisiz harcama yapmasını engeller. Üretimde, bu zorunluluklar kullanıcı gizliliğini korumak için SD-JWT (Seçici Açıklama JWT) kullanır.

AP2 İş Akışı

3. Ortamınızı ayarlama

Google Cloud Proje Kurulumu

Google Cloud projesi oluşturma

  1. Google Cloud Console'daki proje seçici sayfasında bir Google Cloud projesi seçin veya oluşturun.
  2. Cloud projeniz için faturalandırmanın etkinleştirildiğinden emin olun. Bir projede faturalandırmanın etkin olup olmadığını kontrol etmeyi öğrenin.

Cloud Shell'i Başlatma

Cloud Shell, Google Cloud'da çalışan ve gerekli araçların önceden yüklendiği bir komut satırı ortamıdır.

  1. Google Cloud Console'un üst kısmından Cloud Shell'i etkinleştir'i tıklayın.
  2. Cloud Shell'e bağlandıktan sonra kimlik doğrulamanızı onaylayın:
    gcloud auth list
    
  3. Projenizin yapılandırıldığını onaylayın:
    gcloud config get project
    
  4. Projeniz beklendiği gibi ayarlanmamışsa şu şekilde ayarlayın:
    export PROJECT_ID=<YOUR_PROJECT_ID>
    gcloud config set project $PROJECT_ID
    

Gemini modellerine erişim

Cloud Shell ortamınızda aşağıdaki komutları kopyalayıp yapıştırın. Bu işlem, Cine Agent'ın kullanacağı Gemini modellerine erişimi etkinleştirir.

export GOOGLE_CLOUD_PROJECT=$PROJECT_ID
export GOOGLE_CLOUD_LOCATION=global
export GOOGLE_GENAI_USE_VERTEXAI=True

Dizin yapısını ayarlama

Aracı için yeni bir dizin oluşturmak üzere aşağıdaki komutları kopyalayıp yapıştırın:

mkdir -p agent_payments
cd agent_payments

Bağımlılıkları yükleme

Google Cloud Shell, ortamı ve bağımlılıkları yönetmek için uv ile önceden yüklenmiş olarak gelir.

  1. agent_payments klasörünüzün kök dizininde bir pyproject.toml dosyası oluşturun ve aşağıdaki içeriği bu dosyaya ekleyin. Bu dosya, proje meta verilerini ve bağımlılıklarını tanımlar.
[project]
name = "agent-payments-demo"
version = "0.1.0"
description = "CineAgent booking agent using UCP and AP2"
requires-python = ">=3.11"
dependencies = [
    "google-adk>=1.29.0",
    "google-genai>=1.27.0",
    "fastapi>=0.115.0",
    "uvicorn>=0.34.0",
    "httpx>=0.28.0",
    "ap2 @ git+https://github.com/google-agentic-commerce/AP2.git@main",
    "ucp-sdk @ git+https://github.com/Universal-Commerce-Protocol/python-sdk.git@main",
]
  1. Sanal ortamı oluşturmak ve tüm bağımlılıkları yüklemek için aşağıdaki komutu çalıştırın:
uv sync
  1. uv tarafından oluşturulan sanal ortamı etkinleştirin:
source .venv/bin/activate

4. Temsilciyi tanımlama

Herhangi bir araç mantığı yazmadan önce, agent.py adlı bir dosyada aracıyı tanımlayalım. Bu ajan, film rezervasyonu akışının düzenleyicisi olarak hareket eder.

Oluşturma agent.py:

"""CineAgent — movie ticket booking agent using UCP and AP2."""

from google.adk.agents import Agent
from google.adk.tools import FunctionTool
from agent_payments.tools import (
    discover_theaters,
    search_movies,
    get_movie_detail,
    create_checkout,
    complete_purchase,
)

root_agent = Agent(
    model="gemini-3.1-pro-preview",
    name="cineagent",
    description="Movie ticket booking agent using UCP and AP2.",
    instruction="""You are CineAgent, a movie ticket booking assistant.

You help users find and book movie tickets across multiple theaters
using UCP (Universal Commerce Protocol) and AP2 (Agent Payments).

**Your tools:**
- discover_theaters: Find theaters and what they support
- search_movies: Search movies across all theaters
- get_movie_detail: Get showtimes at a specific theater
- create_checkout: Start a checkout session
- complete_purchase: Finalize with AP2 mandate signing

**Rules:**
- Always call discover_theaters first if you haven't yet
- Keep responses concise — summarize and suggest next steps
- Prices from tools are in cents (1500 = $15.00)
- Never invent data — only state what tools return
""",
    tools=[
        discover_theaters,
        search_movies,
        get_movie_detail,
        create_checkout,
        FunctionTool(complete_purchase, require_confirmation=True),
    ],
)

Kod Açıklaması

Bu aracı tanımında neler olduğunu inceleyelim:

  • model="gemini-3.1-pro-preview": Karmaşık akıl yürütme ve araç kullanımı için en yeni Gemini Pro önizleme modelini kullanıyoruz.
  • instruction: Bu istem, aracının davranışını yönlendirir. Aracıya UCP ve AP2'yi kullanmasını açıkça söyler, mevcut araçları listeler ve "Asla veri uydurma" ve "Fiyatlar sent cinsindendir" gibi önemli kurallar belirler.
  • tools: Bu, aracının kullanıcının isteklerine göre çağırmayı seçebileceği Python işlevlerinin (bir sonraki adımda oluşturacağımız) listesidir.
  • require_confirmation: Herhangi bir aracı FunctionTool(my_function,require_confirmation=True) ile sarmalayabilirsiniz. Etkinleştirildiğinde aracı, aracı çalıştırmadan önce duraklatır ve basit bir "evet" veya "hayır" onayı bekler. Burada, complete_purchase aracını yürütmeden önce ajanın işlemi duraklatarak kullanıcı onayı alması sağlanıyor.

Araç Listesi

Temsilci tanımı, ne oluşturmamız gerektiğini belirtir. Her araç, UCP veya AP2 protokolündeki belirli bir işlemle eşlenir:

Araç

Ne işe yarar?

Protokol İşlemi

discover_theaters

Satıcıları ve özelliklerini bulma

Sorgular /.well-known/ucp

search_movies

Satıcılar genelinde kataloglarda arama yapma

JSON-RPC'den MCP uç noktasına

get_movie_detail

Belirli bir satıcıdaki gösterim saatlerini öğrenme

JSON-RPC'den MCP uç noktasına

create_checkout

Ödeme oturumu başlatma

JSON-RPC'den MCP uç noktasına

complete_purchase

Ödemeyi yetkilendirin ve siparişi tamamlayın

AP2 Yetkisini İmzalama ve MCP'ye Gönderme

Gemini modeli, görüşmeye göre her aracın ne zaman çağrılacağına karar verir. Bir sonraki görevimiz, her aracın tools.py içinde ne yaptığını uygulamaktır.

5. Aracı oluşturma araçları: Keşif ve Göz Atma

Şimdi temsilcinin filmlere göz atmak ve filmleri keşfetmek için kullanacağı araçları uygulayalım. Her araç bir UCP işlemini kapsar.

tools.py adlı yeni bir dosya oluşturun ve aşağıdaki kodu kopyalayıp yapıştırın:

"""Agent tools — each one wraps a UCP or AP2 operation."""

import asyncio
import json

from .ucp import UCPClient
from .ap2 import AP2Handler

# Initialize clients directly
_merchant_urls = ["http://localhost:8081", "http://localhost:8082"]
_ucp = UCPClient()
_ap2 = AP2Handler()

Kurulumu Anlama

Bu codelab'in temsilci oluşturmaya odaklanmasını sağlamak için daha sonraki bir adımda inceleyeceğimiz UCPClient ve AP2Handler olmak üzere iki yardımcı sınıf kullanıyoruz.

  • Bunlar nelerdir?: Bunlar, bu codelab için sahte satıcılarla etkileşimi simüle etmek amacıyla oluşturduğumuz elle yazılmış yardımcı sınıflardır. Resmi UCP ve AP2 SDK'ları henüz kullanıma sunulmadığı için bu yardımcıları kullanarak aradaki boşluğu dolduruyoruz. Üretim ortamında, kullanıma sunulduktan sonra resmi SDK'ları kullanırsınız.
  • Şimdilik bunları yardımcı nesneler olarak değerlendirin:
    • _ucp.discover(url): Bir satıcının profilini getirir.
    • _ucp.mcp_call(url, method, params): İşletmenin MCP uç noktasına JSON-RPC 2.0 isteği gönderir.

Tiyatroları keşfetme

Bu araç, UCP akışının ilk adımıdır. Hangi satıcıların olduğunu ve hangi hizmetleri desteklediklerini bulur.

tools.py uygulamasına ekleme:

async def discover_theaters() -> str:
    """Discover available theater merchants and their capabilities via UCP."""
    theaters = []
    for url in _merchant_urls:
        info = await _ucp.discover(url)
        theaters.append(
            {
                "url": url,
                "name": info["name"],
                "capabilities": info["capabilities"],
                "payment_handlers": info["payment_handlers"],
            }
        )
    return json.dumps(theaters, indent=2)

Bu işlem ne yapar?

  1. Sunucu tarafından sağlanan satıcı URL'lerinin listesini yineler. Bu codelab'in ilerleyen bölümünde iki sahte satıcı oluşturacağız.
  2. Her URL için _ucp.discover(url) işlevi çağrılır. Bu işlev, /.well-known/ucp uç noktasına ulaşır.
  3. Adı, özellikleri ve ödeme işleyicileri özet bir listede toplar.
  4. Listeyi, temsilcinin okuması için JSON dizesi olarak döndürür.

Filmlerde Ara

Bu araç, keşfedilen tüm satıcıları arar ve sonuçları birleştirir. Aynı film, farklı formatlarda (IMAX, Dolby) ve farklı fiyatlarla birden fazla sinemada gösteriliyor olabileceğinden bu bilgi çok önemlidir.

tools.py uygulamasına ekleme:

async def search_movies(query: str = "") -> str:
    """Search for movies across all theaters. Use '' to browse all."""
    all_movies = {}
    for url, merchant in _ucp.merchants.items():
        result = await _ucp.mcp_call(url, "search_catalog", {"query": query})
        for product in result.get("products", []):
            mid = product["id"]
            if mid not in all_movies:
                all_movies[mid] = {
                    "id": mid,
                    "title": product["title"],
                    "categories": product.get("categories", []),
                    "theaters": {},
                }
            showtimes = []
            for v in product.get("variants", []):
                opts = {
                    o["name"]: o["value"]
                    for o in v.get("selected_options", [])
                }
                showtimes.append(
                    {
                        "id": v["id"],
                        "format": opts.get("format", "Standard"),
                        "time": opts.get("time", ""),
                        "price": v.get("price", {}),
                        "seats": v.get("availability", {}).get(
                            "seats_available", 0
                        ),
                    }
                )
            all_movies[mid]["theaters"][url] = {
                "name": merchant["name"],
                "showtimes": showtimes,
            }
    return json.dumps(list(all_movies.values()), indent=2)

Bu işlem ne yapar?

  1. Bulunan tüm sinemaları sırayla gösterir.
  2. Satıcının MCP uç noktasına bir arama kataloğu JSON-RPC isteği (_ucp.mcp_call(url, "search_catalog", {"query": query})) gönderir.
  3. Ardından, sonuçları ayrıştırarak filmleri ve "varyantlarını" (belirli gösterim saatlerini ve formatları temsil eder) bulmak için biraz temizlik yapar. Filmleri kimliğe göre gruplandırır. Böylece kullanıcı, yinelenen film girişleri görmez.

Film Ayrıntılarını Alma

Bu araç, belirli bir sinemadaki belirli bir filme ait tüm katalog ayrıntılarını alır.

tools.py uygulamasına ekleme:

async def get_movie_detail(movie_id: str, merchant_url: str) -> str:
    """Get detailed showtimes for a movie at a specific theater."""
    result = await _ucp.mcp_call(
        merchant_url, "lookup_catalog", {"product_id": movie_id}
    )
    return json.dumps(result, indent=2)

Bu işlem ne yapar?

  • Belirli movie_id değerini ileterek satıcının MCP uç noktasında lookup_catalog yöntemini çağırır. Bu işlem, söz konusu sinemayla ilgili gösterim saatleri ve koltuk durumu gibi ayrıntılı bilgileri döndürür.

6. Aracı araçlarını oluşturma: Ödeme

Bu araçlar, ödeme ve satın alma sürecini yönetir. Güvenli işlemler için AP2 protokolü kullanılır.

Ödeme oluşturma

Bu araç, belirli bir gösterim zamanı için belirli bir sinemada ödeme oturumu başlatır.

tools.py uygulamasına ekleme:

async def create_checkout(
    merchant_url: str, showtime_id: str, quantity: int = 1
) -> str:
    """Create a checkout session for tickets at a theater."""
    result = await _ucp.mcp_call(merchant_url, "create_checkout", {
        "checkout": {
            "line_items": [
                {"item": {"id": showtime_id}, "quantity": quantity}
            ],
            "context": {"country": "US", "currency": "USD"},
        }
    })
    return json.dumps(result, indent=2)

Bu işlem ne yapar?

  1. Satıcının MCP uç noktasında create_checkout yöntemini çağırır.
  2. Kullanıcının istediği showtime_id ve quantity değerlerini iletir.
  3. Satıcı, AP2 CartMandate içeren bir JSON nesnesi döndürür.

Not: Yanıt verilerini incelerseniz ap2.cart_mandate içinde merchant_authorization alanı bulunur. Bu, satıcının belirtilen fiyatı kilitleyen kriptografik imzasıdır. Bu ayar daha sonra değiştirilemez.

Satın Alma İşlemini Tamamlayın

Bu araçta üç şey olur:

  1. Ödeme adımından CartMandate'i alır ve doğrularız (mock).
  2. PaymentMandate (mock) oluşturun ve imzalayın.
  3. Satın alma işlemini tamamlamak için imzalı yetki belgesini satıcıya gönderin.

tools.py uygulamasına ekleme:

async def complete_purchase(
    checkout_id: str, merchant_url: str, payment_method: str = "card"
) -> str:
    """Complete purchase with AP2 payment authorization."""
    # 1. Get the CartMandate from the checkout
    checkout = await _ucp.mcp_call(
        merchant_url, "get_checkout", {"checkout": {"id": checkout_id}}
    )
    cart_mandate = _ap2.process_cart_mandate(checkout)
    if not cart_mandate:
        return {"error": "No cart mandate — checkout may have expired"}

    # 2-3. Create and sign the PaymentMandate
    # In production, this call would trigger a user prompt (biometric or device auth)
    # via the AP2 Wallet SDK. In this demo, it just computes a mock SHA-256 hash.
    payment_mandate = _ap2.create_payment_mandate(cart_mandate, payment_method)

    # 4. Send both mandates to complete the purchase
    result = await _ucp.mcp_call(merchant_url, "complete_checkout", {
        "checkout": {
            "id": checkout_id,
            "payment": {
                "instruments": [{
                    "handler_id": f"card_{merchant_url.split(':')[-1]}",
                    "type": "card",
                }],
            },
            "ap2": {"payment_mandate": payment_mandate},
        }
    })
    return json.dumps(result, indent=2)

Neden iki zorunlu kılma? CartMandate, satıcının fiyatı teklif ettikten sonra değiştirmesini engeller. PaymentMandate, aracının kullanıcının izni olmadan ödeme almasını engeller. Akış:

Merchant locks price -> User authorizes charge -> Merchant verifies both -> Order completes.

Kontrol noktası: Tam tools.py

Tam tools.py artık 5 araç işlevine ve UCP ile AP2 istemcileri için modül düzeyinde başlatmaya sahip olmalıdır. Şuna benzediğini doğrulayın:

"""Agent tools — each one wraps a UCP or AP2 operation."""

import asyncio
import json

from ucp import UCPClient
from ap2 import AP2Handler

# Initialize clients directly
_merchant_urls = ["http://localhost:8081", "http://localhost:8082"]
_ucp = UCPClient()
_ap2 = AP2Handler()


async def discover_theaters() -> str:
    """Discover available theater merchants and their capabilities via UCP."""
    theaters = []
    for url in _merchant_urls:
        info = await _ucp.discover(url)
        theaters.append(
            {
                "url": url,
                "name": info["name"],
                "capabilities": info["capabilities"],
                "payment_handlers": info["payment_handlers"],
            }
        )
    return json.dumps(theaters, indent=2)


async def search_movies(query: str = "") -> str:
    """Search for movies across all theaters. Use '' to browse all."""
    all_movies = {}
    for url, merchant in _ucp.merchants.items():
        result = await _ucp.mcp_call(url, "search_catalog", {"query": query})
        for product in result.get("products", []):
            mid = product["id"]
            if mid not in all_movies:
                all_movies[mid] = {
                    "id": mid,
                    "title": product["title"],
                    "categories": product.get("categories", []),
                    "theaters": {},
                }
            showtimes = []
            for v in product.get("variants", []):
                opts = {
                    o["name"]: o["value"]
                    for o in v.get("selected_options", [])
                }
                showtimes.append(
                    {
                        "id": v["id"],
                        "format": opts.get("format", "Standard"),
                        "time": opts.get("time", ""),
                        "price": v.get("price", {}),
                        "seats": v.get("availability", {}).get(
                            "seats_available", 0
                        ),
                    }
                )
            all_movies[mid]["theaters"][url] = {
                "name": merchant["name"],
                "showtimes": showtimes,
            }
    return json.dumps(list(all_movies.values()), indent=2)


async def get_movie_detail(movie_id: str, merchant_url: str) -> str:
    """Get detailed showtimes for a movie at a specific theater."""
    result = await _ucp.mcp_call(
        merchant_url, "lookup_catalog", {"product_id": movie_id}
    )
    return json.dumps(result, indent=2)


async def create_checkout(
    merchant_url: str, showtime_id: str, quantity: int = 1
) -> str:
    """Create a checkout session for tickets at a theater."""
    result = await _ucp.mcp_call(merchant_url, "create_checkout", {
        "checkout": {
            "line_items": [
                {"item": {"id": showtime_id}, "quantity": quantity}
            ],
            "context": {"country": "US", "currency": "USD"},
        }
    })
    return json.dumps(result, indent=2)


async def complete_purchase(
    checkout_id: str, merchant_url: str, payment_method: str = "card"
) -> str:
    """Complete purchase with AP2 payment authorization."""
    # 1. Get the CartMandate from the checkout
    checkout = await _ucp.mcp_call(
        merchant_url, "get_checkout", {"checkout": {"id": checkout_id}}
    )
    cart_mandate = _ap2.process_cart_mandate(checkout)
    if not cart_mandate:
        return {"error": "No cart mandate — checkout may have expired"}

    # 2-3. Create and sign the PaymentMandate
    # In production, this call would trigger a user prompt (biometric or device auth)
    # via the AP2 Wallet SDK. In this demo, it just computes a mock SHA-256 hash.
    payment_mandate = _ap2.create_payment_mandate(cart_mandate, payment_method)

    # 4. Send both mandates to complete the purchase
    result = await _ucp.mcp_call(merchant_url, "complete_checkout", {
        "checkout": {
            "id": checkout_id,
            "payment": {
                "instruments": [{
                    "handler_id": f"card_{merchant_url.split(':')[-1]}",
                    "type": "card",
                }],
            },
            "ap2": {"payment_mandate": payment_mandate},
        }
    })
    return json.dumps(result, indent=2)

7. Çalıştırılabilir hale getirme

Gerçek UCP satıcılarında, temsilciyi URL'lerine yönlendirmeniz yeterli olur. Bu codelab'de yerel olarak test etmek için iki şeye ihtiyacımız var:

  1. Sahte satıcılar: Test edebileceğiniz bir şey olması için UCP uç noktalarını simüle eden yerel sunucular
  2. Protokol yardımcıları: UCP ve AP2 için ince HTTP sarmalayıcıları (üretimde, resmi SDK'lar bunların yerini alır)

Not: Bu kodu dikkatlice okumanız gerekmez. Bu dosyalar, gerçek altyapı ve SDK'ların sağlayacağı özellikleri simüle eder. Bunları olduğu gibi kopyalayın.

Protokol yardımcıları

UCP ve AP2'nin henüz istemci SDK'ları yoktur. Bu iki dosya, HTTP bağlantısını yönetir.

Oluşturma ucp.py:

"""UCP client — discovers merchants and calls their MCP tools."""

import uuid
import httpx


class UCPClient:
    def __init__(self):
        self.client = httpx.AsyncClient(timeout=30)
        self.merchants = {}  # url -> merchant info dict

    async def discover(self, merchant_url: str) -> dict:
        """Fetch a merchant's UCP profile from /.well-known/ucp."""
        resp = await self.client.get(f"{merchant_url}/.well-known/ucp")
        resp.raise_for_status()
        profile = resp.json()
        ucp = profile["ucp"]
        info = {
            "name": merchant_url.split("//")[-1],
            "mcp_endpoint": ucp["services"]["dev.ucp.shopping"][0]["endpoint"],
            "capabilities": list(ucp.get("capabilities", {}).keys()),
            "payment_handlers": list(ucp.get("payment_handlers", {}).keys()),
        }
        self.merchants[merchant_url] = info
        return info

    async def mcp_call(
        self, merchant_url: str, tool_name: str, arguments: dict
    ) -> dict:
        """Call a merchant's MCP tool via JSON-RPC 2.0."""
        merchant = self.merchants[merchant_url]
        resp = await self.client.post(
            merchant["mcp_endpoint"],
            json={
                "jsonrpc": "2.0",
                "id": uuid.uuid4().hex,
                "method": "tools/call",
                "params": {"name": tool_name, "arguments": arguments},
            },
        )
        resp.raise_for_status()
        data = resp.json()
        if "error" in data:
            raise Exception(f"MCP error: {data['error']}")
        return data.get("result", {})

    async def close(self):
        await self.client.aclose()

Oluşturma ap2.py:

"""AP2 mandate handler — creates and signs payment mandates."""

import uuid
import hashlib


class AP2Handler:
    def process_cart_mandate(self, checkout_response: dict) -> dict | None:
        """Extract the merchant-signed CartMandate from a checkout response.

        The CartMandate is the merchant's cryptographic price guarantee —
        it locks the total so it can't change between checkout and payment.
        """
        return checkout_response.get("ap2", {}).get("cart_mandate")

    def create_payment_mandate(
        self, cart_mandate: dict, payment_method: str = "card"
    ) -> dict:
        """Create and sign a PaymentMandate authorizing payment.

        References the merchant's CartMandate and adds user authorization.
        Together they form a two-party agreement: merchant guarantees price,
        user authorizes charge.
        """
        contents = cart_mandate["contents"]
        mandate_id = uuid.uuid4().hex

        return {
            "mandate_id": mandate_id,
            "cart_reference": contents["id"],
            "merchant": contents["merchant_name"],
            "total": contents["total"],
            "payment_method": payment_method,
            "user_authorization": self._sign(mandate_id, contents["id"]),
        }

    def _sign(self, mandate_id: str, checkout_id: str) -> str:
        """Sign the mandate. Production uses real crypto (sd-jwt-vc)."""
        payload = f"{mandate_id}:{checkout_id}"
        return hashlib.sha256(payload.encode()).hexdigest()

Sahte satıcılar

Oluşturma merchants.py:

"""Mock UCP merchant servers — two theaters with different capabilities."""

import uuid
import time
import multiprocessing
from datetime import datetime, timezone, timedelta

import uvicorn
from fastapi import FastAPI

# ── Theater data ────────────────────────────────────────────

THEATERS = {
    8081: {
        "name": "Meridian Cinemas",
        "movies": [
            {
                "id": "opp",
                "title": "Oppenheimer",
                "categories": ["Drama", "History"],
                "showtimes": [
                    {"id": "st_opp_7pm_imax", "format": "IMAX", "time": "7:00 PM", "price": 2200, "seats": 45},
                    {"id": "st_opp_930pm", "format": "Standard", "time": "9:30 PM", "price": 1500, "seats": 80},
                ],
            },
            {
                "id": "dune3",
                "title": "Dune: Part Three",
                "categories": ["Sci-Fi", "Action"],
                "showtimes": [
                    {"id": "st_dune_8pm_imax", "format": "IMAX", "time": "8:00 PM", "price": 2200, "seats": 30},
                ],
            },
        ],
        "discounts": {},
    },
    8082: {
        "name": "StarLight Theaters",
        "movies": [
            {
                "id": "opp",
                "title": "Oppenheimer",
                "categories": ["Drama", "History"],
                "showtimes": [
                    {"id": "st_opp_6pm_atmos", "format": "Dolby Atmos", "time": "6:00 PM", "price": 1800, "seats": 60},
                ],
            },
            {
                "id": "spider",
                "title": "Spider-Verse",
                "categories": ["Animation", "Action"],
                "showtimes": [
                    {"id": "st_spider_4pm", "format": "Standard", "time": "4:00 PM", "price": 1200, "seats": 100},
                ],
            },
        ],
        "discounts": {},
    },
}


def create_app(port):
    theater = THEATERS[port]
    app = FastAPI()
    sessions = {}

    # ── UCP Discovery endpoint ──────────────────────────────
    @app.get("/.well-known/ucp")
    def discovery():
        caps = {
            "dev.ucp.shopping.catalog.search": [{"version": "2026-01-15"}],
            "dev.ucp.shopping.catalog.lookup": [{"version": "2026-01-15"}],
            "dev.ucp.shopping.checkout": [{"version": "2026-01-15"}],
            "dev.ucp.shopping.ap2_mandate": [{"version": "2026-01-15"}],
        }

        return {
            "ucp": {
                "version": "2026-01-15",
                "services": {
                    "dev.ucp.shopping": [
                        {"version": "2026-01-15", "transport": "mcp",
                         "endpoint": f"http://localhost:{port}/mcp"}
                    ]
                },
                "capabilities": caps,
                "payment_handlers": {
                    "com.example.card": [
                        {"id": f"card_{port}", "version": "2026-01-15",
                         "available_instruments": [{"type": "card"}], "config": {}}
                    ]
                },
            }
        }

    # ── MCP JSON-RPC endpoint ───────────────────────────────
    @app.post("/mcp")
    def mcp(body: dict):
        tool = body["params"]["name"]
        args = body["params"].get("arguments", {})
        rid = body.get("id", "1")

        if tool == "search_catalog":
            q = args.get("query", "").lower()
            hits = [m for m in theater["movies"]
                    if not q or q in m["title"].lower()
                    or any(q in c.lower() for c in m["categories"])]
            return _ok(rid, {"products": [_product(m) for m in hits]})

        if tool == "lookup_catalog":
            mid = args.get("product_id") or (args.get("ids", [None])[0])
            movie = next((m for m in theater["movies"] if m["id"] == mid), None)
            if not movie:
                return _err(rid, "Not found")
            return _ok(rid, {"products": [_product(movie)]})

        if tool == "create_checkout":
            co = args.get("checkout", {})
            sid = f"chk_{uuid.uuid4().hex[:12]}"
            items, subtotal = [], 0
            for li in co.get("line_items", []):
                st = _find_showtime(li["item"]["id"])
                if not st:
                    continue
                mv = _find_movie(li["item"]["id"])
                qty = li.get("quantity", 1)
                amt = st["price"] * qty
                subtotal += amt
                items.append({
                    "id": f"li_{uuid.uuid4().hex[:8]}",
                    "item": {
                        "id": st["id"],
                        "title": f"{mv['title']}{st['format']} {st['time']}",
                        "price": st["price"],
                    },
                    "quantity": qty,
                    "totals": [{"type": "subtotal", "amount": amt}],
                })
            tax = int(subtotal * 0.08)
            total = subtotal + tax
            session = {
                "id": sid,
                "status": "ready_for_complete",
                "currency": "USD",
                "line_items": items,
                "totals": [
                    {"type": "subtotal", "display_text": "Subtotal", "amount": subtotal},
                    {"type": "tax", "display_text": "Tax", "amount": tax},
                    {"type": "total", "display_text": "Total", "amount": total},
                ],
                "metadata": {"theater_name": theater["name"]},
                "ap2": {
                    "cart_mandate": {
                        "contents": {
                            "id": sid,
                            "merchant_name": theater["name"],
                            "total": {
                                "label": "Total",
                                "amount": {"currency": "USD", "value": total / 100},
                            },
                            "cart_expiry": (
                                datetime.now(timezone.utc) + timedelta(minutes=10)
                            ).isoformat(),
                        },
                        "merchant_authorization": f"mock_merchant_sig_{sid}",
                    }
                },
            }
            sessions[sid] = session
            return _ok(rid, session)

        if tool == "get_checkout":
            sid = args.get("checkout", {}).get("id") or args.get("id")
            return _ok(rid, sessions.get(sid, {"error": "not_found"}))



        if tool == "complete_checkout":
            co = args.get("checkout", {})
            sid = co.get("id")
            session = sessions.get(sid)
            if not session:
                return _err(rid, "Not found")
            session["status"] = "completed"
            session["order"] = {
                "id": f"ord_{uuid.uuid4().hex[:8]}",
                "created_at": datetime.now(timezone.utc).isoformat(),
                "tickets": [
                    {
                        "movie": li["item"]["title"],
                        "quantity": li["quantity"],
                        "ticket_code": uuid.uuid4().hex[:8].upper(),
                    }
                    for li in session["line_items"]
                ],
            }
            session["ap2"]["payment_mandate_verified"] = True
            return _ok(rid, session)

        return _err(rid, f"Unknown tool: {tool}")

    def _ok(rid, result):
        return {"jsonrpc": "2.0", "id": rid, "result": result}

    def _err(rid, msg):
        return {"jsonrpc": "2.0", "id": rid, "error": {"code": -32000, "message": msg}}

    def _product(movie):
        return {
            "id": movie["id"],
            "title": movie["title"],
            "categories": movie["categories"],
            "variants": [
                {
                    "id": st["id"],
                    "selected_options": [
                        {"name": "format", "value": st["format"]},
                        {"name": "time", "value": st["time"]},
                    ],
                    "price": {"amount": st["price"], "currency": "USD"},
                    "availability": {"available": True, "seats_available": st["seats"]},
                }
                for st in movie["showtimes"]
            ],
        }

    def _find_showtime(sid):
        return next(
            (st for m in theater["movies"] for st in m["showtimes"] if st["id"] == sid),
            None,
        )

    def _find_movie(sid):
        return next(
            (m for m in theater["movies"] for st in m["showtimes"] if st["id"] == sid),
            None,
        )

    return app


def _run(port):
    uvicorn.run(create_app(port), host="0.0.0.0", port=port, log_level="warning")


if __name__ == "__main__":
    for port in THEATERS:
        multiprocessing.Process(target=_run, args=(port,), daemon=True).start()
    print("Merchants running: Meridian (:8081), StarLight (:8082)")
    print("Press Ctrl+C to stop")
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        pass

Bu işlem, her biri iki uç nokta içeren iki FastAPI sunucusu oluşturur:

  • GET /.well-known/ucp: UCP keşfi. Satıcının özelliklerini, MCP uç nokta URL'sini ve kabul edilen ödeme yöntemlerini döndürür.
  • POST /mcp: MCP (Model Context Protocol) işlemleri. Katalog arama, ödeme, indirimler ve ödeme için JSON-RPC 2.0 çağrılarını işler.

Satıcıları yeni bir terminalde başlatın. Satıcıların çalışmaya devam etmesi gerekir:

cd agent_payments
source .venv/bin/activate
python merchants.py

Aşağıdaki bilgileri görürsünüz:

Merchants running: Meridian (:8081), StarLight (:8082)

İlk terminalinize dönerek UCP keşfini doğrulayın:

curl -s http://localhost:8081/.well-known/ucp | python -m json.tool

Satıcının özellikleri, MCP uç nokta URL'si ve ödeme işleyicileri gösterilir.

8. ADK Web ile temsilciyi çalıştırma

ADK CLI'nın yerleşik Web kullanıcı arayüzünü kullanalım. Bu, tarayıcıda bir sohbet arayüzü sağlar ve araç onay istemlerini otomatik olarak işler.

Projeniz artık aşağıdaki gibi görünecektir:

agent_payments/
├── merchants.py      # Mock UCP merchants
├── ucp.py            # UCP client helper
├── ap2.py            # AP2 mandate handler
├── tools.py          # Agent tools
├── agent.py          # Agent definition
└── pyproject.toml

Dene

Mevcut terminalde, üst dizine (agent_payments'nın bir klasör üstü) gidin ve ADK Web kullanıcı arayüzünü başlatın:

cd ../
adk web --allow_origins '*'

Sunucunun çalıştığını belirten bir çıkış görmeniz gerekir. Bu çıkışta bir URL (genellikle http://localhost:8000 veya benzeri) verilir.

Temsilciyle konuşma

  1. adk web tarafından sağlanan URL'yi tarayıcınızda açın.
  2. Bir sohbet arayüzü görürsünüz.
  3. "Hangi filmler oynuyor?" diye sormayı deneyin.
  4. Aracı, perde arkasında sinemaları keşfeder ve kataloglarda arama yapar. Sonuçları, UCP aracılığıyla her iki satıcıdan da toplar.
  5. Bilet rezervasyonu yapmasını isteyin: "Oppenheimer için saat 19:00'da 2 bilet ayırt".
  6. Temsilci complete_purchase numarayı aramaya çalıştığında ADK Web kullanıcı arayüzünün nasıl bir onay iletişim kutusu veya kart gösterdiğine dikkat edin.
  7. İşlemi yetkilendirmek için sohbette şu JSON dizesiyle yanıt verin: {"confirmed": true}.
  8. Temsilci, satın alma işlemini tamamlayıp bilet kodlarıyla birlikte sipariş onayınızı size geri gönderir.

Kamera arkasında yaşananlar:

  1. create_checkout → Satıcı, AP2 CartMandate (imzalı fiyat kilidi) döndürdü.
  2. complete_purchasePaymentMandate oluşturdu, imzaladı (sahte SHA-256) ve her iki yetkiyi de satıcıya gönderdi.
  3. Satıcı her iki imzayı da doğruladı → biletler düzenlendi (mock'ta)

9. Temizleme

Yerel sunucuların çalışır durumda kalmasını önlemek için kaynakları temizleyin:

  1. adk web komutunun çalıştırıldığı terminalde, aracı sunucusunu durdurmak için Ctrl+C tuşlarına basın.
  2. python merchants.py komutunun çalıştırıldığı terminalde, sahte satıcıları durdurmak için Ctrl+C tuşlarına basın.
  3. Aşağıdaki komutu çalıştırarak sanal ortamı her iki terminalde de devre dışı bırakın:
deactivate
  1. (İsteğe bağlı) Bu codelab için yeni bir Google Cloud projesi oluşturduysanız ve bu projeyi silmek istiyorsanız aşağıdaki komutu çalıştırın:
gcloud projects delete $GOOGLE_CLOUD_PROJECT

10. Tebrikler! 🎉

UCP ve AP2 kullanarak satıcıları keşfeden, kataloglara göz atan ve satın alma işlemlerini tamamlayan bir ADK aracısı oluşturdunuz.

Öğrendikleriniz

Bu codelab'de, güvenli ticaret akışlarını işleyen bir ADK aracısı oluşturmuştunuz. Oluşturduğunuz öğeler ve uyguladığınız temel kavramların özeti aşağıda verilmiştir:

Oluşturduğunuz içerik:

  • UCP ve AP2 işlemlerini (keşif, katalog araması, ödeme, ödeme) kapsayan 5 aracı.
  • AP2 talimat imzalama: CartMandate (satıcı fiyatı kilidi) + PaymentMandate (kullanıcı yetkilendirmesi).
  • Çok satıcılı arama: Tek bir temsilci birden fazla tiyatroya sorgu gönderir ve sonuçları birleştirir.

Temel Kavramlar:

Protokol

Ne işe yarar?

İşleyiş şekli

UCP Discovery

Ajan, satıcıları ve özelliklerini bulur.

/.well-known/ucp → özellikler, MCP uç noktası, ödeme yöntemleri

UCP MCP

Ajan, kataloglara göz atar ve ödeme işlemleri oluşturur.

Satıcının MCP uç noktasına yapılan JSON-RPC 2.0 çağrıları

AP2 CartMandate

Satıcı, belirtilen fiyatı sabitler

Satıcı tarafından imzalanır, toplam tutar ve son kullanma tarihi içerir.

AP2 PaymentMandate

Kullanıcı ödemeyi yetkilendirir

Kullanıcı tarafından imzalanmış, CartMandate'i referans alıyor

Üretimde farklı olan nedir?

Bu codelab'de sahte veriler kullanılır. Üretimde:

  • UCP keşfi, sabit kodlanmış localhost URL'leri yerine bir kayıt defterine göre çözümlenir.
  • MCP uç noktaları gerçek satıcılar tarafından barındırılır. Aynı JSON-RPC 2.0 protokolü, gerçek envanter
  • AP2 zorunlulukları, SHA-256 karma değerleriyle değil, sd-jwt-vc ile imzalanır.
  • Ödeme yetkilendirme, kullanıcı izni istemleri içeren bir AP2 Cüzdan SDK'sı kullanır.
  • Ön uç, araç sonuçlarını zengin kullanıcı arayüzü (ürün ızgaraları, ödeme özetleri, onay kartları) olarak oluşturur.

Sonraki adımlar

Referans belgeleri