ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - fix(router): keep llm-first non-domain indexing and prevent followup force into address lane
This commit is contained in:
parent
143cf6efe1
commit
a717ea6b26
|
|
@ -497,6 +497,9 @@ function classifyPayablesLiabilityCategory(row, counterparty) {
|
||||||
};
|
};
|
||||||
const reasons = new Set();
|
const reasons = new Set();
|
||||||
const text = `${counterparty} ${row.registrator} ${row.analytics.join(" ")}`.toLowerCase();
|
const text = `${counterparty} ${row.registrator} ${row.analytics.join(" ")}`.toLowerCase();
|
||||||
|
const hasBankOrCreditSignal = /(?:банк|сбер|втб|альфа|газпромбанк|кредит|депозит|loan|overdraft|deposit|bank)/iu.test(text);
|
||||||
|
const hasTaxOrStateSignal = /(?:уфк|ифнс|фнс|налог|пфр|фсс|сфр|казнач|бюджет|гос|департамент|министер|муницип|город москвы|федерал|налогов)/iu.test(text);
|
||||||
|
const hasCommercialCounterpartySignal = /(?:\bип\b|ооо|ао|зао|пао|подряд|поставщик|supplier|vendor|contractor|заказчик|клиент|counterparty)/iu.test(text);
|
||||||
const accountPrefixes = [extractAccountSectionCode(row.account_dt), extractAccountSectionCode(row.account_kt)].filter((item) => Boolean(item));
|
const accountPrefixes = [extractAccountSectionCode(row.account_dt), extractAccountSectionCode(row.account_kt)].filter((item) => Boolean(item));
|
||||||
if (accountPrefixes.includes("60")) {
|
if (accountPrefixes.includes("60")) {
|
||||||
scores.supplier_or_contractor += 3;
|
scores.supplier_or_contractor += 3;
|
||||||
|
|
@ -511,18 +514,20 @@ function classifyPayablesLiabilityCategory(row, counterparty) {
|
||||||
reasons.add("участие счета 68/69");
|
reasons.add("участие счета 68/69");
|
||||||
}
|
}
|
||||||
if (accountPrefixes.includes("76")) {
|
if (accountPrefixes.includes("76")) {
|
||||||
scores.supplier_or_contractor += 1;
|
scores.other += 1;
|
||||||
reasons.add("участие счета 76");
|
reasons.add("участие счета 76 (прочие расчеты)");
|
||||||
}
|
}
|
||||||
if (/(?:банк|сбер|втб|альфа|газпромбанк|кредит|депозит|loan|overdraft|deposit)/iu.test(text)) {
|
if (hasBankOrCreditSignal) {
|
||||||
scores.bank_or_credit += 3;
|
scores.bank_or_credit += 6;
|
||||||
|
scores.supplier_or_contractor = Math.max(0, scores.supplier_or_contractor - 2);
|
||||||
reasons.add("банк/кредит в аналитике");
|
reasons.add("банк/кредит в аналитике");
|
||||||
}
|
}
|
||||||
if (/(?:уфк|ифнс|фнс|налог|пфр|фсс|сфр|казнач|бюджет|гос|департамент|министер|муницип|город москвы|федерал)/iu.test(text)) {
|
if (hasTaxOrStateSignal) {
|
||||||
scores.tax_or_state += 3;
|
scores.tax_or_state += 6;
|
||||||
|
scores.supplier_or_contractor = Math.max(0, scores.supplier_or_contractor - 2);
|
||||||
reasons.add("налог/госорган в аналитике");
|
reasons.add("налог/госорган в аналитике");
|
||||||
}
|
}
|
||||||
if (/(?:\bип\b|ооо|ао|зао|пао|подряд|поставщик|supplier|vendor|contractor)/iu.test(text)) {
|
if (hasCommercialCounterpartySignal && !hasBankOrCreditSignal && !hasTaxOrStateSignal) {
|
||||||
scores.supplier_or_contractor += 2;
|
scores.supplier_or_contractor += 2;
|
||||||
reasons.add("коммерческий контрагент в аналитике");
|
reasons.add("коммерческий контрагент в аналитике");
|
||||||
}
|
}
|
||||||
|
|
@ -660,6 +665,8 @@ function buildPayablesConfirmedBalanceAggregate(rows, asOfDate) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const classified = classifyPayablesLiabilityCategory(row, name);
|
const classified = classifyPayablesLiabilityCategory(row, name);
|
||||||
|
const contract = extractContractName(row);
|
||||||
|
const sourceRefs = extractPayablesSourceRefs(row, name, contract);
|
||||||
const current = byCounterparty.get(name);
|
const current = byCounterparty.get(name);
|
||||||
if (!current) {
|
if (!current) {
|
||||||
byCounterparty.set(name, {
|
byCounterparty.set(name, {
|
||||||
|
|
@ -673,7 +680,10 @@ function buildPayablesConfirmedBalanceAggregate(rows, asOfDate) {
|
||||||
tax_or_state: classified.scores.tax_or_state,
|
tax_or_state: classified.scores.tax_or_state,
|
||||||
other: classified.scores.other
|
other: classified.scores.other
|
||||||
},
|
},
|
||||||
reasons: new Set(classified.reasons)
|
reasons: new Set(classified.reasons),
|
||||||
|
contracts: new Set(contract ? [contract] : []),
|
||||||
|
documents: new Set(row.registrator ? [row.registrator] : []),
|
||||||
|
sourceRefs: new Set(sourceRefs)
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -692,6 +702,15 @@ function buildPayablesConfirmedBalanceAggregate(rows, asOfDate) {
|
||||||
for (const reason of classified.reasons) {
|
for (const reason of classified.reasons) {
|
||||||
current.reasons.add(reason);
|
current.reasons.add(reason);
|
||||||
}
|
}
|
||||||
|
if (contract) {
|
||||||
|
current.contracts.add(contract);
|
||||||
|
}
|
||||||
|
if (row.registrator) {
|
||||||
|
current.documents.add(row.registrator);
|
||||||
|
}
|
||||||
|
for (const ref of sourceRefs) {
|
||||||
|
current.sourceRefs.add(ref);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return Array.from(byCounterparty.entries())
|
return Array.from(byCounterparty.entries())
|
||||||
.map(([name, item]) => ({
|
.map(([name, item]) => ({
|
||||||
|
|
@ -701,7 +720,10 @@ function buildPayablesConfirmedBalanceAggregate(rows, asOfDate) {
|
||||||
firstPeriod: item.firstPeriod,
|
firstPeriod: item.firstPeriod,
|
||||||
lastPeriod: item.lastPeriod,
|
lastPeriod: item.lastPeriod,
|
||||||
category: resolvePayablesLiabilityCategory(item.categoryScores),
|
category: resolvePayablesLiabilityCategory(item.categoryScores),
|
||||||
categoryReasons: Array.from(item.reasons).slice(0, 2)
|
categoryReasons: Array.from(item.reasons).slice(0, 2),
|
||||||
|
contracts: Array.from(item.contracts).slice(0, 2),
|
||||||
|
documents: Array.from(item.documents).slice(0, 2),
|
||||||
|
sourceRefs: Array.from(item.sourceRefs).slice(0, 3)
|
||||||
}))
|
}))
|
||||||
.filter((item) => item.outstandingAmount > 0.005)
|
.filter((item) => item.outstandingAmount > 0.005)
|
||||||
.sort((left, right) => {
|
.sort((left, right) => {
|
||||||
|
|
@ -887,6 +909,59 @@ function extractContractName(row) {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
function normalizeEntityToken(value) {
|
||||||
|
return String(value ?? "")
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ё/g, "е")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
function extractPayablesSourceRefs(row, counterparty, contract) {
|
||||||
|
const refs = new Set();
|
||||||
|
const counterpartyToken = normalizeEntityToken(counterparty);
|
||||||
|
const contractToken = normalizeEntityToken(contract);
|
||||||
|
const sourceRefTokenPattern = /(?:№|договор|contract|счет|сч[её]т|акт|накладн|плат[её]ж|invoice|payment|order|заявк|реестр|доп\.?\s*соглаш)/iu;
|
||||||
|
for (const token of row.analytics) {
|
||||||
|
const normalized = String(token ?? "").trim();
|
||||||
|
if (!normalized) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (/^(?:0|<пусто>|пустая ссылка)$/iu.test(normalized)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (/^\d{4}-\d{2}-\d{2}/.test(normalized)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const tokenNormalized = normalizeEntityToken(normalized);
|
||||||
|
if (!tokenNormalized) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (tokenNormalized === counterpartyToken || (contractToken && tokenNormalized === contractToken)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (sourceRefTokenPattern.test(normalized) || /(?:[a-zа-я].*\d|\d.*[a-zа-я])/iu.test(normalized)) {
|
||||||
|
refs.add(normalized);
|
||||||
|
}
|
||||||
|
if (refs.size >= 3) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(refs);
|
||||||
|
}
|
||||||
|
function formatPayablesEvidenceSuffix(item) {
|
||||||
|
const parts = [];
|
||||||
|
if (item.contracts.length > 0) {
|
||||||
|
parts.push(`договор: ${item.contracts.slice(0, 2).join("; ")}`);
|
||||||
|
}
|
||||||
|
if (item.documents.length > 0) {
|
||||||
|
const suffix = item.documents.length > 1 ? ` (+${item.documents.length - 1})` : "";
|
||||||
|
parts.push(`документ: ${item.documents[0]}${suffix}`);
|
||||||
|
}
|
||||||
|
if (item.sourceRefs.length > 0) {
|
||||||
|
parts.push(`source refs: ${item.sourceRefs.slice(0, 2).join("; ")}`);
|
||||||
|
}
|
||||||
|
return parts.length > 0 ? ` | ${parts.join(" | ")}` : "";
|
||||||
|
}
|
||||||
function deriveOperationalYearWindow(yearDocs, yearOps) {
|
function deriveOperationalYearWindow(yearDocs, yearOps) {
|
||||||
const docsSeries = [...yearDocs].sort((a, b) => a.year - b.year);
|
const docsSeries = [...yearDocs].sort((a, b) => a.year - b.year);
|
||||||
const fallbackSeries = [...yearOps].sort((a, b) => a.year - b.year);
|
const fallbackSeries = [...yearOps].sort((a, b) => a.year - b.year);
|
||||||
|
|
@ -1816,11 +1891,9 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
||||||
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
||||||
const periodTo = normalizeIsoDateOnly(options.periodTo);
|
const periodTo = normalizeIsoDateOnly(options.periodTo);
|
||||||
const scopeLine = asOfDate
|
const periodScopeLine = !asOfDate && (periodFrom || periodTo)
|
||||||
? `- Дата среза: ${formatDateRu(asOfDate)}.`
|
? `- Период анализа: ${formatDateRu(periodFrom ?? "...")}..${formatDateRu(periodTo ?? "...")}.`
|
||||||
: periodFrom || periodTo
|
: null;
|
||||||
? `- Период анализа: ${formatDateRu(periodFrom ?? "...")}..${formatDateRu(periodTo ?? "...")}.`
|
|
||||||
: null;
|
|
||||||
const carryoverLine = asOfDate || periodFrom || periodTo
|
const carryoverLine = asOfDate || periodFrom || periodTo
|
||||||
? "- В срез могут входить обязательства, возникшие до периода, если они оставались открытыми на дату среза."
|
? "- В срез могут входить обязательства, возникшие до периода, если они оставались открытыми на дату среза."
|
||||||
: null;
|
: null;
|
||||||
|
|
@ -1836,8 +1909,8 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("Блок 2. Что учтено");
|
lines.push("Блок 2. Что учтено");
|
||||||
lines.push(`- Дата среза: ${formatDateRu(payablesAsOfDate)}.`);
|
lines.push(`- Дата среза: ${formatDateRu(payablesAsOfDate)}.`);
|
||||||
if (scopeLine) {
|
if (periodScopeLine) {
|
||||||
lines.push(scopeLine);
|
lines.push(periodScopeLine);
|
||||||
}
|
}
|
||||||
lines.push("- Контур: обязательства по счетам 60/76.");
|
lines.push("- Контур: обязательства по счетам 60/76.");
|
||||||
if (carryoverLine) {
|
if (carryoverLine) {
|
||||||
|
|
@ -1856,7 +1929,7 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("Блок 5. Подтвержденные позиции к оплате");
|
lines.push("Блок 5. Подтвержденные позиции к оплате");
|
||||||
if (confirmedBalances.length > 0) {
|
if (confirmedBalances.length > 0) {
|
||||||
lines.push(...confirmedBalances.slice(0, 10).map((item, index) => `${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoney(item.outstandingAmount)} | операций: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}`));
|
lines.push(...confirmedBalances.slice(0, 10).map((item, index) => `${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoney(item.outstandingAmount)} | операций: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
lines.push("- Подтвержденных открытых обязательств к оплате на дату среза не найдено.");
|
lines.push("- Подтвержденных открытых обязательств к оплате на дату среза не найдено.");
|
||||||
|
|
@ -1977,8 +2050,8 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
`- ${liabilityCategoryLabel("tax_or_state")}: ${categoryCounts.tax_or_state}`,
|
`- ${liabilityCategoryLabel("tax_or_state")}: ${categoryCounts.tax_or_state}`,
|
||||||
`- ${liabilityCategoryLabel("other")}: ${categoryCounts.other}`,
|
`- ${liabilityCategoryLabel("other")}: ${categoryCounts.other}`,
|
||||||
"",
|
"",
|
||||||
"Блок 5. Кому нужно заплатить в первую очередь (по сумме остатка):",
|
"Блок 5. Крупнейшие подтвержденные позиции к оплате (по сумме остатка):",
|
||||||
...confirmedBalances.slice(0, 10).map((item, index) => `${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток к оплате: ${formatMoney(item.outstandingAmount)} | операций в срезе: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}`)
|
...confirmedBalances.slice(0, 10).map((item, index) => `${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток к оплате: ${formatMoney(item.outstandingAmount)} | операций в срезе: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`)
|
||||||
];
|
];
|
||||||
return {
|
return {
|
||||||
responseType: "FACTUAL_LIST",
|
responseType: "FACTUAL_LIST",
|
||||||
|
|
|
||||||
|
|
@ -3792,6 +3792,28 @@ function resolveAssistantOrchestrationDecision(input) {
|
||||||
hasAccountingSignal(repairedEffectiveAddressUserMessage) ||
|
hasAccountingSignal(repairedEffectiveAddressUserMessage) ||
|
||||||
hasDataRetrievalRequestSignal(rawUserMessage) ||
|
hasDataRetrievalRequestSignal(rawUserMessage) ||
|
||||||
hasDataRetrievalRequestSignal(repairedRawUserMessage);
|
hasDataRetrievalRequestSignal(repairedRawUserMessage);
|
||||||
|
const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode);
|
||||||
|
const llmFirstAddressCandidate = Boolean(llmContractMode === "address_query" && llmContractIntent && llmContractIntent !== "unknown");
|
||||||
|
const llmFirstUnsupportedCandidate = Boolean(llmContractMode === "unsupported" &&
|
||||||
|
(!llmContractIntent || llmContractIntent === "unknown"));
|
||||||
|
const dangerOrCoercionSignal = hasDangerOrCoercionSignal(rawUserMessage) ||
|
||||||
|
hasDangerOrCoercionSignal(repairedRawUserMessage) ||
|
||||||
|
hasDangerOrCoercionSignal(effectiveAddressUserMessage) ||
|
||||||
|
hasDangerOrCoercionSignal(repairedEffectiveAddressUserMessage);
|
||||||
|
const explicitAddressFollowupSignal = hasAddressFollowupContextSignal(rawUserMessage) ||
|
||||||
|
hasAddressFollowupContextSignal(repairedRawUserMessage) ||
|
||||||
|
hasAddressFollowupContextSignal(effectiveAddressUserMessage) ||
|
||||||
|
hasAddressFollowupContextSignal(repairedEffectiveAddressUserMessage);
|
||||||
|
const effectiveAddressFollowupSignal = explicitAddressFollowupSignal && !dangerOrCoercionSignal;
|
||||||
|
const deterministicNonDomainGuard = Boolean(!dataScopeMetaQuery &&
|
||||||
|
!capabilityMetaQuery &&
|
||||||
|
!dataRetrievalSignal &&
|
||||||
|
!effectiveAddressFollowupSignal &&
|
||||||
|
modeDetection.mode === "unsupported" &&
|
||||||
|
intentResolution.intent === "unknown");
|
||||||
|
const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate &&
|
||||||
|
deterministicNonDomainGuard &&
|
||||||
|
(llmFirstUnsupportedCandidate || llmContractMode === null));
|
||||||
const hardMetaMode = dataScopeMetaQuery
|
const hardMetaMode = dataScopeMetaQuery
|
||||||
? "data_scope"
|
? "data_scope"
|
||||||
: capabilityMetaQuery && !dataRetrievalSignal
|
: capabilityMetaQuery && !dataRetrievalSignal
|
||||||
|
|
@ -3853,8 +3875,35 @@ function resolveAssistantOrchestrationDecision(input) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (nonDomainQueryIndexed) {
|
||||||
|
return {
|
||||||
|
runAddressLane: false,
|
||||||
|
toolGateDecision: "skip_address_lane",
|
||||||
|
toolGateReason: "non_domain_query_indexed",
|
||||||
|
livingMode: "chat",
|
||||||
|
livingReason: "non_domain_query_indexed",
|
||||||
|
orchestrationContract: {
|
||||||
|
schema_version: "assistant_orchestration_contract_v1",
|
||||||
|
hard_meta_mode: "non_domain",
|
||||||
|
address_mode: modeDetection.mode,
|
||||||
|
address_mode_confidence: modeDetection.confidence,
|
||||||
|
address_intent: intentResolution.intent,
|
||||||
|
address_intent_confidence: intentResolution.confidence,
|
||||||
|
strong_data_signal_detected: strongDataSignal,
|
||||||
|
data_retrieval_signal_detected: dataRetrievalSignal,
|
||||||
|
followup_context_detected: Boolean(followupContext),
|
||||||
|
unsupported_address_intent_fallback_to_deep: false,
|
||||||
|
final_decision: {
|
||||||
|
run_address_lane: false,
|
||||||
|
tool_gate_decision: "skip_address_lane",
|
||||||
|
tool_gate_reason: "non_domain_query_indexed",
|
||||||
|
living_mode: "chat",
|
||||||
|
living_reason: "non_domain_query_indexed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
const baseToolGate = resolveAddressToolGateDecision(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage);
|
const baseToolGate = resolveAddressToolGateDecision(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage);
|
||||||
const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode);
|
|
||||||
const preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected &&
|
const preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected &&
|
||||||
llmPreDecomposeMeta?.applied &&
|
llmPreDecomposeMeta?.applied &&
|
||||||
llmContractMode === "address_query") ||
|
llmContractMode === "address_query") ||
|
||||||
|
|
@ -4160,11 +4209,11 @@ function hasOperationalAdminActionRequestSignal(text) {
|
||||||
return /(?:удаляй?\s+баз|удали\s+баз|снеси\s+баз|delete\s+database|drop\s+database)/i.test(normalized);
|
return /(?:удаляй?\s+баз|удали\s+баз|снеси\s+баз|delete\s+database|drop\s+database)/i.test(normalized);
|
||||||
}
|
}
|
||||||
function hasDangerOrCoercionSignal(text) {
|
function hasDangerOrCoercionSignal(text) {
|
||||||
const lower = compactWhitespace(String(text ?? "").toLowerCase()).replace(/ё/g, "е");
|
const normalized = compactWhitespace(String(text ?? "").toLowerCase()).replace(/ё/g, "е");
|
||||||
if (!lower) {
|
if (!normalized) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return /(?:убьют|убьют|убью|убить|убиют|угрож|опасн|омон|полици|насили|заставля|принужда|шантаж)/i.test(lower);
|
return /(?:убью|убить|убьют|убиют|угрож|опасн|омон|полици|насили|заставля|принужда|шантаж|выпил(?:юсь|иться|ился|илась|иться)|суицид|самоубий|покончу\s+с\s+собой|не\s+хочу\s+жить|хочу\s+умереть|сдохнуть|вскрыть\s+вены|повешусь|повеситься|спрыгну|kill\s+myself|end\s+my\s+life|suicid|self[\s-]?harm)/iu.test(normalized);
|
||||||
}
|
}
|
||||||
function hasDestructiveDataActionSignal(text) {
|
function hasDestructiveDataActionSignal(text) {
|
||||||
const lower = compactWhitespace(String(text ?? "").toLowerCase()).replace(/ё/g, "е");
|
const lower = compactWhitespace(String(text ?? "").toLowerCase()).replace(/ё/g, "е");
|
||||||
|
|
@ -4282,6 +4331,9 @@ function hasLivingChatSignal(text) {
|
||||||
if (!lower) {
|
if (!lower) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (/^(?:а\s+)?(?:тут|здесь|там|сюда|туда)[\s!?.,:;\-]*$/iu.test(lower)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (/^(ага|угу|ок|окей|ясно|понял|поняла|принято|спасибо|благодарю|супер|класс|норм|го|давай|погнали|привет|хай|йо|yo|че\s+там|ч[её]\s+как|че\s+как|hello|hi|thanks?)$/i.test(lower)) {
|
if (/^(ага|угу|ок|окей|ясно|понял|поняла|принято|спасибо|благодарю|супер|класс|норм|го|давай|погнали|привет|хай|йо|yo|че\s+там|ч[её]\s+как|че\s+как|hello|hi|thanks?)$/i.test(lower)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -653,6 +653,9 @@ interface PayablesConfirmedBalanceAggregate {
|
||||||
lastPeriod: string | null;
|
lastPeriod: string | null;
|
||||||
category: PayablesLiabilityCategory;
|
category: PayablesLiabilityCategory;
|
||||||
categoryReasons: string[];
|
categoryReasons: string[];
|
||||||
|
contracts: string[];
|
||||||
|
documents: string[];
|
||||||
|
sourceRefs: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function liabilityCategoryLabel(category: PayablesLiabilityCategory): string {
|
function liabilityCategoryLabel(category: PayablesLiabilityCategory): string {
|
||||||
|
|
@ -680,6 +683,11 @@ function classifyPayablesLiabilityCategory(row: ComposeStageRow, counterparty: s
|
||||||
};
|
};
|
||||||
const reasons = new Set<string>();
|
const reasons = new Set<string>();
|
||||||
const text = `${counterparty} ${row.registrator} ${row.analytics.join(" ")}`.toLowerCase();
|
const text = `${counterparty} ${row.registrator} ${row.analytics.join(" ")}`.toLowerCase();
|
||||||
|
const hasBankOrCreditSignal = /(?:банк|сбер|втб|альфа|газпромбанк|кредит|депозит|loan|overdraft|deposit|bank)/iu.test(text);
|
||||||
|
const hasTaxOrStateSignal =
|
||||||
|
/(?:уфк|ифнс|фнс|налог|пфр|фсс|сфр|казнач|бюджет|гос|департамент|министер|муницип|город москвы|федерал|налогов)/iu.test(text);
|
||||||
|
const hasCommercialCounterpartySignal =
|
||||||
|
/(?:\bип\b|ооо|ао|зао|пао|подряд|поставщик|supplier|vendor|contractor|заказчик|клиент|counterparty)/iu.test(text);
|
||||||
|
|
||||||
const accountPrefixes = [extractAccountSectionCode(row.account_dt), extractAccountSectionCode(row.account_kt)].filter(
|
const accountPrefixes = [extractAccountSectionCode(row.account_dt), extractAccountSectionCode(row.account_kt)].filter(
|
||||||
(item): item is string => Boolean(item)
|
(item): item is string => Boolean(item)
|
||||||
|
|
@ -697,19 +705,21 @@ function classifyPayablesLiabilityCategory(row: ComposeStageRow, counterparty: s
|
||||||
reasons.add("участие счета 68/69");
|
reasons.add("участие счета 68/69");
|
||||||
}
|
}
|
||||||
if (accountPrefixes.includes("76")) {
|
if (accountPrefixes.includes("76")) {
|
||||||
scores.supplier_or_contractor += 1;
|
scores.other += 1;
|
||||||
reasons.add("участие счета 76");
|
reasons.add("участие счета 76 (прочие расчеты)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/(?:банк|сбер|втб|альфа|газпромбанк|кредит|депозит|loan|overdraft|deposit)/iu.test(text)) {
|
if (hasBankOrCreditSignal) {
|
||||||
scores.bank_or_credit += 3;
|
scores.bank_or_credit += 6;
|
||||||
|
scores.supplier_or_contractor = Math.max(0, scores.supplier_or_contractor - 2);
|
||||||
reasons.add("банк/кредит в аналитике");
|
reasons.add("банк/кредит в аналитике");
|
||||||
}
|
}
|
||||||
if (/(?:уфк|ифнс|фнс|налог|пфр|фсс|сфр|казнач|бюджет|гос|департамент|министер|муницип|город москвы|федерал)/iu.test(text)) {
|
if (hasTaxOrStateSignal) {
|
||||||
scores.tax_or_state += 3;
|
scores.tax_or_state += 6;
|
||||||
|
scores.supplier_or_contractor = Math.max(0, scores.supplier_or_contractor - 2);
|
||||||
reasons.add("налог/госорган в аналитике");
|
reasons.add("налог/госорган в аналитике");
|
||||||
}
|
}
|
||||||
if (/(?:\bип\b|ооо|ао|зао|пао|подряд|поставщик|supplier|vendor|contractor)/iu.test(text)) {
|
if (hasCommercialCounterpartySignal && !hasBankOrCreditSignal && !hasTaxOrStateSignal) {
|
||||||
scores.supplier_or_contractor += 2;
|
scores.supplier_or_contractor += 2;
|
||||||
reasons.add("коммерческий контрагент в аналитике");
|
reasons.add("коммерческий контрагент в аналитике");
|
||||||
}
|
}
|
||||||
|
|
@ -852,6 +862,9 @@ function buildPayablesConfirmedBalanceAggregate(
|
||||||
lastPeriod: string | null;
|
lastPeriod: string | null;
|
||||||
categoryScores: Record<PayablesLiabilityCategory, number>;
|
categoryScores: Record<PayablesLiabilityCategory, number>;
|
||||||
reasons: Set<string>;
|
reasons: Set<string>;
|
||||||
|
contracts: Set<string>;
|
||||||
|
documents: Set<string>;
|
||||||
|
sourceRefs: Set<string>;
|
||||||
}
|
}
|
||||||
>();
|
>();
|
||||||
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
|
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
|
||||||
|
|
@ -882,6 +895,8 @@ function buildPayablesConfirmedBalanceAggregate(
|
||||||
}
|
}
|
||||||
|
|
||||||
const classified = classifyPayablesLiabilityCategory(row, name);
|
const classified = classifyPayablesLiabilityCategory(row, name);
|
||||||
|
const contract = extractContractName(row);
|
||||||
|
const sourceRefs = extractPayablesSourceRefs(row, name, contract);
|
||||||
const current = byCounterparty.get(name);
|
const current = byCounterparty.get(name);
|
||||||
if (!current) {
|
if (!current) {
|
||||||
byCounterparty.set(name, {
|
byCounterparty.set(name, {
|
||||||
|
|
@ -895,7 +910,10 @@ function buildPayablesConfirmedBalanceAggregate(
|
||||||
tax_or_state: classified.scores.tax_or_state,
|
tax_or_state: classified.scores.tax_or_state,
|
||||||
other: classified.scores.other
|
other: classified.scores.other
|
||||||
},
|
},
|
||||||
reasons: new Set(classified.reasons)
|
reasons: new Set(classified.reasons),
|
||||||
|
contracts: new Set(contract ? [contract] : []),
|
||||||
|
documents: new Set(row.registrator ? [row.registrator] : []),
|
||||||
|
sourceRefs: new Set(sourceRefs)
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -915,6 +933,15 @@ function buildPayablesConfirmedBalanceAggregate(
|
||||||
for (const reason of classified.reasons) {
|
for (const reason of classified.reasons) {
|
||||||
current.reasons.add(reason);
|
current.reasons.add(reason);
|
||||||
}
|
}
|
||||||
|
if (contract) {
|
||||||
|
current.contracts.add(contract);
|
||||||
|
}
|
||||||
|
if (row.registrator) {
|
||||||
|
current.documents.add(row.registrator);
|
||||||
|
}
|
||||||
|
for (const ref of sourceRefs) {
|
||||||
|
current.sourceRefs.add(ref);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(byCounterparty.entries())
|
return Array.from(byCounterparty.entries())
|
||||||
|
|
@ -925,7 +952,10 @@ function buildPayablesConfirmedBalanceAggregate(
|
||||||
firstPeriod: item.firstPeriod,
|
firstPeriod: item.firstPeriod,
|
||||||
lastPeriod: item.lastPeriod,
|
lastPeriod: item.lastPeriod,
|
||||||
category: resolvePayablesLiabilityCategory(item.categoryScores),
|
category: resolvePayablesLiabilityCategory(item.categoryScores),
|
||||||
categoryReasons: Array.from(item.reasons).slice(0, 2)
|
categoryReasons: Array.from(item.reasons).slice(0, 2),
|
||||||
|
contracts: Array.from(item.contracts).slice(0, 2),
|
||||||
|
documents: Array.from(item.documents).slice(0, 2),
|
||||||
|
sourceRefs: Array.from(item.sourceRefs).slice(0, 3)
|
||||||
}))
|
}))
|
||||||
.filter((item) => item.outstandingAmount > 0.005)
|
.filter((item) => item.outstandingAmount > 0.005)
|
||||||
.sort((left, right) => {
|
.sort((left, right) => {
|
||||||
|
|
@ -1142,6 +1172,69 @@ function extractContractName(row: ComposeStageRow): string | null {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeEntityToken(value: string | null | undefined): string {
|
||||||
|
return String(value ?? "")
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ё/g, "е")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPayablesSourceRefs(
|
||||||
|
row: ComposeStageRow,
|
||||||
|
counterparty: string,
|
||||||
|
contract: string | null
|
||||||
|
): string[] {
|
||||||
|
const refs = new Set<string>();
|
||||||
|
const counterpartyToken = normalizeEntityToken(counterparty);
|
||||||
|
const contractToken = normalizeEntityToken(contract);
|
||||||
|
const sourceRefTokenPattern =
|
||||||
|
/(?:№|договор|contract|счет|сч[её]т|акт|накладн|плат[её]ж|invoice|payment|order|заявк|реестр|доп\.?\s*соглаш)/iu;
|
||||||
|
|
||||||
|
for (const token of row.analytics) {
|
||||||
|
const normalized = String(token ?? "").trim();
|
||||||
|
if (!normalized) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (/^(?:0|<пусто>|пустая ссылка)$/iu.test(normalized)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (/^\d{4}-\d{2}-\d{2}/.test(normalized)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const tokenNormalized = normalizeEntityToken(normalized);
|
||||||
|
if (!tokenNormalized) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (tokenNormalized === counterpartyToken || (contractToken && tokenNormalized === contractToken)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (sourceRefTokenPattern.test(normalized) || /(?:[a-zа-я].*\d|\d.*[a-zа-я])/iu.test(normalized)) {
|
||||||
|
refs.add(normalized);
|
||||||
|
}
|
||||||
|
if (refs.size >= 3) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(refs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPayablesEvidenceSuffix(item: PayablesConfirmedBalanceAggregate): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (item.contracts.length > 0) {
|
||||||
|
parts.push(`договор: ${item.contracts.slice(0, 2).join("; ")}`);
|
||||||
|
}
|
||||||
|
if (item.documents.length > 0) {
|
||||||
|
const suffix = item.documents.length > 1 ? ` (+${item.documents.length - 1})` : "";
|
||||||
|
parts.push(`документ: ${item.documents[0]}${suffix}`);
|
||||||
|
}
|
||||||
|
if (item.sourceRefs.length > 0) {
|
||||||
|
parts.push(`source refs: ${item.sourceRefs.slice(0, 2).join("; ")}`);
|
||||||
|
}
|
||||||
|
return parts.length > 0 ? ` | ${parts.join(" | ")}` : "";
|
||||||
|
}
|
||||||
|
|
||||||
function deriveOperationalYearWindow(
|
function deriveOperationalYearWindow(
|
||||||
yearDocs: YearAggPoint[],
|
yearDocs: YearAggPoint[],
|
||||||
yearOps: YearAggPoint[]
|
yearOps: YearAggPoint[]
|
||||||
|
|
@ -2319,9 +2412,8 @@ export function composeFactualReply(
|
||||||
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
||||||
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
||||||
const periodTo = normalizeIsoDateOnly(options.periodTo);
|
const periodTo = normalizeIsoDateOnly(options.periodTo);
|
||||||
const scopeLine = asOfDate
|
const periodScopeLine =
|
||||||
? `- Дата среза: ${formatDateRu(asOfDate)}.`
|
!asOfDate && (periodFrom || periodTo)
|
||||||
: periodFrom || periodTo
|
|
||||||
? `- Период анализа: ${formatDateRu(periodFrom ?? "...")}..${formatDateRu(periodTo ?? "...")}.`
|
? `- Период анализа: ${formatDateRu(periodFrom ?? "...")}..${formatDateRu(periodTo ?? "...")}.`
|
||||||
: null;
|
: null;
|
||||||
const carryoverLine =
|
const carryoverLine =
|
||||||
|
|
@ -2345,8 +2437,8 @@ export function composeFactualReply(
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("Блок 2. Что учтено");
|
lines.push("Блок 2. Что учтено");
|
||||||
lines.push(`- Дата среза: ${formatDateRu(payablesAsOfDate)}.`);
|
lines.push(`- Дата среза: ${formatDateRu(payablesAsOfDate)}.`);
|
||||||
if (scopeLine) {
|
if (periodScopeLine) {
|
||||||
lines.push(scopeLine);
|
lines.push(periodScopeLine);
|
||||||
}
|
}
|
||||||
lines.push("- Контур: обязательства по счетам 60/76.");
|
lines.push("- Контур: обязательства по счетам 60/76.");
|
||||||
if (carryoverLine) {
|
if (carryoverLine) {
|
||||||
|
|
@ -2371,7 +2463,7 @@ export function composeFactualReply(
|
||||||
lines.push(
|
lines.push(
|
||||||
...confirmedBalances.slice(0, 10).map(
|
...confirmedBalances.slice(0, 10).map(
|
||||||
(item, index) =>
|
(item, index) =>
|
||||||
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoney(item.outstandingAmount)} | операций: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}`
|
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoney(item.outstandingAmount)} | операций: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -2516,10 +2608,10 @@ export function composeFactualReply(
|
||||||
`- ${liabilityCategoryLabel("tax_or_state")}: ${categoryCounts.tax_or_state}`,
|
`- ${liabilityCategoryLabel("tax_or_state")}: ${categoryCounts.tax_or_state}`,
|
||||||
`- ${liabilityCategoryLabel("other")}: ${categoryCounts.other}`,
|
`- ${liabilityCategoryLabel("other")}: ${categoryCounts.other}`,
|
||||||
"",
|
"",
|
||||||
"Блок 5. Кому нужно заплатить в первую очередь (по сумме остатка):",
|
"Блок 5. Крупнейшие подтвержденные позиции к оплате (по сумме остатка):",
|
||||||
...confirmedBalances.slice(0, 10).map(
|
...confirmedBalances.slice(0, 10).map(
|
||||||
(item, index) =>
|
(item, index) =>
|
||||||
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток к оплате: ${formatMoney(item.outstandingAmount)} | операций в срезе: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}`
|
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток к оплате: ${formatMoney(item.outstandingAmount)} | операций в срезе: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -3750,6 +3750,28 @@ export function resolveAssistantOrchestrationDecision(input) {
|
||||||
hasAccountingSignal(repairedEffectiveAddressUserMessage) ||
|
hasAccountingSignal(repairedEffectiveAddressUserMessage) ||
|
||||||
hasDataRetrievalRequestSignal(rawUserMessage) ||
|
hasDataRetrievalRequestSignal(rawUserMessage) ||
|
||||||
hasDataRetrievalRequestSignal(repairedRawUserMessage);
|
hasDataRetrievalRequestSignal(repairedRawUserMessage);
|
||||||
|
const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode);
|
||||||
|
const llmFirstAddressCandidate = Boolean(llmContractMode === "address_query" && llmContractIntent && llmContractIntent !== "unknown");
|
||||||
|
const llmFirstUnsupportedCandidate = Boolean(llmContractMode === "unsupported" &&
|
||||||
|
(!llmContractIntent || llmContractIntent === "unknown"));
|
||||||
|
const dangerOrCoercionSignal = hasDangerOrCoercionSignal(rawUserMessage) ||
|
||||||
|
hasDangerOrCoercionSignal(repairedRawUserMessage) ||
|
||||||
|
hasDangerOrCoercionSignal(effectiveAddressUserMessage) ||
|
||||||
|
hasDangerOrCoercionSignal(repairedEffectiveAddressUserMessage);
|
||||||
|
const explicitAddressFollowupSignal = hasAddressFollowupContextSignal(rawUserMessage) ||
|
||||||
|
hasAddressFollowupContextSignal(repairedRawUserMessage) ||
|
||||||
|
hasAddressFollowupContextSignal(effectiveAddressUserMessage) ||
|
||||||
|
hasAddressFollowupContextSignal(repairedEffectiveAddressUserMessage);
|
||||||
|
const effectiveAddressFollowupSignal = explicitAddressFollowupSignal && !dangerOrCoercionSignal;
|
||||||
|
const deterministicNonDomainGuard = Boolean(!dataScopeMetaQuery &&
|
||||||
|
!capabilityMetaQuery &&
|
||||||
|
!dataRetrievalSignal &&
|
||||||
|
!effectiveAddressFollowupSignal &&
|
||||||
|
modeDetection.mode === "unsupported" &&
|
||||||
|
intentResolution.intent === "unknown");
|
||||||
|
const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate &&
|
||||||
|
deterministicNonDomainGuard &&
|
||||||
|
(llmFirstUnsupportedCandidate || llmContractMode === null));
|
||||||
const hardMetaMode = dataScopeMetaQuery
|
const hardMetaMode = dataScopeMetaQuery
|
||||||
? "data_scope"
|
? "data_scope"
|
||||||
: capabilityMetaQuery && !dataRetrievalSignal
|
: capabilityMetaQuery && !dataRetrievalSignal
|
||||||
|
|
@ -3811,8 +3833,35 @@ export function resolveAssistantOrchestrationDecision(input) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (nonDomainQueryIndexed) {
|
||||||
|
return {
|
||||||
|
runAddressLane: false,
|
||||||
|
toolGateDecision: "skip_address_lane",
|
||||||
|
toolGateReason: "non_domain_query_indexed",
|
||||||
|
livingMode: "chat",
|
||||||
|
livingReason: "non_domain_query_indexed",
|
||||||
|
orchestrationContract: {
|
||||||
|
schema_version: "assistant_orchestration_contract_v1",
|
||||||
|
hard_meta_mode: "non_domain",
|
||||||
|
address_mode: modeDetection.mode,
|
||||||
|
address_mode_confidence: modeDetection.confidence,
|
||||||
|
address_intent: intentResolution.intent,
|
||||||
|
address_intent_confidence: intentResolution.confidence,
|
||||||
|
strong_data_signal_detected: strongDataSignal,
|
||||||
|
data_retrieval_signal_detected: dataRetrievalSignal,
|
||||||
|
followup_context_detected: Boolean(followupContext),
|
||||||
|
unsupported_address_intent_fallback_to_deep: false,
|
||||||
|
final_decision: {
|
||||||
|
run_address_lane: false,
|
||||||
|
tool_gate_decision: "skip_address_lane",
|
||||||
|
tool_gate_reason: "non_domain_query_indexed",
|
||||||
|
living_mode: "chat",
|
||||||
|
living_reason: "non_domain_query_indexed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
const baseToolGate = resolveAddressToolGateDecision(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage);
|
const baseToolGate = resolveAddressToolGateDecision(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage);
|
||||||
const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode);
|
|
||||||
const preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected &&
|
const preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected &&
|
||||||
llmPreDecomposeMeta?.applied &&
|
llmPreDecomposeMeta?.applied &&
|
||||||
llmContractMode === "address_query") ||
|
llmContractMode === "address_query") ||
|
||||||
|
|
@ -4118,11 +4167,11 @@ function hasOperationalAdminActionRequestSignal(text) {
|
||||||
return /(?:удаляй?\s+баз|удали\s+баз|снеси\s+баз|delete\s+database|drop\s+database)/i.test(normalized);
|
return /(?:удаляй?\s+баз|удали\s+баз|снеси\s+баз|delete\s+database|drop\s+database)/i.test(normalized);
|
||||||
}
|
}
|
||||||
function hasDangerOrCoercionSignal(text) {
|
function hasDangerOrCoercionSignal(text) {
|
||||||
const lower = compactWhitespace(String(text ?? "").toLowerCase()).replace(/ё/g, "е");
|
const normalized = compactWhitespace(String(text ?? "").toLowerCase()).replace(/ё/g, "е");
|
||||||
if (!lower) {
|
if (!normalized) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return /(?:убьют|убьют|убью|убить|убиют|угрож|опасн|омон|полици|насили|заставля|принужда|шантаж)/i.test(lower);
|
return /(?:убью|убить|убьют|убиют|угрож|опасн|омон|полици|насили|заставля|принужда|шантаж|выпил(?:юсь|иться|ился|илась|иться)|суицид|самоубий|покончу\s+с\s+собой|не\s+хочу\s+жить|хочу\s+умереть|сдохнуть|вскрыть\s+вены|повешусь|повеситься|спрыгну|kill\s+myself|end\s+my\s+life|suicid|self[\s-]?harm)/iu.test(normalized);
|
||||||
}
|
}
|
||||||
function hasDestructiveDataActionSignal(text) {
|
function hasDestructiveDataActionSignal(text) {
|
||||||
const lower = compactWhitespace(String(text ?? "").toLowerCase()).replace(/ё/g, "е");
|
const lower = compactWhitespace(String(text ?? "").toLowerCase()).replace(/ё/g, "е");
|
||||||
|
|
@ -4240,6 +4289,9 @@ function hasLivingChatSignal(text) {
|
||||||
if (!lower) {
|
if (!lower) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (/^(?:а\s+)?(?:тут|здесь|там|сюда|туда)[\s!?.,:;\-]*$/iu.test(lower)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (/^(ага|угу|ок|окей|ясно|понял|поняла|принято|спасибо|благодарю|супер|класс|норм|го|давай|погнали|привет|хай|йо|yo|че\s+там|ч[её]\s+как|че\s+как|hello|hi|thanks?)$/i.test(lower)) {
|
if (/^(ага|угу|ок|окей|ясно|понял|поняла|принято|спасибо|благодарю|супер|класс|норм|го|давай|погнали|привет|хай|йо|yo|че\s+там|ч[её]\s+как|че\s+как|hello|hi|thanks?)$/i.test(lower)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,18 @@ describe("assistant living router mode decision", () => {
|
||||||
expect(decision.mode).toBe("deep_analysis");
|
expect(decision.mode).toBe("deep_analysis");
|
||||||
expect(decision.reason).toBe("predecompose_unsupported_mode_fallback_to_deep");
|
expect(decision.reason).toBe("predecompose_unsupported_mode_fallback_to_deep");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("routes ultra-short deictic follow-up ('тут?') to chat mode", () => {
|
||||||
|
const decision = resolveLivingAssistantModeDecision({
|
||||||
|
userMessage: "тут?",
|
||||||
|
addressLaneTriggered: false,
|
||||||
|
useMock: false,
|
||||||
|
predecomposeMode: "unsupported",
|
||||||
|
predecomposeModeConfidence: "low"
|
||||||
|
});
|
||||||
|
expect(decision.mode).toBe("chat");
|
||||||
|
expect(decision.reason).toBe("living_chat_signal_detected");
|
||||||
|
});
|
||||||
it("routes capability question to chat even when phrase contains 1С", () => {
|
it("routes capability question to chat even when phrase contains 1С", () => {
|
||||||
const decision = resolveLivingAssistantModeDecision({
|
const decision = resolveLivingAssistantModeDecision({
|
||||||
userMessage: "и 1с можешь настроить?",
|
userMessage: "и 1с можешь настроить?",
|
||||||
|
|
@ -186,6 +198,39 @@ describe("assistant living router mode decision", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("assistant orchestration contract", () => {
|
describe("assistant orchestration contract", () => {
|
||||||
|
it("routes non-domain emotional follow-up to indexed chat path instead of address lane", () => {
|
||||||
|
const decision = resolveAssistantOrchestrationDecision({
|
||||||
|
rawUserMessage: "бля хочу выпилиться от этого ебаного 1с",
|
||||||
|
effectiveAddressUserMessage: "бля хочу выпилиться от этого ебаного 1с",
|
||||||
|
followupContext: {
|
||||||
|
previous_intent: "payables_confirmed_as_of_date",
|
||||||
|
previous_filters: {
|
||||||
|
period_from: "2021-05-01",
|
||||||
|
period_to: "2021-05-31",
|
||||||
|
as_of_date: "2021-05-31"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
llmPreDecomposeMeta: {
|
||||||
|
applied: false,
|
||||||
|
reason: "no_usable_fragment",
|
||||||
|
predecomposeContract: {
|
||||||
|
mode: "unsupported",
|
||||||
|
mode_confidence: "low",
|
||||||
|
intent: "unknown",
|
||||||
|
intent_confidence: "low"
|
||||||
|
}
|
||||||
|
} as any,
|
||||||
|
useMock: false
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(decision.runAddressLane).toBe(false);
|
||||||
|
expect(decision.toolGateDecision).toBe("skip_address_lane");
|
||||||
|
expect(decision.toolGateReason).toBe("non_domain_query_indexed");
|
||||||
|
expect(decision.livingMode).toBe("chat");
|
||||||
|
expect(decision.livingReason).toBe("non_domain_query_indexed");
|
||||||
|
expect(decision.orchestrationContract?.hard_meta_mode).toBe("non_domain");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps VAT payable forecast query in address lane", () => {
|
it("keeps VAT payable forecast query in address lane", () => {
|
||||||
const decision = resolveAssistantOrchestrationDecision({
|
const decision = resolveAssistantOrchestrationDecision({
|
||||||
rawUserMessage: "какой прогноз оплаты ндс за 12 мая 2020",
|
rawUserMessage: "какой прогноз оплаты ндс за 12 мая 2020",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue