Усилить answer contract и агентный аудит для phase105
This commit is contained in:
+6
-3
@@ -1008,7 +1008,8 @@ function loadSessionDialog(runId, caseId) {
|
|||||||
text: toStringSafe(item.text) ?? "",
|
text: toStringSafe(item.text) ?? "",
|
||||||
created_at: toStringSafe(item.created_at),
|
created_at: toStringSafe(item.created_at),
|
||||||
trace_id: toStringSafe(item.trace_id),
|
trace_id: toStringSafe(item.trace_id),
|
||||||
reply_type: toStringSafe(item.reply_type)
|
reply_type: toStringSafe(item.reply_type),
|
||||||
|
debug: item.debug ?? null
|
||||||
}));
|
}));
|
||||||
const turns = toArray(record.turns)
|
const turns = toArray(record.turns)
|
||||||
.map((item) => toRecord(item))
|
.map((item) => toRecord(item))
|
||||||
@@ -1076,7 +1077,8 @@ function buildFallbackDialog(run, caseId) {
|
|||||||
text: userText,
|
text: userText,
|
||||||
created_at: null,
|
created_at: null,
|
||||||
trace_id: null,
|
trace_id: null,
|
||||||
reply_type: null
|
reply_type: null,
|
||||||
|
debug: null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message_id: null,
|
message_id: null,
|
||||||
@@ -1084,7 +1086,8 @@ function buildFallbackDialog(run, caseId) {
|
|||||||
text: assistantSummaryParts.join("\n"),
|
text: assistantSummaryParts.join("\n"),
|
||||||
created_at: null,
|
created_at: null,
|
||||||
trace_id: toStringSafe(targetCase.trace_id),
|
trace_id: toStringSafe(targetCase.trace_id),
|
||||||
reply_type: toStringSafe(targetCase.reply_type)
|
reply_type: toStringSafe(targetCase.reply_type),
|
||||||
|
debug: null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
decomposition: [],
|
decomposition: [],
|
||||||
|
|||||||
@@ -365,6 +365,63 @@ function bankOperationEvidenceLine(rows, preferredDirection = null) {
|
|||||||
}
|
}
|
||||||
return `Основание 1С: ${parts.join("; ")}.`;
|
return `Основание 1С: ${parts.join("; ")}.`;
|
||||||
}
|
}
|
||||||
|
function classifyBankOperationSemanticBucket(row) {
|
||||||
|
const text = [
|
||||||
|
row.registrator,
|
||||||
|
row.operation_kind,
|
||||||
|
row.payment_purpose,
|
||||||
|
row.contract,
|
||||||
|
row.comment
|
||||||
|
]
|
||||||
|
.map((item) => String(item ?? "").toLowerCase())
|
||||||
|
.join(" ");
|
||||||
|
if (/(?:комисс|тариф|эквайр|обслуживан)/iu.test(text)) {
|
||||||
|
return "commission";
|
||||||
|
}
|
||||||
|
if (/(?:депозит|кредит|займ|овердрафт|процент|ссуд)/iu.test(text)) {
|
||||||
|
return "deposit_or_credit";
|
||||||
|
}
|
||||||
|
if (/(?:налог|ндс|взнос|бюджет|фнс|пфр|страхов)/iu.test(text)) {
|
||||||
|
return "tax_or_budget";
|
||||||
|
}
|
||||||
|
if (/(?:возврат|перевод|перечислен|переброс|пополн|инкасс|перенос)/iu.test(text)) {
|
||||||
|
return "transfer_or_return";
|
||||||
|
}
|
||||||
|
return "other";
|
||||||
|
}
|
||||||
|
function bankOperationSemanticBucketLabel(bucket) {
|
||||||
|
if (bucket === "commission") {
|
||||||
|
return "комиссии и банковое обслуживание";
|
||||||
|
}
|
||||||
|
if (bucket === "deposit_or_credit") {
|
||||||
|
return "депозиты, кредиты или проценты";
|
||||||
|
}
|
||||||
|
if (bucket === "tax_or_budget") {
|
||||||
|
return "налоги и бюджетные платежи";
|
||||||
|
}
|
||||||
|
if (bucket === "transfer_or_return") {
|
||||||
|
return "переводы, возвраты или перебросы";
|
||||||
|
}
|
||||||
|
return "прочие банковские операции";
|
||||||
|
}
|
||||||
|
function summarizeBankOperationSemantics(rows) {
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const counts = new Map();
|
||||||
|
for (const row of rows) {
|
||||||
|
const bucket = classifyBankOperationSemanticBucket(row);
|
||||||
|
counts.set(bucket, (counts.get(bucket) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
const ranked = Array.from(counts.entries())
|
||||||
|
.sort((left, right) => right[1] - left[1])
|
||||||
|
.slice(0, 3);
|
||||||
|
if (ranked.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const parts = ranked.map(([bucket, count]) => `${bankOperationSemanticBucketLabel(bucket)} — ${count}`);
|
||||||
|
return `По смыслу это скорее финансовый/банковский контур: ${parts.join("; ")}.`;
|
||||||
|
}
|
||||||
function bankRoleBoundaryLine(userMessage, rows) {
|
function bankRoleBoundaryLine(userMessage, rows) {
|
||||||
const incomingBoundary = hasBankIncomingRoleBoundaryQuestion(userMessage);
|
const incomingBoundary = hasBankIncomingRoleBoundaryQuestion(userMessage);
|
||||||
const outgoingBoundary = hasBankOutgoingRoleBoundaryQuestion(userMessage);
|
const outgoingBoundary = hasBankOutgoingRoleBoundaryQuestion(userMessage);
|
||||||
@@ -3931,13 +3988,31 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||||||
.filter((item) => Boolean(item)));
|
.filter((item) => Boolean(item)));
|
||||||
const counterparty = resolvePreferredCounterpartyDisplayLabel(options.counterpartyHint, rowCounterparties);
|
const counterparty = resolvePreferredCounterpartyDisplayLabel(options.counterpartyHint, rowCounterparties);
|
||||||
const roleBoundary = bankRoleBoundaryLine(options.userMessage, rows);
|
const roleBoundary = bankRoleBoundaryLine(options.userMessage, rows);
|
||||||
const visibleRows = rows.slice(0, Math.min(rows.length, 5));
|
const visibleRows = [...rows]
|
||||||
|
.sort((left, right) => Math.abs(right.amount ?? 0) - Math.abs(left.amount ?? 0) ||
|
||||||
|
(String(right.period ?? "").localeCompare(String(left.period ?? ""), "ru")))
|
||||||
|
.slice(0, Math.min(rows.length, 5));
|
||||||
|
const semanticSummary = summarizeBankOperationSemantics(rows);
|
||||||
|
const compactEvidenceRows = visibleRows.map((row, index) => {
|
||||||
|
const direction = bankOperationDirectionLabel(bankOperationDirection(row));
|
||||||
|
const amount = formatMoneyRub(row.amount ?? 0);
|
||||||
|
const period = row.period ? formatDateRu(row.period) : "дата не указана";
|
||||||
|
const operationKind = String(row.operation_kind ?? "").trim();
|
||||||
|
const paymentPurpose = String(row.payment_purpose ?? "").trim();
|
||||||
|
const detail = operationKind || paymentPurpose
|
||||||
|
? ` | ${[operationKind, paymentPurpose].filter(Boolean).join("; ")}`
|
||||||
|
: "";
|
||||||
|
return `${index + 1}. ${period} | ${direction} | ${amount}${detail}`;
|
||||||
|
});
|
||||||
const lines = [
|
const lines = [
|
||||||
`Коротко: найдено банковских операций${counterparty ? ` по ${counterparty}` : " по контрагенту"} — ${rows.length}.`,
|
`Коротко: найдено банковских операций${counterparty ? ` по ${counterparty}` : " по контрагенту"} — ${rows.length}.`,
|
||||||
summarizeBankOperationDirections(rows),
|
summarizeBankOperationDirections(rows),
|
||||||
roleBoundary ?? "Показываю подтвержденные банковские операции из текущего среза.",
|
roleBoundary ?? "Показываю подтвержденные банковские операции из текущего среза.",
|
||||||
bankOperationEvidenceLine(rows, preferredBankEvidenceDirection(options.userMessage)),
|
bankOperationEvidenceLine(rows, preferredBankEvidenceDirection(options.userMessage)),
|
||||||
...formatTopRows(visibleRows, visibleRows.length)
|
...(semanticSummary ? [semanticSummary] : []),
|
||||||
|
"Примеры строк 1С:",
|
||||||
|
...compactEvidenceRows,
|
||||||
|
"Следующий шаг: могу отдельно разложить назначения платежа, договоры или отделить банковский контур от клиентского/поставщицкого."
|
||||||
];
|
];
|
||||||
if (rows.length > visibleRows.length) {
|
if (rows.length > visibleRows.length) {
|
||||||
lines.push(`Показаны первые ${visibleRows.length} из ${rows.length}; полный список остается в подтвержденном срезе.`);
|
lines.push(`Показаны первые ${visibleRows.length} из ${rows.length}; полный список остается в подтвержденном срезе.`);
|
||||||
|
|||||||
+41
-9
@@ -30,6 +30,23 @@ function findFocusedCounterpartyValuePoint(profileRows, counterpartyHint, deps)
|
|||||||
}
|
}
|
||||||
return profileRows.length === 1 ? profileRows[0] : null;
|
return profileRows.length === 1 ? profileRows[0] : null;
|
||||||
}
|
}
|
||||||
|
function hasProfitAmbiguityCue(normalizedQuestion) {
|
||||||
|
return /(?:заработ|прибыл|прибыль|доход|выручк)/iu.test(normalizedQuestion);
|
||||||
|
}
|
||||||
|
function buildCashflowBoundaryLine(isSupplier) {
|
||||||
|
return isSupplier
|
||||||
|
? "Граница ответа: это подтвержденный денежный поток по поставщику, а не итоговая задолженность."
|
||||||
|
: "Граница ответа: это подтвержденный денежный поток по поступлениям, а не чистая прибыль.";
|
||||||
|
}
|
||||||
|
function buildCashflowNextStepLine(isSupplier, normalizedQuestion) {
|
||||||
|
if (isSupplier) {
|
||||||
|
return "Следующий шаг: могу отдельно показать остаток долга, просрочку или расшифровку по документам.";
|
||||||
|
}
|
||||||
|
if (hasProfitAmbiguityCue(normalizedQuestion)) {
|
||||||
|
return "Следующий шаг: могу отдельно проверить чистую прибыль по закрытию 90/91/99.";
|
||||||
|
}
|
||||||
|
return "Следующий шаг: могу разложить поток по месяцам, документам или контрагентам.";
|
||||||
|
}
|
||||||
function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||||
if (intent === "counterparty_population_and_roles") {
|
if (intent === "counterparty_population_and_roles") {
|
||||||
const rowsByMarker = groupRowsByMarker(rows);
|
const rowsByMarker = groupRowsByMarker(rows);
|
||||||
@@ -396,6 +413,8 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
|||||||
/(?:клиент|заказчик|покупател|контрагент|customer|client|counterparty|buyer)/iu.test(normalizedQuestion);
|
/(?:клиент|заказчик|покупател|контрагент|customer|client|counterparty|buyer)/iu.test(normalizedQuestion);
|
||||||
const semanticSingleBestCounterparty = focus === "top_by_total" && hasSingleBestCounterpartyCue && !asksExplicitRankingList;
|
const semanticSingleBestCounterparty = focus === "top_by_total" && hasSingleBestCounterpartyCue && !asksExplicitRankingList;
|
||||||
const effectiveLimit = asksSingleBestCounterparty || semanticSingleBestCounterparty ? 1 : limit;
|
const effectiveLimit = asksSingleBestCounterparty || semanticSingleBestCounterparty ? 1 : limit;
|
||||||
|
const cashflowBoundaryLine = buildCashflowBoundaryLine(isSupplier);
|
||||||
|
const cashflowNextStepLine = buildCashflowNextStepLine(isSupplier, normalizedQuestion);
|
||||||
const byCounterparty = new Map();
|
const byCounterparty = new Map();
|
||||||
const byYear = new Map();
|
const byYear = new Map();
|
||||||
const deals = [];
|
const deals = [];
|
||||||
@@ -490,10 +509,12 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
|||||||
? `за период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)}`
|
? `за период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)}`
|
||||||
: "за доступное время";
|
: "за доступное время";
|
||||||
const directAnswerLine = isSupplier
|
const directAnswerLine = isSupplier
|
||||||
? `Оборот по ${focusedCounterparty.name} ${periodLabel}: ${deps.formatMoneyRub(focusedCounterparty.total)} по ${focusedCounterparty.ops} подтвержденным исходящим операциям. Это денежный поток по поставщику, а не итоговая задолженность.`
|
? `Оборот по ${focusedCounterparty.name} ${periodLabel}: ${deps.formatMoneyRub(focusedCounterparty.total)} по ${focusedCounterparty.ops} подтвержденным исходящим операциям.`
|
||||||
: `Оборот по ${focusedCounterparty.name} ${periodLabel}: ${deps.formatMoneyRub(focusedCounterparty.total)} по ${focusedCounterparty.ops} подтвержденным входящим операциям. Это денежный поток от клиента, а не чистая прибыль.`;
|
: `Оборот по ${focusedCounterparty.name} ${periodLabel}: ${deps.formatMoneyRub(focusedCounterparty.total)} по ${focusedCounterparty.ops} подтвержденным входящим операциям.`;
|
||||||
const summaryLines = [
|
const summaryLines = [
|
||||||
directAnswerLine,
|
directAnswerLine,
|
||||||
|
cashflowBoundaryLine,
|
||||||
|
...(cashflowNextStepLine ? [cashflowNextStepLine] : []),
|
||||||
"",
|
"",
|
||||||
"Подтверждение:",
|
"Подтверждение:",
|
||||||
`- Контрагент в выборке: ${focusedCounterparty.name}.`,
|
`- Контрагент в выборке: ${focusedCounterparty.name}.`,
|
||||||
@@ -511,11 +532,10 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
|||||||
const periodLine = options.periodFrom && options.periodTo
|
const periodLine = options.periodFrom && options.periodTo
|
||||||
? `За период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)} подтверждено ${deps.formatMoneyRub(totalFlow)} ${isSupplier ? "исходящих выплат" : "входящих поступлений"}.`
|
? `За период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)} подтверждено ${deps.formatMoneyRub(totalFlow)} ${isSupplier ? "исходящих выплат" : "входящих поступлений"}.`
|
||||||
: `За все доступное время подтверждено ${deps.formatMoneyRub(totalFlow)} ${isSupplier ? "исходящих выплат" : "входящих поступлений"}.`;
|
: `За все доступное время подтверждено ${deps.formatMoneyRub(totalFlow)} ${isSupplier ? "исходящих выплат" : "входящих поступлений"}.`;
|
||||||
const directAnswerLine = isSupplier
|
|
||||||
? periodLine
|
|
||||||
: `${periodLine} Это денежный поток от клиентов, а не чистая прибыль.`;
|
|
||||||
const summaryLines = [
|
const summaryLines = [
|
||||||
directAnswerLine,
|
periodLine,
|
||||||
|
cashflowBoundaryLine,
|
||||||
|
...(cashflowNextStepLine ? [cashflowNextStepLine] : []),
|
||||||
"",
|
"",
|
||||||
"Подтверждение:",
|
"Подтверждение:",
|
||||||
`- Операций в выборке: ${totalOperations}.`,
|
`- Операций в выборке: ${totalOperations}.`,
|
||||||
@@ -538,11 +558,17 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
|||||||
const strongestYear = visible[0];
|
const strongestYear = visible[0];
|
||||||
const directAnswerLine = isSupplier
|
const directAnswerLine = isSupplier
|
||||||
? `Самый крупный год по подтвержденным выплатам: ${strongestYear.year} (${deps.formatMoneyRub(strongestYear.total)} по ${strongestYear.ops} операциям).`
|
? `Самый крупный год по подтвержденным выплатам: ${strongestYear.year} (${deps.formatMoneyRub(strongestYear.total)} по ${strongestYear.ops} операциям).`
|
||||||
: `Самый доходный год по подтвержденным поступлениям: ${strongestYear.year} (${deps.formatMoneyRub(strongestYear.total)} по ${strongestYear.ops} операциям). Это денежный поток, а не чистая прибыль.`;
|
: `Самый доходный год по подтвержденным поступлениям: ${strongestYear.year} (${deps.formatMoneyRub(strongestYear.total)} по ${strongestYear.ops} операциям).`;
|
||||||
const heading = isSupplier
|
const heading = isSupplier
|
||||||
? `Топ-${visible.length} лет по сумме выплат:`
|
? `Топ-${visible.length} лет по сумме выплат:`
|
||||||
: `Топ-${visible.length} лет по сумме поступлений:`;
|
: `Топ-${visible.length} лет по сумме поступлений:`;
|
||||||
lines.unshift(heading);
|
lines.unshift(heading);
|
||||||
|
if (!isSupplier) {
|
||||||
|
lines.unshift(cashflowBoundaryLine);
|
||||||
|
if (cashflowNextStepLine) {
|
||||||
|
lines.unshift(cashflowNextStepLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
lines.unshift(directAnswerLine);
|
lines.unshift(directAnswerLine);
|
||||||
lines.push(...visible.map((item, index) => `${index + 1}. ${item.year} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops} | контрагентов: ${item.counterparties.size} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)}`));
|
lines.push(...visible.map((item, index) => `${index + 1}. ${item.year} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops} | контрагентов: ${item.counterparties.size} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)}`));
|
||||||
}
|
}
|
||||||
@@ -622,11 +648,17 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
|||||||
const directAnswerLine = singleCandidateOnly
|
const directAnswerLine = singleCandidateOnly
|
||||||
? isSupplier
|
? isSupplier
|
||||||
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
||||||
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг; сумма является денежным потоком, а не чистой прибылью.`
|
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
||||||
: isSupplier
|
: isSupplier
|
||||||
? `Крупнейший поставщик по подтвержденным выплатам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
? `Крупнейший поставщик по подтвержденным выплатам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||||
: `Самый доходный клиент ${rankingPeriodLabel} по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это денежный поток, а не чистая прибыль.`;
|
: `Самый доходный клиент ${rankingPeriodLabel} по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`;
|
||||||
lines.unshift(directAnswerLine);
|
lines.unshift(directAnswerLine);
|
||||||
|
if (!isSupplier) {
|
||||||
|
lines.splice(1, 0, cashflowBoundaryLine);
|
||||||
|
if (cashflowNextStepLine) {
|
||||||
|
lines.splice(2, 0, cashflowNextStepLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
lines.push(...visible.map((item, index) => {
|
lines.push(...visible.map((item, index) => {
|
||||||
const avgCheck = item.ops > 0 ? item.total / item.ops : 0;
|
const avgCheck = item.ops > 0 ? item.total / item.ops : 0;
|
||||||
|
|||||||
+5
-3
@@ -87,10 +87,9 @@ function composeInventoryReply(intent, rows, options, deps) {
|
|||||||
const positions = deps.buildInventoryOnHandAggregate(rows, asOfDate);
|
const positions = deps.buildInventoryOnHandAggregate(rows, asOfDate);
|
||||||
const uniqueItems = deps.uniqueStrings(positions.map((item) => item.item));
|
const uniqueItems = deps.uniqueStrings(positions.map((item) => item.item));
|
||||||
const uniqueWarehouses = deps.uniqueStrings(positions.map((item) => String(item.warehouse ?? "").trim()).filter((item) => item.length > 0));
|
const uniqueWarehouses = deps.uniqueStrings(positions.map((item) => String(item.warehouse ?? "").trim()).filter((item) => item.length > 0));
|
||||||
const totalQuantity = positions.reduce((sum, item) => sum + item.quantity, 0);
|
|
||||||
const totalAmount = positions.reduce((sum, item) => sum + item.amount, 0);
|
const totalAmount = positions.reduce((sum, item) => sum + item.amount, 0);
|
||||||
const directAnswerLine = positions.length > 0
|
const directAnswerLine = positions.length > 0
|
||||||
? `На ${deps.formatDateRu(asOfDate)} на складе подтверждено ${deps.formatNumberWithDots(positions.length)} позиций с остатком на ${deps.formatMoneyRub(totalAmount)}.`
|
? `На ${deps.formatDateRu(asOfDate)} на складе подтверждено ${deps.formatNumberWithDots(positions.length)} позиций на ${deps.formatMoneyRub(totalAmount)}.`
|
||||||
: `На ${deps.formatDateRu(asOfDate)} подтвержденных товарных остатков по счету 41.01 не найдено.`;
|
: `На ${deps.formatDateRu(asOfDate)} подтвержденных товарных остатков по счету 41.01 не найдено.`;
|
||||||
const lines = [directAnswerLine];
|
const lines = [directAnswerLine];
|
||||||
if (positions.length > 0) {
|
if (positions.length > 0) {
|
||||||
@@ -115,11 +114,14 @@ function composeInventoryReply(intent, rows, options, deps) {
|
|||||||
`Позиции с остатком: ${deps.formatNumberWithDots(positions.length)}.`,
|
`Позиции с остатком: ${deps.formatNumberWithDots(positions.length)}.`,
|
||||||
`Уникальных товаров: ${deps.formatNumberWithDots(uniqueItems.length)}.`,
|
`Уникальных товаров: ${deps.formatNumberWithDots(uniqueItems.length)}.`,
|
||||||
`Уникальных складов: ${deps.formatNumberWithDots(uniqueWarehouses.length)}.`,
|
`Уникальных складов: ${deps.formatNumberWithDots(uniqueWarehouses.length)}.`,
|
||||||
`Суммарное количество: ${deps.formatNumberWithDots(totalQuantity, 3)}.`
|
"Общее количество не свожу в один управленческий показатель, потому что в остатках смешаны разнородные позиции."
|
||||||
]);
|
]);
|
||||||
if (rows.length !== positions.length) {
|
if (rows.length !== positions.length) {
|
||||||
lines.push(`- Проверенных строк движения: ${deps.formatNumberWithDots(rows.length)}.`);
|
lines.push(`- Проверенных строк движения: ${deps.formatNumberWithDots(rows.length)}.`);
|
||||||
}
|
}
|
||||||
|
if (positions.length > 0) {
|
||||||
|
lines.push("- Следующий шаг: могу раскрыть полный список, разложить остатки по складам или сравнить с другой датой.");
|
||||||
|
}
|
||||||
return positions.length > 0
|
return positions.length > 0
|
||||||
? (0, replyContracts_1.buildFactualListReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)("strong"))
|
? (0, replyContracts_1.buildFactualListReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)("strong"))
|
||||||
: (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)("medium"));
|
: (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)("medium"));
|
||||||
|
|||||||
+23
@@ -942,6 +942,29 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
|||||||
return joinBusinessReplyLines(lines);
|
return joinBusinessReplyLines(lines);
|
||||||
}
|
}
|
||||||
if (rankingNeed) {
|
if (rankingNeed) {
|
||||||
|
const explicitPeriodRankingOverview = period &&
|
||||||
|
!/(?:все\s+доступное|все\s+время|all\s+time)/iu.test(period) &&
|
||||||
|
(incomingAmount || outgoingAmount || netAmount);
|
||||||
|
if (explicitPeriodRankingOverview) {
|
||||||
|
lines.push(`Коротко: ${organizationPrefix}${period} денежная картина подтверждена по найденным строкам 1С.`);
|
||||||
|
lines.push(`Деньги: входящие ${incomingAmount ?? "0 руб."}, исходящие ${outgoingAmount ?? "0 руб."}, расчетное операционное нетто ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`);
|
||||||
|
if (customerName && customerAmount) {
|
||||||
|
lines.push(topCustomerLooksFinancial
|
||||||
|
? `Топ входящих: 1. ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}. Это финансовый/банковский контур, не считаю его клиентской выручкой без назначения платежа.${nonFinancialCustomer ? ` 2. Крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}.` : ""}`
|
||||||
|
: `Крупнейший входящий контрагент: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}.`);
|
||||||
|
}
|
||||||
|
if (topSupplier) {
|
||||||
|
lines.push(topSupplierLooksFinancial
|
||||||
|
? `Топ исходящих: 1. ${topSupplier}. Это финансовый/банковский контур, не считаю его обычным поставщиком без назначения платежа и договора.${nonFinancialSupplier ? ` 2. Крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}.` : ""}`
|
||||||
|
: `Крупнейший получатель исходящих денег: ${topSupplier}.`);
|
||||||
|
}
|
||||||
|
lines.push(`Вывод: по движению денег период ${netDirection}; это не чистая прибыль и не бухгалтерский финрезультат.`);
|
||||||
|
if (requestedFinancialBoundaryLine) {
|
||||||
|
lines.push(requestedFinancialBoundaryLine);
|
||||||
|
}
|
||||||
|
lines.push("Следующий шаг: могу отдельно посчитать чистую прибыль через закрытие 90/91/99 или разложить этот период по контрагентам.");
|
||||||
|
return joinBusinessReplyLines(lines);
|
||||||
|
}
|
||||||
const incomingLeader = strongestIncomingYear(overview);
|
const incomingLeader = strongestIncomingYear(overview);
|
||||||
const canRankYearlyNet = !limitLine;
|
const canRankYearlyNet = !limitLine;
|
||||||
const netLeader = canRankYearlyNet ? strongestNetYear(overview) : null;
|
const netLeader = canRankYearlyNet ? strongestNetYear(overview) : null;
|
||||||
|
|||||||
+105
-30
@@ -1139,13 +1139,38 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||||||
const rawEffectiveText = toNonEmptyString(input.effectiveMessage);
|
const rawEffectiveText = toNonEmptyString(input.effectiveMessage);
|
||||||
const repairedUserText = rawUserText ? (0, addressTextRepair_1.repairAddressMojibakeText)(rawUserText) : null;
|
const repairedUserText = rawUserText ? (0, addressTextRepair_1.repairAddressMojibakeText)(rawUserText) : null;
|
||||||
const repairedEffectiveText = rawEffectiveText ? (0, addressTextRepair_1.repairAddressMojibakeText)(rawEffectiveText) : null;
|
const repairedEffectiveText = rawEffectiveText ? (0, addressTextRepair_1.repairAddressMojibakeText)(rawEffectiveText) : null;
|
||||||
|
const rawUserSignalSourceText = repairedUserText ?? rawUserText ?? "";
|
||||||
const rawSignalSourceText = `${repairedUserText ?? rawUserText ?? ""} ${repairedEffectiveText ?? rawEffectiveText ?? ""}`.trim();
|
const rawSignalSourceText = `${repairedUserText ?? rawUserText ?? ""} ${repairedEffectiveText ?? rawEffectiveText ?? ""}`.trim();
|
||||||
const rawEntitySourceText = repairedUserText ?? rawUserText ?? repairedEffectiveText ?? rawEffectiveText ?? rawSignalSourceText;
|
const rawEntitySourceText = repairedUserText ?? rawUserText ?? repairedEffectiveText ?? rawEffectiveText ?? rawSignalSourceText;
|
||||||
|
const rawUserEntitySourceText = rawUserSignalSourceText || rawEntitySourceText;
|
||||||
|
const rawUserTextOnly = compactLower(rawUserSignalSourceText);
|
||||||
|
const rawAssistantEntityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
|
||||||
|
const rawUserPrimaryBusinessOverviewSignal = hasBusinessOverviewSignal(rawUserTextOnly);
|
||||||
|
const rawUserLifecyclePivotTextSignal = !rawUserPrimaryBusinessOverviewSignal && hasLifecycleSignal(rawUserTextOnly);
|
||||||
|
const rawUserBidirectionalValueFlowPivotTextSignal = !rawUserPrimaryBusinessOverviewSignal &&
|
||||||
|
!rawUserLifecyclePivotTextSignal &&
|
||||||
|
hasBidirectionalValueFlowSignal(rawUserTextOnly);
|
||||||
|
const rawUserScopedEntityCandidate = rawUserSignalSourceText
|
||||||
|
? rawScopedEntityCandidateFromText(rawUserEntitySourceText)
|
||||||
|
: null;
|
||||||
|
const rawUserCounterpartyBidirectionalOverride = Boolean(rawUserBidirectionalValueFlowPivotTextSignal &&
|
||||||
|
(rawUserScopedEntityCandidate ||
|
||||||
|
predecomposeEntities.counterparty ||
|
||||||
|
rawAssistantEntityCandidates.find((candidate) => !isInvalidEntityCandidate(candidate))));
|
||||||
const rawText = compactLower(rawSignalSourceText);
|
const rawText = compactLower(rawSignalSourceText);
|
||||||
const rawReferentialDocumentExclusionSignal = hasReferentialDocumentExclusionFollowupSignal(repairedUserText ?? rawUserText ?? "");
|
const rawReferentialDocumentExclusionSignal = hasReferentialDocumentExclusionFollowupSignal(repairedUserText ?? rawUserText ?? "");
|
||||||
const rawPrimaryBusinessOverviewSignal = hasBusinessOverviewSignal(rawText);
|
const rawPrimaryBusinessOverviewSignal = hasBusinessOverviewSignal(rawText) && !rawUserCounterpartyBidirectionalOverride;
|
||||||
const explicitVatQuestionSignal = hasExplicitVatQuestionSignal(rawText);
|
const explicitVatQuestionSignal = hasExplicitVatQuestionSignal(rawText);
|
||||||
const explicitVatMovementEvidenceSignal = hasExplicitVatMovementEvidenceSignal(rawText);
|
const explicitVatMovementEvidenceSignal = hasExplicitVatMovementEvidenceSignal(rawText);
|
||||||
|
const rawLifecyclePivotTextSignal = !rawPrimaryBusinessOverviewSignal && hasLifecycleSignal(rawText);
|
||||||
|
const rawBidirectionalValueFlowPivotTextSignal = !rawPrimaryBusinessOverviewSignal &&
|
||||||
|
!rawLifecyclePivotTextSignal &&
|
||||||
|
hasBidirectionalValueFlowSignal(rawText);
|
||||||
|
const rawValueFlowPivotTextSignal = !rawPrimaryBusinessOverviewSignal &&
|
||||||
|
!rawLifecyclePivotTextSignal &&
|
||||||
|
(hasValueFlowSignal(rawText) ||
|
||||||
|
hasValueRankingSignal(rawText) ||
|
||||||
|
rawBidirectionalValueFlowPivotTextSignal);
|
||||||
const explicitVatSuppressesBusinessOverviewContinuation = Boolean(explicitVatQuestionSignal && !rawPrimaryBusinessOverviewSignal);
|
const explicitVatSuppressesBusinessOverviewContinuation = Boolean(explicitVatQuestionSignal && !rawPrimaryBusinessOverviewSignal);
|
||||||
const businessOverviewContinuationSignal = hasBusinessOverviewFollowupSeed(followupSeed) &&
|
const businessOverviewContinuationSignal = hasBusinessOverviewFollowupSeed(followupSeed) &&
|
||||||
hasBusinessOverviewContinuationSignal(rawText) &&
|
hasBusinessOverviewContinuationSignal(rawText) &&
|
||||||
@@ -1163,7 +1188,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||||||
hasMetadataSignal(rawText);
|
hasMetadataSignal(rawText);
|
||||||
const rawEntityResolutionSignal = !rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
|
const rawEntityResolutionSignal = !rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
|
||||||
const rawPayoutSignal = rawValueFlowSignal && !rawBidirectionalValueFlowSignal && hasPayoutSignal(rawText);
|
const rawPayoutSignal = rawValueFlowSignal && !rawBidirectionalValueFlowSignal && hasPayoutSignal(rawText);
|
||||||
const rawValueFlowAggregateQuestionSignal = rawValueFlowSignal && hasValueFlowAggregateQuestionSignal(rawText);
|
const rawValueFlowAggregateQuestionSignal = (rawValueFlowSignal || rawValueFlowPivotTextSignal) && hasValueFlowAggregateQuestionSignal(rawText);
|
||||||
const monthlyAggregationSignal = hasMonthlyAggregationSignal(rawText);
|
const monthlyAggregationSignal = hasMonthlyAggregationSignal(rawText);
|
||||||
const rawAllTimeScopeSignal = hasAllTimeScopeHint(rawText);
|
const rawAllTimeScopeSignal = hasAllTimeScopeHint(rawText);
|
||||||
const dateScopeSignalText = stripNegatedTaxDateScopeClauses(rawText);
|
const dateScopeSignalText = stripNegatedTaxDateScopeClauses(rawText);
|
||||||
@@ -1216,6 +1241,32 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||||||
: profitMarginBusinessOverviewSignal
|
: profitMarginBusinessOverviewSignal
|
||||||
? "profit_margin_boundary"
|
? "profit_margin_boundary"
|
||||||
: "broad_evaluation";
|
: "broad_evaluation";
|
||||||
|
const assistantTurnMeaningDateScope = toNonEmptyString(assistantTurnMeaning?.explicit_date_scope);
|
||||||
|
const rawAssistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
|
||||||
|
const assistantTurnMeaningOrganizationScope = isReferentialOrganizationPlaceholder(rawAssistantTurnMeaningOrganizationScope)
|
||||||
|
? null
|
||||||
|
: rawAssistantTurnMeaningOrganizationScope;
|
||||||
|
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
|
||||||
|
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
|
||||||
|
const currentTurnFreshOrganizationScope = predecomposeEntities.organization ?? rawOrganizationScope;
|
||||||
|
const currentTurnOrganizationScope = currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope;
|
||||||
|
const predecomposeOrganizationMirrorsCounterparty = sameScopedName(predecomposeEntities.counterparty, predecomposeEntities.organization);
|
||||||
|
const organizationMirrorsPredecomposeCounterpartyForPivot = Boolean(sameScopedName(predecomposeEntities.counterparty, assistantTurnMeaningOrganizationScope) ||
|
||||||
|
sameScopedName(predecomposeEntities.counterparty, currentTurnOrganizationScope) ||
|
||||||
|
predecomposeOrganizationMirrorsCounterparty);
|
||||||
|
const normalizedPredecomposeCounterpartyForPivot = organizationMirrorsPredecomposeCounterpartyForPivot
|
||||||
|
? null
|
||||||
|
: normalizeFollowupCounterpartyCandidate(predecomposeEntities.counterparty);
|
||||||
|
const rawExplicitCounterpartyPivotCandidate = rawScopedEntityCandidate ??
|
||||||
|
rawAssistantEntityCandidates.find((candidate) => !isInvalidEntityCandidate(candidate) &&
|
||||||
|
!sameScopedName(candidate, currentTurnOrganizationScope)) ??
|
||||||
|
normalizedPredecomposeCounterpartyForPivot ??
|
||||||
|
null;
|
||||||
|
const businessOverviewCounterpartyValueFlowPivot = Boolean(businessOverviewContinuationSignal &&
|
||||||
|
!rawPrimaryBusinessOverviewSignal &&
|
||||||
|
rawValueFlowPivotTextSignal &&
|
||||||
|
rawExplicitCounterpartyPivotCandidate &&
|
||||||
|
(rawTopicSwitchSignal || rawValueFlowAggregateQuestionSignal));
|
||||||
const businessOverviewUnsupportedFamily = inventoryReserveBusinessOverviewSignal
|
const businessOverviewUnsupportedFamily = inventoryReserveBusinessOverviewSignal
|
||||||
? "inventory_reserve_liquidation_boundary"
|
? "inventory_reserve_liquidation_boundary"
|
||||||
: debtDueDateBusinessOverviewSignal
|
: debtDueDateBusinessOverviewSignal
|
||||||
@@ -1225,8 +1276,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||||||
: profitMarginBusinessOverviewSignal
|
: profitMarginBusinessOverviewSignal
|
||||||
? "profit_margin_boundary"
|
? "profit_margin_boundary"
|
||||||
: "broad_business_evaluation";
|
: "broad_business_evaluation";
|
||||||
const businessOverviewSignal = rawBusinessOverviewSignal ||
|
const businessOverviewSignal = !businessOverviewCounterpartyValueFlowPivot &&
|
||||||
seededBusinessOverviewSignal;
|
(rawBusinessOverviewSignal || seededBusinessOverviewSignal);
|
||||||
const businessOverviewSeparateCounterpartySignal = Boolean(businessOverviewSignal && hasBusinessOverviewSeparateCounterpartySignal(rawText));
|
const businessOverviewSeparateCounterpartySignal = Boolean(businessOverviewSignal && hasBusinessOverviewSeparateCounterpartySignal(rawText));
|
||||||
const businessOverviewSeparateCounterpartyCandidate = businessOverviewSeparateCounterpartySignal
|
const businessOverviewSeparateCounterpartyCandidate = businessOverviewSeparateCounterpartySignal
|
||||||
? businessOverviewSeparateCounterpartyCandidateFromText(rawText)
|
? businessOverviewSeparateCounterpartyCandidateFromText(rawText)
|
||||||
@@ -1244,15 +1295,6 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||||||
hasSimpleMovementLanePivotSignal(rawText) ||
|
hasSimpleMovementLanePivotSignal(rawText) ||
|
||||||
hasMovementEvidenceFollowupSignal(rawText) ||
|
hasMovementEvidenceFollowupSignal(rawText) ||
|
||||||
hasPronounMovementEvidenceFollowupSignal(rawText);
|
hasPronounMovementEvidenceFollowupSignal(rawText);
|
||||||
const assistantTurnMeaningDateScope = toNonEmptyString(assistantTurnMeaning?.explicit_date_scope);
|
|
||||||
const rawAssistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
|
|
||||||
const assistantTurnMeaningOrganizationScope = isReferentialOrganizationPlaceholder(rawAssistantTurnMeaningOrganizationScope)
|
|
||||||
? null
|
|
||||||
: rawAssistantTurnMeaningOrganizationScope;
|
|
||||||
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
|
|
||||||
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
|
|
||||||
const currentTurnFreshOrganizationScope = predecomposeEntities.organization ?? rawOrganizationScope;
|
|
||||||
const currentTurnOrganizationScope = currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope;
|
|
||||||
const followupCounterpartyIsMetadataOrganizationScope = Boolean(followupSeed.subjectResolutionOptional &&
|
const followupCounterpartyIsMetadataOrganizationScope = Boolean(followupSeed.subjectResolutionOptional &&
|
||||||
followupSeed.counterparty &&
|
followupSeed.counterparty &&
|
||||||
(followupSeed.metadataScopeHint ||
|
(followupSeed.metadataScopeHint ||
|
||||||
@@ -1288,7 +1330,6 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||||||
const rawOpenScopeValueFlowOrganizationSignal = Boolean(rawValueFlowSignal &&
|
const rawOpenScopeValueFlowOrganizationSignal = Boolean(rawValueFlowSignal &&
|
||||||
!rawBidirectionalValueFlowSignal &&
|
!rawBidirectionalValueFlowSignal &&
|
||||||
explicitOrganizationScopeSignal);
|
explicitOrganizationScopeSignal);
|
||||||
const predecomposeOrganizationMirrorsCounterparty = sameScopedName(predecomposeEntities.counterparty, predecomposeEntities.organization);
|
|
||||||
const organizationMirrorsPredecomposeCounterparty = Boolean((rawBidirectionalValueFlowSignal ||
|
const organizationMirrorsPredecomposeCounterparty = Boolean((rawBidirectionalValueFlowSignal ||
|
||||||
hasValueRankingSignal(rawText) ||
|
hasValueRankingSignal(rawText) ||
|
||||||
rawOpenScopeValueFlowOrganizationSignal ||
|
rawOpenScopeValueFlowOrganizationSignal ||
|
||||||
@@ -1564,14 +1605,25 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||||||
const lifecycleSignal = !businessOverviewSignal && (rawLifecycleSignal || seededDomain === "counterparty_lifecycle");
|
const lifecycleSignal = !businessOverviewSignal && (rawLifecycleSignal || seededDomain === "counterparty_lifecycle");
|
||||||
const bidirectionalValueFlowSignal = !businessOverviewSignal &&
|
const bidirectionalValueFlowSignal = !businessOverviewSignal &&
|
||||||
!lifecycleSignal &&
|
!lifecycleSignal &&
|
||||||
(rawBidirectionalValueFlowSignal || seededAction === "net_value_flow");
|
((businessOverviewCounterpartyValueFlowPivot
|
||||||
|
? rawBidirectionalValueFlowPivotTextSignal
|
||||||
|
: rawBidirectionalValueFlowSignal) ||
|
||||||
|
seededAction === "net_value_flow");
|
||||||
const valueFlowSignal = !businessOverviewSignal &&
|
const valueFlowSignal = !businessOverviewSignal &&
|
||||||
!lifecycleSignal &&
|
!lifecycleSignal &&
|
||||||
!metadataGroundedMovementLaneApplicable &&
|
!metadataGroundedMovementLaneApplicable &&
|
||||||
(rawValueFlowSignal || seededDomain === "counterparty_value");
|
((businessOverviewCounterpartyValueFlowPivot
|
||||||
|
? rawValueFlowPivotTextSignal
|
||||||
|
: rawValueFlowSignal) ||
|
||||||
|
seededDomain === "counterparty_value");
|
||||||
const payoutSignal = valueFlowSignal &&
|
const payoutSignal = valueFlowSignal &&
|
||||||
!bidirectionalValueFlowSignal &&
|
!bidirectionalValueFlowSignal &&
|
||||||
(rawPayoutSignal || seededAction === "payout");
|
((businessOverviewCounterpartyValueFlowPivot
|
||||||
|
? rawValueFlowPivotTextSignal &&
|
||||||
|
!rawBidirectionalValueFlowPivotTextSignal &&
|
||||||
|
hasPayoutSignal(rawText)
|
||||||
|
: rawPayoutSignal) ||
|
||||||
|
seededAction === "payout");
|
||||||
const semanticDataNeed = metadataAmbiguityLaneClarificationApplicable
|
const semanticDataNeed = metadataAmbiguityLaneClarificationApplicable
|
||||||
? "metadata lane clarification"
|
? "metadata lane clarification"
|
||||||
: semanticNeedFor({
|
: semanticNeedFor({
|
||||||
@@ -1579,17 +1631,37 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||||||
? "movements"
|
? "movements"
|
||||||
: businessOverviewSignal
|
: businessOverviewSignal
|
||||||
? "business_overview"
|
? "business_overview"
|
||||||
: rawDomain ?? seededDomain,
|
: lifecycleSignal
|
||||||
|
? "counterparty_lifecycle"
|
||||||
|
: valueFlowSignal
|
||||||
|
? "counterparty_value"
|
||||||
|
: rawDomain ?? seededDomain,
|
||||||
action: explicitVatMovementEvidenceSignal
|
action: explicitVatMovementEvidenceSignal
|
||||||
? "list_movements"
|
? "list_movements"
|
||||||
: businessOverviewSignal
|
: businessOverviewSignal
|
||||||
? businessOverviewActionFamily
|
? businessOverviewActionFamily
|
||||||
: rawAction ?? seededAction,
|
: lifecycleSignal
|
||||||
|
? "activity_duration"
|
||||||
|
: valueFlowSignal
|
||||||
|
? bidirectionalValueFlowSignal
|
||||||
|
? "net_value_flow"
|
||||||
|
: payoutSignal
|
||||||
|
? "payout"
|
||||||
|
: rawAction ?? seededAction ?? "turnover"
|
||||||
|
: rawAction ?? seededAction,
|
||||||
unsupported: explicitVatMovementEvidenceSignal
|
unsupported: explicitVatMovementEvidenceSignal
|
||||||
? "movement_evidence"
|
? "movement_evidence"
|
||||||
: businessOverviewSignal
|
: businessOverviewSignal
|
||||||
? businessOverviewUnsupportedFamily
|
? businessOverviewUnsupportedFamily
|
||||||
: unsupported ?? seededUnsupported,
|
: lifecycleSignal
|
||||||
|
? "counterparty_lifecycle"
|
||||||
|
: valueFlowSignal
|
||||||
|
? bidirectionalValueFlowSignal
|
||||||
|
? "counterparty_bidirectional_value_flow_or_netting"
|
||||||
|
: payoutSignal
|
||||||
|
? "counterparty_payouts_or_outflow"
|
||||||
|
: seededUnsupported ?? "counterparty_value_or_turnover"
|
||||||
|
: unsupported ?? seededUnsupported,
|
||||||
lifecycleSignal,
|
lifecycleSignal,
|
||||||
valueFlowSignal,
|
valueFlowSignal,
|
||||||
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
|
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
|
||||||
@@ -1853,16 +1925,16 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||||||
subject_resolution_optional: metadataScopedLaneWithoutSubject || undefined,
|
subject_resolution_optional: metadataScopedLaneWithoutSubject || undefined,
|
||||||
unsupported_but_understood_family: businessOverviewSignal
|
unsupported_but_understood_family: businessOverviewSignal
|
||||||
? businessOverviewUnsupportedFamily
|
? businessOverviewUnsupportedFamily
|
||||||
: unsupported ??
|
: lifecycleSignal
|
||||||
(lifecycleSignal
|
? "counterparty_lifecycle"
|
||||||
? "counterparty_lifecycle"
|
: valueFlowSignal
|
||||||
: valueFlowSignal
|
? bidirectionalValueFlowSignal
|
||||||
? bidirectionalValueFlowSignal
|
? "counterparty_bidirectional_value_flow_or_netting"
|
||||||
? "counterparty_bidirectional_value_flow_or_netting"
|
: payoutSignal
|
||||||
: payoutSignal
|
? "counterparty_payouts_or_outflow"
|
||||||
? "counterparty_payouts_or_outflow"
|
: seededUnsupported ?? "counterparty_value_or_turnover"
|
||||||
: seededUnsupported ?? "counterparty_value_or_turnover"
|
: unsupported ??
|
||||||
: metadataGroundedMovementLaneApplicable
|
(metadataGroundedMovementLaneApplicable
|
||||||
? "movement_evidence"
|
? "movement_evidence"
|
||||||
: metadataGroundedDocumentLaneApplicable
|
: metadataGroundedDocumentLaneApplicable
|
||||||
? "document_evidence"
|
? "document_evidence"
|
||||||
@@ -2137,6 +2209,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||||||
if (businessOverviewSignal) {
|
if (businessOverviewSignal) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_broad_business_evaluation_route_candidate");
|
pushReason(reasonCodes, "mcp_discovery_broad_business_evaluation_route_candidate");
|
||||||
}
|
}
|
||||||
|
if (businessOverviewCounterpartyValueFlowPivot) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_business_overview_followup_pivoted_to_counterparty_value_flow");
|
||||||
|
}
|
||||||
if (businessOverviewContinuationSignal) {
|
if (businessOverviewContinuationSignal) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_from_followup_context");
|
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_from_followup_context");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,6 +133,57 @@ function detectCounterpartyTurnoverFamily(text) {
|
|||||||
entity
|
entity
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function detectScopedCounterpartyEntity(text) {
|
||||||
|
const patterns = [
|
||||||
|
/(?:^|[\s,.;:!?])(?:\u043f\u043e|\u0443|\u0434\u043b\u044f|by|for)\s+(.+?)(?=$|[,.;:!?]|\s+(?:\u0437\u0430|\u043d\u0430|\u0432|\u0432\u043e|\u043a|\u043f\u043e|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a|\u043a\u0430\u043a|\u043a\u0430\u043a\u043e\u0435|\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f|\u043a\u0430\u043a\u0438\u0435|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*|\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u0435\u0436\p{L}*|\u043f\u043b\u0430\u0442[\u0435\u0451]\u0436\p{L}*|\u0438\u0441\u0445\u043e\u0434\p{L}*|\u0432\u0445\u043e\u0434\p{L}*)(?=$|[\s,.;:!?]))/iu,
|
||||||
|
/(?:^|[\s,.;:!?])(?:\u043f\u043e|\u0443|\u0434\u043b\u044f|by|for)\s+([\p{L}\d._-]{2,})(?=$|[\s,.;:!?])/iu
|
||||||
|
];
|
||||||
|
const ignored = new Set([
|
||||||
|
"\u0433\u043e\u0434",
|
||||||
|
"\u0433\u043e\u0434\u0430",
|
||||||
|
"\u043f\u0435\u0440\u0438\u043e\u0434",
|
||||||
|
"\u043f\u0435\u0440\u0438\u043e\u0434\u0430",
|
||||||
|
"\u043c\u0435\u0441\u044f\u0446",
|
||||||
|
"\u043c\u0435\u0441\u044f\u0446\u0430",
|
||||||
|
"\u043a\u0432\u0430\u0440\u0442\u0430\u043b",
|
||||||
|
"\u043a\u0432\u0430\u0440\u0442\u0430\u043b\u0430",
|
||||||
|
"\u0434\u0435\u043d\u044c\u0433\u0438",
|
||||||
|
"\u043d\u0435\u0442\u0442\u043e",
|
||||||
|
"\u0441\u0430\u043b\u044c\u0434\u043e",
|
||||||
|
"year",
|
||||||
|
"period",
|
||||||
|
"month",
|
||||||
|
"quarter",
|
||||||
|
"net"
|
||||||
|
]);
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const rawEntity = text.match(pattern)?.[1]?.trim() ?? "";
|
||||||
|
if (!rawEntity) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const entity = rawEntity.replace(/^["'«»]+|["'«»]+$/gu, "").trim();
|
||||||
|
if (entity.length >= 2 && !ignored.has(entity)) {
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function detectCounterpartyBidirectionalValueFlowFamily(text) {
|
||||||
|
const hasNetCue = /(?:\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|net\s+(?:flow|cash|payment)|cash\s+net)/iu.test(text);
|
||||||
|
const hasIncomingCue = /(?:\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0432\u0445\u043e\u0434\p{L}*|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*|received|incoming)/iu.test(text);
|
||||||
|
const hasOutgoingCue = /(?:\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*|\u0438\u0441\u0445\u043e\u0434\p{L}*|\u0441\u043f\u0438\u0441\u0430\u043d\p{L}*|paid|outgoing|payment)/iu.test(text);
|
||||||
|
if (!(hasNetCue || (hasIncomingCue && hasOutgoingCue))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const entity = detectScopedCounterpartyEntity(text);
|
||||||
|
if (!entity) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
family: "counterparty_bidirectional_value_flow_or_netting",
|
||||||
|
entity
|
||||||
|
};
|
||||||
|
}
|
||||||
function hasExplicitCounterpartyValueObject(text) {
|
function hasExplicitCounterpartyValueObject(text) {
|
||||||
return /(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u0442\u043e\u0432\u0430\u0440|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442\u0443\u0440|\u0441\u0434\u0435\u043b\u043a|customer|client|counterparty|supplier|vendor|contract|item|product|deal)/iu.test(text);
|
return /(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u0442\u043e\u0432\u0430\u0440|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442\u0443\u0440|\u0441\u0434\u0435\u043b\u043a|customer|client|counterparty|supplier|vendor|contract|item|product|deal)/iu.test(text);
|
||||||
}
|
}
|
||||||
@@ -279,14 +330,14 @@ function detectBroadBusinessEvaluation(text) {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
function buildEntityCandidates(counterpartyTurnover) {
|
function buildEntityCandidates(entityFamily) {
|
||||||
if (!counterpartyTurnover?.entity) {
|
if (!entityFamily?.entity) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
type: "counterparty",
|
type: "counterparty",
|
||||||
value: counterpartyTurnover.entity,
|
value: entityFamily.entity,
|
||||||
source: "current_turn_loose_entity_tail"
|
source: "current_turn_loose_entity_tail"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -299,22 +350,30 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
|
|||||||
const effectiveText = normalizeTurnText(effectiveMessage, deps);
|
const effectiveText = normalizeTurnText(effectiveMessage, deps);
|
||||||
const joinedText = fallbackCompactWhitespace(`${rawText} ${effectiveText}`);
|
const joinedText = fallbackCompactWhitespace(`${rawText} ${effectiveText}`);
|
||||||
const supportedIntent = detectSupportedIntent(joinedText, deps);
|
const supportedIntent = detectSupportedIntent(joinedText, deps);
|
||||||
|
const counterpartyBidirectionalValueFlow = detectCounterpartyBidirectionalValueFlowFamily(joinedText);
|
||||||
const counterpartyTurnover = detectCounterpartyTurnoverFamily(joinedText);
|
const counterpartyTurnover = detectCounterpartyTurnoverFamily(joinedText);
|
||||||
const selectedObjectInventoryExact = hasSelectedObjectInventoryExactSignal(joinedText);
|
const selectedObjectInventoryExact = hasSelectedObjectInventoryExactSignal(joinedText);
|
||||||
const broadBusinessEvaluation = selectedObjectInventoryExact ? null : detectBroadBusinessEvaluation(joinedText);
|
const broadBusinessEvaluation = selectedObjectInventoryExact || counterpartyBidirectionalValueFlow?.family
|
||||||
|
? null
|
||||||
|
: detectBroadBusinessEvaluation(joinedText);
|
||||||
const llmIntent = toNonEmptyString(input?.llmPreDecomposeMeta?.predecomposeContract?.intent, deps);
|
const llmIntent = toNonEmptyString(input?.llmPreDecomposeMeta?.predecomposeContract?.intent, deps);
|
||||||
const explicitIntentCandidate = broadBusinessEvaluation?.family
|
const explicitIntentCandidate = broadBusinessEvaluation?.family
|
||||||
? null
|
? null
|
||||||
: supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
|
: supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
|
||||||
const unsupportedFamily = broadBusinessEvaluation?.family
|
const unsupportedFamily = broadBusinessEvaluation?.family
|
||||||
? broadBusinessEvaluation.family
|
? broadBusinessEvaluation.family
|
||||||
: !explicitIntentCandidate && counterpartyTurnover?.family
|
: !explicitIntentCandidate && counterpartyBidirectionalValueFlow?.family
|
||||||
? counterpartyTurnover.family
|
? counterpartyBidirectionalValueFlow.family
|
||||||
: null;
|
: !explicitIntentCandidate && counterpartyTurnover?.family
|
||||||
|
? counterpartyTurnover.family
|
||||||
|
: null;
|
||||||
const reasonCodes = [];
|
const reasonCodes = [];
|
||||||
if (supportedIntent?.reason) {
|
if (supportedIntent?.reason) {
|
||||||
reasonCodes.push(supportedIntent.reason);
|
reasonCodes.push(supportedIntent.reason);
|
||||||
}
|
}
|
||||||
|
if (counterpartyBidirectionalValueFlow?.family) {
|
||||||
|
reasonCodes.push("counterparty_bidirectional_value_flow_current_turn_signal");
|
||||||
|
}
|
||||||
if (counterpartyTurnover?.family) {
|
if (counterpartyTurnover?.family) {
|
||||||
reasonCodes.push("counterparty_turnover_current_turn_signal");
|
reasonCodes.push("counterparty_turnover_current_turn_signal");
|
||||||
}
|
}
|
||||||
@@ -338,32 +397,39 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
|
|||||||
? "inventory"
|
? "inventory"
|
||||||
: broadBusinessEvaluation?.family
|
: broadBusinessEvaluation?.family
|
||||||
? "business_summary"
|
? "business_summary"
|
||||||
: explicitIntentCandidate?.includes("counterparty")
|
: counterpartyBidirectionalValueFlow?.family
|
||||||
? "counterparty"
|
? "counterparty_value"
|
||||||
: counterpartyTurnover?.family
|
: explicitIntentCandidate?.includes("counterparty")
|
||||||
? "counterparty"
|
? "counterparty"
|
||||||
: null;
|
: counterpartyTurnover?.family
|
||||||
|
? "counterparty"
|
||||||
|
: null;
|
||||||
const askedActionFamily = explicitIntentCandidate === "receivables_confirmed_as_of_date" ||
|
const askedActionFamily = explicitIntentCandidate === "receivables_confirmed_as_of_date" ||
|
||||||
explicitIntentCandidate === "payables_confirmed_as_of_date" ||
|
explicitIntentCandidate === "payables_confirmed_as_of_date" ||
|
||||||
explicitIntentCandidate === "inventory_on_hand_as_of_date"
|
explicitIntentCandidate === "inventory_on_hand_as_of_date"
|
||||||
? "confirmed_snapshot"
|
? "confirmed_snapshot"
|
||||||
: broadBusinessEvaluation?.family
|
: broadBusinessEvaluation?.family
|
||||||
? "broad_evaluation"
|
? "broad_evaluation"
|
||||||
: explicitIntentCandidate === "customer_revenue_and_payments" ||
|
: counterpartyBidirectionalValueFlow?.family
|
||||||
explicitIntentCandidate === "supplier_payouts_profile"
|
? "net_value_flow"
|
||||||
? "counterparty_value_or_turnover"
|
: explicitIntentCandidate === "customer_revenue_and_payments" ||
|
||||||
: explicitIntentCandidate === "vat_liability_confirmed_for_tax_period"
|
explicitIntentCandidate === "supplier_payouts_profile"
|
||||||
? "confirmed_tax_period"
|
? "counterparty_value_or_turnover"
|
||||||
: explicitIntentCandidate === "vat_payable_confirmed_as_of_date"
|
: explicitIntentCandidate === "vat_liability_confirmed_for_tax_period"
|
||||||
? "confirmed_snapshot"
|
? "confirmed_tax_period"
|
||||||
: explicitIntentCandidate === "vat_payable_forecast"
|
: explicitIntentCandidate === "vat_payable_confirmed_as_of_date"
|
||||||
? "forecast"
|
? "confirmed_snapshot"
|
||||||
: explicitIntentCandidate === "list_documents_by_counterparty"
|
: explicitIntentCandidate === "vat_payable_forecast"
|
||||||
? "list_documents"
|
? "forecast"
|
||||||
: counterpartyTurnover?.family
|
: explicitIntentCandidate === "list_documents_by_counterparty"
|
||||||
? "counterparty_value_or_turnover"
|
? "list_documents"
|
||||||
: null;
|
: counterpartyTurnover?.family
|
||||||
const staleReplayForbidden = Boolean(unsupportedFamily || broadBusinessEvaluation?.family || (counterpartyTurnover?.entity && !explicitIntentCandidate));
|
? "counterparty_value_or_turnover"
|
||||||
|
: null;
|
||||||
|
const staleReplayForbidden = Boolean(unsupportedFamily ||
|
||||||
|
broadBusinessEvaluation?.family ||
|
||||||
|
(counterpartyBidirectionalValueFlow?.entity && !explicitIntentCandidate) ||
|
||||||
|
(counterpartyTurnover?.entity && !explicitIntentCandidate));
|
||||||
return {
|
return {
|
||||||
schema_version: "assistant_turn_meaning_v1",
|
schema_version: "assistant_turn_meaning_v1",
|
||||||
raw_message: rawMessage,
|
raw_message: rawMessage,
|
||||||
@@ -373,10 +439,13 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
|
|||||||
asked_domain_family: askedDomainFamily,
|
asked_domain_family: askedDomainFamily,
|
||||||
asked_action_family: askedActionFamily,
|
asked_action_family: askedActionFamily,
|
||||||
explicit_intent_candidate: explicitIntentCandidate,
|
explicit_intent_candidate: explicitIntentCandidate,
|
||||||
explicit_entity_candidates: broadBusinessEvaluation?.family ? [] : buildEntityCandidates(counterpartyTurnover),
|
explicit_entity_candidates: broadBusinessEvaluation?.family
|
||||||
|
? []
|
||||||
|
: buildEntityCandidates(counterpartyBidirectionalValueFlow ?? counterpartyTurnover),
|
||||||
meaning_confidence: broadBusinessEvaluation?.family
|
meaning_confidence: broadBusinessEvaluation?.family
|
||||||
? "medium"
|
? "medium"
|
||||||
: supportedIntent?.confidence ?? (counterpartyTurnover?.family ? "medium" : "low"),
|
: supportedIntent?.confidence ??
|
||||||
|
(counterpartyBidirectionalValueFlow?.family || counterpartyTurnover?.family ? "medium" : "low"),
|
||||||
intent_override_strength: explicitIntentCandidate
|
intent_override_strength: explicitIntentCandidate
|
||||||
? "explicit_current_turn_intent"
|
? "explicit_current_turn_intent"
|
||||||
: staleReplayForbidden
|
: staleReplayForbidden
|
||||||
|
|||||||
+27
-1
@@ -31,6 +31,8 @@ function toRouteHintSummaryV1(normalized) {
|
|||||||
}
|
}
|
||||||
const ACCOUNT_HINT_PATTERN = /(?:\b(?:account|acct|schet|счет|сч)\s*[:#]?\s*(?:[1-9][0-9](?:[./-][0-9]{1,2})?)|\b(?:19|20|21|23|25|26|28|29|44|51|60|62|68)\b)/i;
|
const ACCOUNT_HINT_PATTERN = /(?:\b(?:account|acct|schet|счет|сч)\s*[:#]?\s*(?:[1-9][0-9](?:[./-][0-9]{1,2})?)|\b(?:19|20|21|23|25|26|28|29|44|51|60|62|68)\b)/i;
|
||||||
const PERIOD_PATTERN = /\b20\d{2}(?:[-./](?:0[1-9]|1[0-2]))?\b/i;
|
const PERIOD_PATTERN = /\b20\d{2}(?:[-./](?:0[1-9]|1[0-2]))?\b/i;
|
||||||
|
const BIDIRECTIONAL_VALUE_FLOW_PATTERN = /(?:\b(?:receive(?:d)?|received|get|got|incoming|inflow|paid|payment|payments|outgoing|outflow|net|netto|cash\s*flow)\b|получ(?:ить|ил[аи]?|ено|аем|или)|поступ(?:ил[аи]?|ление|ления)|заплат(?:ить|ил[аи]?|или)|оплат(?:ить|ил[аи]?|ы|или)|входящ(?:ий|ие|их)|исходящ(?:ий|ие|их)|нетто|сальдо)/iu;
|
||||||
|
const COUNTERPARTY_SCOPE_PATTERN = /(?:\b(?:counterparty|supplier|customer|vendor|client|bank)\b|контрагент|поставщик|покупател|клиент|заказчик|банк|сбербанк|по\s+(?:ип|ооо|пао|зао|оао|группа)\b)/iu;
|
||||||
const SYMPTOM_MARKER_PATTERN = /(?:\bsymptom\b|\banomaly\b|\bproblem\b|\bissue\b|\btail\b|\bhanging\b|\bblocked\b|\bincomplete\b|remains?\s+open|not\s+(?:confirmed|observed|resolved|closed)|не\s+(?:подтвержден|закрыт|наблюдается)|хвост|сбой|проблем)/i;
|
const SYMPTOM_MARKER_PATTERN = /(?:\bsymptom\b|\banomaly\b|\bproblem\b|\bissue\b|\btail\b|\bhanging\b|\bblocked\b|\bincomplete\b|remains?\s+open|not\s+(?:confirmed|observed|resolved|closed)|не\s+(?:подтвержден|закрыт|наблюдается)|хвост|сбой|проблем)/i;
|
||||||
const LIFECYCLE_MARKER_PATTERN = /(?:\blifecycle\b|\bchain\b|\btransition\b|\bstep\b|\btrace\b|цепоч|этап|переход|связк|где\s+разрыв)/i;
|
const LIFECYCLE_MARKER_PATTERN = /(?:\blifecycle\b|\bchain\b|\btransition\b|\bstep\b|\btrace\b|цепоч|этап|переход|связк|где\s+разрыв)/i;
|
||||||
const CHAIN_BREAK_PATTERN = /(?:\bbreak\b|\bbroken\b|\bgap\b|missing\s+(?:transition|step|link)|chain\s+break|разрыв|обрыв|нет\s+переход|не\s+дошл|не\s+наблюд)/i;
|
const CHAIN_BREAK_PATTERN = /(?:\bbreak\b|\bbroken\b|\bgap\b|missing\s+(?:transition|step|link)|chain\s+break|разрыв|обрыв|нет\s+переход|не\s+дошл|не\s+наблюд)/i;
|
||||||
@@ -54,6 +56,13 @@ exports.ROUTE_DISCIPLINE_RULE_TABLE = [
|
|||||||
forbidden_fallback: ["store_canonical", "hybrid_store_plus_live"],
|
forbidden_fallback: ["store_canonical", "hybrid_store_plus_live"],
|
||||||
description: "Ranking and period summary queries require analytical batch path."
|
description: "Ranking and period summary queries require analytical batch path."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
query_class: "bidirectional_value_flow",
|
||||||
|
required_route: "hybrid_store_plus_live",
|
||||||
|
allowed_fallback: ["no_route"],
|
||||||
|
forbidden_fallback: ["store_canonical"],
|
||||||
|
description: "Scoped bidirectional value-flow questions require hybrid evidence path."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
query_class: "symptom_first",
|
query_class: "symptom_first",
|
||||||
required_route: "hybrid_store_plus_live",
|
required_route: "hybrid_store_plus_live",
|
||||||
@@ -155,6 +164,17 @@ function hasAmbiguitySignal(fragment, lowerText) {
|
|||||||
function hasAccountOrPeriodAnchor(fragment, lowerText) {
|
function hasAccountOrPeriodAnchor(fragment, lowerText) {
|
||||||
return fragment.account_hints.length > 0 || ACCOUNT_HINT_PATTERN.test(lowerText) || PERIOD_PATTERN.test(lowerText);
|
return fragment.account_hints.length > 0 || ACCOUNT_HINT_PATTERN.test(lowerText) || PERIOD_PATTERN.test(lowerText);
|
||||||
}
|
}
|
||||||
|
function hasBidirectionalValueFlowSignal(fragment, lowerText) {
|
||||||
|
if (fragment.flags.asks_for_ranking_or_top || fragment.flags.asks_for_period_summary) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return BIDIRECTIONAL_VALUE_FLOW_PATTERN.test(lowerText);
|
||||||
|
}
|
||||||
|
function hasCounterpartyScopeSignal(fragment, lowerText) {
|
||||||
|
return (COUNTERPARTY_SCOPE_PATTERN.test(lowerText) ||
|
||||||
|
fragment.entity_hints.some((hint) => hint.trim().length > 0) ||
|
||||||
|
fragment.candidate_labels.includes("cross_entity"));
|
||||||
|
}
|
||||||
function resolveRouteClass(fragment) {
|
function resolveRouteClass(fragment) {
|
||||||
const lowerText = mergedFragmentText(fragment);
|
const lowerText = mergedFragmentText(fragment);
|
||||||
const symptomSignal = hasSymptomSignal(fragment, lowerText);
|
const symptomSignal = hasSymptomSignal(fragment, lowerText);
|
||||||
@@ -164,12 +184,17 @@ function resolveRouteClass(fragment) {
|
|||||||
const causalSignal = hasCausalSignal(lowerText);
|
const causalSignal = hasCausalSignal(lowerText);
|
||||||
const ambiguitySignal = hasAmbiguitySignal(fragment, lowerText);
|
const ambiguitySignal = hasAmbiguitySignal(fragment, lowerText);
|
||||||
const accountOrPeriodAnchor = hasAccountOrPeriodAnchor(fragment, lowerText);
|
const accountOrPeriodAnchor = hasAccountOrPeriodAnchor(fragment, lowerText);
|
||||||
|
const bidirectionalValueFlowSignal = hasBidirectionalValueFlowSignal(fragment, lowerText);
|
||||||
|
const counterpartyScopeSignal = hasCounterpartyScopeSignal(fragment, lowerText);
|
||||||
if (fragment.flags.asks_for_exact_object_trace) {
|
if (fragment.flags.asks_for_exact_object_trace) {
|
||||||
return ROUTE_DISCIPLINE_RULE_MAP.get("exact_object_trace");
|
return ROUTE_DISCIPLINE_RULE_MAP.get("exact_object_trace");
|
||||||
}
|
}
|
||||||
if (fragment.flags.asks_for_ranking_or_top || fragment.flags.asks_for_period_summary) {
|
if (fragment.flags.asks_for_ranking_or_top || fragment.flags.asks_for_period_summary) {
|
||||||
return ROUTE_DISCIPLINE_RULE_MAP.get("ranking_or_period_summary");
|
return ROUTE_DISCIPLINE_RULE_MAP.get("ranking_or_period_summary");
|
||||||
}
|
}
|
||||||
|
if (bidirectionalValueFlowSignal && counterpartyScopeSignal) {
|
||||||
|
return ROUTE_DISCIPLINE_RULE_MAP.get("bidirectional_value_flow");
|
||||||
|
}
|
||||||
if (ambiguitySignal && (symptomSignal || lifecycleSignal || chainBreakSignal || periodImpactSignal || causalSignal)) {
|
if (ambiguitySignal && (symptomSignal || lifecycleSignal || chainBreakSignal || periodImpactSignal || causalSignal)) {
|
||||||
return ROUTE_DISCIPLINE_RULE_MAP.get("mixed_ambiguity");
|
return ROUTE_DISCIPLINE_RULE_MAP.get("mixed_ambiguity");
|
||||||
}
|
}
|
||||||
@@ -205,7 +230,8 @@ function shouldPromoteFromNoRoute(fragment, rule) {
|
|||||||
hasLifecycleSignal(fragment, lowerText) ||
|
hasLifecycleSignal(fragment, lowerText) ||
|
||||||
hasChainBreakSignal(lowerText) ||
|
hasChainBreakSignal(lowerText) ||
|
||||||
hasPeriodImpactSignal(lowerText) ||
|
hasPeriodImpactSignal(lowerText) ||
|
||||||
hasCausalSignal(lowerText);
|
hasCausalSignal(lowerText) ||
|
||||||
|
(hasBidirectionalValueFlowSignal(fragment, lowerText) && hasCounterpartyScopeSignal(fragment, lowerText));
|
||||||
const hasAnchor = hasAccountOrPeriodAnchor(fragment, lowerText) ||
|
const hasAnchor = hasAccountOrPeriodAnchor(fragment, lowerText) ||
|
||||||
fragment.candidate_labels.includes("cross_entity") ||
|
fragment.candidate_labels.includes("cross_entity") ||
|
||||||
DOMAIN_LEXICAL_ANCHOR_PATTERN.test(lowerText);
|
DOMAIN_LEXICAL_ANCHOR_PATTERN.test(lowerText);
|
||||||
|
|||||||
@@ -1287,7 +1287,8 @@ function loadSessionDialog(runId: string, caseId: string): {
|
|||||||
text: toStringSafe(item.text) ?? "",
|
text: toStringSafe(item.text) ?? "",
|
||||||
created_at: toStringSafe(item.created_at),
|
created_at: toStringSafe(item.created_at),
|
||||||
trace_id: toStringSafe(item.trace_id),
|
trace_id: toStringSafe(item.trace_id),
|
||||||
reply_type: toStringSafe(item.reply_type)
|
reply_type: toStringSafe(item.reply_type),
|
||||||
|
debug: item.debug ?? null
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const turns = toArray(record.turns)
|
const turns = toArray(record.turns)
|
||||||
@@ -1364,7 +1365,8 @@ function buildFallbackDialog(run: IndexedRun, caseId: string): {
|
|||||||
text: userText,
|
text: userText,
|
||||||
created_at: null,
|
created_at: null,
|
||||||
trace_id: null,
|
trace_id: null,
|
||||||
reply_type: null
|
reply_type: null,
|
||||||
|
debug: null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message_id: null,
|
message_id: null,
|
||||||
@@ -1372,7 +1374,8 @@ function buildFallbackDialog(run: IndexedRun, caseId: string): {
|
|||||||
text: assistantSummaryParts.join("\n"),
|
text: assistantSummaryParts.join("\n"),
|
||||||
created_at: null,
|
created_at: null,
|
||||||
trace_id: toStringSafe(targetCase.trace_id),
|
trace_id: toStringSafe(targetCase.trace_id),
|
||||||
reply_type: toStringSafe(targetCase.reply_type)
|
reply_type: toStringSafe(targetCase.reply_type),
|
||||||
|
debug: null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
decomposition: [],
|
decomposition: [],
|
||||||
|
|||||||
@@ -524,6 +524,73 @@ function bankOperationEvidenceLine(
|
|||||||
return `Основание 1С: ${parts.join("; ")}.`;
|
return `Основание 1С: ${parts.join("; ")}.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BankOperationSemanticBucket =
|
||||||
|
| "commission"
|
||||||
|
| "deposit_or_credit"
|
||||||
|
| "tax_or_budget"
|
||||||
|
| "transfer_or_return"
|
||||||
|
| "other";
|
||||||
|
|
||||||
|
function classifyBankOperationSemanticBucket(row: ComposeStageRow): BankOperationSemanticBucket {
|
||||||
|
const text = [
|
||||||
|
row.registrator,
|
||||||
|
row.operation_kind,
|
||||||
|
row.payment_purpose,
|
||||||
|
row.contract,
|
||||||
|
row.comment
|
||||||
|
]
|
||||||
|
.map((item) => String(item ?? "").toLowerCase())
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
if (/(?:комисс|тариф|эквайр|обслуживан)/iu.test(text)) {
|
||||||
|
return "commission";
|
||||||
|
}
|
||||||
|
if (/(?:депозит|кредит|займ|овердрафт|процент|ссуд)/iu.test(text)) {
|
||||||
|
return "deposit_or_credit";
|
||||||
|
}
|
||||||
|
if (/(?:налог|ндс|взнос|бюджет|фнс|пфр|страхов)/iu.test(text)) {
|
||||||
|
return "tax_or_budget";
|
||||||
|
}
|
||||||
|
if (/(?:возврат|перевод|перечислен|переброс|пополн|инкасс|перенос)/iu.test(text)) {
|
||||||
|
return "transfer_or_return";
|
||||||
|
}
|
||||||
|
return "other";
|
||||||
|
}
|
||||||
|
|
||||||
|
function bankOperationSemanticBucketLabel(bucket: BankOperationSemanticBucket): string {
|
||||||
|
if (bucket === "commission") {
|
||||||
|
return "комиссии и банковое обслуживание";
|
||||||
|
}
|
||||||
|
if (bucket === "deposit_or_credit") {
|
||||||
|
return "депозиты, кредиты или проценты";
|
||||||
|
}
|
||||||
|
if (bucket === "tax_or_budget") {
|
||||||
|
return "налоги и бюджетные платежи";
|
||||||
|
}
|
||||||
|
if (bucket === "transfer_or_return") {
|
||||||
|
return "переводы, возвраты или перебросы";
|
||||||
|
}
|
||||||
|
return "прочие банковские операции";
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeBankOperationSemantics(rows: ComposeStageRow[]): string | null {
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const counts = new Map<BankOperationSemanticBucket, number>();
|
||||||
|
for (const row of rows) {
|
||||||
|
const bucket = classifyBankOperationSemanticBucket(row);
|
||||||
|
counts.set(bucket, (counts.get(bucket) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
const ranked = Array.from(counts.entries())
|
||||||
|
.sort((left, right) => right[1] - left[1])
|
||||||
|
.slice(0, 3);
|
||||||
|
if (ranked.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const parts = ranked.map(([bucket, count]) => `${bankOperationSemanticBucketLabel(bucket)} — ${count}`);
|
||||||
|
return `По смыслу это скорее финансовый/банковский контур: ${parts.join("; ")}.`;
|
||||||
|
}
|
||||||
function bankRoleBoundaryLine(userMessage: string | null | undefined, rows: ComposeStageRow[]): string | null {
|
function bankRoleBoundaryLine(userMessage: string | null | undefined, rows: ComposeStageRow[]): string | null {
|
||||||
const incomingBoundary = hasBankIncomingRoleBoundaryQuestion(userMessage);
|
const incomingBoundary = hasBankIncomingRoleBoundaryQuestion(userMessage);
|
||||||
const outgoingBoundary = hasBankOutgoingRoleBoundaryQuestion(userMessage);
|
const outgoingBoundary = hasBankOutgoingRoleBoundaryQuestion(userMessage);
|
||||||
@@ -5013,13 +5080,34 @@ function composeFactualReplyBody(
|
|||||||
);
|
);
|
||||||
const counterparty = resolvePreferredCounterpartyDisplayLabel(options.counterpartyHint, rowCounterparties);
|
const counterparty = resolvePreferredCounterpartyDisplayLabel(options.counterpartyHint, rowCounterparties);
|
||||||
const roleBoundary = bankRoleBoundaryLine(options.userMessage, rows);
|
const roleBoundary = bankRoleBoundaryLine(options.userMessage, rows);
|
||||||
const visibleRows = rows.slice(0, Math.min(rows.length, 5));
|
const visibleRows = [...rows]
|
||||||
|
.sort(
|
||||||
|
(left, right) =>
|
||||||
|
Math.abs(right.amount ?? 0) - Math.abs(left.amount ?? 0) ||
|
||||||
|
(String(right.period ?? "").localeCompare(String(left.period ?? ""), "ru"))
|
||||||
|
)
|
||||||
|
.slice(0, Math.min(rows.length, 5));
|
||||||
|
const semanticSummary = summarizeBankOperationSemantics(rows);
|
||||||
|
const compactEvidenceRows = visibleRows.map((row, index) => {
|
||||||
|
const direction = bankOperationDirectionLabel(bankOperationDirection(row));
|
||||||
|
const amount = formatMoneyRub(row.amount ?? 0);
|
||||||
|
const period = row.period ? formatDateRu(row.period) : "дата не указана";
|
||||||
|
const operationKind = String(row.operation_kind ?? "").trim();
|
||||||
|
const paymentPurpose = String(row.payment_purpose ?? "").trim();
|
||||||
|
const detail = operationKind || paymentPurpose
|
||||||
|
? ` | ${[operationKind, paymentPurpose].filter(Boolean).join("; ")}`
|
||||||
|
: "";
|
||||||
|
return `${index + 1}. ${period} | ${direction} | ${amount}${detail}`;
|
||||||
|
});
|
||||||
const lines = [
|
const lines = [
|
||||||
`Коротко: найдено банковских операций${counterparty ? ` по ${counterparty}` : " по контрагенту"} — ${rows.length}.`,
|
`Коротко: найдено банковских операций${counterparty ? ` по ${counterparty}` : " по контрагенту"} — ${rows.length}.`,
|
||||||
summarizeBankOperationDirections(rows),
|
summarizeBankOperationDirections(rows),
|
||||||
roleBoundary ?? "Показываю подтвержденные банковские операции из текущего среза.",
|
roleBoundary ?? "Показываю подтвержденные банковские операции из текущего среза.",
|
||||||
bankOperationEvidenceLine(rows, preferredBankEvidenceDirection(options.userMessage)),
|
bankOperationEvidenceLine(rows, preferredBankEvidenceDirection(options.userMessage)),
|
||||||
...formatTopRows(visibleRows, visibleRows.length)
|
...(semanticSummary ? [semanticSummary] : []),
|
||||||
|
"Примеры строк 1С:",
|
||||||
|
...compactEvidenceRows,
|
||||||
|
"Следующий шаг: могу отдельно разложить назначения платежа, договоры или отделить банковский контур от клиентского/поставщицкого."
|
||||||
];
|
];
|
||||||
if (rows.length > visibleRows.length) {
|
if (rows.length > visibleRows.length) {
|
||||||
lines.push(`Показаны первые ${visibleRows.length} из ${rows.length}; полный список остается в подтвержденном срезе.`);
|
lines.push(`Показаны первые ${visibleRows.length} из ${rows.length}; полный список остается в подтвержденном срезе.`);
|
||||||
|
|||||||
+44
-9
@@ -120,6 +120,26 @@ function findFocusedCounterpartyValuePoint(
|
|||||||
return profileRows.length === 1 ? profileRows[0] : null;
|
return profileRows.length === 1 ? profileRows[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasProfitAmbiguityCue(normalizedQuestion: string): boolean {
|
||||||
|
return /(?:заработ|прибыл|прибыль|доход|выручк)/iu.test(normalizedQuestion);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCashflowBoundaryLine(isSupplier: boolean): string {
|
||||||
|
return isSupplier
|
||||||
|
? "Граница ответа: это подтвержденный денежный поток по поставщику, а не итоговая задолженность."
|
||||||
|
: "Граница ответа: это подтвержденный денежный поток по поступлениям, а не чистая прибыль.";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCashflowNextStepLine(isSupplier: boolean, normalizedQuestion: string): string | null {
|
||||||
|
if (isSupplier) {
|
||||||
|
return "Следующий шаг: могу отдельно показать остаток долга, просрочку или расшифровку по документам.";
|
||||||
|
}
|
||||||
|
if (hasProfitAmbiguityCue(normalizedQuestion)) {
|
||||||
|
return "Следующий шаг: могу отдельно проверить чистую прибыль по закрытию 90/91/99.";
|
||||||
|
}
|
||||||
|
return "Следующий шаг: могу разложить поток по месяцам, документам или контрагентам.";
|
||||||
|
}
|
||||||
|
|
||||||
export function composeCounterpartyAnalyticsReply(
|
export function composeCounterpartyAnalyticsReply(
|
||||||
intent: AddressIntent,
|
intent: AddressIntent,
|
||||||
rows: ComposeStageRow[],
|
rows: ComposeStageRow[],
|
||||||
@@ -546,6 +566,8 @@ export function composeCounterpartyAnalyticsReply(
|
|||||||
const semanticSingleBestCounterparty =
|
const semanticSingleBestCounterparty =
|
||||||
focus === "top_by_total" && hasSingleBestCounterpartyCue && !asksExplicitRankingList;
|
focus === "top_by_total" && hasSingleBestCounterpartyCue && !asksExplicitRankingList;
|
||||||
const effectiveLimit = asksSingleBestCounterparty || semanticSingleBestCounterparty ? 1 : limit;
|
const effectiveLimit = asksSingleBestCounterparty || semanticSingleBestCounterparty ? 1 : limit;
|
||||||
|
const cashflowBoundaryLine = buildCashflowBoundaryLine(isSupplier);
|
||||||
|
const cashflowNextStepLine = buildCashflowNextStepLine(isSupplier, normalizedQuestion);
|
||||||
|
|
||||||
const byCounterparty = new Map<string, CounterpartyValuePoint>();
|
const byCounterparty = new Map<string, CounterpartyValuePoint>();
|
||||||
const byYear = new Map<number, CounterpartyYearPoint>();
|
const byYear = new Map<number, CounterpartyYearPoint>();
|
||||||
@@ -655,10 +677,12 @@ export function composeCounterpartyAnalyticsReply(
|
|||||||
? `за период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)}`
|
? `за период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)}`
|
||||||
: "за доступное время";
|
: "за доступное время";
|
||||||
const directAnswerLine = isSupplier
|
const directAnswerLine = isSupplier
|
||||||
? `Оборот по ${focusedCounterparty.name} ${periodLabel}: ${deps.formatMoneyRub(focusedCounterparty.total)} по ${focusedCounterparty.ops} подтвержденным исходящим операциям. Это денежный поток по поставщику, а не итоговая задолженность.`
|
? `Оборот по ${focusedCounterparty.name} ${periodLabel}: ${deps.formatMoneyRub(focusedCounterparty.total)} по ${focusedCounterparty.ops} подтвержденным исходящим операциям.`
|
||||||
: `Оборот по ${focusedCounterparty.name} ${periodLabel}: ${deps.formatMoneyRub(focusedCounterparty.total)} по ${focusedCounterparty.ops} подтвержденным входящим операциям. Это денежный поток от клиента, а не чистая прибыль.`;
|
: `Оборот по ${focusedCounterparty.name} ${periodLabel}: ${deps.formatMoneyRub(focusedCounterparty.total)} по ${focusedCounterparty.ops} подтвержденным входящим операциям.`;
|
||||||
const summaryLines = [
|
const summaryLines = [
|
||||||
directAnswerLine,
|
directAnswerLine,
|
||||||
|
cashflowBoundaryLine,
|
||||||
|
...(cashflowNextStepLine ? [cashflowNextStepLine] : []),
|
||||||
"",
|
"",
|
||||||
"Подтверждение:",
|
"Подтверждение:",
|
||||||
`- Контрагент в выборке: ${focusedCounterparty.name}.`,
|
`- Контрагент в выборке: ${focusedCounterparty.name}.`,
|
||||||
@@ -678,11 +702,10 @@ export function composeCounterpartyAnalyticsReply(
|
|||||||
options.periodFrom && options.periodTo
|
options.periodFrom && options.periodTo
|
||||||
? `За период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)} подтверждено ${deps.formatMoneyRub(totalFlow)} ${isSupplier ? "исходящих выплат" : "входящих поступлений"}.`
|
? `За период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)} подтверждено ${deps.formatMoneyRub(totalFlow)} ${isSupplier ? "исходящих выплат" : "входящих поступлений"}.`
|
||||||
: `За все доступное время подтверждено ${deps.formatMoneyRub(totalFlow)} ${isSupplier ? "исходящих выплат" : "входящих поступлений"}.`;
|
: `За все доступное время подтверждено ${deps.formatMoneyRub(totalFlow)} ${isSupplier ? "исходящих выплат" : "входящих поступлений"}.`;
|
||||||
const directAnswerLine = isSupplier
|
|
||||||
? periodLine
|
|
||||||
: `${periodLine} Это денежный поток от клиентов, а не чистая прибыль.`;
|
|
||||||
const summaryLines = [
|
const summaryLines = [
|
||||||
directAnswerLine,
|
periodLine,
|
||||||
|
cashflowBoundaryLine,
|
||||||
|
...(cashflowNextStepLine ? [cashflowNextStepLine] : []),
|
||||||
"",
|
"",
|
||||||
"Подтверждение:",
|
"Подтверждение:",
|
||||||
`- Операций в выборке: ${totalOperations}.`,
|
`- Операций в выборке: ${totalOperations}.`,
|
||||||
@@ -709,11 +732,17 @@ export function composeCounterpartyAnalyticsReply(
|
|||||||
const strongestYear = visible[0];
|
const strongestYear = visible[0];
|
||||||
const directAnswerLine = isSupplier
|
const directAnswerLine = isSupplier
|
||||||
? `Самый крупный год по подтвержденным выплатам: ${strongestYear.year} (${deps.formatMoneyRub(strongestYear.total)} по ${strongestYear.ops} операциям).`
|
? `Самый крупный год по подтвержденным выплатам: ${strongestYear.year} (${deps.formatMoneyRub(strongestYear.total)} по ${strongestYear.ops} операциям).`
|
||||||
: `Самый доходный год по подтвержденным поступлениям: ${strongestYear.year} (${deps.formatMoneyRub(strongestYear.total)} по ${strongestYear.ops} операциям). Это денежный поток, а не чистая прибыль.`;
|
: `Самый доходный год по подтвержденным поступлениям: ${strongestYear.year} (${deps.formatMoneyRub(strongestYear.total)} по ${strongestYear.ops} операциям).`;
|
||||||
const heading = isSupplier
|
const heading = isSupplier
|
||||||
? `Топ-${visible.length} лет по сумме выплат:`
|
? `Топ-${visible.length} лет по сумме выплат:`
|
||||||
: `Топ-${visible.length} лет по сумме поступлений:`;
|
: `Топ-${visible.length} лет по сумме поступлений:`;
|
||||||
lines.unshift(heading);
|
lines.unshift(heading);
|
||||||
|
if (!isSupplier) {
|
||||||
|
lines.unshift(cashflowBoundaryLine);
|
||||||
|
if (cashflowNextStepLine) {
|
||||||
|
lines.unshift(cashflowNextStepLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
lines.unshift(directAnswerLine);
|
lines.unshift(directAnswerLine);
|
||||||
lines.push(
|
lines.push(
|
||||||
...visible.map(
|
...visible.map(
|
||||||
@@ -829,11 +858,17 @@ export function composeCounterpartyAnalyticsReply(
|
|||||||
const directAnswerLine = singleCandidateOnly
|
const directAnswerLine = singleCandidateOnly
|
||||||
? isSupplier
|
? isSupplier
|
||||||
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
||||||
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг; сумма является денежным потоком, а не чистой прибылью.`
|
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
||||||
: isSupplier
|
: isSupplier
|
||||||
? `Крупнейший поставщик по подтвержденным выплатам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
? `Крупнейший поставщик по подтвержденным выплатам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||||
: `Самый доходный клиент ${rankingPeriodLabel} по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это денежный поток, а не чистая прибыль.`;
|
: `Самый доходный клиент ${rankingPeriodLabel} по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`;
|
||||||
lines.unshift(directAnswerLine);
|
lines.unshift(directAnswerLine);
|
||||||
|
if (!isSupplier) {
|
||||||
|
lines.splice(1, 0, cashflowBoundaryLine);
|
||||||
|
if (cashflowNextStepLine) {
|
||||||
|
lines.splice(2, 0, cashflowNextStepLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
lines.push(
|
lines.push(
|
||||||
...visible.map((item, index) => {
|
...visible.map((item, index) => {
|
||||||
|
|||||||
@@ -175,11 +175,10 @@ export function composeInventoryReply(
|
|||||||
const uniqueWarehouses = deps.uniqueStrings(
|
const uniqueWarehouses = deps.uniqueStrings(
|
||||||
positions.map((item) => String(item.warehouse ?? "").trim()).filter((item) => item.length > 0)
|
positions.map((item) => String(item.warehouse ?? "").trim()).filter((item) => item.length > 0)
|
||||||
);
|
);
|
||||||
const totalQuantity = positions.reduce((sum, item) => sum + item.quantity, 0);
|
|
||||||
const totalAmount = positions.reduce((sum, item) => sum + item.amount, 0);
|
const totalAmount = positions.reduce((sum, item) => sum + item.amount, 0);
|
||||||
const directAnswerLine =
|
const directAnswerLine =
|
||||||
positions.length > 0
|
positions.length > 0
|
||||||
? `На ${deps.formatDateRu(asOfDate)} на складе подтверждено ${deps.formatNumberWithDots(positions.length)} позиций с остатком на ${deps.formatMoneyRub(totalAmount)}.`
|
? `На ${deps.formatDateRu(asOfDate)} на складе подтверждено ${deps.formatNumberWithDots(positions.length)} позиций на ${deps.formatMoneyRub(totalAmount)}.`
|
||||||
: `На ${deps.formatDateRu(asOfDate)} подтвержденных товарных остатков по счету 41.01 не найдено.`;
|
: `На ${deps.formatDateRu(asOfDate)} подтвержденных товарных остатков по счету 41.01 не найдено.`;
|
||||||
const lines: string[] = [directAnswerLine];
|
const lines: string[] = [directAnswerLine];
|
||||||
|
|
||||||
@@ -213,11 +212,14 @@ export function composeInventoryReply(
|
|||||||
`Позиции с остатком: ${deps.formatNumberWithDots(positions.length)}.`,
|
`Позиции с остатком: ${deps.formatNumberWithDots(positions.length)}.`,
|
||||||
`Уникальных товаров: ${deps.formatNumberWithDots(uniqueItems.length)}.`,
|
`Уникальных товаров: ${deps.formatNumberWithDots(uniqueItems.length)}.`,
|
||||||
`Уникальных складов: ${deps.formatNumberWithDots(uniqueWarehouses.length)}.`,
|
`Уникальных складов: ${deps.formatNumberWithDots(uniqueWarehouses.length)}.`,
|
||||||
`Суммарное количество: ${deps.formatNumberWithDots(totalQuantity, 3)}.`
|
"Общее количество не свожу в один управленческий показатель, потому что в остатках смешаны разнородные позиции."
|
||||||
]);
|
]);
|
||||||
if (rows.length !== positions.length) {
|
if (rows.length !== positions.length) {
|
||||||
lines.push(`- Проверенных строк движения: ${deps.formatNumberWithDots(rows.length)}.`);
|
lines.push(`- Проверенных строк движения: ${deps.formatNumberWithDots(rows.length)}.`);
|
||||||
}
|
}
|
||||||
|
if (positions.length > 0) {
|
||||||
|
lines.push("- Следующий шаг: могу раскрыть полный список, разложить остатки по складам или сравнить с другой датой.");
|
||||||
|
}
|
||||||
|
|
||||||
return positions.length > 0
|
return positions.length > 0
|
||||||
? buildFactualListReply(lines, buildConfirmedBalanceSemantics("strong"))
|
? buildFactualListReply(lines, buildConfirmedBalanceSemantics("strong"))
|
||||||
|
|||||||
@@ -1128,6 +1128,40 @@ function buildCompactBusinessOverviewReply(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (rankingNeed) {
|
if (rankingNeed) {
|
||||||
|
const explicitPeriodRankingOverview =
|
||||||
|
period &&
|
||||||
|
!/(?:все\s+доступное|все\s+время|all\s+time)/iu.test(period) &&
|
||||||
|
(incomingAmount || outgoingAmount || netAmount);
|
||||||
|
if (explicitPeriodRankingOverview) {
|
||||||
|
lines.push(
|
||||||
|
`Коротко: ${organizationPrefix}${period} денежная картина подтверждена по найденным строкам 1С.`
|
||||||
|
);
|
||||||
|
lines.push(
|
||||||
|
`Деньги: входящие ${incomingAmount ?? "0 руб."}, исходящие ${outgoingAmount ?? "0 руб."}, расчетное операционное нетто ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
|
||||||
|
);
|
||||||
|
if (customerName && customerAmount) {
|
||||||
|
lines.push(
|
||||||
|
topCustomerLooksFinancial
|
||||||
|
? `Топ входящих: 1. ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}. Это финансовый/банковский контур, не считаю его клиентской выручкой без назначения платежа.${nonFinancialCustomer ? ` 2. Крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}.` : ""}`
|
||||||
|
: `Крупнейший входящий контрагент: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (topSupplier) {
|
||||||
|
lines.push(
|
||||||
|
topSupplierLooksFinancial
|
||||||
|
? `Топ исходящих: 1. ${topSupplier}. Это финансовый/банковский контур, не считаю его обычным поставщиком без назначения платежа и договора.${nonFinancialSupplier ? ` 2. Крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}.` : ""}`
|
||||||
|
: `Крупнейший получатель исходящих денег: ${topSupplier}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
lines.push(
|
||||||
|
`Вывод: по движению денег период ${netDirection}; это не чистая прибыль и не бухгалтерский финрезультат.`
|
||||||
|
);
|
||||||
|
if (requestedFinancialBoundaryLine) {
|
||||||
|
lines.push(requestedFinancialBoundaryLine);
|
||||||
|
}
|
||||||
|
lines.push("Следующий шаг: могу отдельно посчитать чистую прибыль через закрытие 90/91/99 или разложить этот период по контрагентам.");
|
||||||
|
return joinBusinessReplyLines(lines);
|
||||||
|
}
|
||||||
const incomingLeader = strongestIncomingYear(overview);
|
const incomingLeader = strongestIncomingYear(overview);
|
||||||
const canRankYearlyNet = !limitLine;
|
const canRankYearlyNet = !limitLine;
|
||||||
const netLeader = canRankYearlyNet ? strongestNetYear(overview) : null;
|
const netLeader = canRankYearlyNet ? strongestNetYear(overview) : null;
|
||||||
|
|||||||
@@ -1600,15 +1600,48 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||||||
const rawEffectiveText = toNonEmptyString(input.effectiveMessage);
|
const rawEffectiveText = toNonEmptyString(input.effectiveMessage);
|
||||||
const repairedUserText = rawUserText ? repairAddressMojibakeText(rawUserText) : null;
|
const repairedUserText = rawUserText ? repairAddressMojibakeText(rawUserText) : null;
|
||||||
const repairedEffectiveText = rawEffectiveText ? repairAddressMojibakeText(rawEffectiveText) : null;
|
const repairedEffectiveText = rawEffectiveText ? repairAddressMojibakeText(rawEffectiveText) : null;
|
||||||
|
const rawUserSignalSourceText = repairedUserText ?? rawUserText ?? "";
|
||||||
const rawSignalSourceText = `${repairedUserText ?? rawUserText ?? ""} ${repairedEffectiveText ?? rawEffectiveText ?? ""}`.trim();
|
const rawSignalSourceText = `${repairedUserText ?? rawUserText ?? ""} ${repairedEffectiveText ?? rawEffectiveText ?? ""}`.trim();
|
||||||
const rawEntitySourceText = repairedUserText ?? rawUserText ?? repairedEffectiveText ?? rawEffectiveText ?? rawSignalSourceText;
|
const rawEntitySourceText = repairedUserText ?? rawUserText ?? repairedEffectiveText ?? rawEffectiveText ?? rawSignalSourceText;
|
||||||
|
const rawUserEntitySourceText = rawUserSignalSourceText || rawEntitySourceText;
|
||||||
|
const rawUserTextOnly = compactLower(rawUserSignalSourceText);
|
||||||
|
const rawAssistantEntityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
|
||||||
|
const rawUserPrimaryBusinessOverviewSignal = hasBusinessOverviewSignal(rawUserTextOnly);
|
||||||
|
const rawUserLifecyclePivotTextSignal =
|
||||||
|
!rawUserPrimaryBusinessOverviewSignal && hasLifecycleSignal(rawUserTextOnly);
|
||||||
|
const rawUserBidirectionalValueFlowPivotTextSignal =
|
||||||
|
!rawUserPrimaryBusinessOverviewSignal &&
|
||||||
|
!rawUserLifecyclePivotTextSignal &&
|
||||||
|
hasBidirectionalValueFlowSignal(rawUserTextOnly);
|
||||||
|
const rawUserScopedEntityCandidate = rawUserSignalSourceText
|
||||||
|
? rawScopedEntityCandidateFromText(rawUserEntitySourceText)
|
||||||
|
: null;
|
||||||
|
const rawUserCounterpartyBidirectionalOverride = Boolean(
|
||||||
|
rawUserBidirectionalValueFlowPivotTextSignal &&
|
||||||
|
(rawUserScopedEntityCandidate ||
|
||||||
|
predecomposeEntities.counterparty ||
|
||||||
|
rawAssistantEntityCandidates.find((candidate) => !isInvalidEntityCandidate(candidate)))
|
||||||
|
);
|
||||||
const rawText = compactLower(rawSignalSourceText);
|
const rawText = compactLower(rawSignalSourceText);
|
||||||
const rawReferentialDocumentExclusionSignal = hasReferentialDocumentExclusionFollowupSignal(
|
const rawReferentialDocumentExclusionSignal = hasReferentialDocumentExclusionFollowupSignal(
|
||||||
repairedUserText ?? rawUserText ?? ""
|
repairedUserText ?? rawUserText ?? ""
|
||||||
);
|
);
|
||||||
const rawPrimaryBusinessOverviewSignal = hasBusinessOverviewSignal(rawText);
|
const rawPrimaryBusinessOverviewSignal =
|
||||||
|
hasBusinessOverviewSignal(rawText) && !rawUserCounterpartyBidirectionalOverride;
|
||||||
const explicitVatQuestionSignal = hasExplicitVatQuestionSignal(rawText);
|
const explicitVatQuestionSignal = hasExplicitVatQuestionSignal(rawText);
|
||||||
const explicitVatMovementEvidenceSignal = hasExplicitVatMovementEvidenceSignal(rawText);
|
const explicitVatMovementEvidenceSignal = hasExplicitVatMovementEvidenceSignal(rawText);
|
||||||
|
const rawLifecyclePivotTextSignal =
|
||||||
|
!rawPrimaryBusinessOverviewSignal && hasLifecycleSignal(rawText);
|
||||||
|
const rawBidirectionalValueFlowPivotTextSignal =
|
||||||
|
!rawPrimaryBusinessOverviewSignal &&
|
||||||
|
!rawLifecyclePivotTextSignal &&
|
||||||
|
hasBidirectionalValueFlowSignal(rawText);
|
||||||
|
const rawValueFlowPivotTextSignal =
|
||||||
|
!rawPrimaryBusinessOverviewSignal &&
|
||||||
|
!rawLifecyclePivotTextSignal &&
|
||||||
|
(hasValueFlowSignal(rawText) ||
|
||||||
|
hasValueRankingSignal(rawText) ||
|
||||||
|
rawBidirectionalValueFlowPivotTextSignal);
|
||||||
const explicitVatSuppressesBusinessOverviewContinuation = Boolean(
|
const explicitVatSuppressesBusinessOverviewContinuation = Boolean(
|
||||||
explicitVatQuestionSignal && !rawPrimaryBusinessOverviewSignal
|
explicitVatQuestionSignal && !rawPrimaryBusinessOverviewSignal
|
||||||
);
|
);
|
||||||
@@ -1634,7 +1667,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||||||
!rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
|
!rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
|
||||||
const rawPayoutSignal = rawValueFlowSignal && !rawBidirectionalValueFlowSignal && hasPayoutSignal(rawText);
|
const rawPayoutSignal = rawValueFlowSignal && !rawBidirectionalValueFlowSignal && hasPayoutSignal(rawText);
|
||||||
const rawValueFlowAggregateQuestionSignal =
|
const rawValueFlowAggregateQuestionSignal =
|
||||||
rawValueFlowSignal && hasValueFlowAggregateQuestionSignal(rawText);
|
(rawValueFlowSignal || rawValueFlowPivotTextSignal) && hasValueFlowAggregateQuestionSignal(rawText);
|
||||||
const monthlyAggregationSignal = hasMonthlyAggregationSignal(rawText);
|
const monthlyAggregationSignal = hasMonthlyAggregationSignal(rawText);
|
||||||
const rawAllTimeScopeSignal = hasAllTimeScopeHint(rawText);
|
const rawAllTimeScopeSignal = hasAllTimeScopeHint(rawText);
|
||||||
const dateScopeSignalText = stripNegatedTaxDateScopeClauses(rawText);
|
const dateScopeSignalText = stripNegatedTaxDateScopeClauses(rawText);
|
||||||
@@ -1702,6 +1735,47 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||||||
: profitMarginBusinessOverviewSignal
|
: profitMarginBusinessOverviewSignal
|
||||||
? "profit_margin_boundary"
|
? "profit_margin_boundary"
|
||||||
: "broad_evaluation";
|
: "broad_evaluation";
|
||||||
|
const assistantTurnMeaningDateScope = toNonEmptyString(assistantTurnMeaning?.explicit_date_scope);
|
||||||
|
const rawAssistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
|
||||||
|
const assistantTurnMeaningOrganizationScope = isReferentialOrganizationPlaceholder(
|
||||||
|
rawAssistantTurnMeaningOrganizationScope
|
||||||
|
)
|
||||||
|
? null
|
||||||
|
: rawAssistantTurnMeaningOrganizationScope;
|
||||||
|
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
|
||||||
|
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
|
||||||
|
const currentTurnFreshOrganizationScope = predecomposeEntities.organization ?? rawOrganizationScope;
|
||||||
|
const currentTurnOrganizationScope =
|
||||||
|
currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope;
|
||||||
|
const predecomposeOrganizationMirrorsCounterparty = sameScopedName(
|
||||||
|
predecomposeEntities.counterparty,
|
||||||
|
predecomposeEntities.organization
|
||||||
|
);
|
||||||
|
const organizationMirrorsPredecomposeCounterpartyForPivot = Boolean(
|
||||||
|
sameScopedName(predecomposeEntities.counterparty, assistantTurnMeaningOrganizationScope) ||
|
||||||
|
sameScopedName(predecomposeEntities.counterparty, currentTurnOrganizationScope) ||
|
||||||
|
predecomposeOrganizationMirrorsCounterparty
|
||||||
|
);
|
||||||
|
const normalizedPredecomposeCounterpartyForPivot =
|
||||||
|
organizationMirrorsPredecomposeCounterpartyForPivot
|
||||||
|
? null
|
||||||
|
: normalizeFollowupCounterpartyCandidate(predecomposeEntities.counterparty);
|
||||||
|
const rawExplicitCounterpartyPivotCandidate =
|
||||||
|
rawScopedEntityCandidate ??
|
||||||
|
rawAssistantEntityCandidates.find(
|
||||||
|
(candidate) =>
|
||||||
|
!isInvalidEntityCandidate(candidate) &&
|
||||||
|
!sameScopedName(candidate, currentTurnOrganizationScope)
|
||||||
|
) ??
|
||||||
|
normalizedPredecomposeCounterpartyForPivot ??
|
||||||
|
null;
|
||||||
|
const businessOverviewCounterpartyValueFlowPivot = Boolean(
|
||||||
|
businessOverviewContinuationSignal &&
|
||||||
|
!rawPrimaryBusinessOverviewSignal &&
|
||||||
|
rawValueFlowPivotTextSignal &&
|
||||||
|
rawExplicitCounterpartyPivotCandidate &&
|
||||||
|
(rawTopicSwitchSignal || rawValueFlowAggregateQuestionSignal)
|
||||||
|
);
|
||||||
const businessOverviewUnsupportedFamily = inventoryReserveBusinessOverviewSignal
|
const businessOverviewUnsupportedFamily = inventoryReserveBusinessOverviewSignal
|
||||||
? "inventory_reserve_liquidation_boundary"
|
? "inventory_reserve_liquidation_boundary"
|
||||||
: debtDueDateBusinessOverviewSignal
|
: debtDueDateBusinessOverviewSignal
|
||||||
@@ -1712,8 +1786,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||||||
? "profit_margin_boundary"
|
? "profit_margin_boundary"
|
||||||
: "broad_business_evaluation";
|
: "broad_business_evaluation";
|
||||||
const businessOverviewSignal =
|
const businessOverviewSignal =
|
||||||
rawBusinessOverviewSignal ||
|
!businessOverviewCounterpartyValueFlowPivot &&
|
||||||
seededBusinessOverviewSignal;
|
(rawBusinessOverviewSignal || seededBusinessOverviewSignal);
|
||||||
const businessOverviewSeparateCounterpartySignal = Boolean(
|
const businessOverviewSeparateCounterpartySignal = Boolean(
|
||||||
businessOverviewSignal && hasBusinessOverviewSeparateCounterpartySignal(rawText)
|
businessOverviewSignal && hasBusinessOverviewSeparateCounterpartySignal(rawText)
|
||||||
);
|
);
|
||||||
@@ -1735,18 +1809,6 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||||||
hasSimpleMovementLanePivotSignal(rawText) ||
|
hasSimpleMovementLanePivotSignal(rawText) ||
|
||||||
hasMovementEvidenceFollowupSignal(rawText) ||
|
hasMovementEvidenceFollowupSignal(rawText) ||
|
||||||
hasPronounMovementEvidenceFollowupSignal(rawText);
|
hasPronounMovementEvidenceFollowupSignal(rawText);
|
||||||
const assistantTurnMeaningDateScope = toNonEmptyString(assistantTurnMeaning?.explicit_date_scope);
|
|
||||||
const rawAssistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
|
|
||||||
const assistantTurnMeaningOrganizationScope = isReferentialOrganizationPlaceholder(
|
|
||||||
rawAssistantTurnMeaningOrganizationScope
|
|
||||||
)
|
|
||||||
? null
|
|
||||||
: rawAssistantTurnMeaningOrganizationScope;
|
|
||||||
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
|
|
||||||
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
|
|
||||||
const currentTurnFreshOrganizationScope = predecomposeEntities.organization ?? rawOrganizationScope;
|
|
||||||
const currentTurnOrganizationScope =
|
|
||||||
currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope;
|
|
||||||
const followupCounterpartyIsMetadataOrganizationScope = Boolean(
|
const followupCounterpartyIsMetadataOrganizationScope = Boolean(
|
||||||
followupSeed.subjectResolutionOptional &&
|
followupSeed.subjectResolutionOptional &&
|
||||||
followupSeed.counterparty &&
|
followupSeed.counterparty &&
|
||||||
@@ -1792,10 +1854,6 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||||||
!rawBidirectionalValueFlowSignal &&
|
!rawBidirectionalValueFlowSignal &&
|
||||||
explicitOrganizationScopeSignal
|
explicitOrganizationScopeSignal
|
||||||
);
|
);
|
||||||
const predecomposeOrganizationMirrorsCounterparty = sameScopedName(
|
|
||||||
predecomposeEntities.counterparty,
|
|
||||||
predecomposeEntities.organization
|
|
||||||
);
|
|
||||||
const organizationMirrorsPredecomposeCounterparty = Boolean(
|
const organizationMirrorsPredecomposeCounterparty = Boolean(
|
||||||
(rawBidirectionalValueFlowSignal ||
|
(rawBidirectionalValueFlowSignal ||
|
||||||
hasValueRankingSignal(rawText) ||
|
hasValueRankingSignal(rawText) ||
|
||||||
@@ -2130,16 +2188,27 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||||||
const bidirectionalValueFlowSignal =
|
const bidirectionalValueFlowSignal =
|
||||||
!businessOverviewSignal &&
|
!businessOverviewSignal &&
|
||||||
!lifecycleSignal &&
|
!lifecycleSignal &&
|
||||||
(rawBidirectionalValueFlowSignal || seededAction === "net_value_flow");
|
((businessOverviewCounterpartyValueFlowPivot
|
||||||
|
? rawBidirectionalValueFlowPivotTextSignal
|
||||||
|
: rawBidirectionalValueFlowSignal) ||
|
||||||
|
seededAction === "net_value_flow");
|
||||||
const valueFlowSignal =
|
const valueFlowSignal =
|
||||||
!businessOverviewSignal &&
|
!businessOverviewSignal &&
|
||||||
!lifecycleSignal &&
|
!lifecycleSignal &&
|
||||||
!metadataGroundedMovementLaneApplicable &&
|
!metadataGroundedMovementLaneApplicable &&
|
||||||
(rawValueFlowSignal || seededDomain === "counterparty_value");
|
((businessOverviewCounterpartyValueFlowPivot
|
||||||
|
? rawValueFlowPivotTextSignal
|
||||||
|
: rawValueFlowSignal) ||
|
||||||
|
seededDomain === "counterparty_value");
|
||||||
const payoutSignal =
|
const payoutSignal =
|
||||||
valueFlowSignal &&
|
valueFlowSignal &&
|
||||||
!bidirectionalValueFlowSignal &&
|
!bidirectionalValueFlowSignal &&
|
||||||
(rawPayoutSignal || seededAction === "payout");
|
((businessOverviewCounterpartyValueFlowPivot
|
||||||
|
? rawValueFlowPivotTextSignal &&
|
||||||
|
!rawBidirectionalValueFlowPivotTextSignal &&
|
||||||
|
hasPayoutSignal(rawText)
|
||||||
|
: rawPayoutSignal) ||
|
||||||
|
seededAction === "payout");
|
||||||
const semanticDataNeed = metadataAmbiguityLaneClarificationApplicable
|
const semanticDataNeed = metadataAmbiguityLaneClarificationApplicable
|
||||||
? "metadata lane clarification"
|
? "metadata lane clarification"
|
||||||
: semanticNeedFor({
|
: semanticNeedFor({
|
||||||
@@ -2147,17 +2216,37 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||||||
? "movements"
|
? "movements"
|
||||||
: businessOverviewSignal
|
: businessOverviewSignal
|
||||||
? "business_overview"
|
? "business_overview"
|
||||||
|
: lifecycleSignal
|
||||||
|
? "counterparty_lifecycle"
|
||||||
|
: valueFlowSignal
|
||||||
|
? "counterparty_value"
|
||||||
: rawDomain ?? seededDomain,
|
: rawDomain ?? seededDomain,
|
||||||
action: explicitVatMovementEvidenceSignal
|
action: explicitVatMovementEvidenceSignal
|
||||||
? "list_movements"
|
? "list_movements"
|
||||||
: businessOverviewSignal
|
: businessOverviewSignal
|
||||||
? businessOverviewActionFamily
|
? businessOverviewActionFamily
|
||||||
|
: lifecycleSignal
|
||||||
|
? "activity_duration"
|
||||||
|
: valueFlowSignal
|
||||||
|
? bidirectionalValueFlowSignal
|
||||||
|
? "net_value_flow"
|
||||||
|
: payoutSignal
|
||||||
|
? "payout"
|
||||||
|
: rawAction ?? seededAction ?? "turnover"
|
||||||
: rawAction ?? seededAction,
|
: rawAction ?? seededAction,
|
||||||
unsupported: explicitVatMovementEvidenceSignal
|
unsupported: explicitVatMovementEvidenceSignal
|
||||||
? "movement_evidence"
|
? "movement_evidence"
|
||||||
: businessOverviewSignal
|
: businessOverviewSignal
|
||||||
? businessOverviewUnsupportedFamily
|
? businessOverviewUnsupportedFamily
|
||||||
: unsupported ?? seededUnsupported,
|
: lifecycleSignal
|
||||||
|
? "counterparty_lifecycle"
|
||||||
|
: valueFlowSignal
|
||||||
|
? bidirectionalValueFlowSignal
|
||||||
|
? "counterparty_bidirectional_value_flow_or_netting"
|
||||||
|
: payoutSignal
|
||||||
|
? "counterparty_payouts_or_outflow"
|
||||||
|
: seededUnsupported ?? "counterparty_value_or_turnover"
|
||||||
|
: unsupported ?? seededUnsupported,
|
||||||
lifecycleSignal,
|
lifecycleSignal,
|
||||||
valueFlowSignal,
|
valueFlowSignal,
|
||||||
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
|
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
|
||||||
@@ -2469,30 +2558,30 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||||||
unsupported_but_understood_family:
|
unsupported_but_understood_family:
|
||||||
businessOverviewSignal
|
businessOverviewSignal
|
||||||
? businessOverviewUnsupportedFamily
|
? businessOverviewUnsupportedFamily
|
||||||
: unsupported ??
|
: lifecycleSignal
|
||||||
(lifecycleSignal
|
? "counterparty_lifecycle"
|
||||||
? "counterparty_lifecycle"
|
|
||||||
: valueFlowSignal
|
: valueFlowSignal
|
||||||
? bidirectionalValueFlowSignal
|
? bidirectionalValueFlowSignal
|
||||||
? "counterparty_bidirectional_value_flow_or_netting"
|
? "counterparty_bidirectional_value_flow_or_netting"
|
||||||
: payoutSignal
|
: payoutSignal
|
||||||
? "counterparty_payouts_or_outflow"
|
? "counterparty_payouts_or_outflow"
|
||||||
: seededUnsupported ?? "counterparty_value_or_turnover"
|
: seededUnsupported ?? "counterparty_value_or_turnover"
|
||||||
: metadataGroundedMovementLaneApplicable
|
: unsupported ??
|
||||||
? "movement_evidence"
|
(metadataGroundedMovementLaneApplicable
|
||||||
: metadataGroundedDocumentLaneApplicable
|
? "movement_evidence"
|
||||||
? "document_evidence"
|
: metadataGroundedDocumentLaneApplicable
|
||||||
: explicitVatMovementEvidenceSignal
|
? "document_evidence"
|
||||||
? "movement_evidence"
|
: explicitVatMovementEvidenceSignal
|
||||||
: metadataAmbiguityLaneClarificationApplicable
|
? "movement_evidence"
|
||||||
? "metadata_lane_choice_clarification"
|
: metadataAmbiguityLaneClarificationApplicable
|
||||||
: entityResolutionSignal
|
? "metadata_lane_choice_clarification"
|
||||||
? "entity_resolution"
|
: entityResolutionSignal
|
||||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
? "entity_resolution"
|
||||||
? "1c_metadata_surface"
|
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||||
: followupDiscoverySeedApplicable
|
? "1c_metadata_surface"
|
||||||
? seededUnsupported
|
: followupDiscoverySeedApplicable
|
||||||
: null),
|
? seededUnsupported
|
||||||
|
: null),
|
||||||
stale_replay_forbidden: Boolean(
|
stale_replay_forbidden: Boolean(
|
||||||
assistantTurnMeaning?.stale_replay_forbidden ||
|
assistantTurnMeaning?.stale_replay_forbidden ||
|
||||||
businessOverviewSignal ||
|
businessOverviewSignal ||
|
||||||
@@ -2763,6 +2852,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||||||
if (businessOverviewSignal) {
|
if (businessOverviewSignal) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_broad_business_evaluation_route_candidate");
|
pushReason(reasonCodes, "mcp_discovery_broad_business_evaluation_route_candidate");
|
||||||
}
|
}
|
||||||
|
if (businessOverviewCounterpartyValueFlowPivot) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_business_overview_followup_pivoted_to_counterparty_value_flow");
|
||||||
|
}
|
||||||
if (businessOverviewContinuationSignal) {
|
if (businessOverviewContinuationSignal) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_from_followup_context");
|
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_from_followup_context");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,6 +138,62 @@ function detectCounterpartyTurnoverFamily(text) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detectScopedCounterpartyEntity(text) {
|
||||||
|
const patterns = [
|
||||||
|
/(?:^|[\s,.;:!?])(?:\u043f\u043e|\u0443|\u0434\u043b\u044f|by|for)\s+(.+?)(?=$|[,.;:!?]|\s+(?:\u0437\u0430|\u043d\u0430|\u0432|\u0432\u043e|\u043a|\u043f\u043e|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a|\u043a\u0430\u043a|\u043a\u0430\u043a\u043e\u0435|\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f|\u043a\u0430\u043a\u0438\u0435|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*|\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u0435\u0436\p{L}*|\u043f\u043b\u0430\u0442[\u0435\u0451]\u0436\p{L}*|\u0438\u0441\u0445\u043e\u0434\p{L}*|\u0432\u0445\u043e\u0434\p{L}*)(?=$|[\s,.;:!?]))/iu,
|
||||||
|
/(?:^|[\s,.;:!?])(?:\u043f\u043e|\u0443|\u0434\u043b\u044f|by|for)\s+([\p{L}\d._-]{2,})(?=$|[\s,.;:!?])/iu
|
||||||
|
];
|
||||||
|
const ignored = new Set([
|
||||||
|
"\u0433\u043e\u0434",
|
||||||
|
"\u0433\u043e\u0434\u0430",
|
||||||
|
"\u043f\u0435\u0440\u0438\u043e\u0434",
|
||||||
|
"\u043f\u0435\u0440\u0438\u043e\u0434\u0430",
|
||||||
|
"\u043c\u0435\u0441\u044f\u0446",
|
||||||
|
"\u043c\u0435\u0441\u044f\u0446\u0430",
|
||||||
|
"\u043a\u0432\u0430\u0440\u0442\u0430\u043b",
|
||||||
|
"\u043a\u0432\u0430\u0440\u0442\u0430\u043b\u0430",
|
||||||
|
"\u0434\u0435\u043d\u044c\u0433\u0438",
|
||||||
|
"\u043d\u0435\u0442\u0442\u043e",
|
||||||
|
"\u0441\u0430\u043b\u044c\u0434\u043e",
|
||||||
|
"year",
|
||||||
|
"period",
|
||||||
|
"month",
|
||||||
|
"quarter",
|
||||||
|
"net"
|
||||||
|
]);
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const rawEntity = text.match(pattern)?.[1]?.trim() ?? "";
|
||||||
|
if (!rawEntity) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const entity = rawEntity.replace(/^["'«»]+|["'«»]+$/gu, "").trim();
|
||||||
|
if (entity.length >= 2 && !ignored.has(entity)) {
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectCounterpartyBidirectionalValueFlowFamily(text) {
|
||||||
|
const hasNetCue =
|
||||||
|
/(?:\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|net\s+(?:flow|cash|payment)|cash\s+net)/iu.test(text);
|
||||||
|
const hasIncomingCue =
|
||||||
|
/(?:\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0432\u0445\u043e\u0434\p{L}*|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*|received|incoming)/iu.test(text);
|
||||||
|
const hasOutgoingCue =
|
||||||
|
/(?:\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*|\u0438\u0441\u0445\u043e\u0434\p{L}*|\u0441\u043f\u0438\u0441\u0430\u043d\p{L}*|paid|outgoing|payment)/iu.test(text);
|
||||||
|
if (!(hasNetCue || (hasIncomingCue && hasOutgoingCue))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const entity = detectScopedCounterpartyEntity(text);
|
||||||
|
if (!entity) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
family: "counterparty_bidirectional_value_flow_or_netting",
|
||||||
|
entity
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function hasExplicitCounterpartyValueObject(text) {
|
function hasExplicitCounterpartyValueObject(text) {
|
||||||
return /(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u0442\u043e\u0432\u0430\u0440|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442\u0443\u0440|\u0441\u0434\u0435\u043b\u043a|customer|client|counterparty|supplier|vendor|contract|item|product|deal)/iu.test(
|
return /(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u0442\u043e\u0432\u0430\u0440|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442\u0443\u0440|\u0441\u0434\u0435\u043b\u043a|customer|client|counterparty|supplier|vendor|contract|item|product|deal)/iu.test(
|
||||||
text
|
text
|
||||||
@@ -379,14 +435,14 @@ function detectBroadBusinessEvaluation(text) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildEntityCandidates(counterpartyTurnover) {
|
function buildEntityCandidates(entityFamily) {
|
||||||
if (!counterpartyTurnover?.entity) {
|
if (!entityFamily?.entity) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
type: "counterparty",
|
type: "counterparty",
|
||||||
value: counterpartyTurnover.entity,
|
value: entityFamily.entity,
|
||||||
source: "current_turn_loose_entity_tail"
|
source: "current_turn_loose_entity_tail"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -400,9 +456,13 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
|||||||
const effectiveText = normalizeTurnText(effectiveMessage, deps);
|
const effectiveText = normalizeTurnText(effectiveMessage, deps);
|
||||||
const joinedText = fallbackCompactWhitespace(`${rawText} ${effectiveText}`);
|
const joinedText = fallbackCompactWhitespace(`${rawText} ${effectiveText}`);
|
||||||
const supportedIntent = detectSupportedIntent(joinedText, deps);
|
const supportedIntent = detectSupportedIntent(joinedText, deps);
|
||||||
|
const counterpartyBidirectionalValueFlow = detectCounterpartyBidirectionalValueFlowFamily(joinedText);
|
||||||
const counterpartyTurnover = detectCounterpartyTurnoverFamily(joinedText);
|
const counterpartyTurnover = detectCounterpartyTurnoverFamily(joinedText);
|
||||||
const selectedObjectInventoryExact = hasSelectedObjectInventoryExactSignal(joinedText);
|
const selectedObjectInventoryExact = hasSelectedObjectInventoryExactSignal(joinedText);
|
||||||
const broadBusinessEvaluation = selectedObjectInventoryExact ? null : detectBroadBusinessEvaluation(joinedText);
|
const broadBusinessEvaluation =
|
||||||
|
selectedObjectInventoryExact || counterpartyBidirectionalValueFlow?.family
|
||||||
|
? null
|
||||||
|
: detectBroadBusinessEvaluation(joinedText);
|
||||||
const llmIntent = toNonEmptyString(input?.llmPreDecomposeMeta?.predecomposeContract?.intent, deps);
|
const llmIntent = toNonEmptyString(input?.llmPreDecomposeMeta?.predecomposeContract?.intent, deps);
|
||||||
const explicitIntentCandidate =
|
const explicitIntentCandidate =
|
||||||
broadBusinessEvaluation?.family
|
broadBusinessEvaluation?.family
|
||||||
@@ -410,6 +470,8 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
|||||||
: supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
|
: supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
|
||||||
const unsupportedFamily = broadBusinessEvaluation?.family
|
const unsupportedFamily = broadBusinessEvaluation?.family
|
||||||
? broadBusinessEvaluation.family
|
? broadBusinessEvaluation.family
|
||||||
|
: !explicitIntentCandidate && counterpartyBidirectionalValueFlow?.family
|
||||||
|
? counterpartyBidirectionalValueFlow.family
|
||||||
: !explicitIntentCandidate && counterpartyTurnover?.family
|
: !explicitIntentCandidate && counterpartyTurnover?.family
|
||||||
? counterpartyTurnover.family
|
? counterpartyTurnover.family
|
||||||
: null;
|
: null;
|
||||||
@@ -417,6 +479,9 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
|||||||
if (supportedIntent?.reason) {
|
if (supportedIntent?.reason) {
|
||||||
reasonCodes.push(supportedIntent.reason);
|
reasonCodes.push(supportedIntent.reason);
|
||||||
}
|
}
|
||||||
|
if (counterpartyBidirectionalValueFlow?.family) {
|
||||||
|
reasonCodes.push("counterparty_bidirectional_value_flow_current_turn_signal");
|
||||||
|
}
|
||||||
if (counterpartyTurnover?.family) {
|
if (counterpartyTurnover?.family) {
|
||||||
reasonCodes.push("counterparty_turnover_current_turn_signal");
|
reasonCodes.push("counterparty_turnover_current_turn_signal");
|
||||||
}
|
}
|
||||||
@@ -443,6 +508,8 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
|||||||
? "inventory"
|
? "inventory"
|
||||||
: broadBusinessEvaluation?.family
|
: broadBusinessEvaluation?.family
|
||||||
? "business_summary"
|
? "business_summary"
|
||||||
|
: counterpartyBidirectionalValueFlow?.family
|
||||||
|
? "counterparty_value"
|
||||||
: explicitIntentCandidate?.includes("counterparty")
|
: explicitIntentCandidate?.includes("counterparty")
|
||||||
? "counterparty"
|
? "counterparty"
|
||||||
: counterpartyTurnover?.family
|
: counterpartyTurnover?.family
|
||||||
@@ -455,6 +522,8 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
|||||||
? "confirmed_snapshot"
|
? "confirmed_snapshot"
|
||||||
: broadBusinessEvaluation?.family
|
: broadBusinessEvaluation?.family
|
||||||
? "broad_evaluation"
|
? "broad_evaluation"
|
||||||
|
: counterpartyBidirectionalValueFlow?.family
|
||||||
|
? "net_value_flow"
|
||||||
: explicitIntentCandidate === "customer_revenue_and_payments" ||
|
: explicitIntentCandidate === "customer_revenue_and_payments" ||
|
||||||
explicitIntentCandidate === "supplier_payouts_profile"
|
explicitIntentCandidate === "supplier_payouts_profile"
|
||||||
? "counterparty_value_or_turnover"
|
? "counterparty_value_or_turnover"
|
||||||
@@ -470,7 +539,10 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
|||||||
? "counterparty_value_or_turnover"
|
? "counterparty_value_or_turnover"
|
||||||
: null;
|
: null;
|
||||||
const staleReplayForbidden = Boolean(
|
const staleReplayForbidden = Boolean(
|
||||||
unsupportedFamily || broadBusinessEvaluation?.family || (counterpartyTurnover?.entity && !explicitIntentCandidate)
|
unsupportedFamily ||
|
||||||
|
broadBusinessEvaluation?.family ||
|
||||||
|
(counterpartyBidirectionalValueFlow?.entity && !explicitIntentCandidate) ||
|
||||||
|
(counterpartyTurnover?.entity && !explicitIntentCandidate)
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
schema_version: "assistant_turn_meaning_v1",
|
schema_version: "assistant_turn_meaning_v1",
|
||||||
@@ -481,10 +553,13 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
|||||||
asked_domain_family: askedDomainFamily,
|
asked_domain_family: askedDomainFamily,
|
||||||
asked_action_family: askedActionFamily,
|
asked_action_family: askedActionFamily,
|
||||||
explicit_intent_candidate: explicitIntentCandidate,
|
explicit_intent_candidate: explicitIntentCandidate,
|
||||||
explicit_entity_candidates: broadBusinessEvaluation?.family ? [] : buildEntityCandidates(counterpartyTurnover),
|
explicit_entity_candidates: broadBusinessEvaluation?.family
|
||||||
|
? []
|
||||||
|
: buildEntityCandidates(counterpartyBidirectionalValueFlow ?? counterpartyTurnover),
|
||||||
meaning_confidence: broadBusinessEvaluation?.family
|
meaning_confidence: broadBusinessEvaluation?.family
|
||||||
? "medium"
|
? "medium"
|
||||||
: supportedIntent?.confidence ?? (counterpartyTurnover?.family ? "medium" : "low"),
|
: supportedIntent?.confidence ??
|
||||||
|
(counterpartyBidirectionalValueFlow?.family || counterpartyTurnover?.family ? "medium" : "low"),
|
||||||
intent_override_strength: explicitIntentCandidate
|
intent_override_strength: explicitIntentCandidate
|
||||||
? "explicit_current_turn_intent"
|
? "explicit_current_turn_intent"
|
||||||
: staleReplayForbidden
|
: staleReplayForbidden
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ type V2FamilyFragment = V2Family["fragments"][number];
|
|||||||
type RouteQueryClass =
|
type RouteQueryClass =
|
||||||
| "exact_object_trace"
|
| "exact_object_trace"
|
||||||
| "ranking_or_period_summary"
|
| "ranking_or_period_summary"
|
||||||
|
| "bidirectional_value_flow"
|
||||||
| "symptom_first"
|
| "symptom_first"
|
||||||
| "lifecycle_first"
|
| "lifecycle_first"
|
||||||
| "chain_break"
|
| "chain_break"
|
||||||
@@ -66,6 +67,10 @@ interface RouteDisciplineRule {
|
|||||||
const ACCOUNT_HINT_PATTERN =
|
const ACCOUNT_HINT_PATTERN =
|
||||||
/(?:\b(?:account|acct|schet|счет|сч)\s*[:#]?\s*(?:[1-9][0-9](?:[./-][0-9]{1,2})?)|\b(?:19|20|21|23|25|26|28|29|44|51|60|62|68)\b)/i;
|
/(?:\b(?:account|acct|schet|счет|сч)\s*[:#]?\s*(?:[1-9][0-9](?:[./-][0-9]{1,2})?)|\b(?:19|20|21|23|25|26|28|29|44|51|60|62|68)\b)/i;
|
||||||
const PERIOD_PATTERN = /\b20\d{2}(?:[-./](?:0[1-9]|1[0-2]))?\b/i;
|
const PERIOD_PATTERN = /\b20\d{2}(?:[-./](?:0[1-9]|1[0-2]))?\b/i;
|
||||||
|
const BIDIRECTIONAL_VALUE_FLOW_PATTERN =
|
||||||
|
/(?:\b(?:receive(?:d)?|received|get|got|incoming|inflow|paid|payment|payments|outgoing|outflow|net|netto|cash\s*flow)\b|получ(?:ить|ил[аи]?|ено|аем|или)|поступ(?:ил[аи]?|ление|ления)|заплат(?:ить|ил[аи]?|или)|оплат(?:ить|ил[аи]?|ы|или)|входящ(?:ий|ие|их)|исходящ(?:ий|ие|их)|нетто|сальдо)/iu;
|
||||||
|
const COUNTERPARTY_SCOPE_PATTERN =
|
||||||
|
/(?:\b(?:counterparty|supplier|customer|vendor|client|bank)\b|контрагент|поставщик|покупател|клиент|заказчик|банк|сбербанк|по\s+(?:ип|ооо|пао|зао|оао|группа)\b)/iu;
|
||||||
const SYMPTOM_MARKER_PATTERN =
|
const SYMPTOM_MARKER_PATTERN =
|
||||||
/(?:\bsymptom\b|\banomaly\b|\bproblem\b|\bissue\b|\btail\b|\bhanging\b|\bblocked\b|\bincomplete\b|remains?\s+open|not\s+(?:confirmed|observed|resolved|closed)|не\s+(?:подтвержден|закрыт|наблюдается)|хвост|сбой|проблем)/i;
|
/(?:\bsymptom\b|\banomaly\b|\bproblem\b|\bissue\b|\btail\b|\bhanging\b|\bblocked\b|\bincomplete\b|remains?\s+open|not\s+(?:confirmed|observed|resolved|closed)|не\s+(?:подтвержден|закрыт|наблюдается)|хвост|сбой|проблем)/i;
|
||||||
const LIFECYCLE_MARKER_PATTERN =
|
const LIFECYCLE_MARKER_PATTERN =
|
||||||
@@ -96,6 +101,13 @@ export const ROUTE_DISCIPLINE_RULE_TABLE: RouteDisciplineRule[] = [
|
|||||||
forbidden_fallback: ["store_canonical", "hybrid_store_plus_live"],
|
forbidden_fallback: ["store_canonical", "hybrid_store_plus_live"],
|
||||||
description: "Ranking and period summary queries require analytical batch path."
|
description: "Ranking and period summary queries require analytical batch path."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
query_class: "bidirectional_value_flow",
|
||||||
|
required_route: "hybrid_store_plus_live",
|
||||||
|
allowed_fallback: ["no_route"],
|
||||||
|
forbidden_fallback: ["store_canonical"],
|
||||||
|
description: "Scoped bidirectional value-flow questions require hybrid evidence path."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
query_class: "symptom_first",
|
query_class: "symptom_first",
|
||||||
required_route: "hybrid_store_plus_live",
|
required_route: "hybrid_store_plus_live",
|
||||||
@@ -218,6 +230,21 @@ function hasAccountOrPeriodAnchor(fragment: V2FamilyFragment, lowerText: string)
|
|||||||
return fragment.account_hints.length > 0 || ACCOUNT_HINT_PATTERN.test(lowerText) || PERIOD_PATTERN.test(lowerText);
|
return fragment.account_hints.length > 0 || ACCOUNT_HINT_PATTERN.test(lowerText) || PERIOD_PATTERN.test(lowerText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasBidirectionalValueFlowSignal(fragment: V2FamilyFragment, lowerText: string): boolean {
|
||||||
|
if (fragment.flags.asks_for_ranking_or_top || fragment.flags.asks_for_period_summary) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return BIDIRECTIONAL_VALUE_FLOW_PATTERN.test(lowerText);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCounterpartyScopeSignal(fragment: V2FamilyFragment, lowerText: string): boolean {
|
||||||
|
return (
|
||||||
|
COUNTERPARTY_SCOPE_PATTERN.test(lowerText) ||
|
||||||
|
fragment.entity_hints.some((hint) => hint.trim().length > 0) ||
|
||||||
|
fragment.candidate_labels.includes("cross_entity")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function resolveRouteClass(fragment: V2FamilyFragment): RouteDisciplineRule {
|
function resolveRouteClass(fragment: V2FamilyFragment): RouteDisciplineRule {
|
||||||
const lowerText = mergedFragmentText(fragment);
|
const lowerText = mergedFragmentText(fragment);
|
||||||
const symptomSignal = hasSymptomSignal(fragment, lowerText);
|
const symptomSignal = hasSymptomSignal(fragment, lowerText);
|
||||||
@@ -227,6 +254,8 @@ function resolveRouteClass(fragment: V2FamilyFragment): RouteDisciplineRule {
|
|||||||
const causalSignal = hasCausalSignal(lowerText);
|
const causalSignal = hasCausalSignal(lowerText);
|
||||||
const ambiguitySignal = hasAmbiguitySignal(fragment, lowerText);
|
const ambiguitySignal = hasAmbiguitySignal(fragment, lowerText);
|
||||||
const accountOrPeriodAnchor = hasAccountOrPeriodAnchor(fragment, lowerText);
|
const accountOrPeriodAnchor = hasAccountOrPeriodAnchor(fragment, lowerText);
|
||||||
|
const bidirectionalValueFlowSignal = hasBidirectionalValueFlowSignal(fragment, lowerText);
|
||||||
|
const counterpartyScopeSignal = hasCounterpartyScopeSignal(fragment, lowerText);
|
||||||
|
|
||||||
if (fragment.flags.asks_for_exact_object_trace) {
|
if (fragment.flags.asks_for_exact_object_trace) {
|
||||||
return ROUTE_DISCIPLINE_RULE_MAP.get("exact_object_trace")!;
|
return ROUTE_DISCIPLINE_RULE_MAP.get("exact_object_trace")!;
|
||||||
@@ -234,6 +263,9 @@ function resolveRouteClass(fragment: V2FamilyFragment): RouteDisciplineRule {
|
|||||||
if (fragment.flags.asks_for_ranking_or_top || fragment.flags.asks_for_period_summary) {
|
if (fragment.flags.asks_for_ranking_or_top || fragment.flags.asks_for_period_summary) {
|
||||||
return ROUTE_DISCIPLINE_RULE_MAP.get("ranking_or_period_summary")!;
|
return ROUTE_DISCIPLINE_RULE_MAP.get("ranking_or_period_summary")!;
|
||||||
}
|
}
|
||||||
|
if (bidirectionalValueFlowSignal && counterpartyScopeSignal) {
|
||||||
|
return ROUTE_DISCIPLINE_RULE_MAP.get("bidirectional_value_flow")!;
|
||||||
|
}
|
||||||
if (ambiguitySignal && (symptomSignal || lifecycleSignal || chainBreakSignal || periodImpactSignal || causalSignal)) {
|
if (ambiguitySignal && (symptomSignal || lifecycleSignal || chainBreakSignal || periodImpactSignal || causalSignal)) {
|
||||||
return ROUTE_DISCIPLINE_RULE_MAP.get("mixed_ambiguity")!;
|
return ROUTE_DISCIPLINE_RULE_MAP.get("mixed_ambiguity")!;
|
||||||
}
|
}
|
||||||
@@ -272,7 +304,8 @@ function shouldPromoteFromNoRoute(fragment: V2FamilyFragment, rule: RouteDiscipl
|
|||||||
hasLifecycleSignal(fragment, lowerText) ||
|
hasLifecycleSignal(fragment, lowerText) ||
|
||||||
hasChainBreakSignal(lowerText) ||
|
hasChainBreakSignal(lowerText) ||
|
||||||
hasPeriodImpactSignal(lowerText) ||
|
hasPeriodImpactSignal(lowerText) ||
|
||||||
hasCausalSignal(lowerText);
|
hasCausalSignal(lowerText) ||
|
||||||
|
(hasBidirectionalValueFlowSignal(fragment, lowerText) && hasCounterpartyScopeSignal(fragment, lowerText));
|
||||||
|
|
||||||
const hasAnchor =
|
const hasAnchor =
|
||||||
hasAccountOrPeriodAnchor(fragment, lowerText) ||
|
hasAccountOrPeriodAnchor(fragment, lowerText) ||
|
||||||
|
|||||||
@@ -587,6 +587,7 @@ describe("address compose stage utf8 headers", () => {
|
|||||||
expect(reply.text).toContain("не подтвержденная клиентская выручка");
|
expect(reply.text).toContain("не подтвержденная клиентская выручка");
|
||||||
expect(reply.text).toContain("Сводка по направлению");
|
expect(reply.text).toContain("Сводка по направлению");
|
||||||
expect(reply.text).toContain("Основание 1С");
|
expect(reply.text).toContain("Основание 1С");
|
||||||
|
expect(reply.text).toContain("По смыслу это скорее финансовый/банковский контур");
|
||||||
expect(reply.text).toContain("вид операции/назначение платежа/договор");
|
expect(reply.text).toContain("вид операции/назначение платежа/договор");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -613,6 +614,8 @@ describe("address compose stage utf8 headers", () => {
|
|||||||
|
|
||||||
expect(reply.text).toContain("Сводка по направлению");
|
expect(reply.text).toContain("Сводка по направлению");
|
||||||
expect(reply.text).toContain("Это не обычный поставщик автоматически");
|
expect(reply.text).toContain("Это не обычный поставщик автоматически");
|
||||||
|
expect(reply.text).toContain("Примеры строк 1С:");
|
||||||
|
expect(reply.text).toContain("Следующий шаг: могу отдельно разложить назначения платежа");
|
||||||
expect(reply.text).toContain("Показаны первые 5 из 8");
|
expect(reply.text).toContain("Показаны первые 5 из 8");
|
||||||
expect(reply.text).not.toContain("00000000007");
|
expect(reply.text).not.toContain("00000000007");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -443,4 +443,86 @@ describe("address reply builders regressions", () => {
|
|||||||
expect(result.text.split("\n")[0]).toContain("1.000,00");
|
expect(result.text.split("\n")[0]).toContain("1.000,00");
|
||||||
expect(result.text).not.toContain("встречных остатков");
|
expect(result.text).not.toContain("встречных остатков");
|
||||||
});
|
});
|
||||||
|
it("avoids mixed stock quantity as a fake management KPI in on-hand snapshot answers", () => {
|
||||||
|
const result = composeInventoryReply(
|
||||||
|
"inventory_on_hand_as_of_date",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
amount: 695360,
|
||||||
|
quantity: 3,
|
||||||
|
item: "Рабочая станция",
|
||||||
|
warehouse: "Основной склад",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
period: "2017-06-30",
|
||||||
|
registrator: "Остатки"
|
||||||
|
} as any,
|
||||||
|
{
|
||||||
|
amount: 295526.51,
|
||||||
|
quantity: 317000,
|
||||||
|
item: "Лифлеты",
|
||||||
|
warehouse: "Основной склад",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
period: "2017-06-30",
|
||||||
|
registrator: "Остатки"
|
||||||
|
} as any
|
||||||
|
],
|
||||||
|
{
|
||||||
|
userMessage: "какие остатки на июнь 2017",
|
||||||
|
asOfDate: "2017-06-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resolvePayablesAsOfDate: () => "2017-06-30",
|
||||||
|
buildInventoryOnHandAggregate: () => [
|
||||||
|
{
|
||||||
|
item: "Рабочая станция",
|
||||||
|
warehouse: "Основной склад",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
quantity: 3,
|
||||||
|
amount: 695360,
|
||||||
|
operations: 1,
|
||||||
|
firstPeriod: "2017-06-30",
|
||||||
|
lastPeriod: "2017-06-30",
|
||||||
|
sourceRefs: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
item: "Лифлеты",
|
||||||
|
warehouse: "Основной склад",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
quantity: 317000,
|
||||||
|
amount: 295526.51,
|
||||||
|
operations: 1,
|
||||||
|
firstPeriod: "2017-06-30",
|
||||||
|
lastPeriod: "2017-06-30",
|
||||||
|
sourceRefs: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
uniqueStrings: (values: string[]) => Array.from(new Set(values)),
|
||||||
|
formatDateRu: (value: string) => value,
|
||||||
|
formatNumberWithDots: (value: number, fractionDigits = 0) => value.toFixed(fractionDigits),
|
||||||
|
formatMoneyRub: (value: number) => `${value} ₽`,
|
||||||
|
isInventoryPurchaseMovement: () => false,
|
||||||
|
summarizeInventoryTraceRows: () => ({
|
||||||
|
item: null,
|
||||||
|
warehouses: [],
|
||||||
|
organizations: [],
|
||||||
|
counterparties: [],
|
||||||
|
documents: [],
|
||||||
|
firstPeriod: null,
|
||||||
|
lastPeriod: null,
|
||||||
|
totalAmount: 0
|
||||||
|
}),
|
||||||
|
formatInventoryTraceRows: () => [],
|
||||||
|
hasInventoryPurchaseDateActionFocus: () => false,
|
||||||
|
inventoryTraceDateLabel: () => "",
|
||||||
|
extractInventoryCounterpartyCandidates: () => [],
|
||||||
|
buildInventoryAgingByItemAggregate: () => [],
|
||||||
|
formatInventoryAgingRows: () => [],
|
||||||
|
isInventorySaleMovement: () => false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result?.text).not.toContain("Суммарное количество");
|
||||||
|
expect(result?.text).toContain("Общее количество не свожу в один управленческий показатель");
|
||||||
|
expect(result?.text).toContain("Следующий шаг: могу раскрыть полный список");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3342,6 +3342,107 @@ describe("assistant MCP discovery turn input adapter", () => {
|
|||||||
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_followup_context");
|
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_followup_context");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("pivots a business-overview follow-up with explicit counterparty net-flow wording back to counterparty value flow", () => {
|
||||||
|
const orgName =
|
||||||
|
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||||
|
const counterpartyName = "\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a";
|
||||||
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
|
userMessage:
|
||||||
|
"\u0410 \u0442\u0435\u043f\u0435\u0440\u044c \u043f\u043e \u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a \u0437\u0430 2020: \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0434\u0435\u043d\u0435\u0433 \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438, \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438 \u0438 \u043a\u0430\u043a\u043e\u0435 \u043d\u0435\u0442\u0442\u043e?",
|
||||||
|
assistantTurnMeaning: {
|
||||||
|
asked_domain_family: "unknown",
|
||||||
|
asked_action_family: "unknown"
|
||||||
|
},
|
||||||
|
followupContext: {
|
||||||
|
previous_discovery_pilot_scope: "business_overview_route_template_v1",
|
||||||
|
previous_filters: {
|
||||||
|
organization: orgName,
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.adapter_status).toBe("ready");
|
||||||
|
expect(result.should_run_discovery).toBe(true);
|
||||||
|
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
|
||||||
|
expect(result.data_need_graph?.business_fact_family).toBe("value_flow");
|
||||||
|
expect(result.data_need_graph?.subject_candidates).toEqual([counterpartyName]);
|
||||||
|
expect(result.turn_meaning_ref).toMatchObject({
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "net_value_flow",
|
||||||
|
explicit_entity_candidates: [counterpartyName],
|
||||||
|
explicit_organization_scope: orgName,
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting",
|
||||||
|
stale_replay_forbidden: true
|
||||||
|
});
|
||||||
|
expect(result.reason_codes).toContain(
|
||||||
|
"mcp_discovery_business_overview_followup_pivoted_to_counterparty_value_flow"
|
||||||
|
);
|
||||||
|
expect(result.reason_codes).toContain("mcp_discovery_business_overview_continuation_from_followup_context");
|
||||||
|
expect(result.reason_codes).toContain("mcp_discovery_counterparty_from_raw_scope");
|
||||||
|
expect(result.reason_codes).not.toContain("mcp_discovery_broad_business_evaluation_route_candidate");
|
||||||
|
expect(result.reason_codes).not.toContain("mcp_discovery_business_overview_suppressed_stale_counterparty");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("survives poisoned business-overview turn meaning when predecompose still carries the counterparty", () => {
|
||||||
|
const orgName =
|
||||||
|
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||||
|
const counterpartyName = "\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a";
|
||||||
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
|
userMessage:
|
||||||
|
"\u0410 \u0442\u0435\u043f\u0435\u0440\u044c \u043f\u043e \u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a \u0437\u0430 2020: \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0434\u0435\u043d\u0435\u0433 \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438, \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438 \u0438 \u043a\u0430\u043a\u043e\u0435 \u043d\u0435\u0442\u0442\u043e?",
|
||||||
|
effectiveMessage:
|
||||||
|
"\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0441\u0443\u043c\u043c\u0443 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0445 \u0441\u0440\u0435\u0434\u0441\u0442\u0432, \u0441\u0443\u043c\u043c\u0443 \u0432\u044b\u043f\u043b\u0430\u0447\u0435\u043d\u043d\u044b\u0445 \u0441\u0440\u0435\u0434\u0441\u0442\u0432 \u0438 \u0447\u0438\u0441\u0442\u0443\u044e \u043f\u0440\u0438\u0431\u044b\u043b\u044c (\u043d\u0435\u0442\u0442\u043e) \u0434\u043b\u044f \u0433\u0440\u0443\u043f\u043f\u044b \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0439 \u0421\u0412\u041a \u0437\u0430 2020 \u0433\u043e\u0434.",
|
||||||
|
assistantTurnMeaning: {
|
||||||
|
asked_domain_family: "business_overview",
|
||||||
|
asked_action_family: "profit_margin_boundary",
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
unsupported_but_understood_family: "profit_margin_boundary",
|
||||||
|
stale_replay_forbidden: true
|
||||||
|
},
|
||||||
|
predecomposeContract: {
|
||||||
|
entities: {
|
||||||
|
counterparty: counterpartyName
|
||||||
|
},
|
||||||
|
period: {
|
||||||
|
scope: "range",
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
followupContext: {
|
||||||
|
previous_discovery_pilot_scope: "business_overview_route_template_v1",
|
||||||
|
previous_filters: {
|
||||||
|
organization: orgName,
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.adapter_status).toBe("ready");
|
||||||
|
expect(result.should_run_discovery).toBe(true);
|
||||||
|
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
|
||||||
|
expect(result.data_need_graph?.business_fact_family).toBe("value_flow");
|
||||||
|
expect(result.data_need_graph?.subject_candidates).toEqual([counterpartyName]);
|
||||||
|
expect(result.turn_meaning_ref).toMatchObject({
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "net_value_flow",
|
||||||
|
explicit_entity_candidates: [counterpartyName],
|
||||||
|
explicit_organization_scope: orgName,
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting",
|
||||||
|
stale_replay_forbidden: true
|
||||||
|
});
|
||||||
|
expect(result.reason_codes).toContain(
|
||||||
|
"mcp_discovery_business_overview_followup_pivoted_to_counterparty_value_flow"
|
||||||
|
);
|
||||||
|
expect(result.reason_codes).toContain("mcp_discovery_counterparty_from_predecompose");
|
||||||
|
expect(result.reason_codes).not.toContain("mcp_discovery_broad_business_evaluation_route_candidate");
|
||||||
|
});
|
||||||
|
|
||||||
it("lets an explicit VAT follow-up stay on the exact VAT route instead of stale business overview", () => {
|
it("lets an explicit VAT follow-up stay on the exact VAT route instead of stale business overview", () => {
|
||||||
const orgName =
|
const orgName =
|
||||||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||||
|
|||||||
@@ -57,6 +57,34 @@ describe("assistantTurnMeaningPolicy", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps counterparty net-flow wording in counterparty semantics even if effective text mentions profit", () => {
|
||||||
|
const policy = buildPolicy({
|
||||||
|
resolveAddressIntent: () => ({ intent: "unknown", confidence: "low" })
|
||||||
|
});
|
||||||
|
|
||||||
|
const meaning = policy.resolveAssistantTurnMeaning({
|
||||||
|
rawUserMessage:
|
||||||
|
"\u0410 \u0442\u0435\u043f\u0435\u0440\u044c \u043f\u043e \u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a \u0437\u0430 2020: \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0434\u0435\u043d\u0435\u0433 \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438, \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438 \u0438 \u043a\u0430\u043a\u043e\u0435 \u043d\u0435\u0442\u0442\u043e?",
|
||||||
|
effectiveAddressUserMessage:
|
||||||
|
"\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0441\u0443\u043c\u043c\u0443 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0445 \u0441\u0440\u0435\u0434\u0441\u0442\u0432, \u0441\u0443\u043c\u043c\u0443 \u0432\u044b\u043f\u043b\u0430\u0447\u0435\u043d\u043d\u044b\u0445 \u0441\u0440\u0435\u0434\u0441\u0442\u0432 \u0438 \u0447\u0438\u0441\u0442\u0443\u044e \u043f\u0440\u0438\u0431\u044b\u043b\u044c (\u043d\u0435\u0442\u0442\u043e) \u0434\u043b\u044f \u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a \u0437\u0430 2020 \u0433\u043e\u0434."
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(meaning.explicit_intent_candidate).toBeNull();
|
||||||
|
expect(meaning.asked_domain_family).toBe("counterparty_value");
|
||||||
|
expect(meaning.asked_action_family).toBe("net_value_flow");
|
||||||
|
expect(meaning.unsupported_but_understood_family).toBe("counterparty_bidirectional_value_flow_or_netting");
|
||||||
|
expect(meaning.explicit_entity_candidates).toEqual([
|
||||||
|
{
|
||||||
|
type: "counterparty",
|
||||||
|
value: "\u0433\u0440\u0443\u043f\u043f\u0430 \u0441\u0432\u043a",
|
||||||
|
source: "current_turn_loose_entity_tail"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
expect(meaning.reason_codes).toContain("counterparty_bidirectional_value_flow_current_turn_signal");
|
||||||
|
expect(meaning.reason_codes).not.toContain("broad_business_evaluation_current_turn_signal");
|
||||||
|
expect(meaning.stale_replay_forbidden).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("ignores temporal tail words in all-time revenue ranking questions", () => {
|
it("ignores temporal tail words in all-time revenue ranking questions", () => {
|
||||||
const policy = buildPolicy({
|
const policy = buildPolicy({
|
||||||
resolveAddressIntent: (text: string) =>
|
resolveAddressIntent: (text: string) =>
|
||||||
|
|||||||
@@ -109,7 +109,8 @@ describe("counterparty analytics reply builders", () => {
|
|||||||
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
||||||
expect(reply.text).toContain("Оборот по СВК за доступное время: 3.500,00");
|
expect(reply.text).toContain("Оборот по СВК за доступное время: 3.500,00");
|
||||||
expect(reply.text).toContain("по 2 подтвержденным входящим операциям");
|
expect(reply.text).toContain("по 2 подтвержденным входящим операциям");
|
||||||
expect(reply.text).toContain("Это денежный поток от клиента, а не чистая прибыль");
|
expect(reply.text).toContain("Граница ответа: это подтвержденный денежный поток по поступлениям, а не чистая прибыль.");
|
||||||
|
expect(reply.text).toContain("Следующий шаг: могу разложить поток по месяцам, документам или контрагентам.");
|
||||||
expect(reply.text).not.toContain("Самый доходный клиент");
|
expect(reply.text).not.toContain("Самый доходный клиент");
|
||||||
expect(reply.text).not.toContain("Топ-");
|
expect(reply.text).not.toContain("Топ-");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -225,4 +225,63 @@ describe("routeHintAdapter", () => {
|
|||||||
}
|
}
|
||||||
expect(summary.decisions[0]?.route).toBe("store_canonical");
|
expect(summary.decisions[0]?.route).toBe("store_canonical");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("routes counterparty received-paid-net wording to hybrid instead of canonical fact lookup", () => {
|
||||||
|
const summary = toRouteHintSummary({
|
||||||
|
schema_version: "normalized_query_v2_0_2",
|
||||||
|
user_message_raw: "А теперь по Группа СВК за 2020: сколько денег получили, сколько заплатили и какое нетто?",
|
||||||
|
message_in_scope: true,
|
||||||
|
scope_confidence: "high",
|
||||||
|
contains_multiple_tasks: false,
|
||||||
|
fragments: [
|
||||||
|
{
|
||||||
|
fragment_id: "F1",
|
||||||
|
raw_fragment_text: "А теперь по Группа СВК за 2020: сколько денег получили, сколько заплатили и какое нетто?",
|
||||||
|
normalized_fragment_text:
|
||||||
|
"Определить сумму полученных средств, сумму выплаченных средств и чистый остаток (нетто) для контрагента Группа СВК за период 2020 года.",
|
||||||
|
domain_relevance: "in_scope",
|
||||||
|
business_scope: "company_specific_accounting",
|
||||||
|
entity_hints: ["Группа СВК"],
|
||||||
|
account_hints: [],
|
||||||
|
document_hints: [],
|
||||||
|
register_hints: [],
|
||||||
|
time_scope: {
|
||||||
|
type: "explicit",
|
||||||
|
value: "2020",
|
||||||
|
confidence: "high"
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
has_multi_entity_scope: false,
|
||||||
|
asks_for_chain_explanation: false,
|
||||||
|
asks_for_ranking_or_top: false,
|
||||||
|
asks_for_period_summary: false,
|
||||||
|
asks_for_rule_check: false,
|
||||||
|
asks_for_anomaly_scan: false,
|
||||||
|
asks_for_exact_object_trace: false,
|
||||||
|
asks_for_evidence: false,
|
||||||
|
mentions_period_close_context: false
|
||||||
|
},
|
||||||
|
candidate_labels: ["simple_factual"],
|
||||||
|
confidence: "high",
|
||||||
|
execution_readiness: "executable",
|
||||||
|
clarification_reason: null,
|
||||||
|
soft_assumption_used: [],
|
||||||
|
route_status: "routed",
|
||||||
|
no_route_reason: null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
discarded_fragments: [],
|
||||||
|
global_notes: {
|
||||||
|
needs_clarification: false,
|
||||||
|
clarification_reason: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(summary.mode).toBe("deterministic_v2");
|
||||||
|
if (summary.mode !== "deterministic_v2") {
|
||||||
|
throw new Error("Expected deterministic_v2 summary");
|
||||||
|
}
|
||||||
|
expect(summary.decisions[0]?.route).toBe("hybrid_store_plus_live");
|
||||||
|
expect(summary.decisions[0]?.reason).toContain("bidirectional_value_flow");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+2
-2
@@ -4,8 +4,8 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>NDC AI Normalizer Playground</title>
|
<title>NDC AI Normalizer Playground</title>
|
||||||
<script type="module" crossorigin src="/assets/index-6meEannb.js"></script>
|
<script type="module" crossorigin src="/assets/index-9w9i5XPJ.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BURU4_Sm.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DW_SonhM.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import type {
|
|||||||
ManualCaseDecision,
|
ManualCaseDecision,
|
||||||
PromptState
|
PromptState
|
||||||
} from "../state/types";
|
} from "../state/types";
|
||||||
|
import { buildAutoRunDialogExportForCopy, type ConversationExportMode } from "../utils/conversationExport";
|
||||||
import { AssistantPanel } from "./AssistantPanel";
|
import { AssistantPanel } from "./AssistantPanel";
|
||||||
import { ConnectionPanel } from "./ConnectionPanel";
|
import { ConnectionPanel } from "./ConnectionPanel";
|
||||||
import { JsonView } from "./JsonView";
|
import { JsonView } from "./JsonView";
|
||||||
@@ -620,6 +621,37 @@ function CardStopIcon() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function writeTextToClipboard(text: string): Promise<boolean> {
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
// Fall back to the legacy path below.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const textarea = document.createElement("textarea");
|
||||||
|
textarea.value = text;
|
||||||
|
textarea.setAttribute("readonly", "true");
|
||||||
|
textarea.style.position = "fixed";
|
||||||
|
textarea.style.opacity = "0";
|
||||||
|
textarea.style.pointerEvents = "none";
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
|
||||||
|
let copied = false;
|
||||||
|
try {
|
||||||
|
copied = document.execCommand("copy");
|
||||||
|
} catch {
|
||||||
|
copied = false;
|
||||||
|
} finally {
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
}
|
||||||
|
|
||||||
|
return copied;
|
||||||
|
}
|
||||||
|
|
||||||
function GroupChevronIcon({ expanded }: { expanded: boolean }) {
|
function GroupChevronIcon({ expanded }: { expanded: boolean }) {
|
||||||
return (
|
return (
|
||||||
<svg className={expanded ? "autoruns-group-chevron-svg expanded" : "autoruns-group-chevron-svg"} viewBox="0 0 16 16" aria-hidden="true" focusable="false">
|
<svg className={expanded ? "autoruns-group-chevron-svg expanded" : "autoruns-group-chevron-svg"} viewBox="0 0 16 16" aria-hidden="true" focusable="false">
|
||||||
@@ -697,6 +729,9 @@ export function AutoRunsHistoryPanel({
|
|||||||
const [dialogBusy, setDialogBusy] = useState(false);
|
const [dialogBusy, setDialogBusy] = useState(false);
|
||||||
const [annotationsBusy, setAnnotationsBusy] = useState(false);
|
const [annotationsBusy, setAnnotationsBusy] = useState(false);
|
||||||
const [annotationResolutionBusyId, setAnnotationResolutionBusyId] = useState("");
|
const [annotationResolutionBusyId, setAnnotationResolutionBusyId] = useState("");
|
||||||
|
const dialogCopyResetTimerRef = useRef<number | null>(null);
|
||||||
|
const [dialogCopyState, setDialogCopyState] = useState<"idle" | "success" | "error">("idle");
|
||||||
|
const [dialogCopyModeLabel, setDialogCopyModeLabel] = useState<"чат" | "тех">("чат");
|
||||||
const [errorText, setErrorText] = useState("");
|
const [errorText, setErrorText] = useState("");
|
||||||
const [assistantLiveSessionId, setAssistantLiveSessionId] = useState("");
|
const [assistantLiveSessionId, setAssistantLiveSessionId] = useState("");
|
||||||
const [assistantLiveConversation, setAssistantLiveConversation] = useState<AssistantConversationItem[]>([]);
|
const [assistantLiveConversation, setAssistantLiveConversation] = useState<AssistantConversationItem[]>([]);
|
||||||
@@ -941,6 +976,14 @@ export function AutoRunsHistoryPanel({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (dialogCopyResetTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(dialogCopyResetTimerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const copyIdentifierToClipboard = useCallback(
|
const copyIdentifierToClipboard = useCallback(
|
||||||
async (event: React.SyntheticEvent, valueRaw: string, label: string) => {
|
async (event: React.SyntheticEvent, valueRaw: string, label: string) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -950,19 +993,7 @@ export function AutoRunsHistoryPanel({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (navigator?.clipboard?.writeText) {
|
await writeTextToClipboard(value);
|
||||||
await navigator.clipboard.writeText(value);
|
|
||||||
} else {
|
|
||||||
const textarea = document.createElement("textarea");
|
|
||||||
textarea.value = value;
|
|
||||||
textarea.setAttribute("readonly", "true");
|
|
||||||
textarea.style.position = "fixed";
|
|
||||||
textarea.style.opacity = "0";
|
|
||||||
document.body.appendChild(textarea);
|
|
||||||
textarea.select();
|
|
||||||
document.execCommand("copy");
|
|
||||||
document.body.removeChild(textarea);
|
|
||||||
}
|
|
||||||
log(`${label} copied: ${value}`);
|
log(`${label} copied: ${value}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
@@ -973,6 +1004,49 @@ export function AutoRunsHistoryPanel({
|
|||||||
[log]
|
[log]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const copyDialogConversation = useCallback(
|
||||||
|
async (mode: ConversationExportMode) => {
|
||||||
|
if (!dialog || dialog.messages.length === 0 || !selectedRunId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportText = buildAutoRunDialogExportForCopy(
|
||||||
|
{
|
||||||
|
runId: selectedRunId,
|
||||||
|
caseId: selectedCaseId || dialog.case_id || "n/a",
|
||||||
|
sessionId: dialog.session_id,
|
||||||
|
source: dialog.source,
|
||||||
|
messages: dialog.messages,
|
||||||
|
decomposition: dialog.decomposition,
|
||||||
|
assistantMode: dialog.assistant_mode,
|
||||||
|
annotations: dialog.annotations,
|
||||||
|
runSummary: runDetail?.run ?? null,
|
||||||
|
coverage: runDetail?.coverage ?? null,
|
||||||
|
report: runDetail?.report ?? null
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
);
|
||||||
|
|
||||||
|
const copied = await writeTextToClipboard(exportText);
|
||||||
|
setDialogCopyModeLabel(mode === "technical" ? "тех" : "чат");
|
||||||
|
setDialogCopyState(copied ? "success" : "error");
|
||||||
|
|
||||||
|
if (dialogCopyResetTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(dialogCopyResetTimerRef.current);
|
||||||
|
}
|
||||||
|
dialogCopyResetTimerRef.current = window.setTimeout(() => {
|
||||||
|
setDialogCopyState("idle");
|
||||||
|
}, 2200);
|
||||||
|
|
||||||
|
if (copied) {
|
||||||
|
log(`Dialog ${mode === "technical" ? "technical" : "chat"} copied: run=${selectedRunId} case=${selectedCaseId || dialog.case_id}`);
|
||||||
|
} else {
|
||||||
|
log(`Dialog copy failed: run=${selectedRunId} case=${selectedCaseId || dialog.case_id}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dialog, log, runDetail, selectedCaseId, selectedRunId]
|
||||||
|
);
|
||||||
|
|
||||||
function startAssistantLiveStatusTicker(): () => void {
|
function startAssistantLiveStatusTicker(): () => void {
|
||||||
let index = 0;
|
let index = 0;
|
||||||
setAssistantLiveStatus(ASSISTANT_STAGES[0]);
|
setAssistantLiveStatus(ASSISTANT_STAGES[0]);
|
||||||
@@ -3419,6 +3493,34 @@ export function AutoRunsHistoryPanel({
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
<div className="autoruns-dialog-copy-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="assistant-copy-btn"
|
||||||
|
onClick={() => {
|
||||||
|
void copyDialogConversation("default");
|
||||||
|
}}
|
||||||
|
disabled={dialogBusy || detailBusy || (dialog?.messages.length ?? 0) === 0}
|
||||||
|
title="Скопировать question-answer диалог текущего прогона"
|
||||||
|
>
|
||||||
|
Скопировать чат
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="assistant-copy-btn"
|
||||||
|
onClick={() => {
|
||||||
|
void copyDialogConversation("technical");
|
||||||
|
}}
|
||||||
|
disabled={dialogBusy || detailBusy || (dialog?.messages.length ?? 0) === 0}
|
||||||
|
title="Скопировать диалог вместе с debug JSON и метаданными прогона"
|
||||||
|
>
|
||||||
|
Скопировать техчат
|
||||||
|
</button>
|
||||||
|
<div className="autoruns-dialog-copy-status">
|
||||||
|
{dialogCopyState === "success" ? <span className="assistant-copy-feedback success">Скопировано ({dialogCopyModeLabel})</span> : null}
|
||||||
|
{dialogCopyState === "error" ? <span className="assistant-copy-feedback error">Ошибка копирования</span> : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ export interface AutoRunDialogMessage {
|
|||||||
created_at: string | null;
|
created_at: string | null;
|
||||||
trace_id: string | null;
|
trace_id: string | null;
|
||||||
reply_type: string | null;
|
reply_type: string | null;
|
||||||
|
debug?: unknown | null;
|
||||||
message_index: number;
|
message_index: number;
|
||||||
case_id?: string | null;
|
case_id?: string | null;
|
||||||
case_message_index?: number | null;
|
case_message_index?: number | null;
|
||||||
|
|||||||
@@ -1263,6 +1263,21 @@ button:disabled {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.autoruns-dialog-copy-actions {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autoruns-dialog-copy-status {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
min-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
.autoruns-case-list {
|
.autoruns-case-list {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -10,6 +10,33 @@ export interface ConversationExportItem {
|
|||||||
debug?: unknown | null;
|
debug?: unknown | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AutoRunDialogExportItem {
|
||||||
|
message_id: string | null;
|
||||||
|
role: string;
|
||||||
|
text: string;
|
||||||
|
reply_type: string | null;
|
||||||
|
created_at: string | null;
|
||||||
|
trace_id: string | null;
|
||||||
|
message_index: number;
|
||||||
|
case_id?: string | null;
|
||||||
|
case_message_index?: number | null;
|
||||||
|
debug?: unknown | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutoRunDialogExportPayload {
|
||||||
|
runId: string;
|
||||||
|
caseId: string;
|
||||||
|
sessionId: string;
|
||||||
|
source: string;
|
||||||
|
messages: AutoRunDialogExportItem[];
|
||||||
|
decomposition?: string[];
|
||||||
|
assistantMode?: unknown | null;
|
||||||
|
annotations?: unknown[];
|
||||||
|
runSummary?: unknown | null;
|
||||||
|
coverage?: unknown | null;
|
||||||
|
report?: unknown | null;
|
||||||
|
}
|
||||||
|
|
||||||
const DEBUG_SECTION_PATTERN =
|
const DEBUG_SECTION_PATTERN =
|
||||||
/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json|route_summary_json|debug_payload|technical_breakdown)\b/i;
|
/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json|route_summary_json|debug_payload|technical_breakdown)\b/i;
|
||||||
|
|
||||||
@@ -28,6 +55,10 @@ function stringifyDebug(value: unknown): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeRole(value: string): "user" | "assistant" {
|
||||||
|
return value === "assistant" ? "assistant" : "user";
|
||||||
|
}
|
||||||
|
|
||||||
export function sanitizeConversationExportText(value: string): string {
|
export function sanitizeConversationExportText(value: string): string {
|
||||||
const raw = String(value ?? "");
|
const raw = String(value ?? "");
|
||||||
const cutMatch = raw.match(DEBUG_SECTION_PATTERN);
|
const cutMatch = raw.match(DEBUG_SECTION_PATTERN);
|
||||||
@@ -83,3 +114,113 @@ export function buildConversationExportForCopy(
|
|||||||
|
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildAutoRunDialogExportForCopy(
|
||||||
|
payload: AutoRunDialogExportPayload,
|
||||||
|
mode: ConversationExportMode = "default"
|
||||||
|
): string {
|
||||||
|
const includeDebug = mode === "technical";
|
||||||
|
const lines: string[] = [];
|
||||||
|
lines.push("# Autorun dialog export");
|
||||||
|
lines.push(`run_id: ${payload.runId || "n/a"}`);
|
||||||
|
lines.push(`case_id: ${payload.caseId || "n/a"}`);
|
||||||
|
lines.push(`session_id: ${payload.sessionId || "n/a"}`);
|
||||||
|
lines.push(`source: ${payload.source || "n/a"}`);
|
||||||
|
lines.push(`export_mode: ${mode}`);
|
||||||
|
lines.push(`exported_at: ${new Date().toISOString()}`);
|
||||||
|
lines.push("");
|
||||||
|
|
||||||
|
for (let index = 0; index < payload.messages.length; index += 1) {
|
||||||
|
const item = payload.messages[index];
|
||||||
|
const role = normalizeRole(item.role);
|
||||||
|
const safeText = sanitizeConversationExportText(item.text || "");
|
||||||
|
lines.push(`## ${index + 1}. ${role}`);
|
||||||
|
lines.push(`message_index: ${item.message_index}`);
|
||||||
|
if (item.case_id) {
|
||||||
|
lines.push(`case_id: ${item.case_id}`);
|
||||||
|
}
|
||||||
|
if (typeof item.case_message_index === "number") {
|
||||||
|
lines.push(`case_message_index: ${item.case_message_index}`);
|
||||||
|
}
|
||||||
|
if (item.created_at) {
|
||||||
|
lines.push(`created_at: ${item.created_at}`);
|
||||||
|
}
|
||||||
|
if (includeDebug) {
|
||||||
|
lines.push(`reply_type: ${item.reply_type ?? "n/a"}`);
|
||||||
|
if (item.trace_id) {
|
||||||
|
lines.push(`trace_id: ${item.trace_id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.push("");
|
||||||
|
lines.push(safeText || "(empty)");
|
||||||
|
lines.push("");
|
||||||
|
|
||||||
|
if (includeDebug && role === "assistant" && item.debug) {
|
||||||
|
lines.push("### technical_debug_payload_json");
|
||||||
|
lines.push("```json");
|
||||||
|
lines.push(stringifyDebug(item.debug));
|
||||||
|
lines.push("```");
|
||||||
|
lines.push("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!includeDebug) {
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push("### dialog_messages_json");
|
||||||
|
lines.push("```json");
|
||||||
|
lines.push(stringifyDebug(payload.messages));
|
||||||
|
lines.push("```");
|
||||||
|
lines.push("");
|
||||||
|
|
||||||
|
if ((payload.decomposition ?? []).length > 0) {
|
||||||
|
lines.push("### decomposition_json");
|
||||||
|
lines.push("```json");
|
||||||
|
lines.push(stringifyDebug(payload.decomposition));
|
||||||
|
lines.push("```");
|
||||||
|
lines.push("");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.assistantMode) {
|
||||||
|
lines.push("### assistant_mode_json");
|
||||||
|
lines.push("```json");
|
||||||
|
lines.push(stringifyDebug(payload.assistantMode));
|
||||||
|
lines.push("```");
|
||||||
|
lines.push("");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((payload.annotations ?? []).length > 0) {
|
||||||
|
lines.push("### annotations_json");
|
||||||
|
lines.push("```json");
|
||||||
|
lines.push(stringifyDebug(payload.annotations));
|
||||||
|
lines.push("```");
|
||||||
|
lines.push("");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.runSummary) {
|
||||||
|
lines.push("### run_summary_json");
|
||||||
|
lines.push("```json");
|
||||||
|
lines.push(stringifyDebug(payload.runSummary));
|
||||||
|
lines.push("```");
|
||||||
|
lines.push("");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.coverage) {
|
||||||
|
lines.push("### coverage_json");
|
||||||
|
lines.push("```json");
|
||||||
|
lines.push(stringifyDebug(payload.coverage));
|
||||||
|
lines.push("```");
|
||||||
|
lines.push("");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.report) {
|
||||||
|
lines.push("### run_report_json");
|
||||||
|
lines.push("```json");
|
||||||
|
lines.push(stringifyDebug(payload.report));
|
||||||
|
lines.push("```");
|
||||||
|
lines.push("");
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ DEFAULT_INVARIANT_SEVERITY: dict[str, str] = {
|
|||||||
"top_level_noise_present": "P0",
|
"top_level_noise_present": "P0",
|
||||||
"business_direct_answer_missing": "P0",
|
"business_direct_answer_missing": "P0",
|
||||||
"technical_garbage_in_answer": "P0",
|
"technical_garbage_in_answer": "P0",
|
||||||
|
"counterparty_value_flow_misrouted_to_company_profit": "P0",
|
||||||
"answer_layering_noise": "P1",
|
"answer_layering_noise": "P1",
|
||||||
"business_answer_too_verbose": "P1",
|
"business_answer_too_verbose": "P1",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -322,6 +322,7 @@ def append_finding(
|
|||||||
BUSINESS_REVIEW_FINDING_MESSAGES = {
|
BUSINESS_REVIEW_FINDING_MESSAGES = {
|
||||||
"technical_garbage_in_answer": "User-facing answer leaked internal runtime or MCP identifiers.",
|
"technical_garbage_in_answer": "User-facing answer leaked internal runtime or MCP identifiers.",
|
||||||
"business_direct_answer_missing": "The answer did not put the direct business answer first.",
|
"business_direct_answer_missing": "The answer did not put the direct business answer first.",
|
||||||
|
"counterparty_value_flow_misrouted_to_company_profit": "Counterparty received/paid/net flow question was answered with company profit instead of counterparty cashflow.",
|
||||||
"answer_layering_noise": "The answer opened with scaffolding or report framing instead of a clean business result.",
|
"answer_layering_noise": "The answer opened with scaffolding or report framing instead of a clean business result.",
|
||||||
"business_answer_too_verbose": "The answer is too verbose for a direct business question.",
|
"business_answer_too_verbose": "The answer is too verbose for a direct business question.",
|
||||||
}
|
}
|
||||||
@@ -329,6 +330,7 @@ BUSINESS_REVIEW_FINDING_MESSAGES = {
|
|||||||
BUSINESS_REVIEW_FINDING_SEVERITY = {
|
BUSINESS_REVIEW_FINDING_SEVERITY = {
|
||||||
"technical_garbage_in_answer": "critical",
|
"technical_garbage_in_answer": "critical",
|
||||||
"business_direct_answer_missing": "critical",
|
"business_direct_answer_missing": "critical",
|
||||||
|
"counterparty_value_flow_misrouted_to_company_profit": "critical",
|
||||||
"answer_layering_noise": "critical",
|
"answer_layering_noise": "critical",
|
||||||
"business_answer_too_verbose": "warning",
|
"business_answer_too_verbose": "warning",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,6 +111,30 @@ SAFE_FINANCIAL_BOUNDARY_MARKERS = (
|
|||||||
"без назначения платеж",
|
"без назначения платеж",
|
||||||
"без договора",
|
"без договора",
|
||||||
)
|
)
|
||||||
|
COUNTERPARTY_VALUE_FLOW_QUESTION_RE = re.compile(
|
||||||
|
"(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\\s+.*\u043f\u043e\u043b\u0443\u0447|\u0441\u043a\u043e\u043b\u044c\u043a\u043e\\s+.*\u0437\u0430\u043f\u043b\u0430\u0442|\u043a\u0430\u043a\u043e\u0435\\s+\u043d\u0435\u0442\u0442\u043e|\u043a\u0430\u043a\u043e\u0435\\s+\u0441\u0430\u043b\u044c\u0434\u043e)",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
COUNTERPARTY_SCOPE_QUESTION_RE = re.compile(
|
||||||
|
"(?:\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u043a\u043b\u0438\u0435\u043d\u0442|\u0441\u0432\u043a|\u0447\u0435\u043f\u0443\u0440\u043d\u043e\u0432)",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
COUNTERPARTY_VALUE_FLOW_ANSWER_RE = re.compile(
|
||||||
|
"(?:\u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438|\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043e|\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u043e\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|\u0432\\s+\u043d\u0430\u0448\u0443\\s+\u0441\u0442\u043e\u0440\u043e\u043d\u0443|\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445\\s+\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445\\s+\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439)",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
COUNTERPARTY_VALUE_FLOW_REQUIRED_ANSWER_RE = re.compile(
|
||||||
|
"(?:\u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438|\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043e|\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u043e\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445\\s+\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445\\s+\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439)",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
COUNTERPARTY_ANSWER_SCOPE_RE = re.compile(
|
||||||
|
"(?:\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u043a\u043b\u0438\u0435\u043d\u0442|\u0441\u0432\u043a|\u0447\u0435\u043f\u0443\u0440\u043d\u043e\u0432)",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
COMPANY_PROFIT_ANSWER_RE = re.compile(
|
||||||
|
"(?:\u0447\u0438\u0441\u0442\u0430\u044f\\s+\u043f\u0440\u0438\u0431\u044b\u043b\u044c|\u043f\u0440\u0438\u0431\u044b\u043b\u044c\u044e|90/91/99|\u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442|\u0443\u0431\u044b\u0442\u043e\u043a)",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def now_iso() -> str:
|
def now_iso() -> str:
|
||||||
@@ -335,6 +359,7 @@ def build_step_for_pair(pair: dict[str, Any]) -> dict[str, Any]:
|
|||||||
"answer_layering_noise": "P1",
|
"answer_layering_noise": "P1",
|
||||||
"business_answer_too_verbose": "P1",
|
"business_answer_too_verbose": "P1",
|
||||||
"bank_counterparty_misclassified_as_business_partner": "P1",
|
"bank_counterparty_misclassified_as_business_partner": "P1",
|
||||||
|
"counterparty_value_flow_misrouted_to_company_profit": "P0",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,12 +374,41 @@ def marker_hits(text: str, markers: tuple[str, ...]) -> list[str]:
|
|||||||
return [marker for marker in markers if marker and marker.casefold() in lowered]
|
return [marker for marker in markers if marker and marker.casefold() in lowered]
|
||||||
|
|
||||||
|
|
||||||
|
def detect_counterparty_value_flow_profit_mismatch(question: str, assistant_text: str) -> dict[str, Any] | None:
|
||||||
|
question_text = str(question or "")
|
||||||
|
answer_text = str(assistant_text or "")
|
||||||
|
question_flow_match = COUNTERPARTY_VALUE_FLOW_QUESTION_RE.search(question_text)
|
||||||
|
question_scope_match = COUNTERPARTY_SCOPE_QUESTION_RE.search(question_text)
|
||||||
|
if not question_flow_match or not question_scope_match:
|
||||||
|
return None
|
||||||
|
|
||||||
|
profit_match = COMPANY_PROFIT_ANSWER_RE.search(answer_text)
|
||||||
|
if not profit_match:
|
||||||
|
return None
|
||||||
|
|
||||||
|
value_flow_match = COUNTERPARTY_VALUE_FLOW_ANSWER_RE.search(answer_text)
|
||||||
|
required_flow_match = COUNTERPARTY_VALUE_FLOW_REQUIRED_ANSWER_RE.search(answer_text)
|
||||||
|
answer_scope_match = COUNTERPARTY_ANSWER_SCOPE_RE.search(answer_text)
|
||||||
|
if required_flow_match and answer_scope_match:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"question_flow_hit": question_flow_match.group(0),
|
||||||
|
"question_scope_hit": question_scope_match.group(0),
|
||||||
|
"profit_hit": profit_match.group(0),
|
||||||
|
"value_flow_hit": value_flow_match.group(0) if value_flow_match else None,
|
||||||
|
"required_flow_hit": required_flow_match.group(0) if required_flow_match else None,
|
||||||
|
"answer_scope_hit": answer_scope_match.group(0) if answer_scope_match else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def augment_gui_business_review(step_state: dict[str, Any]) -> dict[str, Any]:
|
def augment_gui_business_review(step_state: dict[str, Any]) -> dict[str, Any]:
|
||||||
review = (
|
review = (
|
||||||
dict(step_state.get("business_first_review"))
|
dict(step_state.get("business_first_review"))
|
||||||
if isinstance(step_state.get("business_first_review"), dict)
|
if isinstance(step_state.get("business_first_review"), dict)
|
||||||
else {}
|
else {}
|
||||||
)
|
)
|
||||||
|
question = str(step_state.get("question_resolved") or step_state.get("question_template") or "")
|
||||||
assistant_text = str(step_state.get("assistant_text") or "")
|
assistant_text = str(step_state.get("assistant_text") or "")
|
||||||
issue_codes = [str(item) for item in review.get("issue_codes", []) if str(item).strip()]
|
issue_codes = [str(item) for item in review.get("issue_codes", []) if str(item).strip()]
|
||||||
root_layers = [str(item) for item in review.get("suggested_root_cause_layers", []) if str(item).strip()]
|
root_layers = [str(item) for item in review.get("suggested_root_cause_layers", []) if str(item).strip()]
|
||||||
@@ -378,6 +432,17 @@ def augment_gui_business_review(step_state: dict[str, Any]) -> dict[str, Any]:
|
|||||||
if "business_semantic_role_gap" not in root_layers:
|
if "business_semantic_role_gap" not in root_layers:
|
||||||
root_layers.append("business_semantic_role_gap")
|
root_layers.append("business_semantic_role_gap")
|
||||||
|
|
||||||
|
mismatch_details = detect_counterparty_value_flow_profit_mismatch(question, assistant_text)
|
||||||
|
if mismatch_details:
|
||||||
|
issue_code = "counterparty_value_flow_misrouted_to_company_profit"
|
||||||
|
if issue_code not in issue_codes:
|
||||||
|
issue_codes.append(issue_code)
|
||||||
|
if "followup_action_resolution_gap" not in root_layers:
|
||||||
|
root_layers.append("followup_action_resolution_gap")
|
||||||
|
if "answer_shape_mismatch" not in root_layers:
|
||||||
|
root_layers.append("answer_shape_mismatch")
|
||||||
|
review["semantic_mismatch_details"] = mismatch_details
|
||||||
|
|
||||||
review["technical_garbage_present"] = bool(technical_hits)
|
review["technical_garbage_present"] = bool(technical_hits)
|
||||||
review["technical_garbage_hits"] = technical_hits
|
review["technical_garbage_hits"] = technical_hits
|
||||||
review["issue_codes"] = issue_codes
|
review["issue_codes"] = issue_codes
|
||||||
|
|||||||
@@ -170,6 +170,86 @@ class AssistantStage1RunReviewTests(unittest.TestCase):
|
|||||||
self.assertIn("technical_garbage_in_answer", review["summary"]["issue_counts"])
|
self.assertIn("technical_garbage_in_answer", review["summary"]["issue_counts"])
|
||||||
self.assertIn("bank_counterparty_misclassified_as_business_partner", review["summary"]["issue_counts"])
|
self.assertIn("bank_counterparty_misclassified_as_business_partner", review["summary"]["issue_counts"])
|
||||||
|
|
||||||
|
def test_review_flags_counterparty_net_flow_answer_that_slips_into_company_profit(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
sessions_dir = root / "sessions"
|
||||||
|
reports_dir = root / "reports"
|
||||||
|
run_id = "assistant-stage1-counterparty-profit-slip"
|
||||||
|
session_file = sessions_dir / f"{run_id}-SAVED-001.json"
|
||||||
|
report_file = reports_dir / f"{run_id}.md"
|
||||||
|
write_json(
|
||||||
|
session_file,
|
||||||
|
session_payload(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"text": "А теперь по Группа СВК за 2020: сколько денег получили, сколько заплатили и какое нетто?",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"text": "Нет, денежное операционное нетто не стоит считать чистой прибылью. "
|
||||||
|
"По закрытию 90/91/99 подтвержден учетный убыток −7 136 815,85 ₽.",
|
||||||
|
"reply_type": "factual_with_explanation",
|
||||||
|
"message_id": "a-counterparty-profit-slip",
|
||||||
|
"trace_id": "trace-counterparty-profit-slip",
|
||||||
|
"debug": {},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
report_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
report_file.write_text(f"# Assistant Stage 1 Eval Run\n\n- run_id: {run_id}\n", encoding="utf-8")
|
||||||
|
|
||||||
|
review = reviewer.build_run_review(
|
||||||
|
run_id=run_id,
|
||||||
|
session_files=[session_file],
|
||||||
|
report_path=report_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(review["summary"]["overall_business_status"], "fail")
|
||||||
|
self.assertIn("counterparty_value_flow_misrouted_to_company_profit", review["summary"]["issue_counts"])
|
||||||
|
target_by_issue = {item["issue_code"]: item for item in review["repair_targets"]}
|
||||||
|
self.assertEqual(target_by_issue["counterparty_value_flow_misrouted_to_company_profit"]["severity"], "P0")
|
||||||
|
|
||||||
|
def test_review_does_not_flag_counterparty_net_flow_when_received_paid_answer_is_present(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
sessions_dir = root / "sessions"
|
||||||
|
reports_dir = root / "reports"
|
||||||
|
run_id = "assistant-stage1-counterparty-net-clean"
|
||||||
|
session_file = sessions_dir / f"{run_id}-SAVED-001.json"
|
||||||
|
report_file = reports_dir / f"{run_id}.md"
|
||||||
|
write_json(
|
||||||
|
session_file,
|
||||||
|
session_payload(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"text": "А теперь по Группа СВК за 2020: сколько денег получили, сколько заплатили и какое нетто?",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"text": "По Группа СВК за 2020 получили 12 093 465 ₽, заплатили 0 ₽, денежное нетто +12 093 465 ₽ в нашу сторону.",
|
||||||
|
"reply_type": "factual",
|
||||||
|
"message_id": "a-counterparty-net-clean",
|
||||||
|
"trace_id": "trace-counterparty-net-clean",
|
||||||
|
"debug": {},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
report_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
report_file.write_text(f"# Assistant Stage 1 Eval Run\n\n- run_id: {run_id}\n", encoding="utf-8")
|
||||||
|
|
||||||
|
review = reviewer.build_run_review(
|
||||||
|
run_id=run_id,
|
||||||
|
session_files=[session_file],
|
||||||
|
report_path=report_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertNotIn("counterparty_value_flow_misrouted_to_company_profit", review["summary"]["issue_counts"])
|
||||||
|
|
||||||
def test_question_quality_treats_short_natural_followups_as_contextual(self) -> None:
|
def test_question_quality_treats_short_natural_followups_as_contextual(self) -> None:
|
||||||
pairs = [
|
pairs = [
|
||||||
{"pair_index": 1, "user": {"text": "приветик - че как там дела"}},
|
{"pair_index": 1, "user": {"text": "приветик - че как там дела"}},
|
||||||
|
|||||||
Reference in New Issue
Block a user