Tabbit is a small demo for splitting a receipt among people: upload a receipt photo, let an LLM extract line items and totals, then post natural-language claims (“I had the burger and half the fries”) that get mapped to those lines. The stack is a Node + Express + SQLite API and a React + Vite UI.
This repo is also set up to show LaunchDarkly AI Configs driving two LangChain agents on the server: model, prompts, tools, and telemetry are controlled from LaunchDarkly instead of being hardcoded only in the repo.
The server wires LaunchDarkly’s Node SDK together with the AI add-on (@launchdarkly/server-sdk-ai) so each agent run is backed by an AI Config in your LaunchDarkly project.
In server/src/ld.js, the app creates:
ldClient— standard@launchdarkly/node-server-sdkclient (SDK key fromLAUNCHDARKLY_SDK_KEY).aiClient—initAi(ldClient)from@launchdarkly/server-sdk-ai, used to resolve AI Configs at runtime.
Both agents import aiClient (and receipt vision also calls ldClient.flush() after a run so metrics events are sent promptly).
For each AI-powered path, the code follows the same pattern:
-
aiClient.agentConfig(configKey, context, defaults, inputs?)
Fetches the live AI Config from LaunchDarkly for the given key, merged with in-code defaults and optional variation inputs. -
Guard rails
If the config is disabled or there is notracker, the handler throws (the feature is treated as off or misconfigured). -
LangChain model
Model name and temperature come from the resolved config (with local fallbacks). -
createAgent
The agent’s system prompt isagentConfig.instructions(i.e. the instructions from the AI Config in LD). Tools are not hardcoded as a fixed list for the agent; they are derived from the config (see below). -
tracker.trackMetricsOf(...)
The actualagent.invokeruns insidetracker.trackMetricsOf(LangChainProvider.getAIMetricsFromResponse, () => …)so token usage and related metrics are attributed to LaunchDarkly for observability and experiments.
| Config key (LaunchDarkly) | Purpose | Code |
|---|---|---|
receipt-itemizer |
Vision: image → structured JSON (merchant, lines, totals). | server/src/ai/receiptVision.js |
claim-parser |
Text → JSON allocations over known line_items ids. |
server/src/ai/claimItems.js |
Each uses a simple evaluation context (e.g. { kind: "user", key: "receipt_itemization" } or { kind: "user", key: "claim-parser" }). You can replace these with real user or tenant keys when you wire auth.
The claim parser passes line_items as serialized JSON into agentConfig so you can use that value in LaunchDarkly targeting or prompt templates if you configure variations that way.
server/src/ai/toolsHelper.js implements mapAiConfigTools(agentConfig):
- It reads
agentConfig.model.parameters.toolsfrom the AI Config (tool names as LaunchDarkly defines them). - It intersects that list with locally implemented tools in
server/src/ai/receiptMathTools.js(add,subtract,multiply,divide). - Only tools named in the LD config are attached to the LangChain agent.
So which tools the agent can call is controlled from the LaunchDarkly dashboard (and can differ per variation or rollout), not only from code.
-
AI Configs (or equivalent product surface) with keys
receipt-itemizerandclaim-parser, enabled for your environment. -
Variations that set at least:
- Instructions (system prompt),
- Model (e.g.
gpt-4o) and parameters (e.g. temperature), - Tools (subset of
add,subtract,multiply,divideif you want tool use), - Metrics tracking enabled so
trackeris present.
-
Server-side SDK key with access to those configs, set as
LAUNCHDARKLY_SDK_KEYon the API process.
Optional: use targeting rules or multi-variation experiments on the same keys; the server always resolves config at request time via agentConfig.
| Path | Role |
|---|---|
server/ |
Express API, SQLite (better-sqlite3), multer uploads, LangChain agents + LD |
ui/ |
React app; in dev, Vite proxies /api → http://localhost:3000 |
Root package.json exists for shared tooling dependencies; run the app from server/ and ui/ as below.
API (server/)
| Variable | Required | Description |
|---|---|---|
LAUNCHDARKLY_SDK_KEY |
Yes (for AI paths) | LaunchDarkly server SDK key |
OPENAI_API_KEY |
Yes | OpenAI API key (claim parser uses @langchain/openai directly) |
PORT |
No | Default 3000 |
DATABASE_PATH |
No | SQLite file path (defaults in code / Docker volume) |
RECEIPT_VISION_CSV_PATH |
No | Optional CSV append path for receipt vision training exports |
UI (ui/)
| Variable | Description |
|---|---|
VITE_API_BASE |
API origin in production (empty string if same origin) |
Terminal 1 — API
cd server
npm install
# set LAUNCHDARKLY_SDK_KEY and OPENAI_API_KEY in server/.env or the environment
npm run devTerminal 2 — UI
cd ui
npm install
npm run devOpen the Vite URL (usually http://localhost:5173). The dev server proxies /api/* to the API on port 3000.
Health check: GET http://localhost:3000/health
docker-compose.yml builds server and ui images and persists SQLite in a volume. For AI features you still need to inject LAUNCHDARKLY_SDK_KEY and OPENAI_API_KEY into the api service (for example via an env file or compose environment); they are not committed to the repo.
POST /upload— multipart fieldimage; runs the receipt-itemizer agent, then stores the receipt and line items.GET /receipts,GET /receipts/:id— list and detail (including line items and claims).POST /receipts/:id/claims— bodymessage(and optional name); runs the claim-parser agent when line items exist.
Static files for uploaded images are served under /files/....
@launchdarkly/node-server-sdk— feature and config delivery@launchdarkly/server-sdk-ai—aiClient.agentConfig, trackers@launchdarkly/server-sdk-ai-langchain—LangChainProvider.getAIMetricsFromResponselangchain/@langchain/openai— agents and OpenAI chat
The mental model: LaunchDarkly owns the operational definition of each agent (prompt, model params, tool list, enablement); the repo owns HTTP behavior, persistence, local tool implementations, and the glue that maps LD’s config onto LangChain.