Lucy · Source Code

The resolve / escalate / decline gate, a governed toolset, and live SQLite

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; }