Skip to content

Conversation

@AnkitaParakh
Copy link

@AnkitaParakh AnkitaParakh commented Sep 22, 2025

Description

Thank you for opening a Pull Request!
Before submitting your PR, there are a few things you can do to make sure it goes smoothly:

Fixes #<issue_number_goes_here> 🦕

…n, negotiation, and AP2 checkout

- Add WhatsApp Business API integration for customer chat
- Implement AI-powered product curation engine with Google AI
- Add dynamic pricing and negotiation capabilities
- Enhance checkout optimizer with payment, currency conversion, and settlement
- Add real-time analytics and performance monitoring
- Create comprehensive deployment infrastructure (Docker, K8s, CI/CD)
- Add multi-cloud deployment support (AWS, GCP, Azure)
- Implement repository setup and sync automation scripts
- Add complete documentation and setup guides
- Support both development and production environments

This extends the AP2 protocol with intelligent shopping assistance while maintaining full compatibility and sync with upstream changes.
@AnkitaParakh AnkitaParakh requested a review from a team as a code owner September 22, 2025 10:56
@google-cla
Copy link

google-cla bot commented Sep 22, 2025

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @AnkitaParakh, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request establishes the foundational development for an AI Shopping Concierge, transforming the AP2 protocol into a complete, intelligent e-commerce platform. The changes focus on building out core AI functionalities for personalized shopping experiences, enabling multi-channel customer engagement, and providing extensive tooling for seamless deployment and operational management across various cloud environments.

Highlights

  • New AI Shopping Concierge Project: Introduction of a full-fledged AI-powered e-commerce solution built on the AP2 protocol, designed to provide intelligent shopping assistance.
  • Core AI Capabilities: Implementation of intelligent product curation, dynamic negotiation, and advanced analytics engines to personalize the shopping experience.
  • Multi-Channel Communication: Integration with the WhatsApp Business API and a web chat interface for unified and seamless customer interaction across various platforms.
  • Optimized Checkout Flow: Enhanced checkout optimization features including automatic currency conversion, support for multiple payment processors (AP2, Stripe, PayPal), and sophisticated cart abandonment recovery strategies.
  • Comprehensive Deployment & Operations: Provision of detailed setup and deployment guides, Docker configurations (for both development and production), Kubernetes manifests, and cloud deployment scripts (AWS, GCP, Azure) to ensure robust and scalable infrastructure.
  • Automated Development Workflow: Inclusion of extensive automation scripts for repository setup, efficient syncing with upstream AP2 changes, and routine system maintenance tasks.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive AI Shopping Concierge application, including extensive documentation, deployment configurations for Docker and Kubernetes, automation scripts, and several advanced Python modules for curation, negotiation, analytics, and checkout optimization. The overall structure and implementation are very impressive and follow best practices like multi-stage Docker builds, non-root users, health checks, and a modular architecture.

However, there are several areas for improvement. There is some redundancy and inconsistency in the documentation and scripts. A critical issue is a missing import in the negotiation engine that will cause a runtime error. There are also some security concerns in the Kubernetes and Docker Compose production configurations, such as hardcoded credentials and excessive container privileges. I've provided specific comments and suggestions to address these points.

Comment on lines +28 to +42
DB_PASSWORD: cG9zdGdyZXM= # postgres
REDIS_PASSWORD: cmVkaXM= # redis
GOOGLE_AI_API_KEY: ""
WHATSAPP_ACCESS_TOKEN: ""
WHATSAPP_VERIFY_TOKEN: ""
WHATSAPP_PHONE_NUMBER_ID: ""
AP2_MERCHANT_ID: ""
AP2_API_KEY: ""
STRIPE_API_KEY: ""
PAYPAL_CLIENT_ID: ""
PAYPAL_CLIENT_SECRET: ""
SECRET_KEY: ""
SENTRY_DSN: ""
GRAFANA_ADMIN_USER: YWRtaW4= # admin
GRAFANA_ADMIN_PASSWORD: YWRtaW4= # admin
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Hardcoding secrets, even if they are base64-encoded default values, is a significant security risk in a production manifest. These values can be easily decoded. It is strongly recommended to use a proper secret management solution. You should inject these secrets at runtime using Kubernetes Secrets populated from a secure pipeline, or a dedicated secret management tool like HashiCorp Vault, AWS Secrets Manager, or Google Secret Manager.

Comment on lines +238 to +252
fail2ban:
image: lscr.io/linuxserver/fail2ban:latest
environment:
PUID: 1000
PGID: 1000
TZ: UTC
volumes:
- fail2ban_data:/config
- ./logs:/logs:ro
- ./deployment/fail2ban:/config/fail2ban:ro
cap_add:
- NET_ADMIN
- NET_RAW
network_mode: host
restart: unless-stopped
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The fail2ban service is configured with network_mode: host and adds NET_ADMIN and NET_RAW capabilities. This configuration breaks container isolation and grants the container elevated privileges on the host's network stack, which poses a significant security risk. Please review this configuration carefully. If fail2ban is essential, ensure it is properly firewalled and monitored. Consider alternative security approaches that do not require such high privileges.

Comment on lines +1 to +430
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""WhatsApp Business API integration for the AI Shopping Agent.

This module provides a seamless integration between WhatsApp Business API
and the AP2 shopping agent, enabling customers to shop through WhatsApp chat.
"""

import asyncio
import json
import logging
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional

import aiohttp
from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel, Field

from roles.shopping_agent.agent import root_agent
from common.system_utils import get_env_var

logger = logging.getLogger(__name__)


class WhatsAppMessage(BaseModel):
"""WhatsApp message structure."""

from_number: str = Field(..., alias="from")
to_number: str = Field(..., alias="to")
message_type: str = Field(..., alias="type")
text: Optional[str] = None
media_url: Optional[str] = None
media_type: Optional[str] = None
timestamp: datetime
message_id: str


class WhatsAppContact(BaseModel):
"""WhatsApp contact information."""

phone_number: str
name: Optional[str] = None
profile_name: Optional[str] = None


class WhatsAppWebhookEvent(BaseModel):
"""WhatsApp webhook event structure."""

entry: List[Dict[str, Any]]
object: str


class CustomerSession:
"""Manages individual customer shopping sessions."""

def __init__(self, phone_number: str):
self.phone_number = phone_number
self.session_id = f"whatsapp_{phone_number}_{int(datetime.now().timestamp())}"
self.conversation_history: List[Dict[str, Any]] = []
self.shopping_context: Dict[str, Any] = {}
self.last_activity = datetime.now(timezone.utc)
self.cart_items: List[Dict[str, Any]] = []
self.customer_preferences: Dict[str, Any] = {}

def add_message(self, message: str, sender: str):
"""Add a message to conversation history."""
self.conversation_history.append({
"timestamp": datetime.now(timezone.utc).isoformat(),
"sender": sender,
"message": message
})
self.last_activity = datetime.now(timezone.utc)


class WhatsAppShoppingAgent:
"""Main WhatsApp shopping agent integration."""

def __init__(self):
self.whatsapp_token = get_env_var("WHATSAPP_BUSINESS_TOKEN")
self.whatsapp_phone_id = get_env_var("WHATSAPP_PHONE_NUMBER_ID")
self.webhook_verify_token = get_env_var("WHATSAPP_WEBHOOK_VERIFY_TOKEN")
self.base_url = f"https://graph.facebook.com/v18.0/{self.whatsapp_phone_id}"

# Session management
self.active_sessions: Dict[str, CustomerSession] = {}
self.session_timeout = 3600 # 1 hour timeout

# Agent integration
self.shopping_agent = root_agent

async def send_whatsapp_message(
self,
to_number: str,
message: str,
message_type: str = "text"
) -> bool:
"""Send a message via WhatsApp Business API."""

headers = {
"Authorization": f"Bearer {self.whatsapp_token}",
"Content-Type": "application/json"
}

payload = {
"messaging_product": "whatsapp",
"to": to_number,
"type": message_type,
"text": {"body": message}
}

try:
async with aiohttp.ClientSession() as session:
async with session.post(
f"{self.base_url}/messages",
headers=headers,
json=payload
) as response:
if response.status == 200:
logger.info(f"Message sent successfully to {to_number}")
return True
else:
logger.error(f"Failed to send message: {await response.text()}")
return False
except Exception as e:
logger.error(f"Error sending WhatsApp message: {e}")
return False

async def send_interactive_message(
self,
to_number: str,
header: str,
body: str,
buttons: List[Dict[str, str]]
) -> bool:
"""Send an interactive message with buttons."""

headers = {
"Authorization": f"Bearer {self.whatsapp_token}",
"Content-Type": "application/json"
}

interactive_buttons = []
for i, button in enumerate(buttons):
interactive_buttons.append({
"type": "reply",
"reply": {
"id": f"btn_{i}",
"title": button["title"]
}
})

payload = {
"messaging_product": "whatsapp",
"to": to_number,
"type": "interactive",
"interactive": {
"type": "button",
"header": {"type": "text", "text": header},
"body": {"text": body},
"action": {"buttons": interactive_buttons}
}
}

try:
async with aiohttp.ClientSession() as session:
async with session.post(
f"{self.base_url}/messages",
headers=headers,
json=payload
) as response:
return response.status == 200
except Exception as e:
logger.error(f"Error sending interactive message: {e}")
return False

def get_or_create_session(self, phone_number: str) -> CustomerSession:
"""Get existing session or create new one."""

# Clean up expired sessions
self._cleanup_expired_sessions()

if phone_number not in self.active_sessions:
self.active_sessions[phone_number] = CustomerSession(phone_number)
logger.info(f"Created new session for {phone_number}")

return self.active_sessions[phone_number]

def _cleanup_expired_sessions(self):
"""Remove expired sessions."""
current_time = datetime.now(timezone.utc)
expired_sessions = []

for phone_number, session in self.active_sessions.items():
time_diff = (current_time - session.last_activity).total_seconds()
if time_diff > self.session_timeout:
expired_sessions.append(phone_number)

for phone_number in expired_sessions:
del self.active_sessions[phone_number]
logger.info(f"Expired session for {phone_number}")

async def process_incoming_message(self, webhook_data: Dict[str, Any]) -> bool:
"""Process incoming WhatsApp message."""

try:
for entry in webhook_data.get("entry", []):
for change in entry.get("changes", []):
if change.get("field") == "messages":
messages = change.get("value", {}).get("messages", [])

for message in messages:
await self._handle_message(message)

return True

except Exception as e:
logger.error(f"Error processing webhook data: {e}")
return False

async def _handle_message(self, message: Dict[str, Any]):
"""Handle individual WhatsApp message."""

phone_number = message.get("from")
message_type = message.get("type")
timestamp = datetime.fromtimestamp(int(message.get("timestamp")), timezone.utc)

if not phone_number:
logger.error("No phone number in message")
return

# Get or create customer session
session = self.get_or_create_session(phone_number)

# Extract message content
message_text = ""
if message_type == "text":
message_text = message.get("text", {}).get("body", "")
elif message_type == "interactive":
# Handle button responses
button_reply = message.get("interactive", {}).get("button_reply", {})
message_text = button_reply.get("title", "")

if not message_text:
await self.send_whatsapp_message(
phone_number,
"Sorry, I can only handle text messages right now. How can I help you shop today?"
)
return

# Add to conversation history
session.add_message(message_text, "customer")

# Process with shopping agent
agent_response = await self._get_agent_response(session, message_text)

# Send response back to customer
if agent_response:
await self.send_whatsapp_message(phone_number, agent_response)
session.add_message(agent_response, "agent")

async def _get_agent_response(self, session: CustomerSession, message: str) -> str:
"""Get response from the shopping agent."""

try:
# Prepare context for the agent
context = {
"customer_phone": session.phone_number,
"session_id": session.session_id,
"conversation_history": session.conversation_history[-5:], # Last 5 messages
"shopping_context": session.shopping_context,
"channel": "whatsapp"
}

# TODO: Integrate with actual shopping agent
# For now, return a simple response

# Check for common shopping intents
message_lower = message.lower()

if any(word in message_lower for word in ["hi", "hello", "hey", "start"]):
return ("👋 Hi! I'm your AI shopping assistant. I can help you find products, "
"compare prices, create bundles, and complete your purchase right here in WhatsApp!\n\n"
"What are you looking to buy today? For example:\n"
"• 'I need a new phone'\n"
"• 'Show me winter jackets'\n"
"• 'Find me a laptop under $1000'")

elif any(word in message_lower for word in ["buy", "shop", "looking for", "need", "want"]):
# Extract product intent
return (f"Great! I'll help you find '{message}'. "
f"Let me search our catalog for the best options...\n\n"
f"🔍 *Searching for products...*\n\n"
f"I found several great options! Would you like me to:\n"
f"1️⃣ Show you the top 3 recommendations\n"
f"2️⃣ Filter by price range\n"
f"3️⃣ Show bundle deals\n\n"
f"Just reply with 1, 2, or 3!")

elif message_lower in ["1", "2", "3"]:
if message_lower == "1":
return self._generate_product_recommendations()
elif message_lower == "2":
return "💰 What's your budget range?\n\n• Under $50\n• $50-$200\n• $200-$500\n• Over $500\n\nJust tell me your range!"
elif message_lower == "3":
return self._generate_bundle_offers()

else:
return ("I understand you're interested in shopping! Let me help you find what you need. "
"Could you tell me more specifically what you're looking for?")

except Exception as e:
logger.error(f"Error getting agent response: {e}")
return "Sorry, I encountered an error. Please try again!"

def _generate_product_recommendations(self) -> str:
"""Generate mock product recommendations with negotiation options."""
return (
"🛍️ *Top 3 Recommendations:*\n\n"

"1️⃣ **Premium Wireless Headphones**\n"
"💰 $199.99 ~~$249.99~~\n"
"⭐ 4.8/5 stars | Free shipping\n"
"🎵 Noise cancelling, 30hr battery\n\n"

"2️⃣ **Smart Fitness Watch**\n"
"💰 $299.99\n"
"⭐ 4.6/5 stars | 2-day delivery\n"
"❤️ Heart rate, GPS, waterproof\n\n"

"3️⃣ **Bluetooth Speaker Bundle**\n"
"💰 $89.99 for 2 speakers!\n"
"⭐ 4.7/5 stars | Limited time offer\n"
"🔊 360° sound, 20hr battery each\n\n"

"💬 Reply with the number to select, or:\n"
"💸 'Negotiate 1' to discuss pricing\n"
"📦 'Bundle deal' for combo offers\n"
"🔄 'More options' to see alternatives"
)

def _generate_bundle_offers(self) -> str:
"""Generate bundle offers with negotiation."""
return (
"🎁 *Special Bundle Deals:*\n\n"

"📱 **Tech Bundle** - Save $100!\n"
"• Wireless Headphones\n"
"• Phone Case\n"
"• Wireless Charger\n"
"💰 $179.99 (was $279.99)\n\n"

"🏃 **Fitness Bundle** - Save $75!\n"
"• Fitness Watch\n"
"• Bluetooth Earbuds\n"
"• Gym Bag\n"
"💰 $324.99 (was $399.99)\n\n"

"🏠 **Home Audio Bundle** - Save $50!\n"
"• 2x Bluetooth Speakers\n"
"• Smart Display\n"
"• Streaming Device\n"
"💰 $249.99 (was $299.99)\n\n"

"💬 Interested? Reply:\n"
"• Bundle name to select\n"
"• 'Custom bundle' to create your own\n"
"• 'Negotiate' + bundle name to discuss pricing"
)


# FastAPI app for webhook handling
app = FastAPI(title="WhatsApp Shopping Agent", version="1.0.0")
whatsapp_agent = WhatsAppShoppingAgent()


@app.get("/webhook")
async def verify_webhook(request: Request):
"""Verify WhatsApp webhook."""
hub_mode = request.query_params.get("hub.mode")
hub_token = request.query_params.get("hub.verify_token")
hub_challenge = request.query_params.get("hub.challenge")

if hub_mode == "subscribe" and hub_token == whatsapp_agent.webhook_verify_token:
logger.info("Webhook verified successfully")
return int(hub_challenge)
else:
logger.error("Webhook verification failed")
raise HTTPException(status_code=403, detail="Forbidden")


@app.post("/webhook")
async def handle_webhook(request: Request):
"""Handle incoming WhatsApp messages."""
try:
body = await request.json()
logger.info(f"Received webhook: {json.dumps(body, indent=2)}")

success = await whatsapp_agent.process_incoming_message(body)

if success:
return {"status": "ok"}
else:
raise HTTPException(status_code=500, detail="Processing failed")

except Exception as e:
logger.error(f"Webhook handling error: {e}")
raise HTTPException(status_code=500, detail="Internal server error")


@app.get("/health")
async def health_check():
"""Health check endpoint."""
return {"status": "healthy", "service": "WhatsApp Shopping Agent"}


if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000) No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This file defines a WhatsAppShoppingAgent and a complete FastAPI application. This seems to duplicate logic found in unified_chat_manager.py, which also appears to be the main entry point. Having two separate FastAPI apps handling WhatsApp webhooks is redundant and confusing.

It's recommended to consolidate the WhatsApp-specific logic into the WhatsAppAdapter within unified_chat_manager.py and make unified_chat_manager.py the single entry point for all chat channels. This will create a cleaner, more maintainable architecture.

Comment on lines +79 to +81
RUN if [ -d ".git" ]; then \
git submodule update --init --recursive; \
fi
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Running git submodule update inside the Docker build is not a best practice for production images. It requires the .git directory to be part of the build context, which increases the image size and couples the build process to Git. A better approach is to handle submodule checkout in your CI/CD pipeline before starting the Docker build. This keeps the image clean and focused on the application code.

createdb ai_shopping_agent

# Run migrations (if using Django/SQLAlchemy)
python manage.py migrate
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

This guide mentions python manage.py migrate, which is a Django command. However, other documentation (COMPLETE_SETUP_GUIDE.md) and the project structure suggest that alembic is used for database migrations with SQLAlchemy. To ensure consistency and avoid confusion, please update this to use the correct alembic command.

Suggested change
python manage.py migrate
python -m alembic upgrade head

Comment on lines +255 to +284
postgres_data:
driver: local
driver_opts:
type: none
o: bind
device: /data/postgres

redis_data:
driver: local
driver_opts:
type: none
o: bind
device: /data/redis

prometheus_data:
driver: local
driver_opts:
type: none
o: bind
device: /data/prometheus

grafana_data:
driver: local
driver_opts:
type: none
o: bind
device: /data/grafana

fail2ban_data:
driver: local
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

Using the local driver with bind mounts to host paths (e.g., device: /data/postgres) for persistent data in production makes the setup less portable and dependent on the host's directory structure. It's generally better to use Docker-managed named volumes. This allows Docker to manage the storage location, which simplifies management and improves portability across different environments.

@@ -0,0 +1,1042 @@
# AI Shopping Concierge - Deployment Guide
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

There appear to be multiple deployment guides in this pull request (DEPLOYMENT_GUIDE.md, COMPLETE_SETUP_GUIDE.md, and this file). This creates redundancy and can lead to confusion and maintenance overhead. It would be best to consolidate them into a single, comprehensive guide in a clear location.

kubectl apply -f namespace.yaml
kubectl apply -f configmap.yaml
kubectl apply -f postgres.yaml
kubectl apply -f redis.yaml
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

The command kubectl apply -f redis.yaml is mentioned, but the guide does not provide the contents for a redis.yaml file, unlike the other Kubernetes manifests. This omission will cause the setup to fail. Please include the manifest for Redis.

AnkitaParakh and others added 3 commits September 22, 2025 16:41
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
@yanheChen
Copy link
Member

Thanks for submitting this PR.

I've noticed this change is not using the core Agent Payments Protocol (AP2) objects, such as the IntentMandate or CartMandate Verifiable Credentials (VCs). AP2 relies on these cryptographically signed mandates for essential security and authorization.

Could you please clarify the intent or necessity of this PR, and explain how it contributes to the protocol without incorporating these required AP2 objects?

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants