Усилить answer contract и агентный аудит для phase105
This commit is contained in:
parent
9c86407937
commit
bbc257fd6c
|
|
@ -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}; полный список остается в подтвержденном срезе.`);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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"));
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,16 +1631,36 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
? "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
|
||||||
|
: lifecycleSignal
|
||||||
|
? "counterparty_lifecycle"
|
||||||
|
: valueFlowSignal
|
||||||
|
? bidirectionalValueFlowSignal
|
||||||
|
? "counterparty_bidirectional_value_flow_or_netting"
|
||||||
|
: payoutSignal
|
||||||
|
? "counterparty_payouts_or_outflow"
|
||||||
|
: seededUnsupported ?? "counterparty_value_or_turnover"
|
||||||
: unsupported ?? seededUnsupported,
|
: unsupported ?? seededUnsupported,
|
||||||
lifecycleSignal,
|
lifecycleSignal,
|
||||||
valueFlowSignal,
|
valueFlowSignal,
|
||||||
|
|
@ -1853,8 +1925,7 @@ 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
|
||||||
|
|
@ -1862,7 +1933,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
: payoutSignal
|
: payoutSignal
|
||||||
? "counterparty_payouts_or_outflow"
|
? "counterparty_payouts_or_outflow"
|
||||||
: seededUnsupported ?? "counterparty_value_or_turnover"
|
: seededUnsupported ?? "counterparty_value_or_turnover"
|
||||||
: metadataGroundedMovementLaneApplicable
|
: unsupported ??
|
||||||
|
(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,15 +350,20 @@ 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 && counterpartyBidirectionalValueFlow?.family
|
||||||
|
? counterpartyBidirectionalValueFlow.family
|
||||||
: !explicitIntentCandidate && counterpartyTurnover?.family
|
: !explicitIntentCandidate && counterpartyTurnover?.family
|
||||||
? counterpartyTurnover.family
|
? counterpartyTurnover.family
|
||||||
: null;
|
: null;
|
||||||
|
|
@ -315,6 +371,9 @@ 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");
|
||||||
}
|
}
|
||||||
|
|
@ -338,6 +397,8 @@ 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
|
||||||
|
|
@ -349,6 +410,8 @@ 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"
|
||||||
|
|
@ -363,7 +426,10 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
|
||||||
: counterpartyTurnover?.family
|
: counterpartyTurnover?.family
|
||||||
? "counterparty_value_or_turnover"
|
? "counterparty_value_or_turnover"
|
||||||
: null;
|
: null;
|
||||||
const staleReplayForbidden = Boolean(unsupportedFamily || broadBusinessEvaluation?.family || (counterpartyTurnover?.entity && !explicitIntentCandidate));
|
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
|
||||||
|
|
|
||||||
|
|
@ -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}; полный список остается в подтвержденном срезе.`);
|
||||||
|
|
|
||||||
|
|
@ -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,16 +2216,36 @@ 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
|
||||||
|
: lifecycleSignal
|
||||||
|
? "counterparty_lifecycle"
|
||||||
|
: valueFlowSignal
|
||||||
|
? bidirectionalValueFlowSignal
|
||||||
|
? "counterparty_bidirectional_value_flow_or_netting"
|
||||||
|
: payoutSignal
|
||||||
|
? "counterparty_payouts_or_outflow"
|
||||||
|
: seededUnsupported ?? "counterparty_value_or_turnover"
|
||||||
: unsupported ?? seededUnsupported,
|
: unsupported ?? seededUnsupported,
|
||||||
lifecycleSignal,
|
lifecycleSignal,
|
||||||
valueFlowSignal,
|
valueFlowSignal,
|
||||||
|
|
@ -2469,8 +2558,7 @@ 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
|
||||||
|
|
@ -2478,7 +2566,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
: payoutSignal
|
: payoutSignal
|
||||||
? "counterparty_payouts_or_outflow"
|
? "counterparty_payouts_or_outflow"
|
||||||
: seededUnsupported ?? "counterparty_value_or_turnover"
|
: seededUnsupported ?? "counterparty_value_or_turnover"
|
||||||
: metadataGroundedMovementLaneApplicable
|
: unsupported ??
|
||||||
|
(metadataGroundedMovementLaneApplicable
|
||||||
? "movement_evidence"
|
? "movement_evidence"
|
||||||
: metadataGroundedDocumentLaneApplicable
|
: metadataGroundedDocumentLaneApplicable
|
||||||
? "document_evidence"
|
? "document_evidence"
|
||||||
|
|
@ -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
File diff suppressed because one or more lines are too long
|
|
@ -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": "приветик - че как там дела"}},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue