Open-World: усилить аналитический синтез бизнес-обзора

This commit is contained in:
2026-05-04 08:59:33 +03:00
parent 9b26bd05ef
commit 822baedcba
6 changed files with 275 additions and 36 deletions
@@ -822,6 +822,20 @@ function businessOverviewNetDirectionRu(direction) {
}
return "входящий и исходящий денежный поток в проверенном срезе примерно сбалансированы";
}
function amountHumanRu(value) {
const rounded = Math.round(Math.abs(value) * 100) / 100;
return `${new Intl.NumberFormat("ru-RU", { maximumFractionDigits: 2 }).format(rounded)} руб.`;
}
function percentOfTotal(part, total) {
if (!Number.isFinite(part) || !Number.isFinite(total) || total <= 0) {
return null;
}
return Math.round((part / total) * 10_000) / 100;
}
function percentText(part, total) {
const pct = percentOfTotal(part, total);
return pct === null ? null : `${pct}%`;
}
function derivedBusinessOverviewConfirmedLines(pilot) {
const overview = pilot.derived_business_overview;
if (!overview) {
@@ -888,19 +902,94 @@ function derivedBusinessOverviewConfirmedLines(pilot) {
}
return lines;
}
function derivedBusinessOverviewInferenceLine(pilot) {
function businessOverviewCashSynthesisLine(overview) {
const incoming = overview.incoming_customer_revenue;
const outgoing = overview.outgoing_supplier_payout;
if (incoming.rows_with_amount <= 0 && outgoing.rows_with_amount <= 0) {
return null;
}
const checkedOperationalScale = Math.abs(incoming.total_amount) + Math.abs(outgoing.total_amount);
return [
`Аналитический вывод по оборотам: проверенный операционный размах ${amountHumanRu(checkedOperationalScale)}; входящий поток ${incoming.total_amount_human_ru}, исходящий ${outgoing.total_amount_human_ru}.`,
`${businessOverviewNetDirectionRu(overview.net_direction)}; расчетное нетто ${overview.net_amount_human_ru}.`
].join(" ");
}
function businessOverviewCustomerConcentrationLine(overview) {
const leader = overview.top_customers[0];
if (!leader || overview.incoming_customer_revenue.total_amount <= 0) {
return null;
}
const share = percentText(leader.total_amount, overview.incoming_customer_revenue.total_amount);
return share
? `Концентрация входящего потока: крупнейший подтвержденный клиент ${leader.axis_value} дает около ${share} проверенных входящих поступлений (${leader.total_amount_human_ru}). Это сигнал зависимости от клиента, а не полный customer-risk аудит.`
: `Крупнейший подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`;
}
function businessOverviewRiskSynthesisLine(overview) {
const signals = [];
if (overview.tax_position) {
const taxDirection = overview.tax_position.net_vat_direction === "vat_to_pay"
? `НДС к уплате ${overview.tax_position.net_vat_amount_human_ru}`
: overview.tax_position.net_vat_direction === "vat_to_recover_or_offset"
? `НДС к вычету/зачету ${overview.tax_position.net_vat_amount_human_ru}`
: "НДС-позиция сбалансирована";
signals.push(taxDirection);
}
if (overview.debt_position) {
const debtDirection = overview.debt_position.net_debt_position_direction === "net_receivable"
? `дебиторка больше кредиторки на ${overview.debt_position.net_debt_position_amount_human_ru}`
: overview.debt_position.net_debt_position_direction === "net_payable"
? `кредиторка больше дебиторки на ${overview.debt_position.net_debt_position_amount_human_ru}`
: "дебиторка и кредиторка сбалансированы";
signals.push(debtDirection);
}
if (overview.debt_open_settlement_quality?.concentration_top_contract_pct !== null && overview.debt_open_settlement_quality?.top_contracts[0]) {
const topContract = overview.debt_open_settlement_quality.top_contracts[0];
signals.push(`крупнейший открытый договор держит ${overview.debt_open_settlement_quality.concentration_top_contract_pct}% открытых остатков (${topContract.total_amount_human_ru})`);
}
if (overview.debt_open_settlement_quality?.age_signal?.max_age_days !== null && overview.debt_open_settlement_quality?.age_signal?.max_age_days !== undefined) {
signals.push(`самый старый договорный возрастной сигнал ${overview.debt_open_settlement_quality.age_signal.max_age_days} дн.`);
}
if (overview.inventory_position) {
signals.push(`складской остаток на дату ${overview.inventory_position.total_amount_human_ru}`);
if (overview.inventory_position.aging_signal?.max_age_days !== null && overview.inventory_position.aging_signal?.max_age_days !== undefined) {
signals.push(`самый старый складской purchase-date сигнал ${overview.inventory_position.aging_signal.max_age_days} дн.`);
}
}
return signals.length > 0
? `Риски и контуры внимания по подтвержденным данным: ${signals.join("; ")}.`
: null;
}
function businessOverviewExecutiveVerdictLine(overview) {
const hasCash = overview.incoming_customer_revenue.rows_with_amount > 0 || overview.outgoing_supplier_payout.rows_with_amount > 0;
const hasExtraSignals = Boolean(overview.tax_position ||
overview.debt_position ||
overview.debt_open_settlement_quality ||
overview.inventory_position);
if (!hasCash && !hasExtraSignals) {
return null;
}
const cashTone = overview.net_direction === "net_incoming"
? "операционно входящий поток сильнее исходящего"
: overview.net_direction === "net_outgoing"
? "операционно исходящий поток сильнее входящего, это зона внимания к расходам/закупкам"
: "операционный поток выглядит сбалансированным";
const evidenceTone = hasExtraSignals
? "часть налоговых, долговых или складских контуров уже отдельно проверена"
: "налоги, долги и склад еще не дают проверенного управленческого контекста";
return `Сводный LLM-аудит по подтвержденному: ${cashTone}; ${evidenceTone}. Это полезный управленческий срез по найденным строкам 1С, но не финальный вывод о прибыльности, марже или здоровье компании.`;
}
function derivedBusinessOverviewInferenceLines(pilot) {
const overview = pilot.derived_business_overview;
if (!overview) {
return null;
}
if (overview.incoming_customer_revenue.rows_with_amount <= 0 &&
overview.outgoing_supplier_payout.rows_with_amount <= 0) {
return null;
return [];
}
return [
`Расчетное нетто по найденным строкам: ${overview.net_amount_human_ru}; ${businessOverviewNetDirectionRu(overview.net_direction)}.`,
"Это нормальный операционный сигнал, но не прибыль и не маржа: для управленческого вывода нужны отдельные расходы, себестоимость, долги, налоги и склад."
].join(" ");
businessOverviewCashSynthesisLine(overview),
businessOverviewCustomerConcentrationLine(overview),
businessOverviewRiskSynthesisLine(overview),
businessOverviewExecutiveVerdictLine(overview),
"Это аналитическая интерпретация подтвержденных строк, а не прибыль и не маржа: для финального управленческого вывода нужны отдельные расходы, себестоимость, закрывающие документы, долги, налоги и складская оборачиваемость."
].filter((line) => Boolean(line));
}
function businessOverviewUnknownLines(pilot) {
if (!pilot.derived_business_overview) {
@@ -915,17 +1004,22 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
if (pilot.evidence.unknown_facts.length > 0) {
pushReason(reasonCodes, "answer_contains_unknown_fact_boundary");
}
if (pilot.evidence.inferred_facts.length > 0) {
pushReason(reasonCodes, "answer_contains_bounded_inference");
}
const derivedInferenceLine = derivedBusinessOverviewInferenceLine(pilot) ??
derivedActivityInferenceLine(pilot) ??
const businessOverviewInferenceLines = derivedBusinessOverviewInferenceLines(pilot);
const derivedInferenceLine = derivedActivityInferenceLine(pilot) ??
derivedMetadataInferenceLine(pilot) ??
derivedRankedValueFlowInferenceLine(pilot) ??
derivedEntityResolutionInferenceLine(pilot);
const inferenceLines = derivedInferenceLine
? [derivedInferenceLine]
: pilot.evidence.inferred_facts;
const inferenceLines = businessOverviewInferenceLines.length > 0
? businessOverviewInferenceLines
: derivedInferenceLine
? [derivedInferenceLine]
: pilot.evidence.inferred_facts;
if (inferenceLines.length > 0) {
pushReason(reasonCodes, "answer_contains_bounded_inference");
}
if (businessOverviewInferenceLines.length > 0) {
pushReason(reasonCodes, "answer_contains_business_overview_analyst_synthesis");
}
const derivedMetadataLine = derivedMetadataConfirmedLine(pilot);
const derivedEntityResolutionLine = derivedEntityResolutionConfirmedLine(pilot);
const derivedValueLine = derivedBidirectionalValueFlowConfirmedLine(pilot) ??