fixed: AI modules

This commit is contained in:
root 2026-04-14 22:05:28 +05:00
parent 9ccf3cf08c
commit 43149059d4
3 changed files with 302 additions and 71 deletions

View File

@ -8,6 +8,8 @@ import { generateStorefrontReply } from "@/lib/ai/storefrontGroq"
import { applyRateLimit } from "@/lib/rate-limit" import { applyRateLimit } from "@/lib/rate-limit"
import { storefrontChatPostSchema } from "@/lib/api-schemas/public" import { storefrontChatPostSchema } from "@/lib/api-schemas/public"
import { validationErrorResponse } from "@/lib/api-schemas/validate" import { validationErrorResponse } from "@/lib/api-schemas/validate"
import { extractOrderNumberHints } from "@/lib/ai/gmailContext"
import { getOrderDetailForVerifiedEmail } from "@/lib/ai/tools/orderTools"
function normalizeShopDomain(input: string) { function normalizeShopDomain(input: string) {
return input return input
@ -28,6 +30,32 @@ function shopVariants(cleanDomain: string) {
} }
const STOREFRONT_TICKET_TRIGGER = "STOREFRONT_TICKET" const STOREFRONT_TICKET_TRIGGER = "STOREFRONT_TICKET"
const ESCALATION_ALREADY_DISPATCHED_REPLY =
"Your request is already dispatched to our human support team. Please wait for the follow-up email from our team."
const ORDER_VERIFICATION_REQUIRED_REPLY =
"To help with order-specific requests, please verify your email first using the Order help section below."
const ORDER_NOT_FOUND_FOR_VERIFIED_EMAIL_REPLY =
"I could not find that order for your verified email. Please double-check the order number and try again."
function formatEscalationModuleLabel(moduleName?: string): string {
const raw = (moduleName || "CUSTOMER_SUPPORT").trim()
return raw
.split(/[_\s]+/)
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
.join(" ")
}
function buildStorefrontTicketSubject(moduleName?: string): string {
return `${formatEscalationModuleLabel(moduleName)} Request`
}
function buildOrderScopedTicketSubject(moduleName?: string, orderNumber?: string | null): string {
const base = buildStorefrontTicketSubject(moduleName)
const clean = (orderNumber || "").replace(/^\s*#?\s*/, "").trim()
if (!clean) return base
return `${base} - Order ${clean}`
}
function shouldCreateEscalationTicket(ai: { content: string; meta?: { usedTools?: string[]; module?: string } }): boolean { function shouldCreateEscalationTicket(ai: { content: string; meta?: { usedTools?: string[]; module?: string } }): boolean {
if (Array.isArray(ai.meta?.usedTools) && ai.meta.usedTools.includes("ticket_intake")) { if (Array.isArray(ai.meta?.usedTools) && ai.meta.usedTools.includes("ticket_intake")) {
@ -122,10 +150,11 @@ export async function POST(req: Request) {
const recentHistory = await prisma.storefrontChatMessage.findMany({ const recentHistory = await prisma.storefrontChatMessage.findMany({
where: { conversationId: conversation.id }, where: { conversationId: conversation.id },
orderBy: { createdAt: "asc" }, orderBy: { createdAt: "desc" },
take: 10, take: 4,
select: { senderType: true, content: true }, select: { senderType: true, content: true },
}) })
const historyForAi = recentHistory.reverse()
const sessionOk = const sessionOk =
Boolean(conversation.emailVerifiedAt) && Boolean(conversation.emailVerifiedAt) &&
@ -137,7 +166,7 @@ export async function POST(req: Request) {
storeName: store.businessName || "Our store", storeName: store.businessName || "Our store",
query: trimmedContent, query: trimmedContent,
visitorId, visitorId,
history: recentHistory.map((m) => ({ history: historyForAi.map((m) => ({
senderType: m.senderType, senderType: m.senderType,
content: m.content, content: m.content,
})), })),
@ -149,21 +178,6 @@ export async function POST(req: Request) {
boundShopifyOrderId: conversation.boundShopifyOrderId, boundShopifyOrderId: conversation.boundShopifyOrderId,
}) })
const aiMessage = await prisma.storefrontChatMessage.create({
data: {
conversationId: conversation.id,
senderType: "AI",
content: ai.content
},
select: { id: true, createdAt: true, senderType: true, content: true }
})
if (shouldCreateEscalationTicket(ai)) {
try {
const owner = await prisma.store.findUnique({
where: { id: store.id },
select: { userId: true },
})
const convoEmail = await prisma.storefrontChatConversation.findUnique({ const convoEmail = await prisma.storefrontChatConversation.findUnique({
where: { id: conversation.id }, where: { id: conversation.id },
select: { customerEmail: true }, select: { customerEmail: true },
@ -172,28 +186,70 @@ export async function POST(req: Request) {
convoEmail?.customerEmail?.trim().toLowerCase() || convoEmail?.customerEmail?.trim().toLowerCase() ||
customerEmail?.trim().toLowerCase() || customerEmail?.trim().toLowerCase() ||
`visitor+${visitorId}@storefront.local` `visitor+${visitorId}@storefront.local`
const ticketSubject = `[Storefront] ${ai.meta.module} · ${conversation.id}` const isEscalation = shouldCreateEscalationTicket(ai)
const verifiedEmail =
sessionOk && conversation.assertedEmail
? conversation.assertedEmail.trim().toLowerCase()
: null
const requestedOrderHint = extractOrderNumberHints(trimmedContent)[0] ?? null
const hasOrderSpecificRequest = Boolean(requestedOrderHint)
const verifiedOrder =
isEscalation && verifiedEmail && requestedOrderHint
? await getOrderDetailForVerifiedEmail(store.id, verifiedEmail, {
orderNumberHint: requestedOrderHint,
})
: null
const ticketSubject = buildOrderScopedTicketSubject(ai.meta?.module, verifiedOrder?.orderNumber)
const existing = await prisma.emailLog.findFirst({ // Security-first guardrail:
// For order-specific requests, only escalate when the referenced order is verified for the
// currently verified email session.
const blockedForMissingVerification = isEscalation && hasOrderSpecificRequest && !verifiedEmail
const blockedForOrderMismatch = isEscalation && hasOrderSpecificRequest && verifiedEmail && !verifiedOrder
const existingEscalation =
isEscalation && !blockedForMissingVerification && !blockedForOrderMismatch
? await prisma.emailLog.findFirst({
where: { where: {
storeId: store.id, storeId: store.id,
trigger: STOREFRONT_TICKET_TRIGGER,
to: ticketTo, to: ticketTo,
subject: ticketSubject, subject: ticketSubject,
trigger: STOREFRONT_TICKET_TRIGGER,
}, },
select: { id: true }, select: { id: true },
}) })
: null
if (existing) { const alreadyEscalated = isEscalation && Boolean(existingEscalation)
await prisma.emailLog.update({ const aiContent = blockedForMissingVerification
where: { id: existing.id }, ? ORDER_VERIFICATION_REQUIRED_REPLY
: blockedForOrderMismatch
? ORDER_NOT_FOUND_FOR_VERIFIED_EMAIL_REPLY
: alreadyEscalated
? ESCALATION_ALREADY_DISPATCHED_REPLY
: ai.content
const aiMessage = await prisma.storefrontChatMessage.create({
data: { data: {
bodyPreview: trimmedContent.slice(0, 2000), conversationId: conversation.id,
sentAt: new Date(), senderType: "AI",
status: "SENT", content: aiContent
}, },
select: { id: true, createdAt: true, senderType: true, content: true }
}) })
} else if (owner?.userId) {
if (
isEscalation &&
!alreadyEscalated &&
!blockedForMissingVerification &&
!blockedForOrderMismatch
) {
try {
const owner = await prisma.store.findUnique({
where: { id: store.id },
select: { userId: true },
})
if (owner?.userId) {
await prisma.emailLog.create({ await prisma.emailLog.create({
data: { data: {
userId: owner.userId, userId: owner.userId,

View File

@ -11,6 +11,58 @@ type WidgetMessage = {
senderType: "CUSTOMER" | "AI" senderType: "CUSTOMER" | "AI"
} }
function isLikelyHttpUrl(value: string) {
return /^https?:\/\/[^\s)]+$/i.test(value.trim())
}
function renderMessageContent(content: string) {
// Supports both markdown links: [label](https://...)
// and plain URLs in free text.
const markdownOrUrl = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)|(https?:\/\/[^\s)]+)/gi
const nodes: Array<React.ReactNode> = []
let lastIndex = 0
let match: RegExpExecArray | null
while ((match = markdownOrUrl.exec(content)) !== null) {
const [full] = match
const start = match.index
const end = start + full.length
if (start > lastIndex) {
nodes.push(content.slice(lastIndex, start))
}
const markdownLabel = match[1]
const markdownUrl = match[2]
const plainUrl = match[3]
const href = markdownUrl || plainUrl || ""
if (isLikelyHttpUrl(href)) {
nodes.push(
<a
key={`${start}-${href}`}
href={href}
target="_blank"
rel="noopener noreferrer"
className="underline underline-offset-2 break-all text-indigo-600 hover:text-indigo-700"
>
{markdownLabel || href}
</a>
)
} else {
nodes.push(full)
}
lastIndex = end
}
if (lastIndex < content.length) {
nodes.push(content.slice(lastIndex))
}
return nodes
}
function getOrCreateVisitorId(shop: string) { function getOrCreateVisitorId(shop: string) {
const key = `juno_storefront_visitor_id_${shop}` const key = `juno_storefront_visitor_id_${shop}`
try { try {
@ -34,6 +86,7 @@ export default function StorefrontChatWidgetPage() {
const [sending, setSending] = useState(false) const [sending, setSending] = useState(false)
const [sendError, setSendError] = useState<string | null>(null) const [sendError, setSendError] = useState<string | null>(null)
const [loadingHistory, setLoadingHistory] = useState(false) const [loadingHistory, setLoadingHistory] = useState(false)
const [aiTyping, setAiTyping] = useState(false)
const [verifyEmail, setVerifyEmail] = useState("") const [verifyEmail, setVerifyEmail] = useState("")
const [otpCode, setOtpCode] = useState("") const [otpCode, setOtpCode] = useState("")
@ -250,6 +303,19 @@ export default function StorefrontChatWidgetPage() {
if (!shop || !visitorId) return if (!shop || !visitorId) return
const text = composer.trim() const text = composer.trim()
if (!text) return if (!text) return
const typingStartedAt = Date.now()
const optimisticCustomerMessage: WidgetMessage = {
id: `local_customer_${Date.now()}`,
content: text,
senderType: "CUSTOMER",
createdAt: new Date().toISOString(),
}
setMessages((prev) => [...prev, optimisticCustomerMessage])
setComposer("")
setAiTyping(true)
setTimeout(() => composerRef.current?.focus(), 0)
try { try {
setSendError(null) setSendError(null)
@ -267,31 +333,25 @@ export default function StorefrontChatWidgetPage() {
const returned = Array.isArray(data?.messages) ? data.messages : [] const returned = Array.isArray(data?.messages) ? data.messages : []
if (returned.length) { if (returned.length) {
setMessages(prev => [ const returnedTyped = returned.map((m: any) => ({
...prev,
...returned.map((m: any) => ({
id: String(m.id || `local_${Date.now()}`), id: String(m.id || `local_${Date.now()}`),
content: String(m.content || ""), content: String(m.content || ""),
senderType: (m.senderType === "AI" ? "AI" : "CUSTOMER") as "AI" | "CUSTOMER", senderType: (m.senderType === "AI" ? "AI" : "CUSTOMER") as "AI" | "CUSTOMER",
createdAt: String(m.createdAt || new Date().toISOString()) createdAt: String(m.createdAt || new Date().toISOString())
})) }))
]) const aiReplies = returnedTyped.filter((m: WidgetMessage) => m.senderType === "AI")
} else { const elapsed = Date.now() - typingStartedAt
setMessages(prev => [ const minTypingMs = 1000
...prev, if (elapsed < minTypingMs) {
{ await new Promise((resolve) => setTimeout(resolve, minTypingMs - elapsed))
id: `local_${Date.now()}`,
content: text,
senderType: "CUSTOMER",
createdAt: new Date().toISOString()
} }
]) setMessages(prev => [...prev, ...aiReplies])
} }
setComposer("")
setTimeout(() => composerRef.current?.focus(), 0)
} catch (e: any) { } catch (e: any) {
setSendError(e?.message || "Failed to send message") setSendError(e?.message || "Failed to send message")
setMessages((prev) => prev.filter((m) => m.id !== optimisticCustomerMessage.id))
} finally { } finally {
setAiTyping(false)
setSending(false) setSending(false)
} }
} }
@ -342,10 +402,21 @@ export default function StorefrontChatWidgetPage() {
: "bg-indigo-600 text-white" : "bg-indigo-600 text-white"
}`} }`}
> >
{m.content} {renderMessageContent(m.content)}
</div> </div>
</div> </div>
))} ))}
{aiTyping ? (
<div className="flex justify-start">
<div className="max-w-[88%] px-3 py-3 rounded-2xl bg-white border border-slate-200">
<div className="flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full bg-slate-400 animate-bounce [animation-delay:-0.2s]" />
<span className="w-2 h-2 rounded-full bg-slate-400 animate-bounce [animation-delay:-0.1s]" />
<span className="w-2 h-2 rounded-full bg-slate-400 animate-bounce" />
</div>
</div>
</div>
) : null}
<div ref={messagesEndRef} /> <div ref={messagesEndRef} />
</div> </div>
)} )}

View File

@ -36,6 +36,8 @@ const WIDGET_CANCEL_GENERAL_HELP =
const FAQ_DISABLED_FALLBACK = const FAQ_DISABLED_FALLBACK =
"I don't know about this—please contact our support team for help." "I don't know about this—please contact our support team for help."
const OUT_OF_SCOPE_WIDGET_FALLBACK =
"I can only help with this store's products, orders, shipping, returns, and policies. Please ask a store-related question."
/** Limit customer-supplied text size (prompt injection + token cost). */ /** Limit customer-supplied text size (prompt injection + token cost). */
const L1_MAX_CUSTOMER_MESSAGE_CHARS = 6000 const L1_MAX_CUSTOMER_MESSAGE_CHARS = 6000
@ -201,6 +203,61 @@ function isGenericDeliveryEtaQuestion(message: string): boolean {
return !orderSpecificSignals return !orderSpecificSignals
} }
function isClearlyOutOfScopeMessage(message: string): boolean {
const text = message.trim().toLowerCase()
if (!text) return false
// Basic math / homework / coding assistant requests that are unrelated to storefront support.
if (
/(^|\b)(solve|calculate|what is|what's)\s+[\d\s+\-*/().^=]+(\?|$)/i.test(text) ||
/(^|\b)(math|algebra|geometry|calculus|equation|integral|derivative|prime number)(\b|$)/i.test(
text,
) ||
/(^|\b)(write code|debug|python|javascript|typescript|java|c\+\+|sql query)(\b|$)/i.test(
text,
)
) {
return true
}
// Storefront-relevant keywords should keep normal handling.
if (
/(order|track|shipping|delivery|return|refund|exchange|cancel|address|product|size|stock|policy|warranty|payment|store|vendor|supplier)/i.test(
text,
)
) {
return false
}
// Generic knowledge chatter is out of scope for the storefront widget.
if (
/(who is|who was|capital of|weather|news|joke|translate|recipe|movie|song|history|science|politics)/i.test(
text,
)
) {
return true
}
return false
}
function extractLastCustomerContext(
history: RunL1Input["history"],
currentMessage: string,
): string | null {
const normalizedCurrent = currentMessage.trim().toLowerCase()
for (let i = history.length - 1; i >= 0; i--) {
const msg = history[i]
if (msg.senderType === "AI" || msg.role === "assistant") continue
const text = (msg.content || "").trim()
if (!text) continue
if (text.length < 3) continue
if (text.toLowerCase() === normalizedCurrent) continue
return text.slice(0, 240)
}
return null
}
async function composeReply(params: { async function composeReply(params: {
storeName: string storeName: string
storeReference: string storeReference: string
@ -281,11 +338,30 @@ function isPolicyLikeReturnOrChangeQuestion(message: string, module: L1Module):
} }
export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result> { export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result> {
if (input.channel === "widget" && isClearlyOutOfScopeMessage(input.message)) {
return {
text: OUT_OF_SCOPE_WIDGET_FALLBACK,
meta: {
module: "UNKNOWN",
pillar: "K",
usedTools: [],
kbHits: 0,
productHits: 0,
},
}
}
const lastCustomerContext = extractLastCustomerContext(input.history, input.message)
const effectiveMessage =
lastCustomerContext
? `${input.message}\nPrior customer context from this same conversation: ${lastCustomerContext}`
: input.message
const modules = parseAiModulesJson(input.aiModulesJson) const modules = parseAiModulesJson(input.aiModulesJson)
const sharedAnswers = parseSharedFieldAnswersJson(input.sharedFieldAnswersJson) const sharedAnswers = parseSharedFieldAnswersJson(input.sharedFieldAnswersJson)
const retrieval = await getBlendedStorefrontContext({ const retrieval = await getBlendedStorefrontContext({
storeId: input.storeId, storeId: input.storeId,
query: input.message, query: effectiveMessage,
}) })
const kbContext = const kbContext =
retrieval.items.length > 0 retrieval.items.length > 0
@ -294,11 +370,11 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
.join("\n") .join("\n")
: "" : ""
const routed = await routeCustomerIntent(input.message) const routed = await routeCustomerIntent(effectiveMessage)
let activeModule: L1Module = routed.module let activeModule: L1Module = routed.module
if ( if (
(activeModule === "SHIPPING_TRACKING" || activeModule === "ORDER_STATUS") && (activeModule === "SHIPPING_TRACKING" || activeModule === "ORDER_STATUS") &&
isGenericDeliveryEtaQuestion(input.message) isGenericDeliveryEtaQuestion(effectiveMessage)
) { ) {
activeModule = "FAQ" activeModule = "FAQ"
} }
@ -324,8 +400,9 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
const type1Facts = formatSharedFieldsForModule(activeModule, sharedAnswers) const type1Facts = formatSharedFieldsForModule(activeModule, sharedAnswers)
const preferKnowledgeReplyOverTicket = const preferKnowledgeReplyOverTicket =
activeModule === "RETURN_EXCHANGE" || activeModule === "ORDER_CHANGE" activeModule === "RETURN_EXCHANGE" || activeModule === "ORDER_CHANGE"
? isPolicyLikeReturnOrChangeQuestion(input.message, activeModule) ? isPolicyLikeReturnOrChangeQuestion(effectiveMessage, activeModule)
: false : false
const forceKnowledgeOnly = activeModule === "WARRANTY"
const categoryMetadetailsOn = const categoryMetadetailsOn =
input.categoryMetadetailsEnabled !== undefined && input.categoryMetadetailsEnabled !== undefined &&
input.categoryMetadetailsEnabled !== null input.categoryMetadetailsEnabled !== null
@ -408,7 +485,7 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
ordersForEmail.length > 0 && ordersForEmail.length > 0 &&
(activeModule === "ORDER_SUMMARY" || activeModule === "ORDER_STATUS") (activeModule === "ORDER_SUMMARY" || activeModule === "ORDER_STATUS")
) { ) {
const listMode = detectOrderListMode(input.message) const listMode = detectOrderListMode(effectiveMessage)
if (listMode) { if (listMode) {
return { return {
text: buildOrderListText(ordersForEmail, listMode), text: buildOrderListText(ordersForEmail, listMode),
@ -435,7 +512,11 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
] ]
.filter(Boolean) .filter(Boolean)
.join("\n\n") .join("\n\n")
if (pillar === "X" && isAutomationEnabled(modules, activeModule, "ticket")) { if (
pillar === "X" &&
isAutomationEnabled(modules, activeModule, "ticket") &&
!forceKnowledgeOnly
) {
return { return {
text: ticketHandoffText(activeModule, input.storeName), text: ticketHandoffText(activeModule, input.storeName),
meta: { meta: {
@ -449,7 +530,8 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
} }
if ( if (
pillar === "T" && pillar === "T" &&
isAutomationEnabled(modules, activeModule, "ticket") isAutomationEnabled(modules, activeModule, "ticket") &&
!forceKnowledgeOnly
) { ) {
return { return {
text: ticketHandoffText(activeModule, input.storeName), text: ticketHandoffText(activeModule, input.storeName),
@ -465,7 +547,7 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
const text = await composeReply({ const text = await composeReply({
storeName: input.storeName, storeName: input.storeName,
storeReference: facts.trim() ? facts : EMPTY_STORE_REFERENCE, storeReference: facts.trim() ? facts : EMPTY_STORE_REFERENCE,
message: input.message, message: effectiveMessage,
history: historyToGroq(input.history), history: historyToGroq(input.history),
}) })
return { return {
@ -484,6 +566,7 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
pillar === "X" && pillar === "X" &&
ordersForEmail.length > 0 && ordersForEmail.length > 0 &&
isAutomationEnabled(modules, activeModule, "ticket") && isAutomationEnabled(modules, activeModule, "ticket") &&
!forceKnowledgeOnly &&
!preferKnowledgeReplyOverTicket !preferKnowledgeReplyOverTicket
) { ) {
return { return {
@ -498,11 +581,11 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
} }
} }
const hints = extractOrderNumberHints(input.message) const hints = extractOrderNumberHints(effectiveMessage)
const hintText = const hintText =
input.channel === "email" input.channel === "email"
? `${input.message}\n${(input.history || []).map((h) => h.content).join("\n")}` ? `${effectiveMessage}\n${(input.history || []).map((h) => h.content).join("\n")}`
: input.message : effectiveMessage
const disambiguate = const disambiguate =
customerEmail && customerEmail &&
@ -517,18 +600,39 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
customerEmail != null && customerEmail != null &&
ORDER_DETAIL_MODULES.includes(activeModule) && ORDER_DETAIL_MODULES.includes(activeModule) &&
!input.boundShopifyOrderId !input.boundShopifyOrderId
? pickMentionedOrderId(input.message, ordersForEmail) ? pickMentionedOrderId(effectiveMessage, ordersForEmail)
: null : null
const effectiveBoundShopifyOrderId = const effectiveBoundShopifyOrderId =
mentionedOrderId ?? mentionedOrderId ??
(shouldAutoPickLatestOrder && ordersForEmail[0] (shouldAutoPickLatestOrder && ordersForEmail[0]
? ordersForEmail[0].shopifyOrderId ? ordersForEmail[0].shopifyOrderId
: input.boundShopifyOrderId ?? null) : input.boundShopifyOrderId ?? null)
const hasExplicitOrderHint = hints.length > 0
if (
ORDER_DETAIL_MODULES.includes(activeModule) &&
customerEmail &&
hasExplicitOrderHint &&
!mentionedOrderId &&
isAutomationEnabled(modules, activeModule, "verifiedLookup")
) {
return {
text: "I could not find that order number for your verified email. Please double-check the order number and try again.",
meta: {
module: activeModule,
pillar,
usedTools: ["list_orders"],
kbHits: retrieval.kbCount,
productHits: retrieval.productCount,
},
}
}
if ( if (
ORDER_DETAIL_MODULES.includes(activeModule) && ORDER_DETAIL_MODULES.includes(activeModule) &&
customerEmail && customerEmail &&
disambiguate && disambiguate &&
!hasExplicitOrderHint &&
!effectiveBoundShopifyOrderId && !effectiveBoundShopifyOrderId &&
isAutomationEnabled(modules, activeModule, "verifiedLookup") isAutomationEnabled(modules, activeModule, "verifiedLookup")
) { ) {
@ -602,7 +706,7 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
isAutomationEnabled(modules, activeModule, "knowledge")) || isAutomationEnabled(modules, activeModule, "knowledge")) ||
(activeModule === "INVENTORY_STOCK" && isModuleEnabled(modules, activeModule)) (activeModule === "INVENTORY_STOCK" && isModuleEnabled(modules, activeModule))
if (runProductSearch) { if (runProductSearch) {
const hits = await searchProductsForStore(input.storeId, input.message, 5) const hits = await searchProductsForStore(input.storeId, effectiveMessage, 5)
usedTools.push("search_products") usedTools.push("search_products")
if (hits.length) { if (hits.length) {
productFacts = hits productFacts = hits
@ -635,7 +739,7 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
const text = await composeReply({ const text = await composeReply({
storeName: input.storeName, storeName: input.storeName,
storeReference: facts.trim() ? facts : EMPTY_STORE_REFERENCE, storeReference: facts.trim() ? facts : EMPTY_STORE_REFERENCE,
message: input.message, message: effectiveMessage,
history: historyToGroq(input.history), history: historyToGroq(input.history),
}) })