demo: P1 — search filter now actually filters by state and role

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.
This commit is contained in:
root 2026-04-27 20:49:15 -05:00
parent ed57eda1d8
commit fb99e92a60
2 changed files with 44 additions and 15 deletions

View File

@ -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<string, string> = {
"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 <STATE>" 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<string, string> = {
"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; }
}
}
}

View File

@ -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