fix(storefront-chat): ground AI replies in catalog and stop KB noise
- Filter stopwords from retrieval term matching so generic words do not score unrelated KB chunks. - For product-availability questions with no product or KB hits, return a deterministic cannot-confirm reply (skip LLM). - Tighten system prompt: no invented inventory, menus, or URLs; lower temperature. Made-with: Cursor
This commit is contained in:
parent
53c2ed7a4e
commit
aa871ff942
@ -7,6 +7,16 @@ type HistoryMessage = {
|
|||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Customer is asking whether the store carries / sells a product (not policy/shipping). */
|
||||||
|
function isAskingAboutProductAvailability(query: string) {
|
||||||
|
return /\b(do you have|have any|have you got|carry|carrying|sell|selling|in stock|out of stock|stock|available|got any|do you sell|do you carry|looking for|can i (get|buy)|can you get)\b/i.test(
|
||||||
|
query.trim()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const NO_EVIDENCE_PRODUCT_REPLY =
|
||||||
|
"I do not see that item in this store’s catalog or FAQs I have access to, so I can’t confirm we carry it. If you have another product name or category from this shop, I can check again."
|
||||||
|
|
||||||
export async function generateStorefrontReply(params: {
|
export async function generateStorefrontReply(params: {
|
||||||
storeId: string
|
storeId: string
|
||||||
query: string
|
query: string
|
||||||
@ -17,6 +27,34 @@ export async function generateStorefrontReply(params: {
|
|||||||
const { storeId, query, visitorId, history } = params
|
const { storeId, query, visitorId, history } = params
|
||||||
|
|
||||||
const retrieval = await getBlendedStorefrontContext({ storeId, query })
|
const retrieval = await getBlendedStorefrontContext({ storeId, query })
|
||||||
|
|
||||||
|
// Hard guard: do not invent inventory. If they ask “do you have X?” and we have no product row and no KB/FAQ hit, answer without the LLM.
|
||||||
|
if (
|
||||||
|
isAskingAboutProductAvailability(query) &&
|
||||||
|
retrieval.productCount === 0 &&
|
||||||
|
retrieval.kbCount === 0
|
||||||
|
) {
|
||||||
|
logger.info("[STOREFRONT_AI_REPLY]", {
|
||||||
|
storeId,
|
||||||
|
visitorId,
|
||||||
|
queryLength: query.length,
|
||||||
|
kbHits: retrieval.kbCount,
|
||||||
|
productHits: retrieval.productCount,
|
||||||
|
latencyMs: Date.now() - startedAt,
|
||||||
|
fallbackUsed: true,
|
||||||
|
groundedSkipLlm: true,
|
||||||
|
error: null,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
content: NO_EVIDENCE_PRODUCT_REPLY,
|
||||||
|
meta: {
|
||||||
|
kbHits: retrieval.kbCount,
|
||||||
|
productHits: retrieval.productCount,
|
||||||
|
fallbackUsed: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const contextLines = retrieval.items.map(
|
const contextLines = retrieval.items.map(
|
||||||
(item, idx) => `[${idx + 1}] (${item.source}) ${item.title}: ${item.content}`
|
(item, idx) => `[${idx + 1}] (${item.source}) ${item.title}: ${item.content}`
|
||||||
)
|
)
|
||||||
@ -30,12 +68,13 @@ export async function generateStorefrontReply(params: {
|
|||||||
})) as GroqChatMessage[]
|
})) as GroqChatMessage[]
|
||||||
|
|
||||||
const system = [
|
const system = [
|
||||||
"You are JUNO storefront assistant for an ecommerce store.",
|
"You are the JUNO storefront assistant. You ONLY answer using the Context block below (products, FAQs, knowledge chunks).",
|
||||||
"Use the provided context first (products, FAQs, knowledge).",
|
"If the Context does not clearly support an answer, say you cannot find it in the catalog or docs you have—do NOT guess.",
|
||||||
"If context is insufficient, ask one concise clarifying question.",
|
"Never claim a product exists, is in stock, or that the store carries a category unless a Context line explicitly supports it.",
|
||||||
"Do not invent exact prices, stock or policy details that are not present.",
|
"Never invent website layout: no tabs, menus, dropdowns, or navigation paths unless they appear verbatim in Context.",
|
||||||
"Keep responses concise, friendly, and sales-supportive.",
|
"Never invent URLs or deep links unless they appear in Context.",
|
||||||
"Return plain text only.",
|
"Do not affirm availability with “yes” unless a PRODUCT line or FAQ/KB line in Context clearly covers what the customer asked.",
|
||||||
|
"Be concise and friendly. Plain text only.",
|
||||||
].join(" ")
|
].join(" ")
|
||||||
|
|
||||||
const user = [
|
const user = [
|
||||||
@ -53,13 +92,13 @@ export async function generateStorefrontReply(params: {
|
|||||||
]
|
]
|
||||||
|
|
||||||
const out = await groqChatCompletion(messages, {
|
const out = await groqChatCompletion(messages, {
|
||||||
temperature: 0.2,
|
temperature: 0.1,
|
||||||
maxTokens: 420,
|
maxTokens: 420,
|
||||||
timeoutMs: 28_000,
|
timeoutMs: 28_000,
|
||||||
})
|
})
|
||||||
|
|
||||||
const fallback =
|
const fallback =
|
||||||
"Thanks for your message. I do not have enough details right now. Could you share the product name or what exactly you want to know?"
|
"I don’t have enough in the store’s catalog or FAQ context to answer that accurately. What product name or topic from this shop should I look for?"
|
||||||
|
|
||||||
const content = (out.content || "").trim() || fallback
|
const content = (out.content || "").trim() || fallback
|
||||||
|
|
||||||
|
|||||||
@ -7,12 +7,128 @@ type RetrievedContextItem = {
|
|||||||
score: number
|
score: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Strip generic words so "do you have socks?" does not match random KB via "you"/"do". */
|
||||||
|
const STOPWORDS = new Set([
|
||||||
|
"the",
|
||||||
|
"a",
|
||||||
|
"an",
|
||||||
|
"and",
|
||||||
|
"or",
|
||||||
|
"but",
|
||||||
|
"in",
|
||||||
|
"on",
|
||||||
|
"at",
|
||||||
|
"to",
|
||||||
|
"for",
|
||||||
|
"of",
|
||||||
|
"as",
|
||||||
|
"by",
|
||||||
|
"with",
|
||||||
|
"from",
|
||||||
|
"is",
|
||||||
|
"are",
|
||||||
|
"was",
|
||||||
|
"were",
|
||||||
|
"be",
|
||||||
|
"been",
|
||||||
|
"being",
|
||||||
|
"have",
|
||||||
|
"has",
|
||||||
|
"had",
|
||||||
|
"do",
|
||||||
|
"does",
|
||||||
|
"did",
|
||||||
|
"will",
|
||||||
|
"would",
|
||||||
|
"could",
|
||||||
|
"should",
|
||||||
|
"may",
|
||||||
|
"might",
|
||||||
|
"must",
|
||||||
|
"can",
|
||||||
|
"this",
|
||||||
|
"that",
|
||||||
|
"these",
|
||||||
|
"those",
|
||||||
|
"i",
|
||||||
|
"me",
|
||||||
|
"my",
|
||||||
|
"you",
|
||||||
|
"your",
|
||||||
|
"it",
|
||||||
|
"its",
|
||||||
|
"we",
|
||||||
|
"our",
|
||||||
|
"they",
|
||||||
|
"them",
|
||||||
|
"their",
|
||||||
|
"what",
|
||||||
|
"which",
|
||||||
|
"who",
|
||||||
|
"whom",
|
||||||
|
"whose",
|
||||||
|
"where",
|
||||||
|
"when",
|
||||||
|
"why",
|
||||||
|
"how",
|
||||||
|
"all",
|
||||||
|
"each",
|
||||||
|
"every",
|
||||||
|
"both",
|
||||||
|
"few",
|
||||||
|
"more",
|
||||||
|
"most",
|
||||||
|
"other",
|
||||||
|
"some",
|
||||||
|
"such",
|
||||||
|
"no",
|
||||||
|
"nor",
|
||||||
|
"not",
|
||||||
|
"only",
|
||||||
|
"own",
|
||||||
|
"same",
|
||||||
|
"so",
|
||||||
|
"than",
|
||||||
|
"too",
|
||||||
|
"very",
|
||||||
|
"just",
|
||||||
|
"any",
|
||||||
|
"there",
|
||||||
|
"here",
|
||||||
|
"about",
|
||||||
|
"into",
|
||||||
|
"through",
|
||||||
|
"during",
|
||||||
|
"before",
|
||||||
|
"after",
|
||||||
|
"above",
|
||||||
|
"below",
|
||||||
|
"between",
|
||||||
|
"under",
|
||||||
|
"again",
|
||||||
|
"further",
|
||||||
|
"then",
|
||||||
|
"once",
|
||||||
|
"please",
|
||||||
|
"thanks",
|
||||||
|
"thank",
|
||||||
|
"hello",
|
||||||
|
"hi",
|
||||||
|
"hey",
|
||||||
|
"yes",
|
||||||
|
"no",
|
||||||
|
"tell",
|
||||||
|
"give",
|
||||||
|
"get",
|
||||||
|
"got",
|
||||||
|
])
|
||||||
|
|
||||||
function tokenize(text: string) {
|
function tokenize(text: string) {
|
||||||
return text
|
return text
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.split(/[^a-z0-9]+/g)
|
.split(/[^a-z0-9]+/g)
|
||||||
.map((t) => t.trim())
|
.map((t) => t.trim())
|
||||||
.filter((t) => t.length >= 2)
|
.filter((t) => t.length >= 2 && !STOPWORDS.has(t))
|
||||||
}
|
}
|
||||||
|
|
||||||
function scoreByTerms(text: string, terms: string[]) {
|
function scoreByTerms(text: string, terms: string[]) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user