From 89ac6a9b5b4a7264dec9fd7f5909026bbda6e831 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 27 Apr 2026 20:49:15 -0500 Subject: [PATCH] =?UTF-8?q?demo:=20P1=20=E2=80=94=20search=20filter=20now?= =?UTF-8?q?=20actually=20filters=20by=20state=20and=20role?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Co-Pilot search box read state and role from the dropdowns (#sst, #srl) but appended them to the message string as ' in '+st. The server's NL parser then matched the literal preposition "in" against the case-insensitive regex /\b(IL|IN|...)\b/i and assigned state IN (Indiana) to every search. Result: typing "forklift in IL" returned Indiana workers. Same for WI, TX, any state — all silently became Indiana. That was the "cached/generic response" the legacy staffing client was seeing. Two prongs: 1. search.html doSearch() now passes structured fields: {message, state, role} instead of munging into the message text. Dropdown selections bypass NL parsing entirely. 2. /intelligence/chat smart_search route accepts those structured fields and prefers them over regex archaeology. Falls back to NL parsing only when fields aren't provided. Fixed the regex too: the prepositional form (?:in|from)\s+(STATE) wins, the standalone form requires uppercase (drops /i flag) so the lowercase preposition "in" can no longer match. Verified live: - POST /intelligence/chat {"message":"forklift","state":"IL"} → 167 IL forklift operators (Galesburg, Joliet, ...) - POST /intelligence/chat {"message":"forklift","state":"WI","role":"Forklift Operator"} → 16 WI Forklift Operators (Milwaukee, Madison, ...) - POST /intelligence/chat {"message":"forklift in IL"} (NL fallback) → 167 IL workers (regex now correctly distinguishes preposition from state code) Playwright drove the live UI through devop.live/lakehouse and confirmed the front-end posts the structured body and the result panel renders the right state. Restart sequence: kill old bun :3700, bun run mcp-server/index.ts. --- mcp-server/index.ts | 47 +++++++++++++++++++++++++++++++++--------- mcp-server/search.html | 12 ++++++----- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/mcp-server/index.ts b/mcp-server/index.ts index de804fa..9b4134b 100644 --- a/mcp-server/index.ts +++ b/mcp-server/index.ts @@ -1698,7 +1698,14 @@ async function main() { const filters: string[] = ["CAST(reliability AS DOUBLE) >= 0.5"]; const understood: string[] = []; - // Extract role keywords + // Structured input from the search-form dropdowns. When set, + // these win over NL parsing — typing "forklift in IL" used to + // misparse the preposition "in" as state IN (Indiana). Trust + // explicit user selection over regex archaeology. + const explicitState = String(b.state || "").trim().toUpperCase(); + const explicitRole = String(b.role || "").trim(); + + // Extract role keywords (skip if dropdown picked one) const roleKeywords: Record = { "warehouse": "warehouse", "forklift": "forklift", "welder": "weld", "assembler": "assembl", "loader": "loader", "machine operator": "machine operator", "shipping": "shipping", @@ -1707,8 +1714,13 @@ async function main() { "line lead": "line lead", "electrician": "electric", "packaging": "packaging", "tool and die": "tool", "logistics": "logistics", "safety": "safety", "cnc": "cnc", }; - for (const [kw, sqlPart] of Object.entries(roleKeywords)) { - if (lower.includes(kw)) { filters.push(`LOWER(role) LIKE '%${sqlPart}%'`); understood.push(`role: ${kw}`); break; } + if (explicitRole) { + filters.push(`LOWER(role) LIKE '%${explicitRole.toLowerCase().replace(/'/g, "''")}%'`); + understood.push(`role: ${explicitRole}`); + } else { + for (const [kw, sqlPart] of Object.entries(roleKeywords)) { + if (lower.includes(kw)) { filters.push(`LOWER(role) LIKE '%${sqlPart}%'`); understood.push(`role: ${kw}`); break; } + } } // Extract city @@ -1726,18 +1738,33 @@ async function main() { } } - // Extract state + // Extract state — dropdown wins; otherwise NL parse, but + // require either an explicit "in/from " preposition + // OR an UPPERCASE 2-letter code, never a bare lowercase + // 2-letter token. Old regex matched "in" (preposition) as + // state IN (Indiana) because the /i flag made the standalone + // pattern case-insensitive — "forklift in IL" always returned + // Indiana workers. const stateNames: Record = { "illinois":"IL","indiana":"IN","ohio":"OH","missouri":"MO","tennessee":"TN", "kentucky":"KY","wisconsin":"WI","michigan":"MI","iowa":"IA","minnesota":"MN" }; - const stateMatch = lower.match(/\b(IL|IN|OH|MO|TN|KY|WI|MI|IA|MN)\b/i); - if (stateMatch && !understood.some(u => u.startsWith('city'))) { - filters.push(`state = '${stateMatch[1].toUpperCase()}'`); - understood.push(`state: ${stateMatch[1].toUpperCase()}`); + if (explicitState) { + if (!understood.some(u => u.startsWith('city'))) { + filters.push(`state = '${explicitState.replace(/'/g, "''")}'`); + understood.push(`state: ${explicitState}`); + } } else { - for (const [name, abbr] of Object.entries(stateNames)) { - if (lower.includes(name)) { filters.push(`state = '${abbr}'`); understood.push(`state: ${abbr}`); break; } + const prepMatch = q.match(/\b(?:in|from)\s+(IL|IN|OH|MO|TN|KY|WI|MI|IA|MN)\b/i); + const upperMatch = q.match(/\b(IL|IN|OH|MO|TN|KY|WI|MI|IA|MN)\b/); // no /i — must be uppercase + const stateMatch = prepMatch || upperMatch; + if (stateMatch && !understood.some(u => u.startsWith('city'))) { + filters.push(`state = '${stateMatch[1].toUpperCase()}'`); + understood.push(`state: ${stateMatch[1].toUpperCase()}`); + } else { + for (const [name, abbr] of Object.entries(stateNames)) { + if (lower.includes(name)) { filters.push(`state = '${abbr}'`); understood.push(`state: ${abbr}`); break; } + } } } diff --git a/mcp-server/search.html b/mcp-server/search.html index 7a15c1c..1a3c4c1 100644 --- a/mcp-server/search.html +++ b/mcp-server/search.html @@ -2274,13 +2274,15 @@ function doSearch(){ var q=document.getElementById('sq').value.trim();if(!q)return; lastQuery=q; var st=document.getElementById('sst').value,rl=document.getElementById('srl').value; - // Append dropdown filters to the query so the smart parser picks them up - var fullQ=q; - if(st&&q.indexOf(st)<0)fullQ+=' in '+st; - if(rl&&q.toLowerCase().indexOf(rl.toLowerCase())<0)fullQ+=' '+rl; + // Pass dropdown filters as structured fields. Old code appended + // ' in '+st to the message, which the server misparsed: the + // preposition "in" matched the regex for state code "IN" (Indiana) + // and every search returned Indiana workers regardless of dropdown. + // Sending structured state/role lets the server skip NL parsing + // for those fields entirely. var out=document.getElementById('sresults');out.textContent='Finding the best matches...'; fetch(A+'/intelligence/chat',{method:'POST',headers:{'Content-Type':'application/json'}, - body:JSON.stringify({message:fullQ}) + body:JSON.stringify({message:q,state:st||undefined,role:rl||undefined}) }).then(function(r){return r.json()}).then(function(d){ out.textContent=''; // Show what the system understood