Усилить answer contract и агентный аудит для phase105
This commit is contained in:
@@ -1287,7 +1287,8 @@ function loadSessionDialog(runId: string, caseId: string): {
|
||||
text: toStringSafe(item.text) ?? "",
|
||||
created_at: toStringSafe(item.created_at),
|
||||
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)
|
||||
@@ -1364,7 +1365,8 @@ function buildFallbackDialog(run: IndexedRun, caseId: string): {
|
||||
text: userText,
|
||||
created_at: null,
|
||||
trace_id: null,
|
||||
reply_type: null
|
||||
reply_type: null,
|
||||
debug: null
|
||||
},
|
||||
{
|
||||
message_id: null,
|
||||
@@ -1372,7 +1374,8 @@ function buildFallbackDialog(run: IndexedRun, caseId: string): {
|
||||
text: assistantSummaryParts.join("\n"),
|
||||
created_at: null,
|
||||
trace_id: toStringSafe(targetCase.trace_id),
|
||||
reply_type: toStringSafe(targetCase.reply_type)
|
||||
reply_type: toStringSafe(targetCase.reply_type),
|
||||
debug: null
|
||||
}
|
||||
],
|
||||
decomposition: [],
|
||||
|
||||
@@ -524,6 +524,73 @@ function bankOperationEvidenceLine(
|
||||
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 {
|
||||
const incomingBoundary = hasBankIncomingRoleBoundaryQuestion(userMessage);
|
||||
const outgoingBoundary = hasBankOutgoingRoleBoundaryQuestion(userMessage);
|
||||
@@ -5013,13 +5080,34 @@ function composeFactualReplyBody(
|
||||
);
|
||||
const counterparty = resolvePreferredCounterpartyDisplayLabel(options.counterpartyHint, rowCounterparties);
|
||||
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 = [
|
||||
`Коротко: найдено банковских операций${counterparty ? ` по ${counterparty}` : " по контрагенту"} — ${rows.length}.`,
|
||||
summarizeBankOperationDirections(rows),
|
||||
roleBoundary ?? "Показываю подтвержденные банковские операции из текущего среза.",
|
||||
bankOperationEvidenceLine(rows, preferredBankEvidenceDirection(options.userMessage)),
|
||||
...formatTopRows(visibleRows, visibleRows.length)
|
||||
...(semanticSummary ? [semanticSummary] : []),
|
||||
"Примеры строк 1С:",
|
||||
...compactEvidenceRows,
|
||||
"Следующий шаг: могу отдельно разложить назначения платежа, договоры или отделить банковский контур от клиентского/поставщицкого."
|
||||
];
|
||||
if (rows.length > visibleRows.length) {
|
||||
lines.push(`Показаны первые ${visibleRows.length} из ${rows.length}; полный список остается в подтвержденном срезе.`);
|
||||
|
||||
+44
-9
@@ -120,6 +120,26 @@ function findFocusedCounterpartyValuePoint(
|
||||
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(
|
||||
intent: AddressIntent,
|
||||
rows: ComposeStageRow[],
|
||||
@@ -546,6 +566,8 @@ export function composeCounterpartyAnalyticsReply(
|
||||
const semanticSingleBestCounterparty =
|
||||
focus === "top_by_total" && hasSingleBestCounterpartyCue && !asksExplicitRankingList;
|
||||
const effectiveLimit = asksSingleBestCounterparty || semanticSingleBestCounterparty ? 1 : limit;
|
||||
const cashflowBoundaryLine = buildCashflowBoundaryLine(isSupplier);
|
||||
const cashflowNextStepLine = buildCashflowNextStepLine(isSupplier, normalizedQuestion);
|
||||
|
||||
const byCounterparty = new Map<string, CounterpartyValuePoint>();
|
||||
const byYear = new Map<number, CounterpartyYearPoint>();
|
||||
@@ -655,10 +677,12 @@ export function composeCounterpartyAnalyticsReply(
|
||||
? `за период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)}`
|
||||
: "за доступное время";
|
||||
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 = [
|
||||
directAnswerLine,
|
||||
cashflowBoundaryLine,
|
||||
...(cashflowNextStepLine ? [cashflowNextStepLine] : []),
|
||||
"",
|
||||
"Подтверждение:",
|
||||
`- Контрагент в выборке: ${focusedCounterparty.name}.`,
|
||||
@@ -678,11 +702,10 @@ export function composeCounterpartyAnalyticsReply(
|
||||
options.periodFrom && options.periodTo
|
||||
? `За период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)} подтверждено ${deps.formatMoneyRub(totalFlow)} ${isSupplier ? "исходящих выплат" : "входящих поступлений"}.`
|
||||
: `За все доступное время подтверждено ${deps.formatMoneyRub(totalFlow)} ${isSupplier ? "исходящих выплат" : "входящих поступлений"}.`;
|
||||
const directAnswerLine = isSupplier
|
||||
? periodLine
|
||||
: `${periodLine} Это денежный поток от клиентов, а не чистая прибыль.`;
|
||||
const summaryLines = [
|
||||
directAnswerLine,
|
||||
periodLine,
|
||||
cashflowBoundaryLine,
|
||||
...(cashflowNextStepLine ? [cashflowNextStepLine] : []),
|
||||
"",
|
||||
"Подтверждение:",
|
||||
`- Операций в выборке: ${totalOperations}.`,
|
||||
@@ -709,11 +732,17 @@ export function composeCounterpartyAnalyticsReply(
|
||||
const strongestYear = visible[0];
|
||||
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} операциям).`;
|
||||
const heading = isSupplier
|
||||
? `Топ-${visible.length} лет по сумме выплат:`
|
||||
: `Топ-${visible.length} лет по сумме поступлений:`;
|
||||
lines.unshift(heading);
|
||||
if (!isSupplier) {
|
||||
lines.unshift(cashflowBoundaryLine);
|
||||
if (cashflowNextStepLine) {
|
||||
lines.unshift(cashflowNextStepLine);
|
||||
}
|
||||
}
|
||||
lines.unshift(directAnswerLine);
|
||||
lines.push(
|
||||
...visible.map(
|
||||
@@ -829,11 +858,17 @@ export function composeCounterpartyAnalyticsReply(
|
||||
const directAnswerLine = singleCandidateOnly
|
||||
? 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} операциям). Это не полноценный сравнительный рейтинг.`
|
||||
: 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} операциям).`;
|
||||
lines.unshift(directAnswerLine);
|
||||
if (!isSupplier) {
|
||||
lines.splice(1, 0, cashflowBoundaryLine);
|
||||
if (cashflowNextStepLine) {
|
||||
lines.splice(2, 0, cashflowNextStepLine);
|
||||
}
|
||||
}
|
||||
}
|
||||
lines.push(
|
||||
...visible.map((item, index) => {
|
||||
|
||||
@@ -175,11 +175,10 @@ export function composeInventoryReply(
|
||||
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 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 не найдено.`;
|
||||
const lines: string[] = [directAnswerLine];
|
||||
|
||||
@@ -213,11 +212,14 @@ export function composeInventoryReply(
|
||||
`Позиции с остатком: ${deps.formatNumberWithDots(positions.length)}.`,
|
||||
`Уникальных товаров: ${deps.formatNumberWithDots(uniqueItems.length)}.`,
|
||||
`Уникальных складов: ${deps.formatNumberWithDots(uniqueWarehouses.length)}.`,
|
||||
`Суммарное количество: ${deps.formatNumberWithDots(totalQuantity, 3)}.`
|
||||
"Общее количество не свожу в один управленческий показатель, потому что в остатках смешаны разнородные позиции."
|
||||
]);
|
||||
if (rows.length !== positions.length) {
|
||||
lines.push(`- Проверенных строк движения: ${deps.formatNumberWithDots(rows.length)}.`);
|
||||
}
|
||||
if (positions.length > 0) {
|
||||
lines.push("- Следующий шаг: могу раскрыть полный список, разложить остатки по складам или сравнить с другой датой.");
|
||||
}
|
||||
|
||||
return positions.length > 0
|
||||
? buildFactualListReply(lines, buildConfirmedBalanceSemantics("strong"))
|
||||
|
||||
@@ -1128,6 +1128,40 @@ function buildCompactBusinessOverviewReply(
|
||||
}
|
||||
|
||||
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 canRankYearlyNet = !limitLine;
|
||||
const netLeader = canRankYearlyNet ? strongestNetYear(overview) : null;
|
||||
|
||||
@@ -1600,15 +1600,48 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
const rawEffectiveText = toNonEmptyString(input.effectiveMessage);
|
||||
const repairedUserText = rawUserText ? repairAddressMojibakeText(rawUserText) : null;
|
||||
const repairedEffectiveText = rawEffectiveText ? repairAddressMojibakeText(rawEffectiveText) : null;
|
||||
const rawUserSignalSourceText = repairedUserText ?? rawUserText ?? "";
|
||||
const rawSignalSourceText = `${repairedUserText ?? rawUserText ?? ""} ${repairedEffectiveText ?? rawEffectiveText ?? ""}`.trim();
|
||||
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 rawReferentialDocumentExclusionSignal = hasReferentialDocumentExclusionFollowupSignal(
|
||||
repairedUserText ?? rawUserText ?? ""
|
||||
);
|
||||
const rawPrimaryBusinessOverviewSignal = hasBusinessOverviewSignal(rawText);
|
||||
const rawPrimaryBusinessOverviewSignal =
|
||||
hasBusinessOverviewSignal(rawText) && !rawUserCounterpartyBidirectionalOverride;
|
||||
const explicitVatQuestionSignal = hasExplicitVatQuestionSignal(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
|
||||
);
|
||||
@@ -1634,7 +1667,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
!rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
|
||||
const rawPayoutSignal = rawValueFlowSignal && !rawBidirectionalValueFlowSignal && hasPayoutSignal(rawText);
|
||||
const rawValueFlowAggregateQuestionSignal =
|
||||
rawValueFlowSignal && hasValueFlowAggregateQuestionSignal(rawText);
|
||||
(rawValueFlowSignal || rawValueFlowPivotTextSignal) && hasValueFlowAggregateQuestionSignal(rawText);
|
||||
const monthlyAggregationSignal = hasMonthlyAggregationSignal(rawText);
|
||||
const rawAllTimeScopeSignal = hasAllTimeScopeHint(rawText);
|
||||
const dateScopeSignalText = stripNegatedTaxDateScopeClauses(rawText);
|
||||
@@ -1702,6 +1735,47 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
: profitMarginBusinessOverviewSignal
|
||||
? "profit_margin_boundary"
|
||||
: "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
|
||||
? "inventory_reserve_liquidation_boundary"
|
||||
: debtDueDateBusinessOverviewSignal
|
||||
@@ -1712,8 +1786,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
? "profit_margin_boundary"
|
||||
: "broad_business_evaluation";
|
||||
const businessOverviewSignal =
|
||||
rawBusinessOverviewSignal ||
|
||||
seededBusinessOverviewSignal;
|
||||
!businessOverviewCounterpartyValueFlowPivot &&
|
||||
(rawBusinessOverviewSignal || seededBusinessOverviewSignal);
|
||||
const businessOverviewSeparateCounterpartySignal = Boolean(
|
||||
businessOverviewSignal && hasBusinessOverviewSeparateCounterpartySignal(rawText)
|
||||
);
|
||||
@@ -1735,18 +1809,6 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
hasSimpleMovementLanePivotSignal(rawText) ||
|
||||
hasMovementEvidenceFollowupSignal(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 &&
|
||||
followupSeed.counterparty &&
|
||||
@@ -1792,10 +1854,6 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
!rawBidirectionalValueFlowSignal &&
|
||||
explicitOrganizationScopeSignal
|
||||
);
|
||||
const predecomposeOrganizationMirrorsCounterparty = sameScopedName(
|
||||
predecomposeEntities.counterparty,
|
||||
predecomposeEntities.organization
|
||||
);
|
||||
const organizationMirrorsPredecomposeCounterparty = Boolean(
|
||||
(rawBidirectionalValueFlowSignal ||
|
||||
hasValueRankingSignal(rawText) ||
|
||||
@@ -2130,16 +2188,27 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
const bidirectionalValueFlowSignal =
|
||||
!businessOverviewSignal &&
|
||||
!lifecycleSignal &&
|
||||
(rawBidirectionalValueFlowSignal || seededAction === "net_value_flow");
|
||||
((businessOverviewCounterpartyValueFlowPivot
|
||||
? rawBidirectionalValueFlowPivotTextSignal
|
||||
: rawBidirectionalValueFlowSignal) ||
|
||||
seededAction === "net_value_flow");
|
||||
const valueFlowSignal =
|
||||
!businessOverviewSignal &&
|
||||
!lifecycleSignal &&
|
||||
!metadataGroundedMovementLaneApplicable &&
|
||||
(rawValueFlowSignal || seededDomain === "counterparty_value");
|
||||
((businessOverviewCounterpartyValueFlowPivot
|
||||
? rawValueFlowPivotTextSignal
|
||||
: rawValueFlowSignal) ||
|
||||
seededDomain === "counterparty_value");
|
||||
const payoutSignal =
|
||||
valueFlowSignal &&
|
||||
!bidirectionalValueFlowSignal &&
|
||||
(rawPayoutSignal || seededAction === "payout");
|
||||
((businessOverviewCounterpartyValueFlowPivot
|
||||
? rawValueFlowPivotTextSignal &&
|
||||
!rawBidirectionalValueFlowPivotTextSignal &&
|
||||
hasPayoutSignal(rawText)
|
||||
: rawPayoutSignal) ||
|
||||
seededAction === "payout");
|
||||
const semanticDataNeed = metadataAmbiguityLaneClarificationApplicable
|
||||
? "metadata lane clarification"
|
||||
: semanticNeedFor({
|
||||
@@ -2147,17 +2216,37 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
? "movements"
|
||||
: businessOverviewSignal
|
||||
? "business_overview"
|
||||
: lifecycleSignal
|
||||
? "counterparty_lifecycle"
|
||||
: valueFlowSignal
|
||||
? "counterparty_value"
|
||||
: rawDomain ?? seededDomain,
|
||||
action: explicitVatMovementEvidenceSignal
|
||||
? "list_movements"
|
||||
: businessOverviewSignal
|
||||
? businessOverviewActionFamily
|
||||
: lifecycleSignal
|
||||
? "activity_duration"
|
||||
: valueFlowSignal
|
||||
? bidirectionalValueFlowSignal
|
||||
? "net_value_flow"
|
||||
: payoutSignal
|
||||
? "payout"
|
||||
: rawAction ?? seededAction ?? "turnover"
|
||||
: rawAction ?? seededAction,
|
||||
unsupported: explicitVatMovementEvidenceSignal
|
||||
? "movement_evidence"
|
||||
: businessOverviewSignal
|
||||
? businessOverviewUnsupportedFamily
|
||||
: unsupported ?? seededUnsupported,
|
||||
: lifecycleSignal
|
||||
? "counterparty_lifecycle"
|
||||
: valueFlowSignal
|
||||
? bidirectionalValueFlowSignal
|
||||
? "counterparty_bidirectional_value_flow_or_netting"
|
||||
: payoutSignal
|
||||
? "counterparty_payouts_or_outflow"
|
||||
: seededUnsupported ?? "counterparty_value_or_turnover"
|
||||
: unsupported ?? seededUnsupported,
|
||||
lifecycleSignal,
|
||||
valueFlowSignal,
|
||||
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
|
||||
@@ -2469,30 +2558,30 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
unsupported_but_understood_family:
|
||||
businessOverviewSignal
|
||||
? businessOverviewUnsupportedFamily
|
||||
: unsupported ??
|
||||
(lifecycleSignal
|
||||
? "counterparty_lifecycle"
|
||||
: lifecycleSignal
|
||||
? "counterparty_lifecycle"
|
||||
: valueFlowSignal
|
||||
? bidirectionalValueFlowSignal
|
||||
? "counterparty_bidirectional_value_flow_or_netting"
|
||||
: payoutSignal
|
||||
? "counterparty_payouts_or_outflow"
|
||||
: seededUnsupported ?? "counterparty_value_or_turnover"
|
||||
: metadataGroundedMovementLaneApplicable
|
||||
? "movement_evidence"
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
? "document_evidence"
|
||||
: explicitVatMovementEvidenceSignal
|
||||
? "movement_evidence"
|
||||
: metadataAmbiguityLaneClarificationApplicable
|
||||
? "metadata_lane_choice_clarification"
|
||||
: entityResolutionSignal
|
||||
? "entity_resolution"
|
||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||
? "1c_metadata_surface"
|
||||
: followupDiscoverySeedApplicable
|
||||
? seededUnsupported
|
||||
: null),
|
||||
: unsupported ??
|
||||
(metadataGroundedMovementLaneApplicable
|
||||
? "movement_evidence"
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
? "document_evidence"
|
||||
: explicitVatMovementEvidenceSignal
|
||||
? "movement_evidence"
|
||||
: metadataAmbiguityLaneClarificationApplicable
|
||||
? "metadata_lane_choice_clarification"
|
||||
: entityResolutionSignal
|
||||
? "entity_resolution"
|
||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||
? "1c_metadata_surface"
|
||||
: followupDiscoverySeedApplicable
|
||||
? seededUnsupported
|
||||
: null),
|
||||
stale_replay_forbidden: Boolean(
|
||||
assistantTurnMeaning?.stale_replay_forbidden ||
|
||||
businessOverviewSignal ||
|
||||
@@ -2763,6 +2852,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
if (businessOverviewSignal) {
|
||||
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) {
|
||||
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) {
|
||||
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
|
||||
@@ -379,14 +435,14 @@ function detectBroadBusinessEvaluation(text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildEntityCandidates(counterpartyTurnover) {
|
||||
if (!counterpartyTurnover?.entity) {
|
||||
function buildEntityCandidates(entityFamily) {
|
||||
if (!entityFamily?.entity) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: "counterparty",
|
||||
value: counterpartyTurnover.entity,
|
||||
value: entityFamily.entity,
|
||||
source: "current_turn_loose_entity_tail"
|
||||
}
|
||||
];
|
||||
@@ -400,9 +456,13 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
||||
const effectiveText = normalizeTurnText(effectiveMessage, deps);
|
||||
const joinedText = fallbackCompactWhitespace(`${rawText} ${effectiveText}`);
|
||||
const supportedIntent = detectSupportedIntent(joinedText, deps);
|
||||
const counterpartyBidirectionalValueFlow = detectCounterpartyBidirectionalValueFlowFamily(joinedText);
|
||||
const counterpartyTurnover = detectCounterpartyTurnoverFamily(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 explicitIntentCandidate =
|
||||
broadBusinessEvaluation?.family
|
||||
@@ -410,6 +470,8 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
||||
: supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
|
||||
const unsupportedFamily = broadBusinessEvaluation?.family
|
||||
? broadBusinessEvaluation.family
|
||||
: !explicitIntentCandidate && counterpartyBidirectionalValueFlow?.family
|
||||
? counterpartyBidirectionalValueFlow.family
|
||||
: !explicitIntentCandidate && counterpartyTurnover?.family
|
||||
? counterpartyTurnover.family
|
||||
: null;
|
||||
@@ -417,6 +479,9 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
||||
if (supportedIntent?.reason) {
|
||||
reasonCodes.push(supportedIntent.reason);
|
||||
}
|
||||
if (counterpartyBidirectionalValueFlow?.family) {
|
||||
reasonCodes.push("counterparty_bidirectional_value_flow_current_turn_signal");
|
||||
}
|
||||
if (counterpartyTurnover?.family) {
|
||||
reasonCodes.push("counterparty_turnover_current_turn_signal");
|
||||
}
|
||||
@@ -443,6 +508,8 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
||||
? "inventory"
|
||||
: broadBusinessEvaluation?.family
|
||||
? "business_summary"
|
||||
: counterpartyBidirectionalValueFlow?.family
|
||||
? "counterparty_value"
|
||||
: explicitIntentCandidate?.includes("counterparty")
|
||||
? "counterparty"
|
||||
: counterpartyTurnover?.family
|
||||
@@ -455,6 +522,8 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
||||
? "confirmed_snapshot"
|
||||
: broadBusinessEvaluation?.family
|
||||
? "broad_evaluation"
|
||||
: counterpartyBidirectionalValueFlow?.family
|
||||
? "net_value_flow"
|
||||
: explicitIntentCandidate === "customer_revenue_and_payments" ||
|
||||
explicitIntentCandidate === "supplier_payouts_profile"
|
||||
? "counterparty_value_or_turnover"
|
||||
@@ -470,7 +539,10 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
||||
? "counterparty_value_or_turnover"
|
||||
: null;
|
||||
const staleReplayForbidden = Boolean(
|
||||
unsupportedFamily || broadBusinessEvaluation?.family || (counterpartyTurnover?.entity && !explicitIntentCandidate)
|
||||
unsupportedFamily ||
|
||||
broadBusinessEvaluation?.family ||
|
||||
(counterpartyBidirectionalValueFlow?.entity && !explicitIntentCandidate) ||
|
||||
(counterpartyTurnover?.entity && !explicitIntentCandidate)
|
||||
);
|
||||
return {
|
||||
schema_version: "assistant_turn_meaning_v1",
|
||||
@@ -481,10 +553,13 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
||||
asked_domain_family: askedDomainFamily,
|
||||
asked_action_family: askedActionFamily,
|
||||
explicit_intent_candidate: explicitIntentCandidate,
|
||||
explicit_entity_candidates: broadBusinessEvaluation?.family ? [] : buildEntityCandidates(counterpartyTurnover),
|
||||
explicit_entity_candidates: broadBusinessEvaluation?.family
|
||||
? []
|
||||
: buildEntityCandidates(counterpartyBidirectionalValueFlow ?? counterpartyTurnover),
|
||||
meaning_confidence: broadBusinessEvaluation?.family
|
||||
? "medium"
|
||||
: supportedIntent?.confidence ?? (counterpartyTurnover?.family ? "medium" : "low"),
|
||||
: supportedIntent?.confidence ??
|
||||
(counterpartyBidirectionalValueFlow?.family || counterpartyTurnover?.family ? "medium" : "low"),
|
||||
intent_override_strength: explicitIntentCandidate
|
||||
? "explicit_current_turn_intent"
|
||||
: staleReplayForbidden
|
||||
|
||||
@@ -46,6 +46,7 @@ type V2FamilyFragment = V2Family["fragments"][number];
|
||||
type RouteQueryClass =
|
||||
| "exact_object_trace"
|
||||
| "ranking_or_period_summary"
|
||||
| "bidirectional_value_flow"
|
||||
| "symptom_first"
|
||||
| "lifecycle_first"
|
||||
| "chain_break"
|
||||
@@ -66,6 +67,10 @@ interface RouteDisciplineRule {
|
||||
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 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 LIFECYCLE_MARKER_PATTERN =
|
||||
@@ -96,6 +101,13 @@ export const ROUTE_DISCIPLINE_RULE_TABLE: RouteDisciplineRule[] = [
|
||||
forbidden_fallback: ["store_canonical", "hybrid_store_plus_live"],
|
||||
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",
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
const lowerText = mergedFragmentText(fragment);
|
||||
const symptomSignal = hasSymptomSignal(fragment, lowerText);
|
||||
@@ -227,6 +254,8 @@ function resolveRouteClass(fragment: V2FamilyFragment): RouteDisciplineRule {
|
||||
const causalSignal = hasCausalSignal(lowerText);
|
||||
const ambiguitySignal = hasAmbiguitySignal(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) {
|
||||
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) {
|
||||
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)) {
|
||||
return ROUTE_DISCIPLINE_RULE_MAP.get("mixed_ambiguity")!;
|
||||
}
|
||||
@@ -272,7 +304,8 @@ function shouldPromoteFromNoRoute(fragment: V2FamilyFragment, rule: RouteDiscipl
|
||||
hasLifecycleSignal(fragment, lowerText) ||
|
||||
hasChainBreakSignal(lowerText) ||
|
||||
hasPeriodImpactSignal(lowerText) ||
|
||||
hasCausalSignal(lowerText);
|
||||
hasCausalSignal(lowerText) ||
|
||||
(hasBidirectionalValueFlowSignal(fragment, lowerText) && hasCounterpartyScopeSignal(fragment, lowerText));
|
||||
|
||||
const hasAnchor =
|
||||
hasAccountOrPeriodAnchor(fragment, lowerText) ||
|
||||
|
||||
Reference in New Issue
Block a user