Cartographer
An autonomous shopping agent that builds a personalised cart conversationally — one product category at a time, with the customer choosing at every step.
The problem
Product search makes the customer do the translating. Someone who knows they have sensitive skin and £150 to spend still has to turn that into categories, filters and comparisons themselves. Cartographer inverts it: the customer states the goal in their own words, and the agent does the decomposition — planning which categories the goal implies, then working through them one at a time.
How it works
The customer's opening message goes to an agent loop built on the Vercel AI SDK, which streams tokens and tool calls back over server-sent events. The agent's first move is always to produce a shopping plan, rendered in the UI as a card so the customer can see the route before any product appears. It then works the plan one category at a time: a search tool queries Postgres by category and JSONB attribute filters and returns exactly three options, the customer picks one, and an add-to-cart tool writes the choice and updates the cart panel. The agent never adds a product on its own — every item in the final cart was chosen by a human. What makes it more than a demo is that neither the catalogue nor the model is compiled in: the tool schemas and the system prompt are both generated from a domain config file at startup.
Stack
Decisions and trade-offs
The domain is a config file, not code
Categories and facets live in a JSON config, and both the tool schemas and the system prompt are generated from it at startup. Swapping the store from beauty to electronics is an env var, not a refactor — the agent simply knows what it is selling. Hardcoding the catalogue would have been faster and would have made the second domain a rewrite.
Provider-agnostic through the AI SDK
Streaming and tool calling are normalised across Anthropic, OpenAI, Google and OpenRouter, so the provider is one env var and no code changes. Committing to a single provider's SDK would have meant rewriting the loop to evaluate a different model.
SSE rather than WebSockets
The stream only runs one way — server to client. SSE reconnects on its own and passes through ordinary HTTP infrastructure; a bidirectional socket would have been more surface for no gain.
A mock mode that needs no API key
MOCK_LLM=true runs a scripted agent loop against the real database, real cart
and real streaming. It keeps the demo cheap to show and gives the UI a
deterministic path to be tested against.
What it does not do
There is no checkout, no payment and no accounts — the cart is the end of the journey, keyed to a session. It sells one domain at a time rather than searching across several, and the electronics config ships without seed data. The catalogue is mock data, not a real retailer feed.