By Sagar Shankaran, Founder of CallSphere
Design and implement an AI recommendation agent that combines user preference modeling, inventory-aware filtering, and LLM-powered explanation generation for personalized product suggestions.
Key takeaways
Traditional recommendation systems rely on collaborative filtering ("users who bought X also bought Y") or content-based filtering (matching item attributes to user profiles). These approaches work at scale but produce opaque suggestions that users cannot interrogate. An agentic recommendation system adds a conversational layer: the agent asks clarifying questions, explains why it recommends each product, and adapts in real time as the user provides feedback.
The agent needs a structured representation of user preferences that it can update during the conversation. We store both explicit preferences (stated by the user) and implicit signals (derived from browsing behavior).
flowchart LR
CALLER(["Shopper"])
subgraph TEL["Telephony"]
SIP["Twilio SIP and PSTN"]
end
subgraph BRAIN["E-commerce AI Agent"]
STT["Streaming STT<br/>Deepgram or Whisper"]
NLU{"Intent and<br/>Entity Extraction"}
TOOLS["Tool Calls"]
TTS["Streaming TTS<br/>ElevenLabs or Rime"]
end
subgraph DATA["Live Data Plane"]
CRM[("CRM and Notes")]
CAL[("Calendar and<br/>Schedule")]
KB[("Knowledge Base<br/>and Policies")]
end
subgraph OUT["Outcomes"]
O1(["Order status answered"])
O2(["Return RMA created"])
O3(["Specialist handoff"])
end
CALLER --> SIP --> STT --> NLU
NLU -->|Lookup| TOOLS
TOOLS <--> CRM
TOOLS <--> CAL
TOOLS <--> KB
NLU --> TTS --> SIP --> CALLER
NLU -->|Resolved| O1
NLU -->|Schedule| O2
NLU -->|Escalate| O3
style CALLER fill:#f1f5f9,stroke:#64748b,color:#0f172a
style NLU fill:#4f46e5,stroke:#4338ca,color:#fff
style O1 fill:#059669,stroke:#047857,color:#fff
style O2 fill:#0ea5e9,stroke:#0369a1,color:#fff
style O3 fill:#f59e0b,stroke:#d97706,color:#1f2937
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class UserPreferences:
user_id: str
budget_min: Optional[float] = None
budget_max: Optional[float] = None
preferred_categories: list[str] = field(default_factory=list)
preferred_brands: list[str] = field(default_factory=list)
excluded_brands: list[str] = field(default_factory=list)
use_case: Optional[str] = None
priority: str = "balanced" # "price", "quality", "balanced"
viewed_products: list[str] = field(default_factory=list)
purchased_products: list[str] = field(default_factory=list)
def update_from_message(self, parsed: dict):
if "budget_max" in parsed:
self.budget_max = parsed["budget_max"]
if "budget_min" in parsed:
self.budget_min = parsed["budget_min"]
if "categories" in parsed:
self.preferred_categories.extend(parsed["categories"])
if "use_case" in parsed:
self.use_case = parsed["use_case"]
if "priority" in parsed:
self.priority = parsed["priority"]
Recommendations are useless if the product is out of stock. The search layer queries your product catalog and filters by availability, price range, and preference match.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
from dataclasses import dataclass
import asyncpg
@dataclass
class Product:
id: str
name: str
category: str
brand: str
price: float
rating: float
stock_count: int
description: str
features: list[str]
async def search_products(
pool: asyncpg.Pool,
prefs: UserPreferences,
limit: int = 10,
) -> list[Product]:
conditions = ["p.stock_count > 0"]
params = []
idx = 1
if prefs.budget_max:
conditions.append(f"p.price <= ${idx}")
params.append(prefs.budget_max)
idx += 1
if prefs.budget_min:
conditions.append(f"p.price >= ${idx}")
params.append(prefs.budget_min)
idx += 1
if prefs.preferred_categories:
conditions.append(f"p.category = ANY(${idx})")
params.append(prefs.preferred_categories)
idx += 1
if prefs.excluded_brands:
conditions.append(f"p.brand != ALL(${idx})")
params.append(prefs.excluded_brands)
idx += 1
where_clause = " AND ".join(conditions)
order = "p.rating DESC" if prefs.priority == "quality" else "p.price ASC"
query = f"""
SELECT id, name, category, brand, price, rating,
stock_count, description, features
FROM products p
WHERE {where_clause}
ORDER BY {order}
LIMIT {limit}
"""
rows = await pool.fetch(query, *params)
return [Product(**dict(row)) for row in rows]
The agent does not just return a ranked list — it explains each recommendation in context of what the user asked for. This builds trust and helps users make faster decisions.
from openai import AsyncOpenAI
client = AsyncOpenAI()
RECOMMENDATION_PROMPT = """You are a product recommendation assistant.
User preferences:
- Budget: {budget_min} to {budget_max}
- Use case: {use_case}
- Priority: {priority}
- Preferred categories: {categories}
Available products (JSON):
{products_json}
For each recommended product, provide:
1. The product name and price
2. A 1-2 sentence explanation of why it fits this user's needs
3. One potential drawback to be transparent about
Recommend the top 3 products. Be specific about why each matches
the user's stated preferences.
"""
async def generate_recommendations(
prefs: UserPreferences, products: list[Product]
) -> str:
import json
products_data = [
{
"name": p.name,
"brand": p.brand,
"price": p.price,
"rating": p.rating,
"description": p.description,
"features": p.features,
}
for p in products
]
response = await client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": RECOMMENDATION_PROMPT.format(
budget_min=prefs.budget_min or "No minimum",
budget_max=prefs.budget_max or "No maximum",
use_case=prefs.use_case or "General",
priority=prefs.priority,
categories=", ".join(prefs.preferred_categories) or "Any",
products_json=json.dumps(products_data, indent=2),
),
}],
)
return response.choices[0].message.content
The recommendation logic becomes a tool the conversational agent can call. The agent handles preference extraction from natural language while the tool handles the database query and LLM-powered ranking.
from agents import Agent, function_tool
@function_tool
async def recommend_products(
category: str = "",
budget_max: float = 0,
use_case: str = "",
priority: str = "balanced",
) -> str:
"""Find and recommend products based on user preferences."""
prefs = UserPreferences(
user_id="session-user",
budget_max=budget_max if budget_max > 0 else None,
preferred_categories=[category] if category else [],
use_case=use_case,
priority=priority,
)
pool = await get_db_pool()
products = await search_products(pool, prefs)
if not products:
return "No products match your criteria. Try adjusting your budget or category."
return await generate_recommendations(prefs, products)
recommendation_agent = Agent(
name="ProductAdvisor",
instructions="""You help customers find the right product.
Ask about their budget, use case, and priorities before
making recommendations. Use the recommend_products tool
to fetch personalized suggestions.""",
tools=[recommend_products],
)
Start with a short preference elicitation conversation. Ask 2-3 targeted questions about budget, use case, and brand preferences before making any recommendations. This gives the agent enough signal to produce useful results without requiring purchase history.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
Use both. SQL filtering handles hard constraints like price range and stock availability efficiently. Semantic search via embeddings excels at matching vague user descriptions ("something for outdoor cooking that is easy to clean") against product descriptions. Run SQL first to narrow the candidate set, then re-rank with embedding similarity.
Include the user's stated priority in the prompt and in the SQL ordering. If the user says "best value," order by price ascending before passing products to the LLM. Also add an instruction in the system prompt that the agent should recommend products across the user's price range rather than clustering at the top.
#RecommendationEngine #Personalization #ECommerceAI #ProductDiscovery #Python #AgenticAI #LearnAI #AIEngineering

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
Step-by-step build of a working agent with the OpenAI Agents SDK — Agent class, tools, handoffs, tracing — plus an eval pipeline that catches regressions before merge.
Personalizing agents for one user is easy. Personalizing them for a million users is a memory-tier problem. The hot/warm/cold split and what each tier optimizes for.
Smolagents lets agents write Python instead of JSON. Why code-as-action reduces tool errors and where the security trade-offs are for production deployments.
Personalizing onboarding agents lifts trial-to-paid by 18% in published case studies. The memory architecture that makes it work and the metrics it actually moves.
Modal turns a Python function into autoscaling serverless compute with optional GPU. Deploy a LiveKit Agent with one command and get pay-per-second billing.
Pydantic AI's April release tightens the typed-agent loop and adds structured tool definitions. Why type-safe agents reduce production bugs and speed iteration.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.