Уточнить ответ о заработке через учетный финрезультат

This commit is contained in:
dctouch 2026-05-25 23:03:37 +03:00
parent c577ac6b00
commit d1536ed84b
3 changed files with 148 additions and 0 deletions

View File

@ -858,6 +858,8 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph); const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph);
const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph); const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph);
const rawMessage = toNonEmptyString(turnMeaning?.raw_message) ?? toNonEmptyString(turnMeaning?.effective_message); const rawMessage = toNonEmptyString(turnMeaning?.raw_message) ?? toNonEmptyString(turnMeaning?.effective_message);
const earningsAmountRequested = /(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0434\u043e\u0445\u043e\u0434\p{L}*)/iu.test(normalizeQuestionText(rawMessage ?? ""));
const accountingFinancialResultForEarnings = toRecordObject(overview.accounting_financial_result);
const rawMessageComparable = compactComparable(rawMessage); const rawMessageComparable = compactComparable(rawMessage);
const organizationScopeComparable = compactComparable(organizationScope); const organizationScopeComparable = compactComparable(organizationScope);
const plainOrganizationClarificationSelection = Boolean(separateSubject && const plainOrganizationClarificationSelection = Boolean(separateSubject &&
@ -868,6 +870,36 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
rawMessageComparable.includes(organizationScopeComparable) && rawMessageComparable.includes(organizationScopeComparable) &&
rawMessage.length <= 90 && rawMessage.length <= 90 &&
!/(?:сравн|подтвержд|деньг|сколько|что\s+|покаж|дай|вывод|нельзя|клиент|поставщик|\?)/iu.test(rawMessage)); !/(?:сравн|подтвержд|деньг|сколько|что\s+|покаж|дай|вывод|нельзя|клиент|поставщик|\?)/iu.test(rawMessage));
if (compactCashflowRequested &&
!rankingNeed &&
!profitMarginBoundary &&
!cashflowPolarityRequested &&
earningsAmountRequested &&
accountingFinancialResultForEarnings) {
const direction = toNonEmptyString(accountingFinancialResultForEarnings.final_result_direction);
const amount = moneyText(accountingFinancialResultForEarnings.final_result_amount_human_ru);
const periodScope = toNonEmptyString(accountingFinancialResultForEarnings.period_scope) ?? period;
const marginPct = typeof accountingFinancialResultForEarnings.net_margin_to_revenue_pct === "number" &&
Number.isFinite(accountingFinancialResultForEarnings.net_margin_to_revenue_pct)
? `${accountingFinancialResultForEarnings.net_margin_to_revenue_pct}%`
: null;
const directionText = direction === "profit"
? "\u0443\u0447\u0435\u0442\u043d\u0430\u044f \u043f\u0440\u0438\u0431\u044b\u043b\u044c"
: direction === "loss"
? "\u0443\u0447\u0435\u0442\u043d\u044b\u0439 \u0443\u0431\u044b\u0442\u043e\u043a"
: "\u043d\u0443\u043b\u0435\u0432\u043e\u0439 \u0443\u0447\u0435\u0442\u043d\u044b\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442";
const amountText = amount
? direction === "loss"
? `\u043c\u0438\u043d\u0443\u0441 ${amount}`
: amount
: "\u0441\u0443\u043c\u043c\u0430 \u043d\u0435 \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u043d\u0430";
lines.push(`${organizationPrefix}\u0437\u0430 ${periodScope} \u043f\u043e \u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044e \u0441\u0447\u0435\u0442\u043e\u0432 90/91/99 \u0432 1\u0421 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0430 ${directionText}: ${amountText}${marginPct ? `; \u043c\u0430\u0440\u0436\u0430 \u043a \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043d\u043e\u0439 \u0432\u044b\u0440\u0443\u0447\u043a\u0435 ${marginPct}` : ""}.`);
if (incomingAmount || outgoingAmount || netAmount) {
lines.push(`\u0414\u043b\u044f \u0441\u0432\u0435\u0440\u043a\u0438 \u043f\u043e \u0434\u0435\u043d\u044c\u0433\u0430\u043c: \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 ${incomingAmount ?? "0 \u0440\u0443\u0431."}; \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438/\u0441\u043f\u0438\u0441\u0430\u043b\u0438 ${outgoingAmount ?? "0 \u0440\u0443\u0431."}; \u0434\u0435\u043d\u0435\u0436\u043d\u043e\u0435 \u043d\u0435\u0442\u0442\u043e ${sentenceAmount(netAmount) ?? netAmount ?? "0 \u0440\u0443\u0431."}.`);
}
lines.push("\u0412\u0430\u0436\u043d\u043e: \u0443\u0447\u0435\u0442\u043d\u044b\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u0438 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435 \u0434\u0435\u043d\u0435\u0433 - \u0440\u0430\u0437\u043d\u044b\u0435 \u043c\u0435\u0442\u0440\u0438\u043a\u0438; \u043d\u0435 \u0441\u043c\u0435\u0448\u0438\u0432\u0430\u044e \u0438\u0445 \u0432 \u043e\u0434\u043d\u0443 \"\u043f\u0440\u0438\u0431\u044b\u043b\u044c\".");
return joinBusinessReplyLines(lines);
}
if (compactCashflowRequested && !rankingNeed && (incomingAmount || outgoingAmount || netAmount)) { if (compactCashflowRequested && !rankingNeed && (incomingAmount || outgoingAmount || netAmount)) {
const netDisplay = sentenceAmount(netAmount) ?? netAmount ?? "0 \u0440\u0443\u0431."; const netDisplay = sentenceAmount(netAmount) ?? netAmount ?? "0 \u0440\u0443\u0431.";
const signedNetDisplay = cashflowPolarityRequested && netDisplay && !String(netDisplay).trim().startsWith("-") const signedNetDisplay = cashflowPolarityRequested && netDisplay && !String(netDisplay).trim().startsWith("-")

View File

@ -1032,6 +1032,10 @@ function buildCompactBusinessOverviewReply(
const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph); const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph);
const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph); const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph);
const rawMessage = toNonEmptyString(turnMeaning?.raw_message) ?? toNonEmptyString(turnMeaning?.effective_message); const rawMessage = toNonEmptyString(turnMeaning?.raw_message) ?? toNonEmptyString(turnMeaning?.effective_message);
const earningsAmountRequested = /(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0434\u043e\u0445\u043e\u0434\p{L}*)/iu.test(
normalizeQuestionText(rawMessage ?? "")
);
const accountingFinancialResultForEarnings = toRecordObject(overview.accounting_financial_result);
const rawMessageComparable = compactComparable(rawMessage); const rawMessageComparable = compactComparable(rawMessage);
const organizationScopeComparable = compactComparable(organizationScope); const organizationScopeComparable = compactComparable(organizationScope);
const plainOrganizationClarificationSelection = Boolean( const plainOrganizationClarificationSelection = Boolean(
@ -1045,6 +1049,47 @@ function buildCompactBusinessOverviewReply(
!/(?:сравн|подтвержд|деньг|сколько|что\s+|покаж|дай|вывод|нельзя|клиент|поставщик|\?)/iu.test(rawMessage) !/(?:сравн|подтвержд|деньг|сколько|что\s+|покаж|дай|вывод|нельзя|клиент|поставщик|\?)/iu.test(rawMessage)
); );
if (
compactCashflowRequested &&
!rankingNeed &&
!profitMarginBoundary &&
!cashflowPolarityRequested &&
earningsAmountRequested &&
accountingFinancialResultForEarnings
) {
const direction = toNonEmptyString(accountingFinancialResultForEarnings.final_result_direction);
const amount = moneyText(accountingFinancialResultForEarnings.final_result_amount_human_ru);
const periodScope = toNonEmptyString(accountingFinancialResultForEarnings.period_scope) ?? period;
const marginPct =
typeof accountingFinancialResultForEarnings.net_margin_to_revenue_pct === "number" &&
Number.isFinite(accountingFinancialResultForEarnings.net_margin_to_revenue_pct)
? `${accountingFinancialResultForEarnings.net_margin_to_revenue_pct}%`
: null;
const directionText =
direction === "profit"
? "\u0443\u0447\u0435\u0442\u043d\u0430\u044f \u043f\u0440\u0438\u0431\u044b\u043b\u044c"
: direction === "loss"
? "\u0443\u0447\u0435\u0442\u043d\u044b\u0439 \u0443\u0431\u044b\u0442\u043e\u043a"
: "\u043d\u0443\u043b\u0435\u0432\u043e\u0439 \u0443\u0447\u0435\u0442\u043d\u044b\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442";
const amountText = amount
? direction === "loss"
? `\u043c\u0438\u043d\u0443\u0441 ${amount}`
: amount
: "\u0441\u0443\u043c\u043c\u0430 \u043d\u0435 \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u043d\u0430";
lines.push(
`${organizationPrefix}\u0437\u0430 ${periodScope} \u043f\u043e \u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044e \u0441\u0447\u0435\u0442\u043e\u0432 90/91/99 \u0432 1\u0421 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0430 ${directionText}: ${amountText}${marginPct ? `; \u043c\u0430\u0440\u0436\u0430 \u043a \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043d\u043e\u0439 \u0432\u044b\u0440\u0443\u0447\u043a\u0435 ${marginPct}` : ""}.`
);
if (incomingAmount || outgoingAmount || netAmount) {
lines.push(
`\u0414\u043b\u044f \u0441\u0432\u0435\u0440\u043a\u0438 \u043f\u043e \u0434\u0435\u043d\u044c\u0433\u0430\u043c: \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 ${incomingAmount ?? "0 \u0440\u0443\u0431."}; \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438/\u0441\u043f\u0438\u0441\u0430\u043b\u0438 ${outgoingAmount ?? "0 \u0440\u0443\u0431."}; \u0434\u0435\u043d\u0435\u0436\u043d\u043e\u0435 \u043d\u0435\u0442\u0442\u043e ${sentenceAmount(netAmount) ?? netAmount ?? "0 \u0440\u0443\u0431."}.`
);
}
lines.push(
"\u0412\u0430\u0436\u043d\u043e: \u0443\u0447\u0435\u0442\u043d\u044b\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u0438 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435 \u0434\u0435\u043d\u0435\u0433 - \u0440\u0430\u0437\u043d\u044b\u0435 \u043c\u0435\u0442\u0440\u0438\u043a\u0438; \u043d\u0435 \u0441\u043c\u0435\u0448\u0438\u0432\u0430\u044e \u0438\u0445 \u0432 \u043e\u0434\u043d\u0443 \"\u043f\u0440\u0438\u0431\u044b\u043b\u044c\"."
);
return joinBusinessReplyLines(lines);
}
if (compactCashflowRequested && !rankingNeed && (incomingAmount || outgoingAmount || netAmount)) { if (compactCashflowRequested && !rankingNeed && (incomingAmount || outgoingAmount || netAmount)) {
const netDisplay = sentenceAmount(netAmount) ?? netAmount ?? "0 \u0440\u0443\u0431."; const netDisplay = sentenceAmount(netAmount) ?? netAmount ?? "0 \u0440\u0443\u0431.";
const signedNetDisplay = const signedNetDisplay =

View File

@ -705,6 +705,77 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).not.toContain("wide overview"); expect(candidate.reply_text).not.toContain("wide overview");
}); });
it("leads earnings wording with accounting result when explicit-period proof is available", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
turn_meaning_ref: {
raw_message: "а за 2017 мы скок заработали?",
asked_action_family: "broad_evaluation",
unsupported_but_understood_family: "broad_business_evaluation",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2017"
},
data_need_graph: {
business_fact_family: "business_overview",
ranking_need: null,
reason_codes: [
"data_need_graph_family_business_overview",
"data_need_graph_business_overview_direct_money_answer"
]
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
pilot: {
pilot_scope: "business_overview_route_template_v1",
derived_business_overview: {
period_scope: "2017",
incoming_customer_revenue: {
total_amount_human_ru: "16 932 063,96 руб."
},
outgoing_supplier_payout: {
total_amount_human_ru: "15 617 432,52 руб."
},
net_amount_human_ru: "1 314 631,44 руб.",
net_direction: "net_incoming",
top_customers: [],
top_suppliers: [],
accounting_financial_result: {
final_result_direction: "profit",
final_result_amount_human_ru: "116 938,23 руб.",
net_margin_to_revenue_pct: 1.03,
period_scope: "2017"
}
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "wide overview should not leak",
confirmed_lines: [],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
);
expect(candidate.reply_text).toContain("учетная прибыль");
expect(candidate.reply_text).toContain("116 938,23");
expect(candidate.reply_text).toContain("маржа");
expect(candidate.reply_text).toContain("1.03%");
expect(candidate.reply_text).toContain("денежное нетто");
expect(candidate.reply_text).toContain("1 314 631,44");
expect(candidate.reply_text).toContain("разные метрики");
expect(candidate.reply_text).not.toContain("wide overview");
});
it("uses compact cashflow output for one-line no-breakdown wording", () => { it("uses compact cashflow output for one-line no-breakdown wording", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate( const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({ entryPoint({