Завершить phase99-105 schema primitive closure

This commit is contained in:
2026-05-17 21:49:02 +03:00
parent b1a5e4435d
commit a5fa940953
83 changed files with 5289 additions and 111 deletions
@@ -306,8 +306,43 @@ function bankOperationDirectionLabel(direction) {
}
return "банковская операция без надежно распознанного направления";
}
function bankOperationEvidenceLine(rows) {
const sample = rows[0];
function summarizeBankOperationDirections(rows) {
const summary = {
incoming: { count: 0, amount: 0 },
outgoing: { count: 0, amount: 0 },
unknown: { count: 0, amount: 0 }
};
for (const row of rows) {
const direction = bankOperationDirection(row);
const amount = typeof row.amount === "number" && Number.isFinite(row.amount) ? Math.abs(row.amount) : 0;
summary[direction].count += 1;
summary[direction].amount += amount;
}
const parts = [];
if (summary.incoming.count > 0) {
parts.push(`входящие: ${formatMoneyRub(summary.incoming.amount)} (${summary.incoming.count} строк)`);
}
if (summary.outgoing.count > 0) {
parts.push(`исходящие: ${formatMoneyRub(summary.outgoing.amount)} (${summary.outgoing.count} строк)`);
}
if (summary.unknown.count > 0) {
parts.push(`без распознанного направления: ${formatMoneyRub(summary.unknown.amount)} (${summary.unknown.count} строк)`);
}
return parts.length > 0
? `Сводка по направлению: ${parts.join("; ")}.`
: "Сводка по направлению: подтвержденные строки не найдены.";
}
function preferredBankEvidenceDirection(userMessage) {
if (hasBankIncomingRoleBoundaryQuestion(userMessage)) {
return "incoming";
}
if (hasBankOutgoingRoleBoundaryQuestion(userMessage)) {
return "outgoing";
}
return null;
}
function bankOperationEvidenceLine(rows, preferredDirection = null) {
const sample = (preferredDirection ? rows.find((row) => bankOperationDirection(row) === preferredDirection) : null) ?? rows[0];
if (!sample) {
return "Проверенная строка 1С не найдена.";
}
@@ -341,12 +376,12 @@ function bankRoleBoundaryLine(userMessage, rows) {
const hasOutgoingRow = directions.includes("outgoing");
if (incomingBoundary) {
return hasIncomingRow
? "Выручкой от обычного клиента это не называю автоматически: для банка/финорганизации нужен вид операции, назначение платежа и договор; кредитный, депозитный или возвратный смысл без этих полей не исключаю и не притягиваю."
? "Это не обычный клиент и не клиентская выручка автоматически: для банка/финорганизации нужен вид операции, назначение платежа и договор; кредитный, депозитный или возвратный смысл без этих полей не исключаю и не притягиваю."
: hasOutgoingRow
? "В найденных строках по банку подтверждено исходящее списание, а входящее поступление от банка в этом срезе не подтверждено; клиентскую выручку, кредит или депозит по этой строке не доказываю."
: "Входящее поступление от банка в найденных строках не подтверждено; клиентскую выручку, кредитный или депозитный смысл без вида операции/назначения платежа не доказываю.";
? "В найденных строках по банку подтверждено исходящее списание, а входящее поступление от банка в этом срезе не подтверждено; это не подтвержденная клиентская выручка, кредит или депозит."
: "Входящее поступление от банка в найденных строках не подтверждено; это не подтвержденная клиентская выручка, кредитный или депозитный смысл.";
}
return "Обычным поставщиком это не называю автоматически: для банка/финорганизации нужен вид операции, назначение платежа и договор; текущий срез подтверждает банковский платежный контур, а не бизнес-роль поставщика.";
return "Это не обычный поставщик автоматически: для банка/финорганизации нужен вид операции, назначение платежа и договор; текущий срез подтверждает банковский платежный контур, а не бизнес-роль поставщика.";
}
function hasInventoryPurchaseDateActionFocus(userMessage) {
const text = normalizeQuestionText(userMessage);
@@ -3896,23 +3931,34 @@ function composeFactualReplyBody(intent, rows, options = {}) {
.filter((item) => Boolean(item)));
const counterparty = resolvePreferredCounterpartyDisplayLabel(options.counterpartyHint, rowCounterparties);
const roleBoundary = bankRoleBoundaryLine(options.userMessage, rows);
const visibleRows = rows.slice(0, Math.min(rows.length, 5));
const lines = [
`Коротко: найдено банковских операций${counterparty ? ` по ${counterparty}` : " по контрагенту"} — ${rows.length}.`,
summarizeBankOperationDirections(rows),
roleBoundary ?? "Показываю подтвержденные банковские операции из текущего среза.",
bankOperationEvidenceLine(rows),
...formatTopRows(rows, rows.length)
bankOperationEvidenceLine(rows, preferredBankEvidenceDirection(options.userMessage)),
...formatTopRows(visibleRows, visibleRows.length)
];
if (rows.length > visibleRows.length) {
lines.push(`Показаны первые ${visibleRows.length} из ${rows.length}; полный список остается в подтвержденном срезе.`);
}
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
if (intent === "bank_operations_by_contract") {
const visibleRows = rows.slice(0, Math.min(rows.length, 5));
const lines = [
`Коротко: найдено банковских операций по договору — ${rows.length}.`,
summarizeBankOperationDirections(rows),
"Показываю подтвержденные банковские операции из текущего среза.",
...formatTopRows(rows, rows.length)
bankOperationEvidenceLine(rows),
...formatTopRows(visibleRows, visibleRows.length)
];
if (rows.length > visibleRows.length) {
lines.push(`Показаны первые ${visibleRows.length} из ${rows.length}; полный список остается в подтвержденном срезе.`);
}
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
@@ -139,6 +139,7 @@ const FOLLOWUP_LOW_QUALITY_COUNTERPARTY_TOKENS = new Set([
"что",
"все",
"всё",
"или",
"кроме",
"помимо",
"этого",
@@ -157,6 +158,30 @@ const FOLLOWUP_LOW_QUALITY_COUNTERPARTY_TOKENS = new Set([
"договора",
"контрагент",
"контрагента",
"контрагенту",
"клиент",
"клиента",
"клиенту",
"клиентом",
"клиенты",
"поставщик",
"поставщика",
"поставщику",
"поставщиком",
"поставщики",
"покупатель",
"покупателя",
"покупателю",
"заказчик",
"заказчика",
"заказчику",
"обычный",
"обычная",
"обычное",
"обычные",
"обычного",
"обычному",
"обычным",
"еще",
"ещё",
"другие",
@@ -654,6 +679,19 @@ function hasBroadCounterpartyRankingCue(text) {
}
return /(?:\bкто\b|\bкакие\b|\bкакой\b|\bтоп\b|\bсписок\b|\bвсе\b|\bвсех\b|\bвсего\b|\bclients?\b|\bcounterpart(?:y|ies)\b|контрагент|клиент|заказчик)/iu.test(normalized);
}
function isBroadDebtPolarityQuestion(intent, text) {
if (intent !== "payables_confirmed_as_of_date" && intent !== "receivables_confirmed_as_of_date") {
return false;
}
const normalized = textWithRepairedVariant(String(text ?? "")).toLowerCase().replace(/ё/g, "е");
if (!/(?:долж|задолж|дебитор|кредитор|обязательств)/iu.test(normalized)) {
return false;
}
if (/(?:по\s+(?:нему|ней|ним|этому|этой|этому\s+контрагенту|этой\s+компании|поставщику|клиенту|покупателю|заказчику)|\bон\b|\bона\b)/iu.test(normalized)) {
return false;
}
return /(?:^|[\s,.;:!?()\-])(?:кто|кому|какие|какой|список|топ|все|всех|всего)(?=$|[\s,.;:!?()\-])/iu.test(normalized);
}
function mergeFollowupFilters(current, intent, userMessage, followupContext) {
const merged = { ...current };
const reasons = [];
@@ -823,10 +861,15 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
const inheritedCounterparty = previousCounterparty ??
(followupContext.previous_anchor_type === "counterparty" ? previousAnchorValue : null);
const currentCounterparty = toNonEmptyString(merged.counterparty);
const shouldInheritCounterparty = !currentCounterparty ||
(Boolean(inheritedCounterparty) &&
isLowQualityCounterpartyAnchor(currentCounterparty) &&
!isLowQualityCounterpartyAnchor(inheritedCounterparty));
const suppressCounterpartyForBroadDebtQuestion = isBroadDebtPolarityQuestion(intent, userMessage) && !currentCounterparty;
const shouldInheritCounterparty = !suppressCounterpartyForBroadDebtQuestion &&
(!currentCounterparty ||
(Boolean(inheritedCounterparty) &&
isLowQualityCounterpartyAnchor(currentCounterparty) &&
!isLowQualityCounterpartyAnchor(inheritedCounterparty)));
if (inheritedCounterparty && suppressCounterpartyForBroadDebtQuestion) {
reasons.push("counterparty_carryover_suppressed_for_broad_debt_polarity_question");
}
if (inheritedCounterparty && shouldInheritCounterparty) {
merged.counterparty = inheritedCounterparty;
reasons.push(currentCounterparty ? "counterparty_replaced_from_followup_context" : "counterparty_from_followup_context");