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:
parent
c3c9c2174a
commit
89ac6a9b5b
@ -1698,7 +1698,14 @@ async function main() {
|
|||||||
const filters: string[] = ["CAST(reliability AS DOUBLE) >= 0.5"];
|
const filters: string[] = ["CAST(reliability AS DOUBLE) >= 0.5"];
|
||||||
const understood: string[] = [];
|
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> = {
|
const roleKeywords: Record<string, string> = {
|
||||||
"warehouse": "warehouse", "forklift": "forklift", "welder": "weld", "assembler": "assembl",
|
"warehouse": "warehouse", "forklift": "forklift", "welder": "weld", "assembler": "assembl",
|
||||||
"loader": "loader", "machine operator": "machine operator", "shipping": "shipping",
|
"loader": "loader", "machine operator": "machine operator", "shipping": "shipping",
|
||||||
@ -1707,8 +1714,13 @@ async function main() {
|
|||||||
"line lead": "line lead", "electrician": "electric", "packaging": "packaging",
|
"line lead": "line lead", "electrician": "electric", "packaging": "packaging",
|
||||||
"tool and die": "tool", "logistics": "logistics", "safety": "safety", "cnc": "cnc",
|
"tool and die": "tool", "logistics": "logistics", "safety": "safety", "cnc": "cnc",
|
||||||
};
|
};
|
||||||
for (const [kw, sqlPart] of Object.entries(roleKeywords)) {
|
if (explicitRole) {
|
||||||
if (lower.includes(kw)) { filters.push(`LOWER(role) LIKE '%${sqlPart}%'`); understood.push(`role: ${kw}`); break; }
|
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
|
// 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> = {
|
const stateNames: Record<string, string> = {
|
||||||
"illinois":"IL","indiana":"IN","ohio":"OH","missouri":"MO","tennessee":"TN",
|
"illinois":"IL","indiana":"IN","ohio":"OH","missouri":"MO","tennessee":"TN",
|
||||||
"kentucky":"KY","wisconsin":"WI","michigan":"MI","iowa":"IA","minnesota":"MN"
|
"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 (explicitState) {
|
||||||
if (stateMatch && !understood.some(u => u.startsWith('city'))) {
|
if (!understood.some(u => u.startsWith('city'))) {
|
||||||
filters.push(`state = '${stateMatch[1].toUpperCase()}'`);
|
filters.push(`state = '${explicitState.replace(/'/g, "''")}'`);
|
||||||
understood.push(`state: ${stateMatch[1].toUpperCase()}`);
|
understood.push(`state: ${explicitState}`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const [name, abbr] of Object.entries(stateNames)) {
|
const prepMatch = q.match(/\b(?:in|from)\s+(IL|IN|OH|MO|TN|KY|WI|MI|IA|MN)\b/i);
|
||||||
if (lower.includes(name)) { filters.push(`state = '${abbr}'`); understood.push(`state: ${abbr}`); break; }
|
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; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2274,13 +2274,15 @@ function doSearch(){
|
|||||||
var q=document.getElementById('sq').value.trim();if(!q)return;
|
var q=document.getElementById('sq').value.trim();if(!q)return;
|
||||||
lastQuery=q;
|
lastQuery=q;
|
||||||
var st=document.getElementById('sst').value,rl=document.getElementById('srl').value;
|
var st=document.getElementById('sst').value,rl=document.getElementById('srl').value;
|
||||||
// Append dropdown filters to the query so the smart parser picks them up
|
// Pass dropdown filters as structured fields. Old code appended
|
||||||
var fullQ=q;
|
// ' in '+st to the message, which the server misparsed: the
|
||||||
if(st&&q.indexOf(st)<0)fullQ+=' in '+st;
|
// preposition "in" matched the regex for state code "IN" (Indiana)
|
||||||
if(rl&&q.toLowerCase().indexOf(rl.toLowerCase())<0)fullQ+=' '+rl;
|
// 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...';
|
var out=document.getElementById('sresults');out.textContent='Finding the best matches...';
|
||||||
fetch(A+'/intelligence/chat',{method:'POST',headers:{'Content-Type':'application/json'},
|
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){
|
}).then(function(r){return r.json()}).then(function(d){
|
||||||
out.textContent='';
|
out.textContent='';
|
||||||
// Show what the system understood
|
// Show what the system understood
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user