Skip to content

Commit 1f68210

Browse files
committed
Add PartsLine: voice agent for auto parts counters (LiveKit + Moss)
1 parent f5323a1 commit 1f68210

65 files changed

Lines changed: 7504 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/partsline/.env.example

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
LIVEKIT_URL=
2+
LIVEKIT_API_KEY=
3+
LIVEKIT_API_SECRET=
4+
DEEPGRAM_API_KEY=
5+
CARTESIA_API_KEY=
6+
GROQ_API_KEY=
7+
DARTMOUTH_CHAT_API_KEY=
8+
DARTMOUTH_CHAT_BASE_URL=
9+
DARTMOUTH_CHAT_MODEL=
10+
MOSS_API_KEY=
11+
MOSS_PROJECT=
12+
CALL_LOG_DB_PATH=./data/calls.db
13+
SESSION_IDLE_TIMEOUT_SECONDS=120
14+
SESSION_MAX_DURATION_SECONDS=900

apps/partsline/.gitignore

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
.env
2+
.env.*
3+
!.env.example
4+
5+
__pycache__/
6+
*.py[cod]
7+
*$py.class
8+
.pytest_cache/
9+
.mypy_cache/
10+
.ruff_cache/
11+
12+
node_modules/
13+
14+
venv/
15+
.venv/
16+
17+
# Build artifacts
18+
build/
19+
dist/
20+
out/
21+
.next/
22+
coverage/
23+
htmlcov/
24+
*.egg-info/
25+
tsconfig.tsbuildinfo
26+
27+
# Runtime logs
28+
*.log
29+
.agent.lock

apps/partsline/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 dongm58
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

