АДРЕСНЫЙ РЕЖИМ -Step-4: Batch-2 lifecycle закрыт gate 36/36, nightly 102/102 + 25/25 PASS

This commit is contained in:
2026-04-08 13:39:40 +03:00
parent 1f9bb886ee
commit 71762af575
41 changed files with 50617 additions and 53 deletions
@@ -268,6 +268,14 @@ const COUNTERPARTY_ACTIVITY_LIFECYCLE_HINTS = [
"регулярные поставщики",
"эпизодические поставщики",
"давно не использовались поставщики",
"всех заков",
"кто был активен",
"потом отвалился",
"ровно один раз",
"и пропал",
"самые старые по сотрудничеству",
"разбей поставщиков на регуляр и разовые",
"кто новые в этом году",
"active customers",
"customer activity list",
"counterparty lifecycle"
@@ -605,13 +613,13 @@ function hasCounterpartyPopulationAndRolesSignal(text: string): boolean {
}
function hasLifecycleSegmentationSignal(text: string): boolean {
return /(?:вперв|нов(?:ые|ых|ые\s+контрагент|ые\s+клиент|ые\s+заказчик)|исчез|ушед|ушл|только\s+один\s+раз|однораз|дольше\s+всех|долгожив|регулярн|эпизодич|давно\s+не\s+использ|неиспольз|потом\s+перестал)/iu.test(
return /(?:вперв|нов(?:ые|ых|ые\s+контрагент|ые\s+клиент|ые\s+заказчик)|исчез|ушед|ушл|пропал|отвал|только\s+один\s+раз|ровно\s+один\s+раз|однораз|дольше\s+всех|долгожив|самые\s+старые|старые\s+по\s+сотрудничеству|регуляр|эпизодич|разов(?:ые|ой|ые\s+поставщик)|давно\s+не\s+использ|неиспольз|потом\s+перестал)/iu.test(
text
);
}
function hasCounterpartyActivityLifecycleSignal(text: string): boolean {
if (hasDocumentSignal(text) || hasBankOperationSignal(text)) {
if ((hasDocumentSignal(text) || hasBankOperationSignal(text)) && !hasLifecycleSegmentationSignal(text)) {
return false;
}
if (hasAny(text, COUNTERPARTY_ACTIVITY_LIFECYCLE_HINTS)) {
@@ -633,6 +641,10 @@ function hasCounterpartyActivityLifecycleSignal(text: string): boolean {
);
const hasListVerb = /(?:какие|кто|покажи|выведи|список|list|show)/iu.test(text);
const hasRosterQualifier = /(?:у\s+нас|вообще|в\s+баз[еы]|какие\s+есть|кто\s+есть|who\s+are)/iu.test(text);
const hasImplicitCounterpartyQuestion =
/(?:кто\s+с\s+нами|кто\s+у\s+нас|всех?\s+зак(?:ов|а|и)?|все\s+заки|кто\s+нов(?:ые|ых|ый)\b|кто\s+был\s+активен|самые\s+старые\s+по\s+сотрудничеству)/iu.test(
text
);
const hasListWithWindow = hasCounterpartyLexeme && hasListVerb && hasTimeWindowLexeme;
if (hasListWithWindow) {
return true;
@@ -643,6 +655,12 @@ function hasCounterpartyActivityLifecycleSignal(text: string): boolean {
if (hasCounterpartyLexeme && hasLifecycleSegmentationSignal(text)) {
return true;
}
if (hasImplicitCounterpartyQuestion && (hasLifecycleSegmentationSignal(text) || hasTimeWindowLexeme || hasActivityLexeme)) {
return true;
}
if (!hasCounterpartyLexeme && hasListVerb && hasLifecycleSegmentationSignal(text) && /\bкто\b/iu.test(text)) {
return true;
}
return hasCounterpartyLexeme && hasActivityLexeme && (hasTimeWindowLexeme || hasListVerb);
}
@@ -57,6 +57,10 @@ function hasSameDateHint(text: string): boolean {
);
}
function hasExplicitPeriodLiteral(text: string): boolean {
return /\b(?:19|20)\d{2}(?:[./-](?:0?[1-9]|1[0-2]))?\b/.test(String(text ?? ""));
}
function hasOpenItemsHint(text: string): boolean {
return /(?:open\s+items|unclosed\s+items|хвост|висят|незакрыт|не\s+закрыт|открыт|долг|задолж|позиц)/iu.test(String(text ?? ""));
}
@@ -393,6 +397,20 @@ function mergeFollowupFilters(
return { filters: merged, reasons };
}
if (
intent === "counterparty_activity_lifecycle" &&
hasAddressFollowupContextSignal(userMessage) &&
!hasExplicitPeriodLiteral(userMessage)
) {
const currentPeriodFrom = toNonEmptyString(merged.period_from);
const currentPeriodTo = toNonEmptyString(merged.period_to);
const todayIso = new Date().toISOString().slice(0, 10);
if (!currentPeriodFrom && currentPeriodTo === todayIso) {
delete merged.period_to;
reasons.push("period_to_cleared_for_lifecycle_followup");
}
}
const currentHasPeriod = hasExplicitPeriodWindow(merged);
const previousHasPeriod = hasExplicitPeriodWindow(previous);
if (!currentHasPeriod && previousHasPeriod && hasAddressFollowupContextSignal(userMessage)) {
@@ -543,9 +561,20 @@ export function runAddressDecomposeStage(
followupContext: AddressFollowupContext | null
): AddressDecomposeStageResult | null {
const detectedMode = detectAddressQuestionMode(userMessage);
const shape = classifyAddressQueryShape(userMessage);
if (shape.shape === "EXPLAIN_OR_REASON") {
return null;
}
const detectedIntent = resolveAddressIntent(userMessage);
const mode =
detectedMode.mode === "address_query"
? detectedMode
: detectedIntent.intent !== "unknown"
? {
mode: "address_query" as const,
confidence: "medium" as const,
reasons: [...detectedMode.reasons, "address_mode_from_resolved_intent"]
}
: followupContext && hasAddressFollowupContextSignal(userMessage)
? {
mode: "address_query" as const,
@@ -556,13 +585,6 @@ export function runAddressDecomposeStage(
if (mode.mode !== "address_query") {
return null;
}
const shape = classifyAddressQueryShape(userMessage);
if (shape.shape === "EXPLAIN_OR_REASON") {
return null;
}
const detectedIntent = resolveAddressIntent(userMessage);
const intent = deriveIntentWithFollowupContext(detectedIntent, userMessage, followupContext);
const extractedFilters = extractAddressFilters(userMessage, intent.intent);
const followupMerged = mergeFollowupFilters(extractedFilters.extracted_filters, intent.intent, userMessage, followupContext);
@@ -1391,10 +1391,10 @@ function hasAccountingSignal(text) {
}
function hasFollowupMarker(text) {
const compact = compactWhitespace(text.toLowerCase());
return /^(и|а еще|а ещё|еще|ещё|добав|уточн|продолж|также|а если|а теперь|теперь|plus|also|dobav|utochn|prodolzh|then|now)/i.test(compact);
return /^(и|а\s+кто|а еще|а ещё|еще|ещё|добав|уточн|продолж|также|а если|а теперь|теперь|plus|also|dobav|utochn|prodolzh|then|now)/i.test(compact);
}
function hasReferentialPointer(text) {
return /(по этому|по тому|это же|этой|этим|этому|из этого|в этом|тот же|same thing|that one|po etomu|po tomu)/i.test(text.toLowerCase());
return /(по этому|по тому|это же|этой|этим|этому|из этого|из них|из этих|из тех|в этом|тот же|same thing|that one|po etomu|po tomu)/i.test(text.toLowerCase());
}
function hasSmallTalkSignal(text) {
return /(привет|как дела|спасибо|благодарю|thanks|thank you|hello|hi)\b/i.test(text.toLowerCase());
@@ -2374,6 +2374,30 @@ function readAddressFilterString(addressDebug, key) {
}
return toNonEmptyString(filters[key]);
}
function isAddressLaneDebugPayload(debug) {
if (!debug || typeof debug !== "object") {
return false;
}
if (debug.detected_mode === "address_query") {
return true;
}
if (typeof debug.selected_recipe === "string" && debug.selected_recipe.trim().length > 0) {
return true;
}
if (typeof debug.mcp_call_status === "string" && debug.mcp_call_status.trim().length > 0) {
return true;
}
if (typeof debug.anchor_type === "string" && debug.anchor_type.trim().length > 0) {
return true;
}
if (debug.extracted_filters && typeof debug.extracted_filters === "object") {
const keys = Object.keys(debug.extracted_filters);
if (keys.length > 0 && typeof debug.detected_intent === "string" && debug.detected_intent.trim().length > 0) {
return true;
}
}
return false;
}
function findLastAddressAssistantDebug(items) {
for (let index = items.length - 1; index >= 0; index -= 1) {
const item = items[index];
@@ -2381,7 +2405,7 @@ function findLastAddressAssistantDebug(items) {
continue;
}
const debug = item.debug;
if (debug.detected_mode === "address_query" || debug.prompt_version === "address_query_runtime_v1") {
if (isAddressLaneDebugPayload(debug)) {
return debug;
}
}
@@ -2394,7 +2418,7 @@ function findRecentAddressFilterValue(items, key) {
continue;
}
const debug = item.debug;
if (!(debug.detected_mode === "address_query" || debug.prompt_version === "address_query_runtime_v1")) {
if (!isAddressLaneDebugPayload(debug)) {
continue;
}
const directFilterValue = readAddressFilterString(debug, key);
@@ -2438,6 +2462,13 @@ function hasAddressFollowupContextSignal(userMessage) {
if (shortFollowup && /(?:^|\s)(?:также|тоже|also|same|again|ещ[её]|теперь|then|now)(?=$|[\s,.;:!?])/iu.test(text)) {
return true;
}
if (shortFollowup &&
/(?:кто\s+из\s+(?:них|этих|тех)|кто\s+нов(?:ые|ых|ый)|кто\s+потом\s+исчез|кто\s+был\s+(?:только|ровно)\s+один\s+раз)/iu.test(text)) {
return true;
}
if (shortFollowup && /^(?:а|и)\s+кто\b/iu.test(text)) {
return true;
}
if (shortFollowup &&
/(?:^|\s)по\s+[a-zа-яё][a-zа-яё0-9._-]{1,}(?=$|[\s,.;:!?])/iu.test(text) &&
!/(?:по\s+этому|по\s+тому|по\s+нему|по\s+ней|по\s+ним)/iu.test(text)) {