feat: force a seller scan over active conversations from the inbox
Adds a "Scan for sellers" button to the conversations header, backed by POST /api/conversations/seller-scan. The sweep reuses the existing seller detection rather than inventing a second one: the configured keyword/domain list, the seller-link funnel patterns, and the aiReplyScreening confidences all decide the verdict, so a swept conversation lands in the state the live reply loop would have given it. Being operator-forced, it deliberately ignores filterSellers and aiReplyScreening.enabled. AI is the last resort. Conversations are skipped when already flagged or killed, settled for free by the deterministic rules, or skipped against a durable sellerScreenedKey watermark when nothing new has arrived since the last clean sweep. Only the remainder reaches the model, batched eight at a time through a new screenConversationsForSellers, so the system prompt and round trip are paid per batch instead of per conversation. A classifier outage degrades the sweep instead of failing it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0a4e0064f2
commit
827eb3a1e0
15 files changed
+1169
-1
No files matched your search
@@ -31,6 +31,12 @@ export interface ConversationState {
|
||||
boundaries: string[];
|
||||
/** Durable marker set when configured seller or upsell signals are detected. */
|
||||
sellerDetected: boolean;
|
||||
/**
|
||||
* Watermark of the last inbound message an operator-forced seller sweep has
|
||||
* already screened and cleared. Re-running the sweep skips a conversation
|
||||
* whose watermark is unchanged, so repeated presses cost no AI calls.
|
||||
*/
|
||||
sellerScreenedKey?: string;
|
||||
/** Kept for backwards compatibility with state written before automation. */
|
||||
askedForMedia: boolean;
|
||||
topics: string[];
|
||||
@@ -178,6 +184,7 @@ export function mergeConversationState(
|
||||
},
|
||||
boundaries: [...new Set([...saved.boundaries, ...observed.boundaries])],
|
||||
sellerDetected: saved.sellerDetected || observed.sellerDetected,
|
||||
...(saved.sellerScreenedKey ? { sellerScreenedKey: saved.sellerScreenedKey } : {}),
|
||||
askedForMedia: saved.askedForMedia || observed.askedForMedia,
|
||||
topics: [...new Set([...saved.topics, ...observed.topics])],
|
||||
automation: saved.automation,
|
||||
@@ -282,6 +289,7 @@ export function parseConversationState(value: string | null | undefined): Conver
|
||||
sellerDetected: typeof parsed.sellerDetected === 'boolean'
|
||||
? parsed.sellerDetected
|
||||
: automation.handoffReason?.startsWith('Potential upsell detected') === true,
|
||||
...(typeof parsed.sellerScreenedKey === 'string' ? { sellerScreenedKey: parsed.sellerScreenedKey } : {}),
|
||||
askedForMedia: parsed.askedForMedia,
|
||||
topics: parsed.topics.filter((value): value is string => typeof value === 'string'),
|
||||
automation,
|
||||
|
||||
Reference in new issue
Block a user