Очистить бизнес-язык ответов phase98

This commit is contained in:
2026-05-13 02:26:08 +03:00
parent 0028ef24a0
commit b1a5e4435d
11 changed files with 673 additions and 113 deletions
@@ -120,7 +120,7 @@ function userFacingLines(values: string[]): string[] {
}
function sanitizeUserFacingMechanics(value: string): string {
return String(value ?? "").replace(/MCP-срез(?:ом|у|е|а)?/giu, (match) => {
let text = String(value ?? "").replace(/MCP-срез(?:ом|у|е|а)?/giu, (match) => {
const normalized = match.toLowerCase();
if (normalized.endsWith("ом")) {
return "срезом 1С";
@@ -136,6 +136,28 @@ function sanitizeUserFacingMechanics(value: string): string {
}
return "срез 1С";
});
const replacements: Array<[RegExp, string]> = [
[/\bprocurement-concentration route\b/giu, "проверка концентрации закупок/исходящих платежей"],
[/\breviewed vendor-risk route\b/giu, "отдельная проверка поставщицкого риска"],
[/\bvendor-risk route\b/giu, "проверка поставщицкого риска"],
[/\bdue-date route\b/giu, "проверка просрочки по срокам оплаты"],
[/\bdebt-quality proxy\b/giu, "ограниченный долговой сигнал"],
[/\bstaleness-risk proxy\b/giu, "косвенный признак залежалости"],
[/\bstaleness risk proxy\b/giu, "косвенный признак залежалости"],
[/\boperating-flow proxy\b/giu, "денежный операционный показатель"],
[/\btrading-margin proxy\b/giu, "товарная маржинальность по проверенным документам"],
[/\bprocurement concentration proxy\b/giu, "сигнал концентрации закупок/исходящих платежей"],
[/\boutgoing cash concentration proxy\b/giu, "сигнал концентрации исходящих денег"],
[/\bproxy-сигналы\b/giu, "косвенные признаки"],
[/\bproxy\b/giu, "косвенный показатель"],
[/\bsales-to-stock\b/giu, "отношение продаж к остатку"],
[/\boverdue\/due-date aging\b/giu, "просрочку по договорным срокам"],
[/\bP&L\b/gu, "полный отчет о прибылях и убытках"]
];
for (const [pattern, replacement] of replacements) {
text = text.replace(pattern, replacement);
}
return text;
}
function localizeLine(value: string): string {
@@ -454,6 +476,11 @@ function businessOverviewCoverageLimitLine(overview: Record<string, unknown>): s
: null;
}
function joinBusinessReplyLines(lines: string[]): string | null {
const reply = userFacingLines(lines.map(localizeLine)).join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
}
function businessOverviewYearRowsLine(overview: Record<string, unknown>): string | null {
const years = Array.isArray(overview.yearly_breakdown) ? overview.yearly_breakdown : [];
const values = years
@@ -670,8 +697,7 @@ function buildCompactBidirectionalValueFlowReply(
lines.push(`Следующий шаг: ${localizeLine(fallbackNextStep)}`);
}
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
return joinBusinessReplyLines(lines);
}
function compactComparable(value: string | null): string {
@@ -883,8 +909,7 @@ function buildCompactBusinessOverviewReply(
lines.push(
"Это учетный финрезультат по найденным строкам закрытия периода в 1С, а не внешний аудит и не юридически подтвержденная отчетность."
);
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
return joinBusinessReplyLines(lines);
}
const headline = toNonEmptyString(draft.headline);
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
@@ -909,8 +934,7 @@ function buildCompactBusinessOverviewReply(
if (limitLine) {
lines.push(limitLine);
}
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
return joinBusinessReplyLines(lines);
}
if (debtDueDateBoundary) {
@@ -928,14 +952,15 @@ function buildCompactBusinessOverviewReply(
typeof dueDateAging.rows_with_amount === "number" && Number.isFinite(dueDateAging.rows_with_amount)
? dueDateAging.rows_with_amount
: null;
const dueDateScopePrefix = organizationScope ? `по компании ${organizationScope} ` : "";
if (status === "confirmed_overdue") {
lines.push(
`Коротко: на ${asOfDate} подтвержденная просрочка есть: ${overdueAmount ?? "сумма не распознана"} по ${dueDateAging.overdue_rows ?? "найденным"} строкам.`
`Коротко: ${dueDateScopePrefix}на ${asOfDate} подтвержденная просрочка есть: ${overdueAmount ?? "сумма не распознана"} по ${dueDateAging.overdue_rows ?? "найденным"} строкам.`
);
lines.push("Основа ответа: открытые расчеты 60/62/76, договорный срок оплаты и дата расчетного документа; это уже due-date route, не старение договора как proxy.");
lines.push("Основа ответа: открытые расчеты 60/62/76, договорный срок оплаты и дата расчетного документа; это проверка просрочки по срокам оплаты, а не просто возраст договора.");
} else if (status === "no_payment_terms_configured") {
lines.push(
`Коротко: на ${asOfDate} подтвержденной просрочки нет: открытые расчеты проверены${grossAmount ? ` на ${grossAmount}` : ""}, но в найденных договорах срок оплаты не установлен.`
`Коротко: ${dueDateScopePrefix}на ${asOfDate} подтвержденной просрочки нет: открытые расчеты проверены${grossAmount ? ` на ${grossAmount}` : ""}, но в найденных договорах срок оплаты не установлен.`
);
lines.push(
rowsWithAmount !== null
@@ -944,31 +969,29 @@ function buildCompactBusinessOverviewReply(
);
} else if (status === "insufficient_due_date_basis") {
lines.push(
`Коротко: due-date route запущен на ${asOfDate}, но просрочка не подтверждена: по строкам с установленным сроком оплаты не хватило даты расчетного документа.`
`Коротко: ${dueDateScopePrefix}на ${asOfDate} просрочка не подтверждена: по строкам с установленным сроком оплаты не хватило даты расчетного документа.`
);
if (rowsWithPaymentTerms !== null) {
lines.push(`Строк с установленным сроком оплаты: ${rowsWithPaymentTerms}; нужен документ-основание с датой для расчета due date.`);
lines.push(`Строк с установленным сроком оплаты: ${rowsWithPaymentTerms}; нужен документ-основание с датой, чтобы посчитать договорный срок оплаты.`);
}
} else {
lines.push(
`Коротко: due-date route на ${asOfDate} проверен, подтвержденной просрочки не найдено${rowsWithPaymentTerms !== null ? `; строк с установленным сроком оплаты ${rowsWithPaymentTerms}` : ""}.`
`Коротко: ${dueDateScopePrefix}на ${asOfDate} проверка просрочки по срокам оплаты выполнена, подтвержденной просрочки не найдено${rowsWithPaymentTerms !== null ? `; строк с установленным сроком оплаты ${rowsWithPaymentTerms}` : ""}.`
);
}
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
return joinBusinessReplyLines(lines);
}
const headline = toNonEmptyString(draft.headline);
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
lines.push(
cleanHeadline
? `Коротко: ${localizeLine(cleanHeadline)}`
: "Коротко: нельзя точно определить, какая дебиторка просрочена, по текущему срезу 1С; есть только debt-quality proxy, но нет проверенного due-date маршрута."
: "Коротко: нельзя точно определить, какая дебиторка просрочена, по текущему срезу 1С; есть только ограниченный долговой сигнал, но нет проверки договорных сроков оплаты."
);
lines.push(
"Проверить нужно отдельно: договоры, сроки оплаты, погашение и закрытие задолженности; без этого нельзя доказать overdue/due-date aging."
"Проверить нужно отдельно: договоры, сроки оплаты, погашение и закрытие задолженности; без этого нельзя доказать просрочку по договорным срокам."
);
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
return joinBusinessReplyLines(lines);
}
if (vendorRiskBoundary) {
@@ -996,7 +1019,7 @@ function buildCompactBusinessOverviewReply(
const totalText = totalOutgoing ? `; всего исходящих платежей в проверенном срезе ${totalOutgoing}` : "";
if (status === "financial_institution_leads_outgoing_cash") {
lines.push(
`Коротко: проверенный procurement-concentration route за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`
`Коротко: проверка концентрации закупок/исходящих платежей за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`
);
const financialHintText = financialFlowHintTextRuFromRecord(topOutgoingRecord);
if (financialHintText) {
@@ -1009,11 +1032,11 @@ function buildCompactBusinessOverviewReply(
}
} else if (status === "reviewed_procurement_concentration") {
lines.push(
`Коротко: проверенный procurement-concentration route за ${periodScope} нашел основную зависимость исходящего потока: ${topOutgoingName ?? nonFinancialName ?? "получатель не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : nonFinancialShare ? ` держит около ${nonFinancialShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}${totalText}.`
`Коротко: точный риск зависимости от одного поставщика не подтвержден полностью; проверка концентрации закупок/исходящих платежей за ${periodScope} нашла крупнейшего получателя исходящего потока: ${topOutgoingName ?? nonFinancialName ?? "получатель не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : nonFinancialShare ? ` держит около ${nonFinancialShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}${totalText}.`
);
} else {
lines.push(
`Коротко: procurement-concentration route за ${periodScope} отработал, но надежной небанковской концентрации поставщика по найденным исходящим платежам не хватает${totalText}.`
`Коротко: проверка концентрации закупок/исходящих платежей за ${periodScope} выполнена, но надежной небанковской концентрации поставщика по найденным исходящим платежам не хватает${totalText}.`
);
}
const contractText =
@@ -1023,10 +1046,9 @@ function buildCompactBusinessOverviewReply(
: ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts} договоров.`
: "";
lines.push(
`Что не доказано этим маршрутом: надежность поставщика, качество поставок, договорные условия, назначение каждого платежа и полная структура всех расходов.${contractText}`
`Что не доказано этим срезом: надежность поставщика, качество поставок, договорные условия, назначение каждого платежа и полная структура всех расходов.${contractText}`
);
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
return joinBusinessReplyLines(lines);
}
const supplierBasis = topSupplier
? topSupplierLooksFinancial
@@ -1035,7 +1057,9 @@ function buildCompactBusinessOverviewReply(
: outgoingAmount
? `исходящие платежи/закупочный поток в проверенном срезе: ${outgoingAmount}`
: "есть только ограниченный срез исходящих платежей без полного vendor-risk профиля";
const proxyLabel = topSupplierLooksFinancial ? "outgoing cash concentration proxy" : "procurement concentration proxy";
const proxyLabel = topSupplierLooksFinancial
? "сигнал концентрации исходящих денег"
: "сигнал концентрации закупок/исходящих платежей";
lines.push(
`Коротко: точный риск зависимости от одного поставщика по текущим данным не подтвержден; есть только ${proxyLabel}: ${supplierBasis}.`
);
@@ -1043,27 +1067,23 @@ function buildCompactBusinessOverviewReply(
"Это сигнал концентрации закупок/исходящих платежей, а не полный аудит надежности поставщиков, условий, качества и структуры всех расходов."
);
lines.push(
"Для точного вывода нужен отдельный reviewed vendor-risk route: поставщики, договорные условия, качество поставок, сроки, доля в закупках и полная структура расходов."
"Для точного вывода нужна отдельная проверка поставщицкого риска: поставщики, договорные условия, качество поставок, сроки, доля в закупках и полная структура расходов."
);
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
return joinBusinessReplyLines(lines);
}
if (inventoryReserveBoundary) {
const headline = toNonEmptyString(draft.headline);
const inventoryQualityEvents = toRecordObject(overview.inventory_quality_events);
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
const reserveBasis = cleanHeadline ? localizeLine(cleanHeadline).replace(/^проверил/iu, "Проверены") : null;
lines.push(
cleanHeadline
? `Коротко: ${localizeLine(cleanHeadline)}`
reserveBasis
? `Коротко: точно подтвердить резерв под неликвиды нельзя. ${reserveBasis}`
: "Коротко: точно подтвердить резерв под неликвиды по текущим данным нельзя."
);
if (inventoryQualityEvents) {
if (limitLine) {
lines.push(limitLine);
}
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
return joinBusinessReplyLines(lines);
}
const boundaryLines = userFacingLines([
...toStringList(draft.unknown_lines),
@@ -1075,10 +1095,9 @@ function buildCompactBusinessOverviewReply(
lines.push(...boundaryLines.map(localizeLine));
}
lines.push(
"Проверить нужно отдельно: складской срез на дату, учетную политику резервов, списания и ликвидационную стоимость; proxy-сигналы нельзя выдавать за доказанный факт резерва."
"Проверить нужно отдельно: складской срез на дату, учетную политику резервов, списания и ликвидационную стоимость; косвенные признаки нельзя выдавать за доказанный факт резерва."
);
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
return joinBusinessReplyLines(lines);
}
if (crossScopeExecutiveSummary && separateSubject && previousCounterpartySummary && (incomingAmount || outgoingAmount || netAmount)) {
@@ -1095,8 +1114,7 @@ function buildCompactBusinessOverviewReply(
if (limitLine) {
lines.push(limitLine);
}
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
return joinBusinessReplyLines(lines);
}
if (rankingNeed) {
@@ -1205,8 +1223,7 @@ function buildCompactBusinessOverviewReply(
lines.push(limitLine);
}
lines.push("Для ответа именно про чистую прибыль нужно отдельно считать себестоимость, расходы и закрытие периода.");
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
return joinBusinessReplyLines(lines);
}
function statusFrom(entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null): AssistantMcpDiscoveryResponseCandidateStatus {