Code Lookup Bot for Field Techs
The Problem
Junior tech texts the senior 5 times a day: 'What is the clearance for a water heater in a garage?' Senior's phone buzzes constantly. This automation builds a text-based RAG bot that searches uploaded code books and returns the exact code section with a plain-English explanation in 30 seconds.
How It Works
Text question from field tech
RAG searches uploaded code books (IRC, IPC, NEC, local amendments). Returns code section + plain-English explanation.
Answer with exact citation in 30 seconds. Senior's phone stops ringing.
Importable Templates
PRD
# Product Requirements Document
Recipe 097 -- Code Lookup Bot for Field Techs
THE AI TRADES Platform
---
Recipe Slug: `code-lookup-bot`
Recipe Number: 097
Difficulty: Replit Build
Time Estimate: Full day
Category: Internal Operations
Software Required: Airtable
Roles: Field Tech, Owner
Trades: HVAC, Plumbing, Electrical
Principles Applied: Replace the Boring 80%, Speed Wins the Job
---
Reusable Modules Referenced:
- Module 0: UX Philosophy (`modules/ux-philosophy-module.md`)
- Module 9: Onboarding Wizard (`modules/onboarding-wizard-module.md`)
- Module 10: Settings Panel (`modules/settings-panel-module.md`)
- Module 11: Notification / Toast (`modules/notification-toast-module.md`)
- Module 14: FAQ / Knowledge Base (`modules/faq-knowledge-base-module.md`)
Integration Docs (include with build):
- OpenAI / Anthropic (`integrations/openai-anthropic.md`) -- LLM for semantic search and answer generation
- PostgreSQL Database -- database, auth
---
Table of Contents
2. The Problem
3. The Solution
5. Architecture and Data Model
7. Step-by-Step Build Instructions
10. Example Output
---
1. Recipe Overview
Your junior tech is standing in a garage staring at a water heater installation. He is not sure about the clearance requirement from combustible materials. He pulls out his phone and calls the senior tech. The senior is elbow-deep in a condenser repair across town. He does not answer. The junior tries again. Voicemail. He texts. The senior responds 20 minutes later with a one-line answer he had memorized from the codebook. This happens 5-7 times a day across your crew.
This recipe builds a searchable code lookup application your techs access from any phone browser. You load it with your local building code requirements, manufacturer specs, and company-specific standards. When a tech has a question, they type it in plain English. The system searches your code database and returns the exact answer with the code reference, section number, and any notes your company has added. Answers come back in under 3 seconds.
The senior tech stops being a human help desk. The junior tech gets answers faster than a phone call ever delivered. Your entire crew operates from the same source of truth instead of relying on tribal knowledge that walks out the door when someone quits.
Input: Local code requirements (IRC, NEC, UPC, or your jurisdiction's amendments), manufacturer installation specs, and company-specific standards or preferences.
Output: Instant answers on any phone. Senior tech stops being a help desk. Every tech has the same code knowledge available 24/7.
---
2. The Problem
Your Senior Tech Is a $45/Hour Help Desk
Every interruption costs you twice. The junior tech stops working while waiting for an answer. The senior tech stops working to give one. If the senior is on a call, the junior either waits (lost labor) or guesses (failed inspection risk).
How the interruptions stack up:
| Metric | Per Incident | Daily (5 calls) | Weekly | Monthly |
|---|---|---|---|---|
| Junior tech waiting time | 5-20 min | 25-100 min | 2-8 hrs | 8-32 hrs |
| Senior tech interruption | 3-5 min | 15-25 min | 1-2 hrs | 4-8 hrs |
| Combined lost labor | 8-25 min | 40-125 min | 3-10 hrs | 12-40 hrs |
| Cost at $45/hr blended rate | $6-$19 | $30-$94 | $135-$450 | $540-$1,800 |
The bigger risk is failed inspections. When the junior cannot reach the senior, he guesses. Wrong guess means a failed inspection. That means a truck roll back to the site, rework time, and a rescheduled inspection. Average cost of a failed inspection: $300-$800 in labor, plus the schedule disruption.
What techs actually ask about:
| Question Category | % of Code Questions | Example |
|---|---|---|
| Clearance and setback requirements | 30% | "What is the clearance for a furnace from combustibles?" |
| Pipe sizing and material allowed | 20% | "Can I use PEX for the first 18 inches off the water heater?" |
| Electrical requirements | 20% | "Does this unit need a disconnect within sight?" |
| Venting requirements | 15% | "What is the max horizontal run for B-vent?" |
| Permit and inspection triggers | 10% | "Does a like-for-like replacement need a permit here?" |
| Other | 5% | Manufacturer-specific installation notes |
Every one of these questions has a definitive answer sitting in a codebook that nobody carries in the field.
---
3. The Solution
A web application optimized for mobile that serves as your company's code and standards knowledge base. Techs open a URL on their phone, type a question in plain English, and get an answer with the code reference.
How it works:
1. Owner or senior tech loads code data into the system: building codes, manufacturer specs, company standards. This can be done by pasting text, uploading PDFs, or entering records manually through an admin interface.
2. The system indexes every entry with embeddings for semantic search. When a tech types "water heater clearance garage," it finds the relevant code section even if the exact words do not match.
3. Techs search from their phone. No app install needed. Just a mobile-friendly web page with a search bar.
4. AI generates a clear answer from the matching code entries, citing the specific section and code reference.
5. Frequently asked questions surface automatically. The system tracks what techs search for most and can highlight those answers.
6. Owner can add company-specific notes to any code entry: "We always do X even though code only requires Y."
7. Search history shows what your crew does not know, so you can target training where it matters.
What changes:
| Metric | Before | After |
|---|---|---|
| Time to get a code answer | 5-20 minutes (call/text senior) | 3 seconds |
| Senior tech interruptions per day | 5-7 | Near zero |
| Failed inspections from guessing | 1-3 per month | Near zero |
| New tech ramp-up time | Months of shadowing | Weeks with code bot as backup |
| Knowledge loss when senior leaves | Catastrophic | Zero (it is in the system) |
---
4. Prerequisites
Before you start, gather these:
- [ ] Replit account (free tier works for build; deploy on Replit Deployments for production)
- [ ] OpenAI API key for embeddings and answer generation ($5-$15/month depending on usage)
- [ ] PostgreSQL database (with pgvector extension) for database, auth, and vector search
- [ ] Your local building codes in digital format. Options:
- PDF of your jurisdiction's adopted codes (IRC, NEC, UPC, IMC)
- Or manually typed entries for the most common questions (start with 50-100 entries)
- [ ] Manufacturer installation manuals for the top 5 brands you install (digital PDF or scanned)
- [ ] Company standards document if you have one (or the senior tech's brain, transcribed into entries)
- [ ] Airtable base (optional) if you want to manage the code entries in Airtable and sync to the app
Total monthly cost: $5-$20 for OpenAI API usage. PostgreSQL and Replit free tiers cover everything else for a crew under 20.
---
5. Architecture and Data Model
System Architecture
```
[Tech's Phone Browser]
|
v
[Replit Web App - React Frontend]
|
v
[Express API Server]
|
+---> [PostgreSQL + pgvector]
| - code_entries table (with embeddings)
| - search_logs table
| - company_notes table
|
+---> [OpenAI API]
- text-embedding-3-small (indexing + search)
- gpt-4o-mini (answer generation)
```
Data Model
Table: code_entries
| Column | Type | Description |
|---|---|---|
| id | uuid | Primary key, auto-generated |
| trade | text | "hvac", "plumbing", "electrical" |
| category | text | "clearance", "venting", "piping", "electrical", "permits", "general" |
| code_reference | text | "IRC M1306.2" or "NEC 440.14" |
| jurisdiction | text | "IRC 2021" or "City of Dallas 2024 amendments" |
| title | text | Short title: "Water Heater Clearance from Combustibles" |
| content | text | Full code text and explanation |
| source_type | text | "building_code", "manufacturer_spec", "company_standard" |
| manufacturer | text | Nullable. "Rheem", "Carrier", etc. |
| tags | text[] | Array of searchable tags |
| embedding | vector(1536) | OpenAI text-embedding-3-small vector |
| created_at | timestamptz | Auto-generated |
| updated_at | timestamptz | Auto-updated |
Table: company_notes
| Column | Type | Description |
|---|---|---|
| id | uuid | Primary key |
| code_entry_id | uuid | FK to code_entries |
| note | text | "We always use 4-inch vent even though 3-inch is code minimum" |
| created_at | timestamptz | Auto-generated |
Table: search_logs
| Column | Type | Description |
|---|---|---|
| id | uuid | Primary key |
| query | text | The tech's search query in plain English |
| matched_entry_id | uuid | FK to code_entries (top result) |
| result_count | integer | Number of results returned |
| created_at | timestamptz | Auto-generated |
Table: profiles
| Column | Type | Description |
|---|---|---|
| id | uuid | Primary key, auto-generated |
| name | text | Tech's display name |
| role | text | "tech", "senior_tech", "admin" |
| trade | text | Primary trade |
Relationships
```
code_entries.id --> company_notes.code_entry_id
code_entries.id --> search_logs.matched_entry_id
```
---
6. Key Screens / UI
Screen 1: Search (Mobile Home Screen)
The primary screen. This is what techs see when they open the app.
- Large search bar at the top, auto-focused so the keyboard opens immediately
- Placeholder text: "What code question do you have?"
- Trade filter pills below the search bar: All, HVAC, Plumbing, Electrical
- Below the search bar: "Frequently Asked" section showing the top 5-10 most searched questions as tappable cards
- Results appear instantly below as the tech types (debounced 300ms)
- Each result card shows: title, code reference, a 2-line preview of the answer, and the source type badge (Code, Manufacturer, Company Standard)
- Tapping a result card expands it inline to show the full answer, code section, and any company notes attached
Screen 2: Answer Detail
Shown when a tech taps a search result.
- Full title at top
- Code reference badge (e.g., "IRC M1306.2")
- Source badge: "Building Code" / "Manufacturer Spec" / "Company Standard"
- Full answer text, formatted for readability on a phone screen
- Company notes section below the answer (yellow background to distinguish from code text)
- "Was this helpful?" thumbs up / thumbs down at the bottom (feeds back to search quality)
- "Ask AI for more detail" button that sends the code entry to GPT-4o-mini with the tech's original question for a more conversational explanation
Screen 3: Admin - Manage Entries
Desktop-focused screen for the owner or senior tech to manage the knowledge base.
- Data table showing all code entries with columns: Title, Trade, Category, Code Ref, Source, Date Added
- Sortable and filterable by trade, category, source type
- "Add Entry" button opens a form: trade, category, code reference, jurisdiction, title, content, source type, manufacturer (optional), tags
- Bulk import: paste tab-separated text or upload a CSV to add multiple entries at once
- Edit and delete actions per row
- "Import from PDF" button that uploads a PDF, sends pages to GPT-4o for extraction, and creates draft entries for review
Screen 4: Admin - Search Analytics
Dashboard for the owner to see what techs are searching for.
- Top 20 most searched queries this week/month
- Queries with zero results (these are knowledge gaps you need to fill)
- Search volume by trade and category
- Most active searchers (see which techs rely on it most)
- "Unanswered questions" list: searches that returned zero or low-relevance results
Screen 5: Onboarding Wizard
First-time setup for the owner.
- Step 1: Company name, primary trades served
- Step 2: Jurisdiction selection (state + city for local code amendments)
- Step 3: Choose starter content. Options: "Load IRC 2021 HVAC basics" / "Load NEC 2023 residential basics" / "Load UPC 2021 basics" / "Start empty and add my own"
- Step 4: Invite techs (enter email addresses, they get a magic link)
- Step 5: Done. Redirect to the admin manage entries screen.
---
7. Step-by-Step Build Instructions
Step 1: Set Up PostgreSQL Database
1. Create a PostgreSQL database (local or hosted)
2. Enable the pgvector extension:
```sql
create extension if not exists vector;
```
3. Create the tables:
```sql
create table profiles (
id uuid primary key default gen_random_uuid(),
name text not null,
role text not null default 'tech' check (role in ('tech', 'senior_tech', 'admin')),
trade text,
created_at timestamptz default now()
);
create table code_entries (
id uuid primary key default gen_random_uuid(),
trade text not null,
category text not null,
code_reference text,
jurisdiction text,
title text not null,
content text not null,
source_type text not null check (source_type in ('building_code', 'manufacturer_spec', 'company_standard')),
manufacturer text,
tags text[] default '{}',
embedding vector(1536),
created_at timestamptz default now(),
updated_at timestamptz default now()
);
create table company_notes (
id uuid primary key default gen_random_uuid(),
code_entry_id uuid references code_entries(id) on delete cascade,
note text not null,
created_at timestamptz default now()
);
create table search_logs (
id uuid primary key default gen_random_uuid(),
query text not null,
matched_entry_id uuid references code_entries(id),
result_count integer default 0,
created_at timestamptz default now()
);
-- Index for vector similarity search
create index on code_entries using ivfflat (embedding vector_cosine_ops) with (lists = 100);
-- Index for text search fallback
create index idx_code_entries_trade on code_entries(trade);
create index idx_code_entries_category on code_entries(category);
```
Step 2: Initialize the Replit Project
1. Create a new Replit project: Node.js template
2. Set up the project structure:
```
/
/client -- React frontend (Vite)
/src
/components
/pages
/hooks
/lib
/server -- Express API
/routes
/services
/middleware
/shared -- Shared types
package.json
vite.config.ts
tsconfig.json
```
3. Install dependencies:
```bash
npm install express pg openai react react-dom react-router-dom
npm install -D vite @vitejs/plugin-react typescript @types/react @types/express tailwindcss
```
Step 3: Build the Embedding Service
This service converts code entry text into vector embeddings for semantic search.
```typescript
// server/services/embeddings.ts
import OpenAI from "openai";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export async function generateEmbedding(text: string): Promise<number[]> {
console.log(`[Embeddings] Generating embedding for: "${text.substring(0, 80)}..."`);
const response = await openai.embeddings.create({
model: "text-embedding-3-small",
input: text,
});
console.log(`[Embeddings] Embedding generated, dimensions: ${response.data[0].embedding.length}`);
return response.data[0].embedding;
}
export async function generateSearchEmbedding(query: string): Promise<number[]> {
console.log(`[Search] Generating query embedding for: "${query}"`);
return generateEmbedding(query);
}
```
Cost: text-embedding-3-small costs $0.02 per 1M tokens. 500 code entries at ~200 tokens each = 100K tokens = $0.002 to index everything. Each search query costs a fraction of a cent.
Step 4: Build the Search API
```typescript
// server/routes/search.ts
import { Router } from "express";
import { db } from "../lib/database";
import { generateSearchEmbedding } from "../services/embeddings";
import { generateAnswer } from "../services/answer-generator";
const router = Router();
router.post("/api/search", async (req, res) => {
const { query, trade } = req.body;
if (!query || query.trim().length < 3) {
return res.status(400).json({ error: "Query must be at least 3 characters" });
}
console.log(`[Search] Query: "${query}", Trade filter: ${trade || "all"}`);
try {
// Generate embedding for the search query
const queryEmbedding = await generateSearchEmbedding(query);
// Semantic search using pgvector
const results = await db.query(
`SELECT * FROM match_code_entries($1, $2, $3, $4)`,
[queryEmbedding, 0.7, 5, trade || null]
);
console.log(`[Search] Found ${results.rows.length} results`);
// Generate a plain-English answer from the top results
let aiAnswer = null;
if (results.rows.length > 0) {
aiAnswer = await generateAnswer(query, results.rows);
}
// Log the search
await db.query(
`INSERT INTO search_logs (user_id, query, matched_entry_id, result_count) VALUES ($1, $2, $3, $4)`,
[req.user?.id, query, results.rows[0]?.id || null, results.rows.length]
);
return res.json({
answer: aiAnswer,
results: results.rows,
query,
});
} catch (err) {
console.error(`[Search] Error: ${err.message}`);
return res.status(500).json({ error: "Search failed" });
}
});
export default router;
```
Step 5: Create the Database Search Function
```sql
create or replace function match_code_entries(
query_embedding vector(1536),
match_threshold float,
match_count int,
filter_trade text default null
)
returns table (
id uuid,
trade text,
category text,
code_reference text,
jurisdiction text,
title text,
content text,
source_type text,
manufacturer text,
tags text[],
similarity float
)
language plpgsql
as $$
begin
return query
select
ce.id,
ce.trade,
ce.category,
ce.code_reference,
ce.jurisdiction,
ce.title,
ce.content,
ce.source_type,
ce.manufacturer,
ce.tags,
1 - (ce.embedding <=> query_embedding) as similarity
from code_entries ce
where
(filter_trade is null or ce.trade = filter_trade)
and 1 - (ce.embedding <=> query_embedding) > match_threshold
order by ce.embedding <=> query_embedding
limit match_count;
end;
$$;
```
Step 6: Build the Answer Generator
```typescript
// server/services/answer-generator.ts
import OpenAI from "openai";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
interface CodeEntry {
title: string;
content: string;
code_reference: string;
source_type: string;
jurisdiction: string;
}
export async function generateAnswer(
query: string,
entries: CodeEntry[]
): Promise<string> {
console.log(`[Answer] Generating answer for: "${query}" from ${entries.length} entries`);
const context = entries
.map(
(e, i) =>
`[Source ${i + 1}] ${e.title}\nReference: ${e.code_reference || "N/A"}\nJurisdiction: ${e.jurisdiction || "N/A"}\nType: ${e.source_type}\n\n${e.content}`
)
.join("\n\n---\n\n");
const response = await openai.chat.completions.create({
model: "gpt-4o-mini",
temperature: 0.2,
max_tokens: 500,
messages: [
{
role: "system",
content: `You are a code lookup assistant for home service contractors (HVAC, plumbing, electrical). A field technician is asking a code question from a job site. Give a direct, clear answer based ONLY on the provided code references. Include the specific code section number. Keep the answer under 150 words. Use plain language. If the sources do not contain enough information to answer confidently, say so.`,
},
{
role: "user",
content: `Question: ${query}\n\nCode references:\n${context}`,
},
],
});
const answer = response.choices[0].message.content;
console.log(`[Answer] Generated: ${answer?.substring(0, 100)}...`);
return answer || "Could not generate an answer. Check the individual results below.";
}
```
Step 7: Build the Mobile Search UI
```tsx
// client/src/pages/Search.tsx
import { useState, useCallback } from "react";
import { debounce } from "../lib/utils";
export default function Search() {
const [query, setQuery] = useState("");
const [trade, setTrade] = useState("all");
const [results, setResults] = useState(null);
const [loading, setLoading] = useState(false);
const doSearch = useCallback(
debounce(async (q: string, t: string) => {
if (q.length < 3) return;
setLoading(true);
try {
const res = await fetch("/api/search", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: q, trade: t === "all" ? null : t }),
});
const data = await res.json();
setResults(data);
} catch (err) {
console.error("Search failed:", err);
} finally {
setLoading(false);
}
}, 300),
[]
);
return (
<div className="min-h-screen bg-white px-4 pt-6 pb-20">
<h1 className="text-xl font-bold mb-4">Code Lookup</h1>
<input
type="text"
autoFocus
placeholder="What code question do you have?"
className="w-full p-4 text-lg border rounded-lg mb-3"
value={query}
onChange={(e) => {
setQuery(e.target.value);
doSearch(e.target.value, trade);
}}
/>
<div className="flex gap-2 mb-6">
{["all", "hvac", "plumbing", "electrical"].map((t) => (
<button
key={t}
onClick={() => { setTrade(t); doSearch(query, t); }}
className={`px-3 py-1 rounded-full text-sm capitalize ${
trade === t ? "bg-blue-600 text-white" : "bg-gray-100 text-gray-700"
}`}
>
{t}
</button>
))}
</div>
{loading && <p className="text-gray-500">Searching...</p>}
{results?.answer && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<p className="text-sm font-semibold text-blue-800 mb-1">Answer</p>
<p className="text-gray-900">{results.answer}</p>
</div>
)}
{results?.results?.map((entry) => (
<div key={entry.id} className="border rounded-lg p-4 mb-3">
<div className="flex items-center gap-2 mb-1">
{entry.code_reference && (
<span className="text-xs bg-gray-200 px-2 py-0.5 rounded">
{entry.code_reference}
</span>
)}
<span className="text-xs bg-green-100 text-green-800 px-2 py-0.5 rounded capitalize">
{entry.source_type.replace("_", " ")}
</span>
</div>
<h3 className="font-semibold text-gray-900">{entry.title}</h3>
<p className="text-sm text-gray-600 mt-1 line-clamp-3">{entry.content}</p>
</div>
))}
</div>
);
}
```
Step 8: Build the Admin Entry Management
Create an admin page at `/admin/entries` with:
1. A data table listing all code entries (use the Data Table module)
2. An "Add Entry" form with fields matching the code_entries schema
3. A "Bulk Import" option that accepts CSV with columns: trade, category, code_reference, jurisdiction, title, content, source_type
4. On save, the server generates the embedding and stores it alongside the entry
```typescript
// server/routes/admin.ts
import { Router } from "express";
import { db } from "../lib/database";
import { generateEmbedding } from "../services/embeddings";
const router = Router();
router.post("/api/admin/entries", async (req, res) => {
const { trade, category, code_reference, jurisdiction, title, content, source_type, manufacturer, tags } = req.body;
console.log(`[Admin] Adding entry: "${title}"`);
// Generate embedding from the combined title + content
const embeddingText = `${title}. ${content}`;
const embedding = await generateEmbedding(embeddingText);
try {
const result = await db.query(
`INSERT INTO code_entries (trade, category, code_reference, jurisdiction, title, content, source_type, manufacturer, tags, embedding, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING *`,
[trade, category, code_reference, jurisdiction, title, content, source_type, manufacturer, tags || [], embedding, req.user?.id]
);
console.log(`[Admin] Entry created: ${result.rows[0].id}`);
return res.json(result.rows[0]);
} catch (error) {
console.error(`[Admin] Insert error: ${error.message}`);
return res.status(500).json({ error: error.message });
}
});
router.post("/api/admin/entries/bulk", async (req, res) => {
const { entries } = req.body; // Array of entry objects
console.log(`[Admin] Bulk importing ${entries.length} entries`);
const results = [];
for (const entry of entries) {
const embeddingText = `${entry.title}. ${entry.content}`;
const embedding = await generateEmbedding(embeddingText);
try {
const result = await db.query(
`INSERT INTO code_entries (trade, category, code_reference, jurisdiction, title, content, source_type, manufacturer, tags, embedding, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING *`,
[entry.trade, entry.category, entry.code_reference, entry.jurisdiction, entry.title, entry.content, entry.source_type, entry.manufacturer, entry.tags || [], embedding, req.user?.id]
);
results.push({ title: entry.title, status: "ok", id: result.rows[0].id });
} catch (error) {
console.error(`[Admin] Bulk insert error for "${entry.title}": ${error.message}`);
results.push({ title: entry.title, status: "error", error: error.message });
}
}
console.log(`[Admin] Bulk import complete. ${results.filter(r => r.status === "ok").length}/${entries.length} succeeded`);
return res.json({ results });
});
export default router;
```
Step 9: Seed Starter Content
Create a seed script that loads common code entries. Here is a sample for HVAC:
```typescript
// server/seed/hvac-basics.ts
export const hvacStarterEntries = [
{
trade: "hvac",
category: "clearance",
code_reference: "IRC M1306.2",
jurisdiction: "IRC 2021",
title: "Clearance from Combustible Materials for Appliances",
content: "Appliances shall be installed with clearances from combustible materials as specified in the listing and manufacturer's instructions. In the absence of listing and manufacturer's instructions, clearances shall not be less than 36 inches from the top of the appliance, 36 inches from the front, 18 inches from the back, and 18 inches from the sides.",
source_type: "building_code",
tags: ["clearance", "combustible", "furnace", "water heater"],
},
{
trade: "hvac",
category: "clearance",
code_reference: "IRC M1307.2",
jurisdiction: "IRC 2021",
title: "Water Heater Clearance in Garage",
content: "Water heaters having an ignition source shall be elevated so that the source of ignition is not less than 18 inches above the garage floor. This applies to gas-fired water heaters installed in garages. The purpose is to keep the flame or pilot above the level where gasoline vapors accumulate on the floor. FVIR (Flammable Vapor Ignition Resistant) water heaters meet this requirement at floor level.",
source_type: "building_code",
tags: ["water heater", "garage", "clearance", "elevation", "18 inches", "FVIR"],
},
{
trade: "hvac",
category: "venting",
code_reference: "IRC G2427",
jurisdiction: "IRC 2021",
title: "Maximum Horizontal Run for B-Vent",
content: "Type B vent horizontal runs shall not exceed 75% of the total vertical height of the vent. The horizontal section must maintain a minimum upward slope of 1/4 inch per foot toward the vent terminal. All horizontal runs must be supported at intervals not exceeding 4 feet.",
source_type: "building_code",
tags: ["b-vent", "horizontal", "venting", "slope"],
},
// Add 20-50 more entries covering your most common code questions
];
```
---
8. Deployment on Replit
Step 1: Environment Variables
In your Replit project, go to Secrets (lock icon in the sidebar) and add:
| Secret | Value |
|---|---|
| OPENAI_API_KEY | Your OpenAI API key |
| DATABASE_URL | Your PostgreSQL connection string |
Step 2: Configure the Run Command
In your `.replit` file:
```
run = "npm run dev"
```
For production deployment, use:
```
run = "npm run build && npm start"
```
Step 3: Deploy
1. Click "Deploy" in the Replit header
2. Choose "Autoscale" deployment type
3. Set the build command: `npm run build`
4. Set the run command: `npm start`
5. Deploy. Replit gives you a `.replit.app` URL.
Step 4: Share with Your Crew
1. Send the URL to your techs. They can bookmark it or add it to their phone's home screen.
2. First user to sign in with the admin email gets the admin role.
3. Techs sign in with magic link (email). No passwords to remember.
---
9. Testing and Validation
Test 1: Seed Data Loads
1. Run the seed script
2. Verify entries appear in the admin table
3. Verify each entry has an embedding (not null)
Pass criteria: All seed entries visible in admin with embeddings generated.
Test 2: Semantic Search Accuracy
Test these queries against your seed data:
| Query | Expected Top Result |
|---|---|
| "water heater clearance garage" | Water Heater Clearance in Garage (IRC M1307.2) |
| "how far does a furnace need to be from the wall" | Clearance from Combustible Materials (IRC M1306.2) |
| "max horizontal run for b vent" | Maximum Horizontal Run for B-Vent (IRC G2427) |
| "can I use pex off a water heater" | Should match piping material entry if seeded |
Pass criteria: Top result matches expected entry for at least 4 of 5 test queries.
Test 3: Answer Quality
For each search, verify the AI-generated answer:
- Cites the specific code section
- Uses plain language a tech would understand
- Does not hallucinate requirements not in the source data
- Stays under 150 words
Pass criteria: All answers cite correct code sections with no hallucinated content.
Test 4: Mobile Usability
Open the app on a phone (or phone simulator). Verify:
- [ ] Search bar is immediately visible and focused
- [ ] Trade filter pills are tappable without zooming
- [ ] Results are readable without horizontal scrolling
- [ ] Answer box is clearly distinguished from individual results
- [ ] The entire search-to-answer flow takes under 3 seconds
Pass criteria: Full search flow under 3 seconds, readable on a 375px-wide screen.
Test 5: Zero Results Handling
Search for something not in the database (e.g., "solar panel mounting requirements"). Verify:
- The app displays a "No matching codes found" message
- The search is still logged for gap analysis
- No error or crash
Pass criteria: Graceful handling with a clear message.
Test 6: Admin Bulk Import
Prepare a CSV with 10 entries. Upload through the admin bulk import. Verify all 10 appear with embeddings and are searchable.
Pass criteria: All 10 entries searchable within 60 seconds of import.
---
10. Example Output
Scenario: Junior Plumber on a Water Heater Install
12:35 PM. Danny, a second-year plumber, is installing a gas water heater in a customer's garage. He knows it needs to be elevated but cannot remember the exact requirement. He pulls out his phone and opens the Code Lookup app.
He types: "water heater height in garage"
The app returns in 2.1 seconds:
> Answer: Gas water heaters installed in a garage must have their ignition source elevated at least 18 inches above the garage floor (IRC M1307.2). This prevents ignition of gasoline vapors that settle at floor level. Exception: FVIR (Flammable Vapor Ignition Resistant) water heaters are designed to meet this requirement and can be installed at floor level.
Top result card:
```
IRC M1307.2 | Building Code
Water Heater Clearance in Garage
Water heaters having an ignition source shall be
elevated so that the source of ignition is not less
than 18 inches above the garage floor...
```
Company note attached (yellow box):
```
Company Standard: We always use an 18" stand even
for FVIR units. Inspector in the Plano district has
failed FVIR units on the floor twice. Do not risk it.
- Added by Mike R., 2024-11-15
```
Danny reads the answer, sees the company note, grabs the 18-inch stand, and keeps working. No phone call. No waiting. No guessing.
12:36 PM. Danny is back to work. Total interruption: under 60 seconds.
---
11. Quick Reference Card
```
CODE LOOKUP BOT
================================
WHAT IT DOES:
Techs search code questions on their phone.
Get answers in 3 seconds with code references.
No more calling the senior tech 5x a day.
TECH USAGE:
1. Open the app URL on your phone
2. Type your question in plain English
3. Read the answer + code reference
4. Check company notes (yellow box) for local rules
ADMIN USAGE:
1. Add entries: Admin > Manage Entries > Add Entry
2. Bulk import: Upload CSV with code data
3. Check gaps: Admin > Search Analytics > Zero Results
4. Add company notes to any code entry
DATA MODEL:
code_entries -- The knowledge base (codes, specs, standards)
company_notes -- Your company-specific add-ons per entry
search_logs -- What techs search for (gap analysis)
profiles -- User accounts and roles
TECH STACK:
Frontend: React + Tailwind (mobile-first)
Backend: Express on Replit
Database: PostgreSQL + pgvector
AI: OpenAI embeddings + GPT-4o-mini
Auth: Magic link
COST:
OpenAI: $5-$15/month
PostgreSQL: Free tier
Replit: Free tier (or $7/mo for always-on)
Total: $5-$22/month
WHAT THIS REPLACES:
5-7 phone calls/day to the senior tech
20+ minutes of lost labor per interruption
Failed inspections from guessing
Knowledge loss when experienced techs leave
STARTER CONTENT:
Load IRC, NEC, or UPC basics from the seed script.
Add your top 50 questions manually.
The system gets smarter as you add more entries.
```
---
Recipe 097 -- Code Lookup Bot for Field Techs
THE AI TRADES Platform
Difficulty: Replit Build | Time: Full day | Category: Internal Operations