Personal Shopper: A Multi-Agent State Machine for Psychological Product Discovery
Three specialized agents routed by state turn a five-question funnel into one of six buyer personas, then frame Perplexity-backed product search in that persona's voice.

The shopper who asks "what would you give me?"
Product discovery in commerce keeps converging on the same shape — search box, grid of results, filter sidebar. The interaction assumes the shopper already knows what they want, or that they'll bounce around long enough to figure it out. That assumption is wrong for most people most of the time.
The shopper who walks into a small store and says "what would you give me?" gets a different — and usually better — experience than the one who types a query into Amazon. The Personal Shopper is my answer to that gap: a conversational agent that profiles users into a buyer persona through five short questions, then adapts every product recommendation to the way that specific user makes decisions. A DEAL_HUNTER and a BRAND_LOYALIST get different answers to the same question, in different language, ordered by different criteria — because they are different decision-makers.
It's also, on purpose, a vehicle for me to practice agent design patterns in a domain where the cost of being wrong is bounded — a missed sale, not a lost customer or a regulatory issue. Everything I learned here transferred directly to the agent UX work I'm doing inside RockWallet and across the Moussemango client roster.
Why a single-agent chatbot collapses here
The one-prompt-one-model-one-personality shape of most chatbots collapses under three pressures specific to shopping:
- State matters. A new user, a partially-profiled user, and a fully-profiled user need three different conversations. Conflating them into one prompt produces an agent that's either over-eager (asks profiling questions to someone who already passed them) or under-eager (recommends products to someone who hasn't even given an email).
- Personality is data, not text. Hard-coding "be friendly to impulse shoppers, be detailed to analytical buyers" into the prompt makes the prompt a giant switch statement. Behavior should be expressed declaratively, with persona-specific logic loaded from a table at query time.
- Hallucination is a product failure, not a quality failure. If the agent invents a product, a price, or a link, the user clicks and trust is gone. The agent needs to be physically incapable of generating a recommendation without calling a search tool first.
Solving all three pushed me toward a multi-agent state machine: three specialized agents, persona-as-data, mandatory tool calls.

Following Anthropic's Building Effective Agents and n8n's multi-agent guidance, I split the work across three specialized agents — each with its own model, memory, and tool surface — routed by user state:
- Email Collector (
GPT-4o-mini, 10-msg buffer, no tools) handlesNEW. - Profiling Agent (
GPT-4o-mini, 15-msg buffer, parses to JSON) handlesONBOARDING. - Shopping Assistant (
GPT-4o, 20-msg Postgres memory, Perplexity + preferences lookup) handlesPROFILED.
State lives in one place — shopping_users.state in Supabase, transitioning NEW → ONBOARDING → PROFILED. The router reads it on every turn. Each agent gets exactly the context it needs and nothing else.
The single most important consequence of this design is that the shopping agent never sees the profiling conversation. The profiling agent never sees the product searches. Context windows stay small, prompts stay focused, and each agent can be tuned independently. When I extend the profiling flow from 5 to 7 questions next month, the shopping agent prompt doesn't change.