apps/partsline/PROJECT.md

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# PartsLine — Voice Agent for Auto Parts Counters
2+
3+
## What it is
4+
A browser-based voice agent that answers the highest-volume call an auto
5+
parts counter gets: "do you have [part] for [vehicle]?" It searches a
6+
parts catalog with vehicle details as metadata filters, checks stock,
7+
quotes price, sets parts aside by name, and transfers anything complex
8+
to a human — with the vehicle/part context already captured.
9+
10+
Built on Moss because the problem is retrieval-shaped: catalogs are too
11+
large to prompt-stuff, one question triggers multiple lookups
12+
(part → fitment → stock), and on a live call those lookups must resolve
13+
mid-sentence (~5-15ms in-process vs ~200-600ms over a cloud DB round trip).
14+
15+
## Who it's for
16+
Demo audience now (incl. as an open-source reference app); architecture
17+
kept honest so a real shop's catalog/POS data could swap in later.
18+
19+
## Guardrails (the "trained new employee" rules)
20+
- Exact-match-or-ask: never quote fitment on an ambiguous vehicle spec;
21+
ask the disambiguating question ("2.5 or 3.6?")
22+
- Grounded-only: every fitment/price/stock claim comes from a retrieval
23+
result; no answers from LLM world-knowledge
24+
- Hedged stock language ("we're showing 2 in stock")
25+
- Immediate cheerful transfer for: modifications, interchange questions,
26+
returns/warranty, fleet/commercial pricing
27+
28+
## MVP scope
29+
Caller: part lookup w/ disambiguation → price + stock quote →
30+
set-aside by first name → or transfer-with-context.
31+
Shop: minimal call-log screen (time, vehicle, part, outcome).
32+
33+
## Explicitly NOT building (v1)
34+
Ordering/payments · order status · returns handling · multi-part calls ·
35+
phone telephony (browser voice only) · callback-number capture ·
36+
real catalog/POS integration · CRM/analytics · auth/multi-tenancy ·
37+
public deployment (laptop-demoed)
38+
39+
## Data model
40+
- PART (part_number, name, category, brand, description, price)
41+
- VEHICLE (year, make, model, engine, trim?)
42+
- FITMENT (part↔vehicle join + condition notes; THE core domain)
43+
- STOCK (separate from PART by design — the future POS-sync seam)
44+
- CALL_LOG (append-only: time, vehicle, parts, outcome, set-aside name)
45+
46+
Implementation note: FITMENT is flattened into one Moss document per
47+
(part × vehicle) combination, with vehicle attributes (make/model/year/
48+
engine) as string metadata fields on each document. Confirmed working
49+
via live test (see Retrieval Findings below).
50+
51+
Synthetic catalog: 3-4 categories (belts, brakes, filters, batteries),
52+
~200-500 parts, ~50-100 vehicles, deliberately messy fitment: dual-engine
53+
vehicles, mid-year splits, superseded part numbers, universal-fit parts,
54+
vehicles absent from catalog.
55+
56+
## Retrieval Findings (from live Moss test — moss-test/seed.py + query.py)
57+
58+
A 12-document trap-laden mini-catalog was pushed to a real Moss index
59+
and queried live. Results, and what they mean for the build:
60+
61+
**CONFIRMED WORKING — dual-engine disambiguation.**
62+
Unfiltered query ("serpentine belt" for 2014 Outback) returned BOTH the
63+
2.5L and 3.6L belts with close scores (1.000 / 0.969) — this is the
64+
visible-ambiguity signal the agent needs to trigger "2.5 or 3.6?".
65+
Adding `engine=2.5` to the filter cleanly isolated the correct part.
66+
→ Build implication: when a filtered-by-known-attributes query returns
67+
>1 result, the agent must ask a disambiguating question before
68+
answering. This is a hard rule, not a nicety.
69+
70+
**CRITICAL FINDING — semantic score is NOT a safe "we don't carry this"
71+
signal.** An unfiltered query for "brake pads for a 2019 RAV4" (a vehicle
72+
NOT in the catalog at all) returned Civic/Camry brake pads scoring
73+
0.909-0.994 — indistinguishable from genuine matches. There is no score
74+
threshold that separates a real match from a confidently-wrong one.
75+
By contrast, the SAME query WITH a vehicle filter (`model=RAV4`)
76+
correctly returned zero results.
77+
→ Build implication (HARD REQUIREMENT): the agent must NEVER accept a
78+
fitment answer from an unfiltered/semantic-only query. Vehicle
79+
identity (make/model/year/engine) must always be applied as a metadata
80+
filter before a part is quoted to a caller. An empty filtered result
81+
is the ONLY trustworthy "we don't carry that" signal. This changes the
82+
guardrail from "prefer filtering" to "filtering is mandatory before
83+
any fitment claim."
84+
85+
**FINDING — discontinued parts can outrank their replacement.**
86+
A discontinued part (A-100, 0 stock, superseded_by="A-100B") scored
87+
HIGHER (0.994) than its current replacement (A-100B, 0.975) on a plain
88+
semantic query. Text similarity has no concept of "this part is dead."
89+
→ Build implication: retrieval logic must explicitly exclude or
90+
deprioritize documents where `superseded_by` is set or `stock="0"`,
91+
rather than trusting raw top-result ranking. Options to evaluate in
92+
build: (a) filter out `stock=0` at query time when alternatives exist,
93+
(b) always chase `superseded_by` chains before presenting a result,
94+
(c) surface both with the agent explicitly saying "that one's been
95+
replaced by X." Needs a decision during Phase 2 build, not left to
96+
the LLM's judgment.
97+
98+
**UNRESOLVED — production-date / mid-year-split filtering not actually
99+
tested.** The test used discrete string tags (`prod_cutoff:
100+
"before-2014-03"` / `"from-2014-03"`) rather than a real date value with
101+
$lte/$gte comparison. Filtering on year=2014 alone correctly returned
102+
BOTH pad versions (expected, since prod_cutoff wasn't filtered on) — but
103+
this does NOT confirm whether Moss's `$lte`/`$gte` operators work
104+
cleanly against real date metadata for mid-year splits.
105+
→ Build implication: re-test with an actual date field before assuming
106+
this pattern works. If date-range filtering proves awkward, fallback
107+
is precomputing the split into a boolean/enum field like `prod_cutoff`
108+
(as tested) and having the agent ask "was this bought before or after
109+
March 2014?" — a viable path either way, but confirm which.
110+
111+
## Evaluation discipline
112+
15-25 held-out caller scenarios written before build, kept OUT of the
113+
repo (and away from coding agents) during development; run manually
114+
against the finished agent; added to the repo as documented evals after
115+
v1. MUST include explicit cases for: dual-engine disambiguation,
116+
superseded-part handling, and an absent-vehicle call (RAV4-style) to
117+
confirm the agent refuses to guess rather than offering a wrong part.
118+
119+
## Presentation
120+
- Demo page: one "talk" button, live transcript, inline lookup-chips
121+
showing each retrieval as it fires (the proof layer)
122+
- Call-log page: single newest-first list
123+
- Open source (MIT), README-first, reproducible via seed script
124+
125+
## Stack
126+
- Voice/transport: LiveKit Agents (Python) — browser WebRTC native;
127+
Moss's own reference integration path
128+
- Retrieval: Moss — catalog index (semantic + $eq/$and metadata
129+
filters), retrieval exposed as LLM function tools; per-call session
130+
optional. Confirmed live: `create_index`, `load_index`, filtered and
131+
unfiltered `query` all work as documented against a real project.
132+
- STT: Deepgram · LLM: GPT-4o (Groq as TTFT upgrade) · TTS: Cartesia
133+
- Frontend: Next.js (demo page + /calls log route)
134+
- DB: SQLite (CALL_LOG only); catalog source-of-truth = JSON in repo,
135+
seeded to Moss via script (see moss-test/seed.py as the working
136+
reference implementation)
137+
- Deploy: none for v1 (local); later Vercel + LiveKit Cloud/Fly.io
138+
139+
## Architecture sketch
140+
Browser (Next.js, LiveKit client)
141+
⇄ LiveKit room ⇄ Agent worker (Python: Deepgram → GPT-4o ⇄ Moss
142+
function tools → Cartesia) → SQLite CALL_LOG → /calls page
143+
144+
Moss function-tool logic (per the findings above) MUST:
145+
1. Always resolve vehicle identity (make/model/year/engine) before
146+
querying for a part — never answer from an unfiltered query.
147+
2. If a filtered query returns >1 result, ask a disambiguating question
148+
instead of picking one.
149+
3. If a filtered query returns 0 results, tell the caller "we don't
150+
carry a match for that vehicle" — do not fall back to unfiltered
151+
semantic search to find a "close enough" answer.
152+
4. Exclude/deprioritize discontinued (`stock=0` or `superseded_by`-set)
153+
parts rather than trusting raw ranking; resolve to the current
154+
replacement when one exists.
155+
156+
## Security-sensitive areas
157+
- All keys in .env (gitignored) + committed .env.example; clean from
158+
first commit (repo is public from day one)
159+
- Spend caps + alerts on all provider accounts (Moss, LiveKit, Deepgram,
160+
OpenAI/Groq, Cartesia), day one
161+
- Post-v1 public deploy adds: rate limiting on the agent endpoint,
162+
abuse pass (strangers can burn LLM/TTS credits via the talk button)
163+
164+
## Build sequencing (mock-first)
165+
1. ~~Next.js page + fake transcript (clickable skeleton)~~
166+
2. ~~Catalog seed script + Moss index + a CLI that answers text
167+
queries~~ — DONE: moss-test/seed.py + query.py, findings above
168+
incorporated. Re-test production-date filtering ($lte/$gte) before
169+
moving on if mid-year-split parts matter for the demo story.
170+
3. LiveKit voice loop with a dumb echo agent (voice proven separately)
171+
4. Wire 2+3: function tools enforcing the four hard rules above,
172+
disambiguation loop, superseded-part handling
173+
5. Lookup-chips in transcript; CALL_LOG + /calls page LAST
174+
6. Run held-out evals (incl. RAV4-style absent-vehicle case); fix;
175+
publish repo
176+
177+
## Open questions carried forward
178+
1. Real-world validation: has anyone confirmed with an actual parts
179+
counter that this is the call volume/pain they'd want solved?
180+
(Deliberately parked, not resolved.)
181+
2. STT disambiguation tolerance: how many "sorry, which one was that?"
182+
re-asks is acceptable before it feels broken? (Resolved as: graceful
183+
re-asking is acceptable, expected behavior — not a defect to eliminate.)
184+
3. Production-date filtering: does Moss's $lte/$gte work cleanly on a
185+
real date field, or does the mid-year-split need to stay a
186+
precomputed boolean tag? RE-TEST BEFORE BUILDING THE FEATURE.
187+
4. Superseded-part resolution strategy: pick (a), (b), or (c) from the
188+
Retrieval Findings section above during Phase 2 build — do not leave
189+
this to implicit LLM judgment, it's a guardrail decision.

apps/partsline/README.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# PartsLine
2+
3+
A browser-based voice agent for auto parts counters, built on [Moss](https://github.com/usemoss/moss) for retrieval and [LiveKit](https://livekit.io) for voice.
4+
5+
A caller asks: *"Do you have front brake pads for a 2013 Honda Civic?"* PartsLine looks it up against a real parts catalog and answers from what it actually finds. If the caller's car isn't in the catalog, it says so, instead of quoting the nearest-sounding part.
6+
7+
## Why auto parts counters
8+
9+
A parts counter is a noisy, small-margin shop, usually with a couple of phones ringing and one person trying to answer them while also helping whoever's standing at the counter. The call is almost always the same shape: someone reads off a year, make, model, and asks if a part's in stock. That's a lot of repetitive lookups against the same catalog, all day.
10+
11+
It's also a good stress test for Moss specifically. A shop's network isn't always great, and nobody's going to tolerate a voice agent that goes quiet for a second while a query round-trips to some cloud vector database — a pause that long just reads as "the line dropped." Moss loads the catalog into the agent's own process, so a lookup is a function call, not a network request. That's what makes it possible to search a real catalog *while someone's mid-sentence* and still sound like a person, not a phone tree.
12+
13+
## What it actually does
14+
15+
- Caller connects in a browser and talks to the agent.
16+
- Every lookup shows up live as a small card in the transcript — what vehicle it searched, and what it found.
17+
- Some cars have two engine options for the same part. If the agent isn't sure which one, it asks.
18+
- Some parts get discontinued and replaced. The agent tells the caller that, then quotes the current part.
19+
- If the vehicle isn't in the catalog at all, it says so — it won't quote something close and hope it fits.
20+
- A caller can ask to have a found part held under their name, or get bumped to a person for anything more complicated (returns, fleet pricing, modifications).
21+
- Every finished call lands in a simple call log — vehicle, part, outcome.
22+
23+
[Demo video](./promo/demo.mp4) <!-- add once recorded -->
24+
25+
## Quickstart
26+
27+
### You'll need
28+
29+
- Python 3.11+
30+
- Node.js 18+
31+
- A [Moss](https://usemoss.dev) account and project credentials
32+
- A [LiveKit Cloud](https://cloud.livekit.io) project (the free tier is enough)
33+
- API keys for Deepgram, Cartesia, and an LLM provider (Anthropic here, but any tool-calling model works)
34+
35+
### 1. Install
36+
37+
```bash
38+
cd apps/partsline
39+
pip install -r agent/requirements.txt
40+
npm install
41+
```
42+
43+
### 2. Set up your `.env`
44+
45+
```bash
46+
cp .env.example .env
47+
```
48+
49+
Fill in `MOSS_PROJECT_ID`, `MOSS_PROJECT_KEY`, `LIVEKIT_URL`, `LIVEKIT_API_KEY`, `LIVEKIT_API_SECRET`, `DEEPGRAM_API_KEY`, `CARTESIA_API_KEY`, `ANTHROPIC_API_KEY`.
50+
51+
### 3. Load the catalog into Moss
52+
53+
```bash
54+
python seed.py
55+
```
56+
57+
`catalog/demo_catalog.json` is a small, intentionally messy catalog: a car with two engine options, a discontinued part with a current replacement, a universal-fit part, and a car that isn't in the catalog at all, on purpose.
58+
59+
### 4. Start the agent
60+
61+
```bash
62+
python -m agent.main dev
63+
```
64+
65+
### 5. Start the web app
66+
67+
```bash
68+
npm run dev
69+
```
70+
71+
Open `http://localhost:3000`, hit **Talk**, and try:
72+
73+
- *"Do you have front brake pads for a 2013 Honda Civic?"* — a normal, in-stock lookup.
74+
- *"I need a serpentine belt for a 2014 Subaru Outback"* — that car has two engine options; leave yours out and see what it asks.
75+
- *"Do you have front brake pads for a 2019 Toyota RAV4?"* — not in the catalog. Watch it say so instead of guessing.
76+
- *"I need an oil filter for a 2015 Toyota Camry"* — this one's discontinued; watch it catch that and quote the replacement.
77+
78+
## How it's put together
79+
80+
```
81+
Browser (Next.js, LiveKit client)
82+
⇄ LiveKit room ⇄ Agent worker (Python)
83+
Deepgram (STT) → Claude Haiku (LLM) ⇄ Moss (retrieval, function tool)
84+
→ Cartesia (TTS)
85+
→ SQLite (call log) → /calls page
86+
```
87+
88+
- **`agent/`** — the voice agent. `main.py` sets up the LiveKit session. `tools/lookup_part.py` is the only retrieval tool the model has, and it won't run without a year, make, and model — there's no path in the code for an unfiltered search to slip through.
89+
- **`agent/tools/set_aside.py`**, **`transfer.py`** — hold a part under a caller's name; hand off to a human with the vehicle and part already noted down.
90+
- **`agent/outcome.py`**, **`db.py`** — the call record, and writing it to SQLite once a call ends.
91+
- **`app/`** — the demo page, the live transcript with the retrieval cards, and the `/calls` log.
92+
- **`catalog/demo_catalog.json`**, **`seed.py`**, **`query.py`** — the seed catalog plus the scripts that load and query it.
93+
94+
The lookup tool builds a Moss filter out of the vehicle info (and guesses the part category from what the caller asked for, so a belt request literally can't come back as a filter), queries an index that's loaded once when the agent starts, and sorts the result into one of four buckets: a clean match, more than one match, a part that's been replaced, or nothing. It never falls back to a fuzzy search to find something "close enough."
95+
96+
## Testing
97+
98+
```bash
99+
pytest
100+
ruff check .
101+
mypy .
102+
npm run lint
103+
npm run typecheck
104+
```
105+
106+
## What's not in here
107+
108+
- One part per call — no handling multiple parts in the same conversation.
109+
- Browser only, no phone line.
110+
- No real POS or inventory system behind it — prices and stock come from the seed catalog.
111+
- Parts that change mid-model-year (like a spec change partway through 2014) aren't handled yet.
112+
113+
## License
114+
115+
MIT — see [LICENSE](./LICENSE).
116+

apps/partsline/agent/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""PartsLine Python agent package."""

0 commit comments

Comments
 (0)