ОРРКЕСТРАЦИЯ - Оркестрация домена: ужесточить автофикс loop и назначать primary repair focus
This commit is contained in:
@@ -345,6 +345,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
const previousContract = toNonEmptyString(previous.contract);
|
||||
const previousAccount = toNonEmptyString(previous.account);
|
||||
const previousItem = toNonEmptyString(previous.item);
|
||||
const previousAnchorItem = followupContext.previous_anchor_type === "item" ? previousAnchorValue : null;
|
||||
const previousOrganization = toNonEmptyString(previous.organization);
|
||||
const previousAsOfDate = toNonEmptyString(previous.as_of_date);
|
||||
const previousPeriodFrom = toNonEmptyString(previous.period_from);
|
||||
@@ -462,10 +463,10 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date") &&
|
||||
!toNonEmptyString(merged.item) &&
|
||||
previousItem) {
|
||||
if (intent !== "inventory_aging_by_purchase_date") {
|
||||
merged.item = previousItem;
|
||||
!toNonEmptyString(merged.item)) {
|
||||
const inheritedItem = previousItem ?? previousAnchorItem;
|
||||
if (inheritedItem && intent !== "inventory_aging_by_purchase_date") {
|
||||
merged.item = inheritedItem;
|
||||
reasons.push("item_from_followup_context");
|
||||
}
|
||||
}
|
||||
@@ -493,6 +494,28 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
reasons.push("as_of_date_from_followup_context");
|
||||
}
|
||||
}
|
||||
if (!sameDateRequested &&
|
||||
(intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date") &&
|
||||
!hasExplicitPeriodLiteral(userMessage) &&
|
||||
!hasExplicitCurrentDateHint(userMessage)) {
|
||||
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
|
||||
const currentAsOfDate = toNonEmptyString(merged.as_of_date);
|
||||
const todayIso = new Date().toISOString().slice(0, 10);
|
||||
const currentLooksDefaultedToToday = currentAsOfDate === todayIso;
|
||||
if (inheritedAsOfDate && (!currentAsOfDate || currentLooksDefaultedToToday) && currentAsOfDate !== inheritedAsOfDate) {
|
||||
merged.as_of_date = inheritedAsOfDate;
|
||||
reasons.push("as_of_date_from_followup_context");
|
||||
}
|
||||
}
|
||||
if (!sameDateRequested &&
|
||||
(intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date") &&
|
||||
hasOpenItemsHint(userMessage)) {
|
||||
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
|
||||
if (inheritedAsOfDate && merged.as_of_date !== inheritedAsOfDate) {
|
||||
merged.as_of_date = inheritedAsOfDate;
|
||||
reasons.push("as_of_date_from_open_items_followup_context");
|
||||
}
|
||||
}
|
||||
if (intent === "inventory_aging_by_purchase_date") {
|
||||
const explicitItemMention = /(?:^|[\s,.;:!?()\-\u2014])(?:товар(?:у|а|ом)?|позици(?:и|я|ю)|item|row|line)(?=$|[\s,.;:!?()\-\u2014])/iu.test(String(userMessage ?? ""));
|
||||
if (toNonEmptyString(merged.item) && !explicitItemMention) {
|
||||
@@ -569,7 +592,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
reasons.push("period_from_followup_context");
|
||||
}
|
||||
}
|
||||
if (!currentHasPeriod && previousHasPeriod && hasFollowupSignal && !(asOfPrimaryIntent && hasExplicitCurrentDateInMessage)) {
|
||||
if (!currentHasPeriod && previousHasPeriod && hasFollowupSignal && !hasExplicitPeriodInMessage) {
|
||||
if (previousPeriodFrom) {
|
||||
merged.period_from = previousPeriodFrom;
|
||||
}
|
||||
|
||||
@@ -101,13 +101,36 @@ function matchesAnchorText(searchable, anchor) {
|
||||
}
|
||||
return searchableNormalized.includes(direct) || searchableLatin.includes(transliterateCyrillicToLatin(direct));
|
||||
}
|
||||
return tokens.every((token) => {
|
||||
const fullMatch = tokens.every((token) => {
|
||||
const variants = anchorTokenVariants(token);
|
||||
return variants.some((variant) => {
|
||||
const tokenLatin = transliterateCyrillicToLatin(variant);
|
||||
return searchableNormalized.includes(variant) || searchableLatin.includes(tokenLatin);
|
||||
});
|
||||
});
|
||||
if (fullMatch) {
|
||||
return true;
|
||||
}
|
||||
// Sale-trace item labels and warehouse labels can differ by punctuation or
|
||||
// compact suffixes in the materialized row text. For those anchors, allow a
|
||||
// narrow token-overlap fallback so the exact selected object still resolves
|
||||
// against live rows instead of dropping into an empty match.
|
||||
const overlapCount = tokens.reduce((count, token) => {
|
||||
const variants = anchorTokenVariants(token);
|
||||
return (count +
|
||||
(variants.some((variant) => {
|
||||
const tokenLatin = transliterateCyrillicToLatin(variant);
|
||||
return searchableNormalized.includes(variant) || searchableLatin.includes(tokenLatin);
|
||||
})
|
||||
? 1
|
||||
: 0));
|
||||
}, 0);
|
||||
const numericTokens = tokens.filter((token) => /\d/.test(token));
|
||||
const numericOverlap = numericTokens.every((token) => {
|
||||
const tokenLatin = transliterateCyrillicToLatin(token);
|
||||
return searchableNormalized.includes(token) || searchableLatin.includes(tokenLatin);
|
||||
});
|
||||
return numericOverlap && overlapCount >= Math.max(2, Math.ceil(tokens.length * 0.75));
|
||||
}
|
||||
function uniqueStrings(values) {
|
||||
return Array.from(new Set(values
|
||||
@@ -118,6 +141,8 @@ function resolvePrimaryAnchor(intent, filters) {
|
||||
const account = typeof filters.account === "string" ? filters.account.trim() : "";
|
||||
const counterparty = typeof filters.counterparty === "string" ? filters.counterparty.trim() : "";
|
||||
const contract = typeof filters.contract === "string" ? filters.contract.trim() : "";
|
||||
const item = typeof filters.item === "string" ? filters.item.trim() : "";
|
||||
const warehouse = typeof filters.warehouse === "string" ? filters.warehouse.trim() : "";
|
||||
const documentRef = typeof filters.document_ref === "string" ? filters.document_ref.trim() : "";
|
||||
if (intent === "account_balance_snapshot" || intent === "documents_forming_balance") {
|
||||
if (account) {
|
||||
@@ -170,6 +195,29 @@ function resolvePrimaryAnchor(intent, filters) {
|
||||
ambiguity_count: 0
|
||||
};
|
||||
}
|
||||
if ((intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date") &&
|
||||
item) {
|
||||
return {
|
||||
anchor_type: "item",
|
||||
anchor_value_raw: item,
|
||||
anchor_value_resolved: item,
|
||||
resolver_confidence: "medium",
|
||||
ambiguity_count: 0
|
||||
};
|
||||
}
|
||||
if (warehouse) {
|
||||
return {
|
||||
anchor_type: "warehouse",
|
||||
anchor_value_raw: warehouse,
|
||||
anchor_value_resolved: warehouse,
|
||||
resolver_confidence: "medium",
|
||||
ambiguity_count: 0
|
||||
};
|
||||
}
|
||||
if (documentRef) {
|
||||
return {
|
||||
anchor_type: "document_ref",
|
||||
@@ -191,15 +239,20 @@ function refineAnchorFromRows(anchor, rows) {
|
||||
if (rows.length === 0) {
|
||||
return anchor;
|
||||
}
|
||||
if (anchor.anchor_type !== "counterparty" && anchor.anchor_type !== "contract") {
|
||||
if (anchor.anchor_type !== "counterparty" &&
|
||||
anchor.anchor_type !== "contract" &&
|
||||
anchor.anchor_type !== "item" &&
|
||||
anchor.anchor_type !== "warehouse") {
|
||||
return anchor;
|
||||
}
|
||||
const needleRaw = String(anchor.anchor_value_raw ?? "").trim();
|
||||
if (!needleRaw) {
|
||||
return anchor;
|
||||
}
|
||||
const candidates = uniqueStrings(rows
|
||||
.flatMap((row) => row.analytics)
|
||||
const searchableRows = anchor.anchor_type === "item" || anchor.anchor_type === "warehouse"
|
||||
? rows.flatMap((row) => [row.registrator, row.item ?? "", row.warehouse ?? "", row.account_dt ?? "", row.account_kt ?? "", ...row.analytics])
|
||||
: rows.flatMap((row) => row.analytics);
|
||||
const candidates = uniqueStrings(searchableRows
|
||||
.map((value) => value.trim())
|
||||
.filter((value) => value.length >= 2 && matchesAnchorText(value, needleRaw)));
|
||||
if (candidates.length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user