Open-World: добавить годовую динамику в бизнес-обзор
This commit is contained in:
@@ -492,6 +492,9 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
||||
) {
|
||||
families.push("денежный поток");
|
||||
}
|
||||
if (overview.yearly_breakdown?.length) {
|
||||
families.push("годовая operating-flow динамика");
|
||||
}
|
||||
if (overview.activity_period) {
|
||||
families.push("активность");
|
||||
}
|
||||
@@ -724,6 +727,7 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract):
|
||||
}
|
||||
if (isBusinessOverviewPilot(pilot)) {
|
||||
claims.push("Do not present business overview cash-flow spread as profit or margin.");
|
||||
claims.push("Do not present business overview yearly operating-flow breakdown as profit, financial result, or a complete annual P&L.");
|
||||
claims.push("Do not present business overview trading-margin proxy as clean profit, accounting financial result, or exact cost-of-sales margin.");
|
||||
claims.push("Do not present business overview supplier concentration as vendor-risk audit, procurement quality, or full expense structure.");
|
||||
claims.push("Do not claim debt quality, VAT position, inventory health, or company health unless those contours were separately checked.");
|
||||
@@ -1043,6 +1047,20 @@ function amountHumanRu(value: number): string {
|
||||
return `${new Intl.NumberFormat("ru-RU", { maximumFractionDigits: 2 }).format(rounded)} руб.`;
|
||||
}
|
||||
|
||||
function yearCountHumanRu(count: number): string {
|
||||
const abs = Math.abs(count) % 100;
|
||||
const last = abs % 10;
|
||||
const noun =
|
||||
abs >= 11 && abs <= 14
|
||||
? "лет"
|
||||
: last === 1
|
||||
? "год"
|
||||
: last >= 2 && last <= 4
|
||||
? "года"
|
||||
: "лет";
|
||||
return `${count} ${noun}`;
|
||||
}
|
||||
|
||||
function percentOfTotal(part: number, total: number): number | null {
|
||||
if (!Number.isFinite(part) || !Number.isFinite(total) || total <= 0) {
|
||||
return null;
|
||||
@@ -1113,6 +1131,11 @@ function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilot
|
||||
`Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value} — ${supplierLeader.total_amount_human_ru}.`
|
||||
);
|
||||
}
|
||||
if (overview.yearly_breakdown?.length) {
|
||||
lines.push(
|
||||
`Годовая раскладка операционного денежного потока построена по подтвержденным строкам 1С за ${yearCountHumanRu(overview.yearly_breakdown.length)}.`
|
||||
);
|
||||
}
|
||||
if (overview.activity_period) {
|
||||
lines.push(
|
||||
`Окно подтвержденной активности в 1С: ${overview.activity_period.first_activity_date} — ${overview.activity_period.latest_activity_date}; ориентировочно ${overview.activity_period.duration_human_ru}.`
|
||||
@@ -1245,6 +1268,33 @@ function businessOverviewSupplierConcentrationLine(overview: BusinessOverview):
|
||||
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`;
|
||||
}
|
||||
|
||||
function businessOverviewYearlyOperatingLine(overview: BusinessOverview): string | null {
|
||||
const years = overview.yearly_breakdown ?? [];
|
||||
if (years.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const strongestIncomingYear = [...years]
|
||||
.filter((bucket) => bucket.incoming_total_amount > 0)
|
||||
.sort((left, right) => right.incoming_total_amount - left.incoming_total_amount || left.year_bucket.localeCompare(right.year_bucket))[0];
|
||||
const strongestNetYear = [...years]
|
||||
.filter((bucket) => bucket.net_amount !== 0)
|
||||
.sort((left, right) => right.net_amount - left.net_amount || left.year_bucket.localeCompare(right.year_bucket))[0];
|
||||
if (!strongestIncomingYear && !strongestNetYear) {
|
||||
return null;
|
||||
}
|
||||
const parts: string[] = [];
|
||||
if (strongestIncomingYear) {
|
||||
parts.push(`самый сильный год по подтвержденным входящим поступлениям ${strongestIncomingYear.year_bucket}: ${strongestIncomingYear.incoming_total_amount_human_ru}`);
|
||||
}
|
||||
if (strongestNetYear) {
|
||||
const netText = strongestNetYear.net_direction === "net_outgoing"
|
||||
? `нетто исходящее ${strongestNetYear.net_amount_human_ru}`
|
||||
: `нетто в плюс ${strongestNetYear.net_amount_human_ru}`;
|
||||
parts.push(`лучший год по расчетному операционному нетто ${strongestNetYear.year_bucket}: ${netText}`);
|
||||
}
|
||||
return `Годовая динамика по проверенным строкам: ${parts.join("; ")}. Это operating-flow proxy, не бухгалтерская прибыль и не финрезультат.`;
|
||||
}
|
||||
|
||||
function businessOverviewRiskSynthesisLine(overview: BusinessOverview): string | null {
|
||||
const signals: string[] = [];
|
||||
if (overview.tax_position) {
|
||||
@@ -1341,6 +1391,7 @@ function derivedBusinessOverviewInferenceLines(pilot: AssistantMcpDiscoveryPilot
|
||||
businessOverviewCashSynthesisLine(overview),
|
||||
businessOverviewCustomerConcentrationLine(overview),
|
||||
businessOverviewSupplierConcentrationLine(overview),
|
||||
businessOverviewYearlyOperatingLine(overview),
|
||||
businessOverviewRiskSynthesisLine(overview),
|
||||
businessOverviewExecutiveVerdictLine(overview),
|
||||
"Это аналитическая интерпретация подтвержденных строк, а не прибыль и не маржа: для финального управленческого вывода нужны отдельные расходы, себестоимость, закрывающие документы, долги, налоги и складская оборачиваемость."
|
||||
@@ -1406,6 +1457,9 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
|
||||
if (pilot.derived_business_overview?.top_suppliers?.length) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_supplier_concentration");
|
||||
}
|
||||
if (pilot.derived_business_overview?.yearly_breakdown?.length) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_yearly_operating_breakdown");
|
||||
}
|
||||
if (pilot.derived_business_overview?.debt_position) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_debt_position");
|
||||
}
|
||||
|
||||
@@ -125,6 +125,19 @@ export interface AssistantMcpDiscoveryBidirectionalValueFlowMonthBucket {
|
||||
net_direction: AssistantMcpDiscoveryNetDirection;
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryBusinessOverviewYearBucket {
|
||||
year_bucket: string;
|
||||
incoming_total_amount: number;
|
||||
incoming_total_amount_human_ru: string;
|
||||
incoming_rows_with_amount: number;
|
||||
outgoing_total_amount: number;
|
||||
outgoing_total_amount_human_ru: string;
|
||||
outgoing_rows_with_amount: number;
|
||||
net_amount: number;
|
||||
net_amount_human_ru: string;
|
||||
net_direction: AssistantMcpDiscoveryNetDirection;
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryDerivedBidirectionalValueFlow {
|
||||
counterparty: string | null;
|
||||
period_scope: string | null;
|
||||
@@ -151,6 +164,7 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverview {
|
||||
net_direction: AssistantMcpDiscoveryNetDirection;
|
||||
top_customers: AssistantMcpDiscoveryRankedValueFlowBucket[];
|
||||
top_suppliers: AssistantMcpDiscoveryRankedValueFlowBucket[];
|
||||
yearly_breakdown: AssistantMcpDiscoveryBusinessOverviewYearBucket[];
|
||||
activity_period: AssistantMcpDiscoveryDerivedActivityPeriod | null;
|
||||
tax_position: AssistantMcpDiscoveryDerivedBusinessOverviewTaxPosition | null;
|
||||
trading_margin_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewTradingMarginProxy | null;
|
||||
@@ -2352,6 +2366,11 @@ function monthBucketFromIsoDate(isoDate: string | null): string | null {
|
||||
return match ? `${match[1]}-${match[2]}` : null;
|
||||
}
|
||||
|
||||
function yearBucketFromIsoDate(isoDate: string | null): string | null {
|
||||
const match = isoDate?.match(/^(\d{4})-\d{2}-\d{2}$/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
function netDirectionFromAmount(amount: number): AssistantMcpDiscoveryNetDirection {
|
||||
if (amount > 0) {
|
||||
return "net_incoming";
|
||||
@@ -2427,6 +2446,20 @@ function formatAmountHumanRu(amount: number): string {
|
||||
return `${formatted} руб.`;
|
||||
}
|
||||
|
||||
function yearCountHumanRu(count: number): string {
|
||||
const abs = Math.abs(count) % 100;
|
||||
const last = abs % 10;
|
||||
const noun =
|
||||
abs >= 11 && abs <= 14
|
||||
? "лет"
|
||||
: last === 1
|
||||
? "год"
|
||||
: last >= 2 && last <= 4
|
||||
? "года"
|
||||
: "лет";
|
||||
return `${count} ${noun}`;
|
||||
}
|
||||
|
||||
function deriveValueFlowMonthBreakdown(
|
||||
result: AssistantMcpDiscoveryCoverageAwareQueryResult | null,
|
||||
aggregationAxis: AssistantMcpDiscoveryAggregationAxis | null
|
||||
@@ -2502,6 +2535,74 @@ function deriveBidirectionalValueFlowMonthBreakdown(input: {
|
||||
});
|
||||
}
|
||||
|
||||
function deriveBusinessOverviewSideYearBreakdown(
|
||||
result: AssistantMcpDiscoveryCoverageAwareQueryResult | null
|
||||
): Array<{ year_bucket: string; rows_with_amount: number; total_amount: number; total_amount_human_ru: string }> {
|
||||
if (!result || result.error) {
|
||||
return [];
|
||||
}
|
||||
const buckets = new Map<string, { rows_with_amount: number; total_amount: number }>();
|
||||
for (const row of result.rows) {
|
||||
const yearBucket = yearBucketFromIsoDate(rowDateValue(row));
|
||||
const amount = rowAmountValue(row);
|
||||
if (!yearBucket || amount === null) {
|
||||
continue;
|
||||
}
|
||||
const current = buckets.get(yearBucket) ?? { rows_with_amount: 0, total_amount: 0 };
|
||||
current.rows_with_amount += 1;
|
||||
current.total_amount += amount;
|
||||
buckets.set(yearBucket, current);
|
||||
}
|
||||
return Array.from(buckets.entries())
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([yearBucket, bucket]) => ({
|
||||
year_bucket: yearBucket,
|
||||
rows_with_amount: bucket.rows_with_amount,
|
||||
total_amount: bucket.total_amount,
|
||||
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount)
|
||||
}));
|
||||
}
|
||||
|
||||
function deriveBusinessOverviewYearlyBreakdown(input: {
|
||||
incomingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null;
|
||||
outgoingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null;
|
||||
}): AssistantMcpDiscoveryBusinessOverviewYearBucket[] {
|
||||
const incomingBuckets = deriveBusinessOverviewSideYearBreakdown(input.incomingResult);
|
||||
const outgoingBuckets = deriveBusinessOverviewSideYearBreakdown(input.outgoingResult);
|
||||
const allYearBuckets = new Set<string>();
|
||||
for (const bucket of incomingBuckets) {
|
||||
allYearBuckets.add(bucket.year_bucket);
|
||||
}
|
||||
for (const bucket of outgoingBuckets) {
|
||||
allYearBuckets.add(bucket.year_bucket);
|
||||
}
|
||||
|
||||
const incomingByYear = new Map(incomingBuckets.map((bucket) => [bucket.year_bucket, bucket]));
|
||||
const outgoingByYear = new Map(outgoingBuckets.map((bucket) => [bucket.year_bucket, bucket]));
|
||||
|
||||
return Array.from(allYearBuckets)
|
||||
.sort((left, right) => left.localeCompare(right))
|
||||
.map((yearBucket) => {
|
||||
const incoming = incomingByYear.get(yearBucket);
|
||||
const outgoing = outgoingByYear.get(yearBucket);
|
||||
const incomingAmount = incoming?.total_amount ?? 0;
|
||||
const outgoingAmount = outgoing?.total_amount ?? 0;
|
||||
const netAmount = incomingAmount - outgoingAmount;
|
||||
return {
|
||||
year_bucket: yearBucket,
|
||||
incoming_total_amount: incomingAmount,
|
||||
incoming_total_amount_human_ru: formatAmountHumanRu(incomingAmount),
|
||||
incoming_rows_with_amount: incoming?.rows_with_amount ?? 0,
|
||||
outgoing_total_amount: outgoingAmount,
|
||||
outgoing_total_amount_human_ru: formatAmountHumanRu(outgoingAmount),
|
||||
outgoing_rows_with_amount: outgoing?.rows_with_amount ?? 0,
|
||||
net_amount: netAmount,
|
||||
net_amount_human_ru: formatAmountHumanRu(Math.abs(netAmount)),
|
||||
net_direction: netDirectionFromAmount(netAmount)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function deriveValueFlow(
|
||||
result: AssistantMcpDiscoveryCoverageAwareQueryResult | null,
|
||||
counterparty: string | null,
|
||||
@@ -3441,6 +3542,10 @@ function deriveBusinessOverview(input: {
|
||||
direction: "outgoing_supplier_payout",
|
||||
rankingNeed: "top_desc"
|
||||
});
|
||||
const yearlyBreakdown = deriveBusinessOverviewYearlyBreakdown({
|
||||
incomingResult: input.incomingResult,
|
||||
outgoingResult: input.outgoingResult
|
||||
});
|
||||
const activityPeriod = deriveActivityPeriod(input.lifecycleResult);
|
||||
const taxPosition = deriveBusinessOverviewTaxPosition(input.taxResult, input.periodScope);
|
||||
const tradingMarginProxy = deriveBusinessOverviewTradingMarginProxy(input.tradingMarginResult, input.periodScope);
|
||||
@@ -3495,6 +3600,7 @@ function deriveBusinessOverview(input: {
|
||||
net_direction: netDirectionFromAmount(netAmount),
|
||||
top_customers: rankedIncoming?.ranked_values ?? [],
|
||||
top_suppliers: rankedOutgoing?.ranked_values ?? [],
|
||||
yearly_breakdown: yearlyBreakdown,
|
||||
activity_period: activityPeriod,
|
||||
tax_position: taxPosition,
|
||||
trading_margin_proxy: tradingMarginProxy,
|
||||
@@ -3611,6 +3717,11 @@ function buildBusinessOverviewConfirmedFacts(derived: AssistantMcpDiscoveryDeriv
|
||||
`Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`
|
||||
);
|
||||
}
|
||||
if (derived.yearly_breakdown.length > 0) {
|
||||
facts.push(
|
||||
`Годовая раскладка операционного денежного потока построена по подтвержденным строкам 1С за ${yearCountHumanRu(derived.yearly_breakdown.length)}.`
|
||||
);
|
||||
}
|
||||
if (derived.activity_period) {
|
||||
facts.push(
|
||||
`Подтвержденное окно активности в 1С: ${derived.activity_period.first_activity_date} — ${derived.activity_period.latest_activity_date}.`
|
||||
@@ -3731,6 +3842,12 @@ function buildBusinessOverviewInferredFacts(derived: AssistantMcpDiscoveryDerive
|
||||
const supplierSharePct = supplierLeader
|
||||
? percentageOfTotal(supplierLeader.total_amount, derived.outgoing_supplier_payout.total_amount)
|
||||
: null;
|
||||
const strongestIncomingYear = [...derived.yearly_breakdown]
|
||||
.filter((bucket) => bucket.incoming_total_amount > 0)
|
||||
.sort((left, right) => right.incoming_total_amount - left.incoming_total_amount || left.year_bucket.localeCompare(right.year_bucket))[0];
|
||||
const strongestNetYear = [...derived.yearly_breakdown]
|
||||
.filter((bucket) => bucket.net_amount !== 0)
|
||||
.sort((left, right) => right.net_amount - left.net_amount || left.year_bucket.localeCompare(right.year_bucket))[0];
|
||||
return [
|
||||
`Расчетное нетто по найденным строкам: ${derived.net_amount_human_ru}; ${direction}.`,
|
||||
supplierLeader
|
||||
@@ -3738,6 +3855,12 @@ function buildBusinessOverviewInferredFacts(derived: AssistantMcpDiscoveryDerive
|
||||
? `Крупнейший подтвержденный поставщик/получатель исходящих платежей ${supplierLeader.axis_value} держит около ${supplierSharePct}% проверенного исходящего потока (${supplierLeader.total_amount_human_ru}). Это procurement concentration proxy по найденным строкам, а не полный vendor-risk аудит.`
|
||||
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value} — ${supplierLeader.total_amount_human_ru}.`
|
||||
: null,
|
||||
strongestIncomingYear
|
||||
? `Самый сильный год по подтвержденным входящим поступлениям: ${strongestIncomingYear.year_bucket} (${strongestIncomingYear.incoming_total_amount_human_ru}).`
|
||||
: null,
|
||||
strongestNetYear
|
||||
? `Лучший год по расчетному операционному нетто найденных строк: ${strongestNetYear.year_bucket} (${netDirectionFromAmount(strongestNetYear.net_amount) === "net_outgoing" ? "нетто исходящее" : "нетто в плюс"} ${strongestNetYear.net_amount_human_ru}). Это не бухгалтерская прибыль.`
|
||||
: null,
|
||||
"Это операционный денежный сигнал по найденным строкам 1С, а не прибыль, маржа или бухгалтерское заключение о здоровье бизнеса."
|
||||
].filter((fact): fact is string => Boolean(fact));
|
||||
}
|
||||
@@ -4839,6 +4962,9 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||
if (derivedBusinessOverview.top_suppliers.length > 0) {
|
||||
pushReason(reasonCodes, "pilot_derived_business_overview_top_suppliers_from_confirmed_rows");
|
||||
}
|
||||
if (derivedBusinessOverview.yearly_breakdown.length > 0) {
|
||||
pushReason(reasonCodes, "pilot_derived_business_overview_yearly_operating_breakdown_from_confirmed_rows");
|
||||
}
|
||||
if (derivedBusinessOverview.activity_period) {
|
||||
pushReason(reasonCodes, "pilot_derived_business_overview_activity_window_from_confirmed_rows");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user