File structure
projects/bloom-house-concierge/
index.html # Lucy, the governed concierge (sql.js)
data/db.js # bloom-house.db embedded as base64 (auto-generated)
bloom-house.db # SQLite source: products, orders
build_db.py # rebuilds the DB + db.js
products/
peony-box.html # birthday pick 1 (RM189)
lily-pink.html # birthday pick 2 (RM169)
cond-stand.html # condolence (RM239)
images/ # product photos
flowers.html occasions.html gifts.html delivery.html track-order.html
showcase.html # in-app code walkthrough
problem-statement.md design.md
1 · The three-way gate
Every message is sorted into one outcome before any answer. Order is the safety model: decision-type goes to a human first; out-of-bounds is declined before it can be mistaken for a handoff.
function decide(text){
const t = text.toLowerCase();
// 1. decision-type (refund, cancel, change, complaint, upset) -> human
const esc = escalationReason(t);
if (esc) return { action:'handoff', reason:esc.reason, situation:esc.situation };
// 2. out of bounds (staff, internal, supplier, non-business) -> decline
const dec = declineReason(t);
if (dec) return { action:'decline', reason:dec };
// 3. information-type -> exactly one approved tool, else hand off
const tool = pickTool(t);
if (!tool) return { action:'handoff', reason:'no approved tool matches' };
return { action:'resolve', tool, reply: run(tool, text) };
}
2 · A governed tool = a fixed SQL query
The agent supplies bound values only. The SQL string is fixed in the page; no injected WHERE or UNION, no write path.
recommend_bouquets({ occasion, maxBudget, colour }){
const FQ = "SELECT id,name,flower,colour,price,url FROM products "
+ "WHERE occasions LIKE '%,'||?||',%' AND price <= ? "
+ "AND (?='' OR colour=?) ORDER BY price DESC LIMIT 2";
let rows = runRows(FQ, [occasion, maxBudget||99999, colour||'', colour||'']);
return rows.map(p => ({ ...p, reason: FLOWER_MEANING[p.flower] }));
}
3 · The decline rule
Nine categories of private, internal, or off-topic questions are refused with a fixed line and never transferred. Ms. Young is a public contact, so naming her is fine; her schedule and personal details are not.
const DEC = { staffCount, staffSchedule, staffContact, staffName,
supplier, margin, ownership, internal, nonbiz };
// "How many staff?", "Her personal number?", "Who supplies you?",
// "Your margin?", "Weather in KL?" ->
// "I'm sorry, but I'm unable to help with that." (no transfer)
4 · Live SQLite in the browser
The database is embedded as base64 and loaded into sql.js, so real fixed queries run client-side with no backend.
initSqlJs({ locateFile: f => CDN + f })
.then(SQL => { DB = new SQL.Database(b64ToBytes(window.BLOOM_DB_B64)); });
function runRows(sql, params){ // read-only helper
const s = DB.prepare(sql); s.bind(params);
const out = []; while (s.step()) out.push(s.getAsObject());
s.free(); return out;
}