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
|
||||
}
|
||||
|
||||
/** 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: {
|
||||
storeId: string
|
||||
query: string
|
||||
@ -17,6 +27,34 @@ export async function generateStorefrontReply(params: {
|
||||
const { storeId, query, visitorId, history } = params
|
||||
|
||||
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(
|
||||
(item, idx) => `[${idx + 1}] (${item.source}) ${item.title}: ${item.content}`
|
||||
)
|
||||
@ -30,12 +68,13 @@ export async function generateStorefrontReply(params: {
|
||||
})) as GroqChatMessage[]
|
||||
|
||||
const system = [
|
||||
"You are JUNO storefront assistant for an ecommerce store.",
|
||||
"Use the provided context first (products, FAQs, knowledge).",
|
||||
"If context is insufficient, ask one concise clarifying question.",
|
||||
"Do not invent exact prices, stock or policy details that are not present.",
|
||||
"Keep responses concise, friendly, and sales-supportive.",
|
||||
"Return plain text only.",
|
||||
"You are the JUNO storefront assistant. You ONLY answer using the Context block below (products, FAQs, knowledge chunks).",
|
||||
"If the Context does not clearly support an answer, say you cannot find it in the catalog or docs you have—do NOT guess.",
|
||||
"Never claim a product exists, is in stock, or that the store carries a category unless a Context line explicitly supports it.",
|
||||
"Never invent website layout: no tabs, menus, dropdowns, or navigation paths unless they appear verbatim in Context.",
|
||||
"Never invent URLs or deep links unless they appear in Context.",
|
||||
"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(" ")
|
||||
|
||||
const user = [
|
||||
@ -53,13 +92,13 @@ export async function generateStorefrontReply(params: {
|
||||
]
|
||||
|
||||
const out = await groqChatCompletion(messages, {
|
||||
temperature: 0.2,
|
||||
temperature: 0.1,
|
||||
maxTokens: 420,
|
||||
timeoutMs: 28_000,
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -7,12 +7,128 @@ type RetrievedContextItem = {
|
||||
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) {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.split(/[^a-z0-9]+/g)
|
||||
.map((t) => t.trim())
|
||||
.filter((t) => t.length >= 2)
|
||||
.filter((t) => t.length >= 2 && !STOPWORDS.has(t))
|
||||
}
|
||||
|
||||
function scoreByTerms(text: string, terms: string[]) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user