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:
+37
-10
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user