Six buyer personas live in the system: IMPULSE_SHOPPER, ANALYTICAL_BUYER, DEAL_HUNTER, BRAND_LOYALIST, ETHICAL_SHOPPER, QUALITY_FOCUSED. Each has a presentation strategy stored in the prompt, but the classification is a value in the shopping_users.persona_type column. To re-personalize a user, you change a value in Supabase Studio. No redeploy.
This pattern — voice as data — is the single most useful agent-design discipline I've internalized in the last year. It's the same pattern I used later in the Patricia personal-finance agent, where the entire persona (identity, soul, operating rules) is a set of rows in a profile table. The minute you put the agent's character in code, you've made every personality change a deploy.
The profiling flow itself is intentionally deterministic — five questions in a fixed order, then the agent outputs a single line (CLASSIFICATION_COMPLETE: [PERSONA_TYPE]) that the workflow parses with a regex and writes to the database. LLM as classifier, regex as parser, SQL as state mutation. Boring on purpose.
Tools, not prompts, enforce behavior
The Shopping Assistant has one job that matters: never invent a product. The system prompt is explicit ("MUST use search tools for every product request — never make up products or prices"), but prompts aren't safety; tools are.
The agent's only path to a product mention is to call search_products against Perplexity's online model (llama-3.1-sonar-large-128k-online), which returns real-time, citation-backed results. Perplexity is configured via HTTP Header Auth — not n8n's dedicated Perplexity credential — because that's the cleanest path for a Bearer token. If the agent returns text without having called the tool, that's a bug; I see it in n8n's execution log and tighten the prompt.
The tool call is also where I attach persona_type as a parameter, so search results are pre-shaped before the LLM frames them. A DEAL_HUNTER query for "running shoes" surfaces sale-priced options at the top of the result set; an ETHICAL_SHOPPER query surfaces certified B-Corp and recycled-material brands first.
Model matching follows the same logic. GPT-4o-mini runs Email Collector and Profiling — their jobs are simple (extract one field, walk through five fixed questions) and a cheaper model handles them fine. GPT-4o runs the Shopping Assistant, where interpreting a persona, framing search results, and maintaining conversational continuity benefits from the bigger model. Each agent is sized to its actual reasoning load.
Key decisions
- Multi-agent over super-agent. Single-agent designs collapse when state branches multiply. Three agents with explicit handoffs is the right shape for "the conversation has phases."
- State as a column, not as conversation history. I could have inferred user state from the chat (look for an email earlier, look for profiling answers). I didn't — that's brittle and slow. State is a string in the DB; the agent doesn't reason about it; the router does.
- Persona as data. Changing presentation strategy is a prompt edit. Changing a user's classification is a SQL update. Neither is a deploy.
- Mandatory tool calls. No product recommendation may exit the agent without a Perplexity call backing it. The difference between a demo and a product.
- Postgres chat memory, capped at 20 messages. Long enough to carry context across a normal session, short enough that prompt cost stays predictable and old failure patterns rotate out.
- Two memory layers per agent. Working memory (chat buffer) and semantic memory (
shopping_users.preferencesJSONB) are different concerns. The buffer rotates; preferences persist. Conflating them produces an agent that forgets what you told it three sessions ago and remembers something you said five minutes ago. - Defer Universal Commerce Protocol integration. The PRD reserves a slot for Google's UCP (direct-checkout) integration, but I intentionally shipped without it. Recommendation quality needs to be right before checkout matters.
What shipped
Surface + state
![Profiling funnel — five fixed questions revealing shopping speed, core values, budget orientation, decision triggers, and emotional drivers, ending in CLASSIFICATION_COMPLETE: [PERSONA_TYPE]](/_next/image?url=https%3A%2F%2Fckezhxnaampuvenyuppo.supabase.co%2Fstorage%2Fv1%2Fobject%2Fpublic%2Fproject-assets%2Fpersonal-shopper-agent%2Fgallery-01.png%3Fv%3D3e0e27083a&w=3840&q=75)

Reflection — where I stopped writing chatbots
The hardest part wasn't the technology; it was the personality taxonomy. Six personas is a design decision, not a model decision. I iterated — there were eight at one point — and pulled the count down because two of them (SOCIAL_PROOF_DRIVEN and BRAND_LOYALIST) collapsed into one signal in profiling. Removing personas is harder than adding them, because every persona-aware section of the system prompt becomes a row in a switch statement, and you have to confirm none of the existing behavior depended on the deleted branch. Lesson: keep persona-aware sections in one place in the prompt, not sprinkled.
The second lesson is about tools. The first version of the Shopping Assistant let the agent return product names without a Perplexity call, with a system-prompt instruction that said "please don't invent products." It invented products. The fix wasn't a better prompt; it was making the search tool the only path to a product mention. Tools, not prompts, enforce behavior. I now apply that rule to every agent I build.
What I'd do differently: ship with the click-tracking and preference-learning loops from day one. Recommendation quality without feedback is guesswork. The hooks are designed (recent_searches, favorite_products JSONB columns are in the schema) but unused. Next iteration.
What I'm proud of: it's a small, focused, opinionated agent that does one thing well, with the architecture honest about its constraints. No vector DB it doesn't need. No fine-tune. No agent framework. Just disciplined composition of off-the-shelf primitives — which is, increasingly, what serious agent work looks like. The Shopping Assistant is where I stopped writing chatbots and started building agents.