A conversational agent for refrigerator and dishwasher parts support: find parts by model, check compatibility, get installation and troubleshooting guidance. The system is domain-locked, evidence-grounded, and built as a case study in structured agent design.
The repo implements a single-domain chat assistant aligned to a PartSelect-style product: users ask in natural language about appliance parts, model numbers, compatibility, and repair; the backend classifies scope, routes to the right capability (RAG troubleshooting, parts list, part lookup, compatibility, or clarification), and returns an answer plus optional Sources (citations) and Suggested parts (product cards). The stack is FastAPI (Python) for the agent and Next.js (App Router) for the chat UI; optional PostgreSQL + pgvector for RAG over repair guides and part catalog.
Main components:
| Layer | Role |
|---|---|
| Scope router | Classifies each message as in-scope (appliance parts/support) or out-of-scope; ambiguous or off-topic requests get a polite redirect. |
| Agent (LangGraph) | State machine: triage or LLM planner → clarify / parts_list / part_lookup / compatibility / find_model_help / retrieve (RAG). Produces answer, citations, product_cards. |
| Tools | get_troubleshooting (RAG), part_lookup, search_parts, check_compatibility, Serp for model/part pages. |
| Web app | Chat UI with markdown answers, Sources list, and product cards linking to PartSelect. |
Goals
- Help users find parts for a given model (parts list, model overview, symptom-based suggestions).
- Check compatibility between a part (e.g. PS number) and a model.
- Provide troubleshooting guidance (not cooling, not draining, leaking, ice maker, etc.) grounded in ingested repair guides, with citations.
- Support “Where is my model number?” with short guidance and links to refrigerator/dishwasher locator pages.
- Stay in-domain: refrigerators and dishwashers only; refuse or redirect off-topic and out-of-scope requests.
- Prefer evidence and structured data (RAG, DB, Serp) over free-form LLM generation where possible.
Non-goals
- No support for other appliances (washer, dryer, oven, microwave, etc.).
- No order placement, cart, or checkout (informational only).
- No medical, legal, coding, or general-knowledge Q&A.
- No open-ended chitchat beyond a short welcome and redirect to parts/support.
- User sends a message in the chat (e.g. “My fridge is not cooling”, “parts for WRF535SWHZ”, “is PS123 compatible with model X?”).
- Scope is classified: in-scope → agent; out-of-scope/ambiguous → fixed redirect message, no graph run.
- Agent runs:
- Triage or LLM planner sets
next_action(clarify, parts_list, part_lookup, compatibility, find_model_help, retrieve). - Clarify: reply with a short question (e.g. “What’s your model number?”) and no links/cards.
- Parts list / part lookup / compatibility / find_model_help: dedicated nodes return an answer plus citations and/or product_cards.
- Retrieve: RAG over repair guides → evidence → compose answer with citations; no product cards for pure troubleshoot.
- Triage or LLM planner sets
- Response is returned as: content (markdown), sources (citations for “Sources” in UI), product_cards (for “Suggested parts” / part links). Frontend renders markdown, source links, and cards; PartSelect base URLs are resolved to model/part pages where possible.
┌─────────────────────────────────────────────────────────────────────────┐
│ Web (Next.js) │
│ Chat UI → /chat, /chat/stream → API │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ API (FastAPI) │
│ Scope router → run_agent(message, scope_label, history) │
│ Agent: build initial state → LangGraph.ainvoke → postprocess │
│ → content, sources, product_cards │
└─────────────────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
config/ app/agent_graph app/tools
scope_contract.json (LangGraph) RAG, part_lookup,
source_policy.json triage / llm_planner search_parts, Serp
state_guide_links → nodes → END check_compatibility
Repository layout:
case-study/
├── config/ # Domain and routing config
│ ├── scope_contract.json # Intents, entities, patterns, forbidden topics
│ ├── source_policy.json # State → symptom tags, appliance, adjacent states (RAG filter)
│ └── state_guide_links.json
├── apps/
│ ├── api/ # FastAPI app
│ │ ├── app/ # scope_router, agent, agent_graph, tools, evidence, retrieval, …
│ │ ├── main.py # /chat, /chat/stream
│ │ ├── schema.sql # Postgres + pgvector (parts, models, chunks, embeddings)
│ │ └── requirements.txt
│ └── web/ # Next.js (App Router)
│ ├── app/ # page, layout
│ ├── components/ # Chat, ProductCard, Header
│ └── package.json
└── scripts/
└── ingest/ # RAG: chunk, embed, load; optional model/parts fetch
The agent is a state machine: each turn it carries forward context (user message, extracted model/part, appliance type, etc.), chooses one next action, runs the matching node, then merges the result back into state. The graph runs until it reaches an answer.
What the agent tracks (state)
TroubleshootingState holds inputs (e.g. message, scope_label, model_number, part_number, appliance_type, intent, current_state), planner outputs (next_action, planner_next_action, action_args, info_type), and outputs (e.g. evidence, answer, citations, product_cards). Each node returns a partial update that gets merged into this state.
How the next action is chosen
- LLM planner (default when
USE_LLM_ROUTER_PLANNER=1): the graph sends the message tollm_router→llm_planner, which setsnext_action(and optional slots like model/part). - Rule-based triage: otherwise, deterministic rules set
next_action. For refrigerators, a cooling_split step can refine the path (e.g. “fridge and freezer both warm” vs “freezer cold, fridge warm”) before clarify or RAG.
What each action does (routing)
next_action |
What happens | Output |
|---|---|---|
| ask_clarify | Ask the user for missing info (e.g. model or part number). | Short question; no citations or product cards. |
| parts_list_answer | Answer “parts for model X” using Serp and optional DB. | Answer + product cards (model/symptom/part links). |
| part_lookup_answer | Look up a part number (e.g. PS…) via part_lookup or Serp. | Answer + optional single product card. |
| compatibility_answer | Check whether a part fits a model. | Yes/no + model link. |
| find_model_help | Explain where to find the model number. | Short text + Sources (refrigerator/dishwasher locator links); no product cards. |
| retrieve | Run RAG over repair guides: get_troubleshooting → evidence → compose answer with citations. | Answer + citations; no product cards for pure troubleshooting. |
Source policy
config/source_policy.json defines, per diagnostic state, which symptom tags and appliance types are allowed or forbidden. That filters RAG and suggested links so refrigerator questions don’t get dishwasher links and vice versa.
- Scope contract (
config/scope_contract.json): allowed intents, entities, brands, forbidden topics, patterns; used by the scope router and kept in sync with product scope. - RAG: Repair guides (HTML) are chunked, embedded (OpenAI), and stored in Postgres (pgvector).
get_troubleshootingretrieves by embedding + optional state/symptom filter; evidence is passed to a compose step (claim-based or fallback) with citations from chunks. Suggested links for troubleshoot (no model) are capped and filtered by appliance type. - Structured data:
parts,models,part_fitment(or compatibility),model_sections,section_partsfor part lookup and compatibility. When DB has no hit, the agent can fall back to Serp + LLM summarization for model or part. - Serp: Used for model/part page discovery, symptom-specific part lists, and URL resolution when only a title is available. No Playwright in the main answer path; optional live fetch behind a feature flag.
| User | System behavior |
|---|---|
| “Where can I find my model number?” | find_model_help: short guidance + Sources (Refrigerator / Dishwasher locator links). No product card. |
| “Parts for WRF535SWHZ” | parts_list: answer + product_cards (model overview, symptom/part links as applicable). |
| “Is PS11752778 compatible with WDT780SAEM1?” | compatibility_answer: Yes/No + model link. |
| “My fridge is not making ice” | retrieve: RAG over repair guides → answer + citations (Suggested links). No suggested parts unless user later asks for parts for a model. |
| “Install PS12345678” (no model/part in message) | ask_clarify: “Share model number or part number so I can help.” |
| “Write me a poem” | Scope router: out-of-scope → redirect to appliance parts/support only. |
- Stack: Next.js (App Router), Tailwind, React Markdown. Chat calls
POST /chator SSEGET /chat/streamwith history; response includescontent,sources,product_cards. - Rendering: Markdown for the reply; Sources as a list of links below the answer; product_cards as compact cards (title, “View on PartSelect →”). PartSelect base URLs are rewritten client-side to model or part URLs when possible (using model from content or card name).
- Scoping: No change to UI for in/out-of-scope; the user sees either the agent reply or the single redirect message.
- New appliances: Extend
scope_contractandsource_policy, add appliance-specific states and routes, and extend RAG/ingest to new guide sets. - New intents: Add nodes and
next_actionvalues (e.g. order status, warranty); wire planner/triage to new actions and keep citations/cards semantics. - Stronger RAG: Finer-grained chunks, hybrid search, or re-ranking; keep citation-to-chunk contract so answers stay grounded.
API (FastAPI)
cd apps/api && python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
uvicorn main:app --reload --port 8000Web (Next.js)
From repo root:
npm run devOr from apps/web: npm install && npm run dev. Open http://localhost:3000. Set NEXT_PUBLIC_API_URL=http://localhost:8000 if the API runs elsewhere.
Database (optional, for full RAG)
createdb partselect
psql partselect -f apps/api/schema.sqlEnvironment
OPENAI_API_KEY: used for scope (optional), LLM planner/slots (whenUSE_LLM_ROUTER_PLANNER=1), RAG compose, and Serp summarization.SERPAPI_API_KEY: recommended for full agent behavior. Used for PartSelect model/part page discovery, “find model number” links, parts list and part lookup fallbacks, and compatibility link resolution. Without it, those flows return empty or skip Serp; get a key at serpapi.com.DATABASE_URL: for RAG and part lookup (optional if using Serp/fallbacks only).- See
apps/api/.env.examplefor API env vars (copy toapps/api/.env); seescripts/ingest/.env.examplefor ingest-related variables.