All articles

Retail AI9 min read

Building a Multi-Agent Retail Merchandiser with Google ADK and Vertex AI

How we orchestrated parallel vector search, re-ranking, and Feature Store enrichment into an AI system that makes merchandising decisions at catalog scale

  • Google ADK
  • Vertex AI
  • Ranking API
  • Feature Store
  • FastAPI
  • Cloud Run

A senior merchandiser at a large retailer holds years of category expertise: they know which products are substitutable, how to rank search results by margin and inventory, and how to respond to stock-out events. Doing this manually at catalog scale — thousands of SKUs, dozens of category updates per day — is impossible. This agent automates the decision loop.

Agent Architecture

Merchandiser Query (natural language or catalog event)
  ↓
Planner Agent (Gemini 2.5 Flash)
  ├── Interprets intent: "find substitutes for SKU-4421 (out of stock)"
  └── Generates execution plan
         ↓
Parallel Sub-Agents (ADK multi-agent routing)
  ├── Vector Search Agent
  │     └── Vertex AI ANN Index → top-150 candidates
  ├── Re-Ranker Agent
  │     └── Vertex AI Ranking API → ranking_score per candidate
  └── Substitution Agent
        └── Similarity scoring + price band matching
         ↓
Feature Store Enrichment (real-time, <2ms)
  ├── inventory_status (in_stock | low | out_of_stock)
  ├── current_price, margin_tier
  └── click_through_rate (last 7 days)
         ↓
Gemini Strategy Layer
  ├── Ranks final candidates by combined score
  ├── Generates merchandising narrative ("recommend SKU-7822 as primary substitute")
  └── Outputs structured decision for catalog system

ADK Multi-Agent Routing

Google ADK's multi-agent framework handles the parallel execution. Each sub-agent is a separate ADK agent with its own tools and system prompt. The root planner decides which agents to invoke and in what order:

python
1from google.adk.agents import Agent, LlmAgent
2from google.adk.tools import FunctionTool
3
4# Sub-agent: vector search specialist
5vector_search_agent = LlmAgent(
6    name="vector_search_agent",
7    model="gemini-2.5-flash",
8    instruction="""You are a product similarity specialist.
9    Given a product query, find the 150 most semantically similar
10    products using the Bringo vector index.""",
11    tools=[
12        FunctionTool(search_product_index),
13        FunctionTool(get_product_metadata),
14    ],
15)
16
17# Sub-agent: re-ranking specialist
18reranker_agent = LlmAgent(
19    name="reranker_agent",
20    model="gemini-2.5-flash",
21    instruction="""You are a search relevance specialist.
22    Re-rank the provided candidates using the Vertex AI Ranking API,
23    integrating real-time Feature Store signals.""",
24    tools=[
25        FunctionTool(rerank_with_ranking_api),
26        FunctionTool(get_feature_store_signals),
27    ],
28)
29
30# Root planner orchestrates everything
31planner = LlmAgent(
32    name="senior_merchandiser",
33    model="gemini-2.5-flash",
34    instruction=MERCHANDISER_SYSTEM_PROMPT,
35    sub_agents=[vector_search_agent, reranker_agent, substitution_agent],
36)

Feature Store Real-Time Lookup

The Feature Store provides real-time signals that change by the hour: inventory levels, price changes, click-through rates. These signals update the ranking so a product that just went out of stock drops in priority automatically — no manual intervention.

python
1from google.cloud.aiplatform_v1beta1 import FeaturestoreOnlineServingServiceClient
2
3serving_client = FeaturestoreOnlineServingServiceClient(
4    client_options={"api_endpoint": f"{REGION}-aiplatform.googleapis.com"}
5)
6
7def get_feature_store_signals(product_ids: list[str]) -> dict:
8    """Batch real-time lookup — <2ms for up to 100 products."""
9    request = {
10        "entity_type": ENTITY_TYPE_PATH,
11        "entity_ids": product_ids,
12        "feature_selector": {
13            "id_matcher": {"ids": ["inventory_status", "current_price", "ctr_7d"]}
14        },
15    }
16    response = serving_client.read_feature_values_batch(request)
17
18    signals = {}
19    for entity_view in response.entity_view:
20        signals[entity_view.entity_id] = {
21            feat.feature_id: feat.value.string_value or feat.value.double_value
22            for feat in entity_view.data
23        }
24    return signals

Substitution Engine

When a product goes out of stock, the substitution agent finds replacements by combining embedding similarity with business rules: the substitute must be in the same price band (within 15%), same category, and available in stock. The ranking_score from the Ranking API serves as the primary sort key.

python
1def find_substitutes(
2    oos_product: dict,
3    candidates: list[dict],
4    feature_signals: dict,
5) -> list[dict]:
6    """Score and rank substitution candidates."""
7    scored = []
8    for c in candidates:
9        if c["product_id"] == oos_product["product_id"]:
10            continue  # Skip the OOS product itself
11
12        signals = feature_signals.get(c["product_id"], {})
13
14        # Exclude OOS candidates
15        if signals.get("inventory_status") == "out_of_stock":
16            continue
17
18        # Price band filter: within 15% of original price
19        price_ratio = c["price"] / oos_product["price"]
20        if not (0.85 <= price_ratio <= 1.15):
21            continue
22
23        # Combined score: ranking API score + inventory bonus
24        inventory_bonus = 0.15 if signals.get("inventory_status") == "in_stock" else 0
25        combined_score = c["ranking_score"] + inventory_bonus
26
27        scored.append({**c, "substitution_score": combined_score})
28
29    return sorted(scored, key=lambda x: x["substitution_score"], reverse=True)[:10]

Business Impact

Search precision improvement
+15–25%
Feature Store real-time lookup
<2ms
Manual catalog curation decisions
0
Stock-out events trigger auto-substitution
100%

Request a discovery call

30 minutesGoogle MeetFree