fixed: AI modules
This commit is contained in:
parent
9ccf3cf08c
commit
43149059d4
@ -8,6 +8,8 @@ import { generateStorefrontReply } from "@/lib/ai/storefrontGroq"
|
||||
import { applyRateLimit } from "@/lib/rate-limit"
|
||||
import { storefrontChatPostSchema } from "@/lib/api-schemas/public"
|
||||
import { validationErrorResponse } from "@/lib/api-schemas/validate"
|
||||
import { extractOrderNumberHints } from "@/lib/ai/gmailContext"
|
||||
import { getOrderDetailForVerifiedEmail } from "@/lib/ai/tools/orderTools"
|
||||
|
||||
function normalizeShopDomain(input: string) {
|
||||
return input
|
||||
@ -28,6 +30,32 @@ function shopVariants(cleanDomain: string) {
|
||||
}
|
||||
|
||||
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 {
|
||||
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({
|
||||
where: { conversationId: conversation.id },
|
||||
orderBy: { createdAt: "asc" },
|
||||
take: 10,
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 4,
|
||||
select: { senderType: true, content: true },
|
||||
})
|
||||
const historyForAi = recentHistory.reverse()
|
||||
|
||||
const sessionOk =
|
||||
Boolean(conversation.emailVerifiedAt) &&
|
||||
@ -137,7 +166,7 @@ export async function POST(req: Request) {
|
||||
storeName: store.businessName || "Our store",
|
||||
query: trimmedContent,
|
||||
visitorId,
|
||||
history: recentHistory.map((m) => ({
|
||||
history: historyForAi.map((m) => ({
|
||||
senderType: m.senderType,
|
||||
content: m.content,
|
||||
})),
|
||||
@ -149,21 +178,6 @@ export async function POST(req: Request) {
|
||||
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({
|
||||
where: { id: conversation.id },
|
||||
select: { customerEmail: true },
|
||||
@ -172,28 +186,70 @@ export async function POST(req: Request) {
|
||||
convoEmail?.customerEmail?.trim().toLowerCase() ||
|
||||
customerEmail?.trim().toLowerCase() ||
|
||||
`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: {
|
||||
storeId: store.id,
|
||||
trigger: STOREFRONT_TICKET_TRIGGER,
|
||||
to: ticketTo,
|
||||
subject: ticketSubject,
|
||||
trigger: STOREFRONT_TICKET_TRIGGER,
|
||||
},
|
||||
select: { id: true },
|
||||
})
|
||||
: null
|
||||
|
||||
if (existing) {
|
||||
await prisma.emailLog.update({
|
||||
where: { id: existing.id },
|
||||
const alreadyEscalated = isEscalation && Boolean(existingEscalation)
|
||||
const aiContent = blockedForMissingVerification
|
||||
? 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: {
|
||||
bodyPreview: trimmedContent.slice(0, 2000),
|
||||
sentAt: new Date(),
|
||||
status: "SENT",
|
||||
conversationId: conversation.id,
|
||||
senderType: "AI",
|
||||
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({
|
||||
data: {
|
||||
userId: owner.userId,
|
||||
|
||||
@ -11,6 +11,58 @@ type WidgetMessage = {
|
||||
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) {
|
||||
const key = `juno_storefront_visitor_id_${shop}`
|
||||
try {
|
||||
@ -34,6 +86,7 @@ export default function StorefrontChatWidgetPage() {
|
||||
const [sending, setSending] = useState(false)
|
||||
const [sendError, setSendError] = useState<string | null>(null)
|
||||
const [loadingHistory, setLoadingHistory] = useState(false)
|
||||
const [aiTyping, setAiTyping] = useState(false)
|
||||
|
||||
const [verifyEmail, setVerifyEmail] = useState("")
|
||||
const [otpCode, setOtpCode] = useState("")
|
||||
@ -250,6 +303,19 @@ export default function StorefrontChatWidgetPage() {
|
||||
if (!shop || !visitorId) return
|
||||
const text = composer.trim()
|
||||
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 {
|
||||
setSendError(null)
|
||||
@ -267,31 +333,25 @@ export default function StorefrontChatWidgetPage() {
|
||||
|
||||
const returned = Array.isArray(data?.messages) ? data.messages : []
|
||||
if (returned.length) {
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
...returned.map((m: any) => ({
|
||||
const returnedTyped = returned.map((m: any) => ({
|
||||
id: String(m.id || `local_${Date.now()}`),
|
||||
content: String(m.content || ""),
|
||||
senderType: (m.senderType === "AI" ? "AI" : "CUSTOMER") as "AI" | "CUSTOMER",
|
||||
createdAt: String(m.createdAt || new Date().toISOString())
|
||||
}))
|
||||
])
|
||||
} else {
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
{
|
||||
id: `local_${Date.now()}`,
|
||||
content: text,
|
||||
senderType: "CUSTOMER",
|
||||
createdAt: new Date().toISOString()
|
||||
const aiReplies = returnedTyped.filter((m: WidgetMessage) => m.senderType === "AI")
|
||||
const elapsed = Date.now() - typingStartedAt
|
||||
const minTypingMs = 1000
|
||||
if (elapsed < minTypingMs) {
|
||||
await new Promise((resolve) => setTimeout(resolve, minTypingMs - elapsed))
|
||||
}
|
||||
])
|
||||
setMessages(prev => [...prev, ...aiReplies])
|
||||
}
|
||||
setComposer("")
|
||||
setTimeout(() => composerRef.current?.focus(), 0)
|
||||
} catch (e: any) {
|
||||
setSendError(e?.message || "Failed to send message")
|
||||
setMessages((prev) => prev.filter((m) => m.id !== optimisticCustomerMessage.id))
|
||||
} finally {
|
||||
setAiTyping(false)
|
||||
setSending(false)
|
||||
}
|
||||
}
|
||||
@ -342,10 +402,21 @@ export default function StorefrontChatWidgetPage() {
|
||||
: "bg-indigo-600 text-white"
|
||||
}`}
|
||||
>
|
||||
{m.content}
|
||||
{renderMessageContent(m.content)}
|
||||
</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>
|
||||
)}
|
||||
|
||||
@ -36,6 +36,8 @@ const WIDGET_CANCEL_GENERAL_HELP =
|
||||
|
||||
const FAQ_DISABLED_FALLBACK =
|
||||
"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). */
|
||||
const L1_MAX_CUSTOMER_MESSAGE_CHARS = 6000
|
||||
@ -201,6 +203,61 @@ function isGenericDeliveryEtaQuestion(message: string): boolean {
|
||||
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: {
|
||||
storeName: string
|
||||
storeReference: string
|
||||
@ -281,11 +338,30 @@ function isPolicyLikeReturnOrChangeQuestion(message: string, module: L1Module):
|
||||
}
|
||||
|
||||
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 sharedAnswers = parseSharedFieldAnswersJson(input.sharedFieldAnswersJson)
|
||||
const retrieval = await getBlendedStorefrontContext({
|
||||
storeId: input.storeId,
|
||||
query: input.message,
|
||||
query: effectiveMessage,
|
||||
})
|
||||
const kbContext =
|
||||
retrieval.items.length > 0
|
||||
@ -294,11 +370,11 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
|
||||
.join("\n")
|
||||
: ""
|
||||
|
||||
const routed = await routeCustomerIntent(input.message)
|
||||
const routed = await routeCustomerIntent(effectiveMessage)
|
||||
let activeModule: L1Module = routed.module
|
||||
if (
|
||||
(activeModule === "SHIPPING_TRACKING" || activeModule === "ORDER_STATUS") &&
|
||||
isGenericDeliveryEtaQuestion(input.message)
|
||||
isGenericDeliveryEtaQuestion(effectiveMessage)
|
||||
) {
|
||||
activeModule = "FAQ"
|
||||
}
|
||||
@ -324,8 +400,9 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
|
||||
const type1Facts = formatSharedFieldsForModule(activeModule, sharedAnswers)
|
||||
const preferKnowledgeReplyOverTicket =
|
||||
activeModule === "RETURN_EXCHANGE" || activeModule === "ORDER_CHANGE"
|
||||
? isPolicyLikeReturnOrChangeQuestion(input.message, activeModule)
|
||||
? isPolicyLikeReturnOrChangeQuestion(effectiveMessage, activeModule)
|
||||
: false
|
||||
const forceKnowledgeOnly = activeModule === "WARRANTY"
|
||||
const categoryMetadetailsOn =
|
||||
input.categoryMetadetailsEnabled !== undefined &&
|
||||
input.categoryMetadetailsEnabled !== null
|
||||
@ -408,7 +485,7 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
|
||||
ordersForEmail.length > 0 &&
|
||||
(activeModule === "ORDER_SUMMARY" || activeModule === "ORDER_STATUS")
|
||||
) {
|
||||
const listMode = detectOrderListMode(input.message)
|
||||
const listMode = detectOrderListMode(effectiveMessage)
|
||||
if (listMode) {
|
||||
return {
|
||||
text: buildOrderListText(ordersForEmail, listMode),
|
||||
@ -435,7 +512,11 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n")
|
||||
if (pillar === "X" && isAutomationEnabled(modules, activeModule, "ticket")) {
|
||||
if (
|
||||
pillar === "X" &&
|
||||
isAutomationEnabled(modules, activeModule, "ticket") &&
|
||||
!forceKnowledgeOnly
|
||||
) {
|
||||
return {
|
||||
text: ticketHandoffText(activeModule, input.storeName),
|
||||
meta: {
|
||||
@ -449,7 +530,8 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
|
||||
}
|
||||
if (
|
||||
pillar === "T" &&
|
||||
isAutomationEnabled(modules, activeModule, "ticket")
|
||||
isAutomationEnabled(modules, activeModule, "ticket") &&
|
||||
!forceKnowledgeOnly
|
||||
) {
|
||||
return {
|
||||
text: ticketHandoffText(activeModule, input.storeName),
|
||||
@ -465,7 +547,7 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
|
||||
const text = await composeReply({
|
||||
storeName: input.storeName,
|
||||
storeReference: facts.trim() ? facts : EMPTY_STORE_REFERENCE,
|
||||
message: input.message,
|
||||
message: effectiveMessage,
|
||||
history: historyToGroq(input.history),
|
||||
})
|
||||
return {
|
||||
@ -484,6 +566,7 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
|
||||
pillar === "X" &&
|
||||
ordersForEmail.length > 0 &&
|
||||
isAutomationEnabled(modules, activeModule, "ticket") &&
|
||||
!forceKnowledgeOnly &&
|
||||
!preferKnowledgeReplyOverTicket
|
||||
) {
|
||||
return {
|
||||
@ -498,11 +581,11 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
|
||||
}
|
||||
}
|
||||
|
||||
const hints = extractOrderNumberHints(input.message)
|
||||
const hints = extractOrderNumberHints(effectiveMessage)
|
||||
const hintText =
|
||||
input.channel === "email"
|
||||
? `${input.message}\n${(input.history || []).map((h) => h.content).join("\n")}`
|
||||
: input.message
|
||||
? `${effectiveMessage}\n${(input.history || []).map((h) => h.content).join("\n")}`
|
||||
: effectiveMessage
|
||||
|
||||
const disambiguate =
|
||||
customerEmail &&
|
||||
@ -517,18 +600,39 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
|
||||
customerEmail != null &&
|
||||
ORDER_DETAIL_MODULES.includes(activeModule) &&
|
||||
!input.boundShopifyOrderId
|
||||
? pickMentionedOrderId(input.message, ordersForEmail)
|
||||
? pickMentionedOrderId(effectiveMessage, ordersForEmail)
|
||||
: null
|
||||
const effectiveBoundShopifyOrderId =
|
||||
mentionedOrderId ??
|
||||
(shouldAutoPickLatestOrder && ordersForEmail[0]
|
||||
? ordersForEmail[0].shopifyOrderId
|
||||
: 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 (
|
||||
ORDER_DETAIL_MODULES.includes(activeModule) &&
|
||||
customerEmail &&
|
||||
disambiguate &&
|
||||
!hasExplicitOrderHint &&
|
||||
!effectiveBoundShopifyOrderId &&
|
||||
isAutomationEnabled(modules, activeModule, "verifiedLookup")
|
||||
) {
|
||||
@ -602,7 +706,7 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
|
||||
isAutomationEnabled(modules, activeModule, "knowledge")) ||
|
||||
(activeModule === "INVENTORY_STOCK" && isModuleEnabled(modules, activeModule))
|
||||
if (runProductSearch) {
|
||||
const hits = await searchProductsForStore(input.storeId, input.message, 5)
|
||||
const hits = await searchProductsForStore(input.storeId, effectiveMessage, 5)
|
||||
usedTools.push("search_products")
|
||||
if (hits.length) {
|
||||
productFacts = hits
|
||||
@ -635,7 +739,7 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
|
||||
const text = await composeReply({
|
||||
storeName: input.storeName,
|
||||
storeReference: facts.trim() ? facts : EMPTY_STORE_REFERENCE,
|
||||
message: input.message,
|
||||
message: effectiveMessage,
|
||||
history: historyToGroq(input.history),
|
||||
})
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user