12 / SYSTEMS
CarrLane — Engineering Part Catalog Chatbot
Three-tier function-calling assistant that turns natural-language part queries into typed API calls over a scraped engineering catalog — built during an industry internship.
- Python
- FastAPI
- SQLModel
- Node/Express
- React
- Chakra UI
- OpenAI function-calling
- BeautifulSoup
- SQLite
- Catalog (prototype)
- 314 parts
- Company catalog
- 10,000+ parts
- LLM tools
- 9 typed function schemas
- API tests
- 8 async endpoint tests
- Dates
- Jun–Aug 2025, Chennai
The one industry entry: an AI / Software Engineering internship at CarrLane Manufacturing (Chennai, Jun–Aug 2025). The product is a chatbot that lets an engineer ask for a part in plain English (“what’s the largest L-pin diameter you stock?”) and get the right part number back, grounded in the actual catalog rather than the model’s training data.
The interesting engineering is not the chat UI. It’s the three-tier separation that keeps the model from ever inventing a part.
Architecture: model on top, deterministic data underneath
+----------------------+
| React + Chakra UI | chat client, optimistic
| (frontend/) | updates, AI-status badge
+----------+-----------+
| HTTP
v
+----------------------+
| Node/Express | the only process that
| orchestrator | talks to OpenAI:
| (chat-server.js) | gpt-4o-mini + 9 tools
+----------+-----------+
| typed REST calls
v
+----------------------+
| FastAPI catalog | source of truth,
| service (backend/) | SQLModel over SQLite
+----------+-----------+
| SQL
v
+----------------------+
| SQLite | segment > line > type
| (catalog db) | > part, specs in JSON
+----------------------+
Fig. 1 — three tiers; requests flow down, and only function results flow back up to the model.
- FastAPI catalog service (
backend/) — the source of truth. SQLModel ORM over SQLite, exposing a hierarchy ofsegment → product line → product type → part. Every part’s specs live in a JSON column, and every lookup is a real query, not a prompt. - Node/Express orchestrator (
orchestrator/chat-server.js) — the only thing that talks to OpenAI. It declares nine typed function schemas togpt-4o-miniand runs the function-call → execute → synthesise round. - React frontend (
frontend/) — Chakra UI chat client with optimistic updates and a live AI-status badge (sending → thinking → calling → rendering; hidden when idle). The committed catalog data isreact-markdown-rendered.
The design rule was: the model decides what to fetch; the API decides what exists. The LLM never sees the database — it only sees function results. If a part isn’t in SQLite, the model cannot return it. There’s no path for it to.
The function-calling round, exactly as built
chat-server.js runs a deliberate single round of tool use, not an open-ended agent loop:
- Sanitise the chat history (drop any message without string content;
gpt-4o-minirejects null-content function turns). - First completion with
function_call: "auto"over the 9 schemas. - If the model picked a function, execute it against the FastAPI service, append the
assistantfunction-call turn and thefunctionresult turn, then make a second completion to turn raw JSON into a sentence. - If no function was picked, return the text directly.
chat history
|
v
sanitise: drop null-content turns
|
v
completion #1 (function_call: "auto", 9 schemas)
|
tool picked? ---- no ----> return text directly
|
yes
|
v
execute tool against FastAPI service
|
v
append assistant fn-call turn + function result turn
|
v
completion #2: raw JSON -> one sentence
|
v
reply to client
Fig. 2 — the fixed round in chat-server.js: at most one tool call and two completions per message.
I chose a fixed round over a recursive agent loop on purpose. The catalog is shallow, and the failure mode of looping LLMs (runaway tool calls, cost blow-ups) wasn’t worth it for a lookup problem. Where genuine chaining is needed (“what’s the max bushing diameter in this type”), the chaining happens server-side, deterministically: getSpecStats calls the catalog’s own functions to resolve names and pull the parts, then computes min/max in code, so the arithmetic is never left to the model.
Exposing a hierarchical catalog as a flat tool set
The nine schemas (orchestrator/functions.js) are the real design work. The catalog is a tree, but function-calling wants flat, typed parameters. So the tools span the whole tree at different granularities: fetchCatalog, fetchProductLines, fetchTypesForLine, fetchParts, fetchPart, getSpecStats and friends, each with a JSON-Schema parameter contract the model has to satisfy.
The model speaks in human titles (“L Pins T Pins And Jig Pins”); the API speaks in integer IDs. Bridging that is a normalisation layer: normalize(s) = s.toLowerCase().replace(/[^a-z0-9]/g, ""), used to match the model’s loose title against the canonical one and resolve it to an ID before hitting the API. When the model only gives a product type and omits its parent line, resolveTypeIdByName walks every line’s types to auto-discover the parent. That’s plain string matching and ID resolution. There is no vector search or embedding store anywhere in this system, and the entry shouldn’t pretend otherwise. The grounding comes from the typed API, not from similarity.
The data quality problem was the actual problem
The catalog was scraped from carrlane.com with BeautifulSoup (scraper/data_ingestion/scraper.py), parsing the spec tables out of the live product pages. Real engineering data is hostile to numeric comparison. Two problems dominated:
- Imperial fractions as strings. Dimensions come through as
"3/16","2-1/2",".1875". The committed prototype has 1,133 spec values carrying a fraction. You cannotmin/maxthose as text.parse_spec_value(backend/app/crud.py) puts the 772 plainn/dvalues through Python’sFraction, so the FastAPI aggregate endpoint gets those right. The remaining 361 are silently dropped: 322 mixed numbers like"2-1/2"(the code doesreplace('-', ' ')to get"2 1/2",Fractionrejects that form, so it returnsNone) and 39 oddities like the thread spec"5/16-18". And the LLM-facing path never reaches that parser at all —getSpecStatsin the orchestrator does its ownparseFloat(raw)in JavaScript, whereparseFloat("3/16")is3. - Inconsistent spec keys. The same physical dimension shows up as
"A DIA NOMINAL","A DIA NOMINAL (mm)","A DIA ACTUAL +0/-.0010"across types. The scraper tooling (scraper/output/add_alias.py) and the JSON catalog store a per-type alias array mapping friendly names to real spec keys, but thegetSpecStatsorchestrator function looks for them in a tab titled"alias"within the product type’sinfoarray (no such tab exists in the scraped data; the actual tabs are"Product Information","Application Information","Material", etc.), so the alias resolution is currently a dead branch:specKeypasses through unchanged.
This is the unglamorous part that makes the answer correct instead of plausible.
What’s proven vs. assumed
The FastAPI layer has 8 async endpoint tests (backend/test/test_api.py, httpx + ASGI transport) covering segment/line/type/part lookups, spec filtering, min/max aggregation, and the 404 paths for unknown names. The catalog correctness is therefore tested, not asserted. The orchestrator’s LLM round is not unit-tested (a Jest harness is declared but no spec files exist). That’s a real gap, honestly noted.
Honest scope
The working prototype covers one product line — L Pins, T Pins and Jig Pins — which is 314 parts across 11 product types, scraped end-to-end into SQLite. The “10,000+ parts” figure is CarrLane’s full company catalog (from the CV), not what this prototype ingests; the system is built to scale to it (the scraper and schema are line-agnostic), but the committed dataset is the one product line. Calling that out matters more than rounding up.
parts ingested vs CarrLane's full catalog
company 10,000+ |#########################################
prototype 314 |#
Fig. 3 — prototype dataset against the full company catalog, bars drawn to scale.
The transferable skill is the pattern, not the domain: when you put an LLM in front of structured data, the win is the typed boundary between the model and the source of truth — typed tool schemas, deterministic resolution, and the hard data-cleaning underneath. That generalises to any structured-lookup problem, a trading instrument table as readily as a parts catalog.
ROLE
AI / Software Engineering Intern. Built the catalog ingestion, the typed REST API over it, and the function-calling orchestration layer that maps natural-language queries to catalog lookups.
DATES / Jun–Aug 2025
Private repository