Закрепить ответы ассистента по бизнес-зависимостям
This commit is contained in:
parent
92473c894f
commit
e3e17ac7db
|
|
@ -0,0 +1,186 @@
|
||||||
|
{
|
||||||
|
"schema_version": "domain_truth_harness_spec_v1",
|
||||||
|
"scenario_id": "hm_business_profile_dependency_mini_pack_20260526",
|
||||||
|
"domain": "assistant_stage1_business_overview",
|
||||||
|
"title": "Hm business profile dependency mini pack",
|
||||||
|
"description": "Mini semantic replay for broad HM business profile: company evaluation, profit boundary, customer/supplier dependency signals, and next checks must stay business-useful, evidence-bounded, and free of internal MCP mechanics.",
|
||||||
|
"bindings": {},
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": "s01_establish_company_top_year",
|
||||||
|
"title": "Establish company context with a top-year question",
|
||||||
|
"question": "какой у ООО \"Альтернатива Плюс\" самый доходный год",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"2015",
|
||||||
|
"136 723 459,73",
|
||||||
|
"не полный бухгалтерский рейтинг"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Коротко:",
|
||||||
|
"capability_id",
|
||||||
|
"mcp_discovery",
|
||||||
|
"query_movements"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"business_overview",
|
||||||
|
"company_context",
|
||||||
|
"yearly_ranking"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s02_broad_company_evaluation",
|
||||||
|
"title": "Broad company evaluation keeps the evidence limit in the first layer",
|
||||||
|
"question": "Как ты оценишь деятельность компании?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual_with_explanation",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"ограниченн",
|
||||||
|
"не аудиторское",
|
||||||
|
"не подтвержден",
|
||||||
|
"чист",
|
||||||
|
"прибыл",
|
||||||
|
"Комитет государственных услуг",
|
||||||
|
"зависим"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Коротко:",
|
||||||
|
"capability_id",
|
||||||
|
"mcp_discovery",
|
||||||
|
"query_movements",
|
||||||
|
"business_overview_route_template_v1"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"business_overview",
|
||||||
|
"broad_business_evaluation",
|
||||||
|
"coverage_honesty",
|
||||||
|
"profit_boundary"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s03_profitability_boundary",
|
||||||
|
"title": "Profitability follow-up does not convert cashflow net into profit",
|
||||||
|
"question": "то есть бизнес прибыльный или это только обороты?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual_with_explanation",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"не подтверждаю",
|
||||||
|
"чист",
|
||||||
|
"прибыл",
|
||||||
|
"денежн",
|
||||||
|
"нетто",
|
||||||
|
"не полный отчет"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Коротко:",
|
||||||
|
"capability_id",
|
||||||
|
"mcp_discovery",
|
||||||
|
"это прибыль"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"business_overview",
|
||||||
|
"profitability_boundary",
|
||||||
|
"cashflow_not_profit"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s04_customer_dependency_signal",
|
||||||
|
"title": "Customer dependency signal stays bounded to incoming money evidence",
|
||||||
|
"question": "от кого мы больше всего зависим по входящим деньгам?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"Комитет государственных услуг",
|
||||||
|
"133.839.880,82",
|
||||||
|
"концентрац",
|
||||||
|
"входящ",
|
||||||
|
"не чистая прибыль"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Коротко:",
|
||||||
|
"capability_id",
|
||||||
|
"mcp_discovery",
|
||||||
|
"query_movements"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"business_overview",
|
||||||
|
"customer_dependency",
|
||||||
|
"incoming_counterparty_concentration"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s05_supplier_dependency_signal",
|
||||||
|
"title": "Supplier dependency signal stays bounded to outgoing money evidence",
|
||||||
|
"question": "а по поставщикам и исходящим платежам от кого зависимость?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"ГТК-Интер",
|
||||||
|
"44 943 460",
|
||||||
|
"32.58",
|
||||||
|
"исходящ",
|
||||||
|
"не аудит"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Коротко:",
|
||||||
|
"capability_id",
|
||||||
|
"mcp_discovery",
|
||||||
|
"query_movements"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"business_overview",
|
||||||
|
"supplier_dependency",
|
||||||
|
"outgoing_counterparty_concentration"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s06_next_checks",
|
||||||
|
"title": "Next-check answer gives concrete analyst path instead of vague advice",
|
||||||
|
"question": "что дальше проверять в первую очередь?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual_with_explanation",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"чист",
|
||||||
|
"прибыл",
|
||||||
|
"90/91/99",
|
||||||
|
"топ-3",
|
||||||
|
"склад",
|
||||||
|
"долг"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Коротко:",
|
||||||
|
"capability_id",
|
||||||
|
"mcp_discovery",
|
||||||
|
"query_movements",
|
||||||
|
"business_overview_route_template_v1"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"business_overview",
|
||||||
|
"next_checks",
|
||||||
|
"analyst_action_plan"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -1869,6 +1869,10 @@ function resolveUnicodeAddressIntentBridge(text) {
|
||||||
/(?:хвост|задержк|проблем|систематическ)/iu.test(normalized)) {
|
/(?:хвост|задержк|проблем|систематическ)/iu.test(normalized)) {
|
||||||
return unicodeBridgeResolution("list_payables_counterparties", "high", "supplier_tail_risk_signal_detected");
|
return unicodeBridgeResolution("list_payables_counterparties", "high", "supplier_tail_risk_signal_detected");
|
||||||
}
|
}
|
||||||
|
if (/(?:поставщик|закуп|исходящ|плат[её]ж|supplier|vendor|procurement|outgoing|payout)/iu.test(normalized) &&
|
||||||
|
/(?:зависим|завязан|концентрац|ключев|основн|критич|dependency|concentration|key|critical)/iu.test(normalized)) {
|
||||||
|
return unicodeBridgeResolution("unknown", "high", "unicode_business_overview_supplier_quality_deferred_to_discovery");
|
||||||
|
}
|
||||||
if (hasDocumentCue && (hasCounterpartyCue || hasLooseCounterpartyByAnchor) && !hasContractCue) {
|
if (hasDocumentCue && (hasCounterpartyCue || hasLooseCounterpartyByAnchor) && !hasContractCue) {
|
||||||
return unicodeBridgeResolution("list_documents_by_counterparty", "high", "unicode_documents_by_counterparty_bridge_signal_detected");
|
return unicodeBridgeResolution("list_documents_by_counterparty", "high", "unicode_documents_by_counterparty_bridge_signal_detected");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3202,8 +3202,14 @@ class AddressQueryService {
|
||||||
shadowRouteAudit
|
shadowRouteAudit
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const rawUserMessageForReply = typeof options.rawUserMessage === "string" && options.rawUserMessage.trim().length > 0
|
||||||
|
? options.rawUserMessage
|
||||||
|
: undefined;
|
||||||
const composeOptionsFromFilters = (filterSet, options = {}) => ({
|
const composeOptionsFromFilters = (filterSet, options = {}) => ({
|
||||||
userMessage,
|
userMessage,
|
||||||
|
rawUserMessage: typeof options.rawUserMessage === "string" && options.rawUserMessage.trim().length > 0
|
||||||
|
? options.rawUserMessage
|
||||||
|
: rawUserMessageForReply,
|
||||||
itemHint: typeof filterSet.item === "string" ? filterSet.item : undefined,
|
itemHint: typeof filterSet.item === "string" ? filterSet.item : undefined,
|
||||||
counterpartyHint: typeof options.counterpartyHint === "string"
|
counterpartyHint: typeof options.counterpartyHint === "string"
|
||||||
? options.counterpartyHint
|
? options.counterpartyHint
|
||||||
|
|
|
||||||
|
|
@ -406,7 +406,11 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||||
const focus = deps.detectValueRankingFocus(options.userMessage);
|
const focus = deps.detectValueRankingFocus(options.userMessage);
|
||||||
const limit = deps.detectRankingLimit(options.userMessage, 20);
|
const limit = deps.detectRankingLimit(options.userMessage, 20);
|
||||||
const minOpsForAvgCheck = deps.detectMinOpsForAvgCheck(options.userMessage);
|
const minOpsForAvgCheck = deps.detectMinOpsForAvgCheck(options.userMessage);
|
||||||
const normalizedQuestion = deps.normalizeQuestionText(options.userMessage);
|
const questionSignalText = [options.rawUserMessage, options.userMessage]
|
||||||
|
.map((value) => String(value ?? "").trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
const normalizedQuestion = deps.normalizeQuestionText(questionSignalText || options.userMessage);
|
||||||
const asksSingleBestCounterparty = focus === "top_by_total" &&
|
const asksSingleBestCounterparty = focus === "top_by_total" &&
|
||||||
/(?:какой|кто|which|who|какой|кто)/iu.test(normalizedQuestion) &&
|
/(?:какой|кто|which|who|какой|кто)/iu.test(normalizedQuestion) &&
|
||||||
/(?:больше\s+всего|сам(?:ый|ая|ое|ые)|наибольш|прин[её]с|highest|most|больше\s+всего|сам(?:ый|ая|РѕРµ|ые)|наибол|РїСЂРёРЅ[её]СЃ)/iu.test(normalizedQuestion) &&
|
/(?:больше\s+всего|сам(?:ый|ая|ое|ые)|наибольш|прин[её]с|highest|most|больше\s+всего|сам(?:ый|ая|РѕРµ|ые)|наибол|РїСЂРёРЅ[её]СЃ)/iu.test(normalizedQuestion) &&
|
||||||
|
|
@ -418,6 +422,7 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||||
const effectiveLimit = asksSingleBestCounterparty || semanticSingleBestCounterparty ? 1 : limit;
|
const effectiveLimit = asksSingleBestCounterparty || semanticSingleBestCounterparty ? 1 : limit;
|
||||||
const cashflowBoundaryLine = buildCashflowBoundaryLine(isSupplier);
|
const cashflowBoundaryLine = buildCashflowBoundaryLine(isSupplier);
|
||||||
const cashflowNextStepLine = buildCashflowNextStepLine(isSupplier, normalizedQuestion);
|
const cashflowNextStepLine = buildCashflowNextStepLine(isSupplier, normalizedQuestion);
|
||||||
|
const dependencyCue = /(?:зависим|завязан|концентрац|ключев|основн|критич|dependency|concentration|key|critical)/iu.test(normalizedQuestion);
|
||||||
const byCounterparty = new Map();
|
const byCounterparty = new Map();
|
||||||
const byYear = new Map();
|
const byYear = new Map();
|
||||||
const deals = [];
|
const deals = [];
|
||||||
|
|
@ -653,13 +658,20 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||||
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
||||||
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
||||||
: isSupplier
|
: isSupplier
|
||||||
? `Крупнейший поставщик по подтвержденным выплатам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
? dependencyCue
|
||||||
: `Самый доходный клиент ${rankingPeriodLabel} по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`;
|
? `Главный сигнал зависимости по исходящим платежам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||||
|
: `Крупнейший поставщик по подтвержденным выплатам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||||
|
: dependencyCue
|
||||||
|
? `Главный сигнал зависимости по входящим деньгам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||||
|
: `Самый доходный клиент ${rankingPeriodLabel} по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`;
|
||||||
lines.unshift(directAnswerLine);
|
lines.unshift(directAnswerLine);
|
||||||
|
if (dependencyCue) {
|
||||||
|
lines.splice(1, 0, "Это сигнал концентрации по найденным денежным строкам 1С, а не подтверждение договорной зависимости, надежности контрагента или чистой прибыли.");
|
||||||
|
}
|
||||||
if (!isSupplier) {
|
if (!isSupplier) {
|
||||||
lines.splice(1, 0, cashflowBoundaryLine);
|
lines.splice(dependencyCue ? 2 : 1, 0, cashflowBoundaryLine);
|
||||||
if (cashflowNextStepLine) {
|
if (cashflowNextStepLine) {
|
||||||
lines.splice(2, 0, cashflowNextStepLine);
|
lines.splice(dependencyCue ? 3 : 2, 0, cashflowNextStepLine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,7 @@ async function runAssistantAddressAttemptRuntime(input) {
|
||||||
};
|
};
|
||||||
const runAddressLaneAttempt = async (messageUsed, carryMeta, analysisDateHint, llmSemanticHints = null) => runAddressLaneAttemptRuntimeSafe((0, assistantAddressLaneAttemptInputBuilder_1.buildAssistantAddressLaneAttemptRuntimeInput)({
|
const runAddressLaneAttempt = async (messageUsed, carryMeta, analysisDateHint, llmSemanticHints = null) => runAddressLaneAttemptRuntimeSafe((0, assistantAddressLaneAttemptInputBuilder_1.buildAssistantAddressLaneAttemptRuntimeInput)({
|
||||||
messageUsed,
|
messageUsed,
|
||||||
|
rawUserMessage: input.userMessage,
|
||||||
carryMeta,
|
carryMeta,
|
||||||
analysisDateHint,
|
analysisDateHint,
|
||||||
llmSemanticHints,
|
llmSemanticHints,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ exports.buildAssistantAddressLaneAttemptRuntimeInput = buildAssistantAddressLane
|
||||||
function buildAssistantAddressLaneAttemptRuntimeInput(input) {
|
function buildAssistantAddressLaneAttemptRuntimeInput(input) {
|
||||||
return {
|
return {
|
||||||
messageUsed: input.messageUsed,
|
messageUsed: input.messageUsed,
|
||||||
|
rawUserMessage: input.rawUserMessage,
|
||||||
carryMeta: input.carryMeta,
|
carryMeta: input.carryMeta,
|
||||||
analysisDateHint: input.analysisDateHint,
|
analysisDateHint: input.analysisDateHint,
|
||||||
llmSemanticHints: input.llmSemanticHints,
|
llmSemanticHints: input.llmSemanticHints,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ function resolveAssistantAddressLaneAttemptFollowupContext(carryMeta) {
|
||||||
}
|
}
|
||||||
function buildAssistantAddressLaneAttemptQueryOptions(input) {
|
function buildAssistantAddressLaneAttemptQueryOptions(input) {
|
||||||
const base = {
|
const base = {
|
||||||
analysisDateHint: input.analysisDateHint
|
analysisDateHint: input.analysisDateHint,
|
||||||
|
rawUserMessage: input.rawUserMessage ?? undefined
|
||||||
};
|
};
|
||||||
if (input.scopedFollowupContext) {
|
if (input.scopedFollowupContext) {
|
||||||
base.followupContext = input.scopedFollowupContext;
|
base.followupContext = input.scopedFollowupContext;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ async function runAssistantAddressLaneAttemptRuntime(input) {
|
||||||
scopedFollowupContext,
|
scopedFollowupContext,
|
||||||
llmSemanticHints: input.llmSemanticHints ?? null,
|
llmSemanticHints: input.llmSemanticHints ?? null,
|
||||||
activeOrganization: input.activeOrganization,
|
activeOrganization: input.activeOrganization,
|
||||||
knownOrganizations: input.knownOrganizations
|
knownOrganizations: input.knownOrganizations,
|
||||||
|
rawUserMessage: input.rawUserMessage ?? input.messageUsed
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,10 @@ function requestsCounterpartyLeaderSurface(turnMeaning, graph) {
|
||||||
}
|
}
|
||||||
return /(?:(?:кто|как\p{L}*|покаж\p{L}*|назов\p{L}*|раскро\p{L}*)[\s\S]{0,100}(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*|топ)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:топ[-\s]*(?:клиент|заказчик|поставщик|получател))|(?:клиент|поставщик)[\s\S]{0,80}(?:главн|крупнейш|основн|ведущ|топ))/iu.test(text);
|
return /(?:(?:кто|как\p{L}*|покаж\p{L}*|назов\p{L}*|раскро\p{L}*)[\s\S]{0,100}(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*|топ)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:топ[-\s]*(?:клиент|заказчик|поставщик|получател))|(?:клиент|поставщик)[\s\S]{0,80}(?:главн|крупнейш|основн|ведущ|топ))/iu.test(text);
|
||||||
}
|
}
|
||||||
|
function requestsBusinessOverviewNextChecks(turnMeaning, graph) {
|
||||||
|
const text = normalizedTurnAndGraphText(turnMeaning, graph);
|
||||||
|
return /(?:что\s+(?:дальше\s+)?провер\w*|что\s+провер\w*\s+дальше|следующ\w*\s+шаг|в\s+первую\s+очередь\s+провер\w*|next\s+checks?|next\s+steps?)/iu.test(text);
|
||||||
|
}
|
||||||
function requestsCompactCashflowAnswer(turnMeaning, graph) {
|
function requestsCompactCashflowAnswer(turnMeaning, graph) {
|
||||||
const text = normalizeQuestionText([
|
const text = normalizeQuestionText([
|
||||||
turnMeaning?.raw_message,
|
turnMeaning?.raw_message,
|
||||||
|
|
@ -429,6 +433,13 @@ function businessOverviewPeriodText(overview) {
|
||||||
const period = toNonEmptyString(overview.period_scope);
|
const period = toNonEmptyString(overview.period_scope);
|
||||||
return period ? `за ${period}` : "за все доступное проверенное окно";
|
return period ? `за ${period}` : "за все доступное проверенное окно";
|
||||||
}
|
}
|
||||||
|
function businessOverviewPeriodPhrase(value) {
|
||||||
|
const normalized = toNonEmptyString(value);
|
||||||
|
if (!normalized) {
|
||||||
|
return "за все доступное проверенное окно";
|
||||||
|
}
|
||||||
|
return /^за\s+/iu.test(normalized) ? normalized : `за ${normalized}`;
|
||||||
|
}
|
||||||
function strongestIncomingYear(overview) {
|
function strongestIncomingYear(overview) {
|
||||||
const years = Array.isArray(overview.yearly_breakdown) ? overview.yearly_breakdown : [];
|
const years = Array.isArray(overview.yearly_breakdown) ? overview.yearly_breakdown : [];
|
||||||
const sorted = years
|
const sorted = years
|
||||||
|
|
@ -857,6 +868,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||||
const compactCashflowRequested = directMoneyOnlyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
|
const compactCashflowRequested = directMoneyOnlyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
|
||||||
const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph);
|
const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph);
|
||||||
const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph);
|
const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph);
|
||||||
|
const nextChecksRequested = requestsBusinessOverviewNextChecks(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 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 accountingFinancialResultForEarnings = toRecordObject(overview.accounting_financial_result);
|
||||||
|
|
@ -1039,9 +1051,12 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||||
? `${vendorProcurementQuality.top_non_financial_supplier_share_pct}%`
|
? `${vendorProcurementQuality.top_non_financial_supplier_share_pct}%`
|
||||||
: null;
|
: null;
|
||||||
const periodScope = toNonEmptyString(vendorProcurementQuality.period_scope) ?? period;
|
const periodScope = toNonEmptyString(vendorProcurementQuality.period_scope) ?? period;
|
||||||
const totalText = totalOutgoing ? `; всего исходящих платежей в проверенном срезе ${totalOutgoing}` : "";
|
const periodPhrase = businessOverviewPeriodPhrase(periodScope);
|
||||||
|
const totalText = totalOutgoing
|
||||||
|
? `; всего исходящих платежей в проверенном срезе ${sentenceAmount(totalOutgoing)}`
|
||||||
|
: "";
|
||||||
if (status === "financial_institution_leads_outgoing_cash") {
|
if (status === "financial_institution_leads_outgoing_cash") {
|
||||||
lines.push(`Коротко: проверка концентрации закупок/исходящих платежей за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`);
|
lines.push(`Коротко: проверка концентрации закупок/исходящих платежей ${periodPhrase} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`);
|
||||||
const financialHintText = financialFlowHintTextRuFromRecord(topOutgoingRecord);
|
const financialHintText = financialFlowHintTextRuFromRecord(topOutgoingRecord);
|
||||||
if (financialHintText) {
|
if (financialHintText) {
|
||||||
lines.push(financialHintText);
|
lines.push(financialHintText);
|
||||||
|
|
@ -1051,17 +1066,17 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (status === "reviewed_procurement_concentration") {
|
else if (status === "reviewed_procurement_concentration") {
|
||||||
lines.push(`Коротко: точный риск зависимости от одного поставщика не подтвержден полностью; проверка концентрации закупок/исходящих платежей за ${periodScope} нашла крупнейшего получателя исходящего потока: ${topOutgoingName ?? nonFinancialName ?? "получатель не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : nonFinancialShare ? ` держит около ${nonFinancialShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}${totalText}.`);
|
lines.push(`Коротко: точный риск зависимости от одного поставщика не подтвержден полностью; проверка концентрации закупок/исходящих платежей ${periodPhrase} нашла крупнейшего получателя исходящего потока: ${topOutgoingName ?? nonFinancialName ?? "получатель не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : nonFinancialShare ? ` держит около ${nonFinancialShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}${totalText}.`);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
lines.push(`Коротко: проверка концентрации закупок/исходящих платежей за ${periodScope} выполнена, но надежной небанковской концентрации поставщика по найденным исходящим платежам не хватает${totalText}.`);
|
lines.push(`Коротко: проверка концентрации закупок/исходящих платежей ${periodPhrase} выполнена, но надежной небанковской концентрации поставщика по найденным исходящим платежам не хватает${totalText}.`);
|
||||||
}
|
}
|
||||||
const contractText = typeof vendorProcurementQuality.used_contracts === "number" && Number.isFinite(vendorProcurementQuality.used_contracts)
|
const contractText = typeof vendorProcurementQuality.used_contracts === "number" && Number.isFinite(vendorProcurementQuality.used_contracts)
|
||||||
? typeof vendorProcurementQuality.total_contracts === "number" && Number.isFinite(vendorProcurementQuality.total_contracts)
|
? typeof vendorProcurementQuality.total_contracts === "number" && Number.isFinite(vendorProcurementQuality.total_contracts)
|
||||||
? ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts}/${vendorProcurementQuality.total_contracts} договоров${typeof vendorProcurementQuality.used_contract_share_pct === "number" && Number.isFinite(vendorProcurementQuality.used_contract_share_pct) ? ` (${vendorProcurementQuality.used_contract_share_pct}%)` : ""}.`
|
? ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts}/${vendorProcurementQuality.total_contracts} договоров${typeof vendorProcurementQuality.used_contract_share_pct === "number" && Number.isFinite(vendorProcurementQuality.used_contract_share_pct) ? ` (${vendorProcurementQuality.used_contract_share_pct}%)` : ""}.`
|
||||||
: ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts} договоров.`
|
: ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts} договоров.`
|
||||||
: "";
|
: "";
|
||||||
lines.push(`Что не доказано этим срезом: надежность поставщика, качество поставок, договорные условия, назначение каждого платежа и полная структура всех расходов.${contractText}`);
|
lines.push(`Это не аудит поставщиков: срез не доказывает надежность поставщика, качество поставок, договорные условия, назначение каждого платежа и полную структуру всех расходов.${contractText}`);
|
||||||
return joinBusinessReplyLines(lines);
|
return joinBusinessReplyLines(lines);
|
||||||
}
|
}
|
||||||
const supplierBasis = topSupplier
|
const supplierBasis = topSupplier
|
||||||
|
|
@ -1102,6 +1117,19 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||||
lines.push("Проверить нужно отдельно: складской срез на дату, учетную политику резервов, списания и ликвидационную стоимость; косвенные признаки нельзя выдавать за доказанный факт резерва.");
|
lines.push("Проверить нужно отдельно: складской срез на дату, учетную политику резервов, списания и ликвидационную стоимость; косвенные признаки нельзя выдавать за доказанный факт резерва.");
|
||||||
return joinBusinessReplyLines(lines);
|
return joinBusinessReplyLines(lines);
|
||||||
}
|
}
|
||||||
|
if (!separateSubject &&
|
||||||
|
!crossScopeExecutiveSummary &&
|
||||||
|
!counterpartyLeaderSurfaceRequested &&
|
||||||
|
!rankingNeed &&
|
||||||
|
nextChecksRequested) {
|
||||||
|
const subject = organizationScope ? `по компании ${organizationScope}` : "по компании";
|
||||||
|
lines.push(`В первую очередь ${subject} надо проверить чистую прибыль через закрытие 90/91/99, зависимость от топ-3 клиентов и поставщиков, долги/зависшие расчеты, складские остатки и НДС-позицию.`);
|
||||||
|
lines.push("Смысл порядка: сначала отделить бухгалтерский финрезультат от денежного нетто, затем проверить концентрацию потоков и только после этого углубляться в склад, долги и налоговую позицию.");
|
||||||
|
if (limitLine) {
|
||||||
|
lines.push(limitLine);
|
||||||
|
}
|
||||||
|
return joinBusinessReplyLines(lines);
|
||||||
|
}
|
||||||
if (!separateSubject &&
|
if (!separateSubject &&
|
||||||
!crossScopeExecutiveSummary &&
|
!crossScopeExecutiveSummary &&
|
||||||
!counterpartyLeaderSurfaceRequested &&
|
!counterpartyLeaderSurfaceRequested &&
|
||||||
|
|
|
||||||
|
|
@ -676,6 +676,15 @@ function hasDebtDueDateBoundaryFollowupSignal(text) {
|
||||||
const hasFollowupShape = /(?:\u0442\u043e\s+\u0435\u0441\u0442\u044c|\u043f\u043e\u0447\u0435\u043c\u0443|\u043a\u043e\u0440\u043e\u0442\w*|\u043d\u0435\u043b\u044c\u0437\u044f|\u043c\u043e\u0436\u043d\u043e\s+\u043b\u0438|\u0437\u043d\u0430\u0447\u0438\u0442|\u0432\u044b\u0445\u043e\u0434\u0438\u0442|why|short|brief|so)/iu.test(text);
|
const hasFollowupShape = /(?:\u0442\u043e\s+\u0435\u0441\u0442\u044c|\u043f\u043e\u0447\u0435\u043c\u0443|\u043a\u043e\u0440\u043e\u0442\w*|\u043d\u0435\u043b\u044c\u0437\u044f|\u043c\u043e\u0436\u043d\u043e\s+\u043b\u0438|\u0437\u043d\u0430\u0447\u0438\u0442|\u0432\u044b\u0445\u043e\u0434\u0438\u0442|why|short|brief|so)/iu.test(text);
|
||||||
return hasDueDateCue && hasFollowupShape;
|
return hasDueDateCue && hasFollowupShape;
|
||||||
}
|
}
|
||||||
|
function hasSupplierDependencyFollowupSignal(text) {
|
||||||
|
if (!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const normalized = compactLower(text);
|
||||||
|
const hasSupplierOrOutgoingCue = /(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0437\u0430\u043a\u0443\u043f|\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u043f\u043b\u0430\u0442[\u0435\u0451]\u0436|\u0437\u0430\u043f\u043b\u0430\u0442|supplier|vendor|procurement|outgoing|payout)/iu.test(normalized);
|
||||||
|
const hasDependencyCue = /(?:\u0437\u0430\u0432\u0438\u0441\u0438\u043c|\u0437\u0430\u0432\u044f\u0437\u0430\u043d|\u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446|\u043a\u043b\u044e\u0447\u0435\u0432|\u043e\u0441\u043d\u043e\u0432\u043d|\u043a\u0440\u0438\u0442\u0438\u0447|dependency|concentration|key|critical)/iu.test(normalized);
|
||||||
|
return hasSupplierOrOutgoingCue && hasDependencyCue;
|
||||||
|
}
|
||||||
function hasOrganizationLevelDebtDueDateOverviewSignal(text) {
|
function hasOrganizationLevelDebtDueDateOverviewSignal(text) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -771,7 +780,7 @@ function hasBusinessOverviewContinuationSignal(text) {
|
||||||
}
|
}
|
||||||
const normalized = compactLower(text);
|
const normalized = compactLower(text);
|
||||||
const hasEvidenceContinuationCue = /(?:по\s+этим\s+данн|на\s+базе\s+этих\s+данн|из\s+этого|по\s+итогам|что\s+подтвержден|чего\s+не\s+хватает|что\s+пока\s+нельзя|что\s+можно\s+сказать|какой\s+вывод|итогов|резюм|вывод)/iu.test(normalized);
|
const hasEvidenceContinuationCue = /(?:по\s+этим\s+данн|на\s+базе\s+этих\s+данн|из\s+этого|по\s+итогам|что\s+подтвержден|чего\s+не\s+хватает|что\s+пока\s+нельзя|что\s+можно\s+сказать|какой\s+вывод|итогов|резюм|вывод)/iu.test(normalized);
|
||||||
const hasAnalystContinuationCue = /(?:можно\s+ли|если\s+нет|proxy|прокси|аудит|оцен|что\s+думаешь|нормальн\p{L}*\s+прибыл|прибыл|марж|рентаб|ликвидн|просроч|качество\s+долг|риск|налогов\p{L}*\s+вывод)/iu.test(normalized);
|
const hasAnalystContinuationCue = /(?:можно\s+ли|если\s+нет|proxy|прокси|аудит|оцен|что\s+думаешь|нормальн\p{L}*\s+прибыл|прибыл|марж|рентаб|ликвидн|просроч|качество\s+долг|риск|зависим|завязан|концентрац|налогов\p{L}*\s+вывод)/iu.test(normalized);
|
||||||
const hasTaxContinuationCue = /(?:ндс|vat)[\s\S]{0,120}(?:позици|период|основан|не\s+хватает|налогов\p{L}*\s+вывод)|(?:позици|налогов\p{L}*\s+вывод)[\s\S]{0,80}(?:ндс|vat)/iu.test(normalized);
|
const hasTaxContinuationCue = /(?:ндс|vat)[\s\S]{0,120}(?:позици|период|основан|не\s+хватает|налогов\p{L}*\s+вывод)|(?:позици|налогов\p{L}*\s+вывод)[\s\S]{0,80}(?:ндс|vat)/iu.test(normalized);
|
||||||
const hasFinalSummaryCue = /(?:\u0447\u0442\u043e\s+\u043c\u044b\s+\u0437\u043d\u0430\u0435\u043c|\u0447\u0442\u043e\s+\u043f\u043e\u043d\u044f\u0442\u043d\u043e|\u0447\u0442\u043e\s+\u043f\u0440\u043e\u0432\u0435\u0440\w*\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u0441\u043b\u0435\u0434\u0443\u044e\u0449\w*\s+\u0448\u0430\u0433|\u0438\u0442\u043e\u0433\w*\s+\u0432\u044b\u0432\u043e\u0434|\u043a\u0430\u043a\u043e\u0439\s+\u0432\u044b\u0432\u043e\u0434|\u0447\u0442\u043e\s+\u0441\s+\u044d\u0442\u0438\u043c\s+\u0434\u0435\u043b\u0430\u0442\u044c|what\s+do\s+we\s+know|what\s+is\s+missing|next\s+step|final\s+summary)/iu.test(normalized);
|
const hasFinalSummaryCue = /(?:\u0447\u0442\u043e\s+\u043c\u044b\s+\u0437\u043d\u0430\u0435\u043c|\u0447\u0442\u043e\s+\u043f\u043e\u043d\u044f\u0442\u043d\u043e|\u0447\u0442\u043e\s+\u043f\u0440\u043e\u0432\u0435\u0440\w*\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u0441\u043b\u0435\u0434\u0443\u044e\u0449\w*\s+\u0448\u0430\u0433|\u0438\u0442\u043e\u0433\w*\s+\u0432\u044b\u0432\u043e\u0434|\u043a\u0430\u043a\u043e\u0439\s+\u0432\u044b\u0432\u043e\u0434|\u0447\u0442\u043e\s+\u0441\s+\u044d\u0442\u0438\u043c\s+\u0434\u0435\u043b\u0430\u0442\u044c|what\s+do\s+we\s+know|what\s+is\s+missing|next\s+step|final\s+summary)/iu.test(normalized);
|
||||||
const hasMoneyBreakdownCue = /(?:\u0440\u0430\u0441\u043a\u0440\u043e\p{L}*\s+\u0434\u0435\u043d\p{L}*|\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+\u0432\u0441\u0435\u0433\u043e\s+\u043f\u043e\u043b\u0443\u0447|\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:\u0432\u0441\u0435\u0433\u043e\s+)?\u0437\u0430\u043f\u043b\u0430\u0442|\u0447\u0438\u0441\u0442\p{L}*\s+\u0434\u0435\u043d\u0435\u0436\u043d\p{L}*\s+\u043f\u043e\u0442\u043e\u043a|\u0433\u043b\u0430\u0432\u043d\p{L}*\s+(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a)|top\s+(?:customer|supplier)|cash\s+breakdown)/iu.test(normalized) &&
|
const hasMoneyBreakdownCue = /(?:\u0440\u0430\u0441\u043a\u0440\u043e\p{L}*\s+\u0434\u0435\u043d\p{L}*|\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+\u0432\u0441\u0435\u0433\u043e\s+\u043f\u043e\u043b\u0443\u0447|\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:\u0432\u0441\u0435\u0433\u043e\s+)?\u0437\u0430\u043f\u043b\u0430\u0442|\u0447\u0438\u0441\u0442\p{L}*\s+\u0434\u0435\u043d\u0435\u0436\u043d\p{L}*\s+\u043f\u043e\u0442\u043e\u043a|\u0433\u043b\u0430\u0432\u043d\p{L}*\s+(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a)|top\s+(?:customer|supplier)|cash\s+breakdown)/iu.test(normalized) &&
|
||||||
|
|
@ -1339,7 +1348,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
const debtDueDateBusinessOverviewSignal = ((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
const debtDueDateBusinessOverviewSignal = ((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
||||||
hasOrganizationLevelDebtDueDateOverviewSignal(rawText)) ||
|
hasOrganizationLevelDebtDueDateOverviewSignal(rawText)) ||
|
||||||
debtDueDateFollowupBusinessOverviewSignal;
|
debtDueDateFollowupBusinessOverviewSignal;
|
||||||
const supplierQualityBusinessOverviewSignal = (rawBusinessOverviewSignal || seededBusinessOverviewSignal) && hasOrganizationLevelSupplierQualityOverviewSignal(rawText);
|
const supplierQualityBusinessOverviewSignal = ((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
||||||
|
(hasOrganizationLevelSupplierQualityOverviewSignal(rawText) || hasSupplierDependencyFollowupSignal(rawText))) ||
|
||||||
|
(businessOverviewContinuationSignal && hasSupplierDependencyFollowupSignal(rawText));
|
||||||
const profitMarginFollowupBusinessOverviewSignal = businessOverviewContinuationSignal && hasProfitMarginBoundaryFollowupSignal(rawText);
|
const profitMarginFollowupBusinessOverviewSignal = businessOverviewContinuationSignal && hasProfitMarginBoundaryFollowupSignal(rawText);
|
||||||
const profitMarginBusinessOverviewSignal = ((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
const profitMarginBusinessOverviewSignal = ((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
||||||
hasOrganizationLevelProfitMarginBoundaryOverviewSignal(rawText)) ||
|
hasOrganizationLevelProfitMarginBoundaryOverviewSignal(rawText)) ||
|
||||||
|
|
@ -2389,6 +2400,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
if (debtDueDateFollowupBusinessOverviewSignal) {
|
if (debtDueDateFollowupBusinessOverviewSignal) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_business_overview_debt_due_date_followup_boundary");
|
pushReason(reasonCodes, "mcp_discovery_business_overview_debt_due_date_followup_boundary");
|
||||||
}
|
}
|
||||||
|
if (supplierQualityBusinessOverviewSignal && hasSupplierDependencyFollowupSignal(rawText)) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_business_overview_supplier_dependency_followup_boundary");
|
||||||
|
}
|
||||||
if (explicitVatSuppressesBusinessOverviewContinuation) {
|
if (explicitVatSuppressesBusinessOverviewContinuation) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_suppressed_by_explicit_vat_question");
|
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_suppressed_by_explicit_vat_question");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -309,6 +309,17 @@ function detectBroadBusinessEvaluation(text) {
|
||||||
family: "broad_business_evaluation"
|
family: "broad_business_evaluation"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (/(?:что\s+(?:дальше\s+)?провер\w*|что\s+провер\w*\s+дальше|следующ\w*\s+шаг|в\s+первую\s+очередь\s+провер\w*|next\s+checks?|next\s+steps?)/iu.test(normalized)) {
|
||||||
|
return {
|
||||||
|
family: "broad_business_evaluation"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (/(?:поставщик|закуп|исходящ|плат[её]ж|supplier|vendor|procurement|outgoing|payout)/iu.test(normalized) &&
|
||||||
|
/(?:зависим|завязан|концентрац|ключев|основн|критич|dependency|concentration|key|critical)/iu.test(normalized)) {
|
||||||
|
return {
|
||||||
|
family: "broad_business_evaluation"
|
||||||
|
};
|
||||||
|
}
|
||||||
if (hasOrganizationLevelEarningsOverviewSignal(normalized)) {
|
if (hasOrganizationLevelEarningsOverviewSignal(normalized)) {
|
||||||
return {
|
return {
|
||||||
family: "broad_business_evaluation"
|
family: "broad_business_evaluation"
|
||||||
|
|
|
||||||
|
|
@ -2558,6 +2558,17 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
/(?:поставщик|закуп|исходящ|плат[её]ж|supplier|vendor|procurement|outgoing|payout)/iu.test(normalized) &&
|
||||||
|
/(?:зависим|завязан|концентрац|ключев|основн|критич|dependency|concentration|key|critical)/iu.test(normalized)
|
||||||
|
) {
|
||||||
|
return unicodeBridgeResolution(
|
||||||
|
"unknown",
|
||||||
|
"high",
|
||||||
|
"unicode_business_overview_supplier_quality_deferred_to_discovery"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (hasDocumentCue && (hasCounterpartyCue || hasLooseCounterpartyByAnchor) && !hasContractCue) {
|
if (hasDocumentCue && (hasCounterpartyCue || hasLooseCounterpartyByAnchor) && !hasContractCue) {
|
||||||
return unicodeBridgeResolution(
|
return unicodeBridgeResolution(
|
||||||
"list_documents_by_counterparty",
|
"list_documents_by_counterparty",
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,7 @@ interface AddressTryHandleOptions {
|
||||||
llmSemanticHints?: AddressLlmSemanticHints | null;
|
llmSemanticHints?: AddressLlmSemanticHints | null;
|
||||||
activeOrganization?: string | null;
|
activeOrganization?: string | null;
|
||||||
knownOrganizations?: string[];
|
knownOrganizations?: string[];
|
||||||
|
rawUserMessage?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AddressCapabilityAudit {
|
interface AddressCapabilityAudit {
|
||||||
|
|
@ -3943,6 +3944,10 @@ export class AddressQueryService {
|
||||||
shadowRouteAudit
|
shadowRouteAudit
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const rawUserMessageForReply =
|
||||||
|
typeof options.rawUserMessage === "string" && options.rawUserMessage.trim().length > 0
|
||||||
|
? options.rawUserMessage
|
||||||
|
: undefined;
|
||||||
const composeOptionsFromFilters = (
|
const composeOptionsFromFilters = (
|
||||||
filterSet: AddressFilterSet,
|
filterSet: AddressFilterSet,
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -3951,9 +3956,14 @@ export class AddressQueryService {
|
||||||
useRubCurrency?: boolean;
|
useRubCurrency?: boolean;
|
||||||
counterpartyHint?: string;
|
counterpartyHint?: string;
|
||||||
accountHint?: string;
|
accountHint?: string;
|
||||||
|
rawUserMessage?: string | null;
|
||||||
} = {}
|
} = {}
|
||||||
) => ({
|
) => ({
|
||||||
userMessage,
|
userMessage,
|
||||||
|
rawUserMessage:
|
||||||
|
typeof options.rawUserMessage === "string" && options.rawUserMessage.trim().length > 0
|
||||||
|
? options.rawUserMessage
|
||||||
|
: rawUserMessageForReply,
|
||||||
itemHint: typeof filterSet.item === "string" ? filterSet.item : undefined,
|
itemHint: typeof filterSet.item === "string" ? filterSet.item : undefined,
|
||||||
counterpartyHint:
|
counterpartyHint:
|
||||||
typeof options.counterpartyHint === "string"
|
typeof options.counterpartyHint === "string"
|
||||||
|
|
|
||||||
|
|
@ -548,7 +548,11 @@ export function composeCounterpartyAnalyticsReply(
|
||||||
const focus = deps.detectValueRankingFocus(options.userMessage);
|
const focus = deps.detectValueRankingFocus(options.userMessage);
|
||||||
const limit = deps.detectRankingLimit(options.userMessage, 20);
|
const limit = deps.detectRankingLimit(options.userMessage, 20);
|
||||||
const minOpsForAvgCheck = deps.detectMinOpsForAvgCheck(options.userMessage);
|
const minOpsForAvgCheck = deps.detectMinOpsForAvgCheck(options.userMessage);
|
||||||
const normalizedQuestion = deps.normalizeQuestionText(options.userMessage);
|
const questionSignalText = [options.rawUserMessage, options.userMessage]
|
||||||
|
.map((value) => String(value ?? "").trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
const normalizedQuestion = deps.normalizeQuestionText(questionSignalText || options.userMessage);
|
||||||
const asksSingleBestCounterparty =
|
const asksSingleBestCounterparty =
|
||||||
focus === "top_by_total" &&
|
focus === "top_by_total" &&
|
||||||
/(?:какой|кто|which|who|какой|кто)/iu.test(normalizedQuestion) &&
|
/(?:какой|кто|which|who|какой|кто)/iu.test(normalizedQuestion) &&
|
||||||
|
|
@ -572,6 +576,10 @@ export function composeCounterpartyAnalyticsReply(
|
||||||
const effectiveLimit = asksSingleBestCounterparty || semanticSingleBestCounterparty ? 1 : limit;
|
const effectiveLimit = asksSingleBestCounterparty || semanticSingleBestCounterparty ? 1 : limit;
|
||||||
const cashflowBoundaryLine = buildCashflowBoundaryLine(isSupplier);
|
const cashflowBoundaryLine = buildCashflowBoundaryLine(isSupplier);
|
||||||
const cashflowNextStepLine = buildCashflowNextStepLine(isSupplier, normalizedQuestion);
|
const cashflowNextStepLine = buildCashflowNextStepLine(isSupplier, normalizedQuestion);
|
||||||
|
const dependencyCue =
|
||||||
|
/(?:зависим|завязан|концентрац|ключев|основн|критич|dependency|concentration|key|critical)/iu.test(
|
||||||
|
normalizedQuestion
|
||||||
|
);
|
||||||
|
|
||||||
const byCounterparty = new Map<string, CounterpartyValuePoint>();
|
const byCounterparty = new Map<string, CounterpartyValuePoint>();
|
||||||
const byYear = new Map<number, CounterpartyYearPoint>();
|
const byYear = new Map<number, CounterpartyYearPoint>();
|
||||||
|
|
@ -864,13 +872,24 @@ export function composeCounterpartyAnalyticsReply(
|
||||||
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
||||||
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
||||||
: isSupplier
|
: isSupplier
|
||||||
? `Крупнейший поставщик по подтвержденным выплатам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
? dependencyCue
|
||||||
: `Самый доходный клиент ${rankingPeriodLabel} по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`;
|
? `Главный сигнал зависимости по исходящим платежам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||||
|
: `Крупнейший поставщик по подтвержденным выплатам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||||
|
: dependencyCue
|
||||||
|
? `Главный сигнал зависимости по входящим деньгам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||||
|
: `Самый доходный клиент ${rankingPeriodLabel} по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`;
|
||||||
lines.unshift(directAnswerLine);
|
lines.unshift(directAnswerLine);
|
||||||
|
if (dependencyCue) {
|
||||||
|
lines.splice(
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
"Это сигнал концентрации по найденным денежным строкам 1С, а не подтверждение договорной зависимости, надежности контрагента или чистой прибыли."
|
||||||
|
);
|
||||||
|
}
|
||||||
if (!isSupplier) {
|
if (!isSupplier) {
|
||||||
lines.splice(1, 0, cashflowBoundaryLine);
|
lines.splice(dependencyCue ? 2 : 1, 0, cashflowBoundaryLine);
|
||||||
if (cashflowNextStepLine) {
|
if (cashflowNextStepLine) {
|
||||||
lines.splice(2, 0, cashflowNextStepLine);
|
lines.splice(dependencyCue ? 3 : 2, 0, cashflowNextStepLine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import type { AddressEvidenceStrength, AddressResponseType, AddressResultMode }
|
||||||
|
|
||||||
export interface ComposeFactualReplyOptions<TVatDirectSourceProbe = unknown> {
|
export interface ComposeFactualReplyOptions<TVatDirectSourceProbe = unknown> {
|
||||||
userMessage?: string;
|
userMessage?: string;
|
||||||
|
rawUserMessage?: string;
|
||||||
itemHint?: string;
|
itemHint?: string;
|
||||||
counterpartyHint?: string;
|
counterpartyHint?: string;
|
||||||
organizationHint?: string;
|
organizationHint?: string;
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,7 @@ export async function runAssistantAddressAttemptRuntime<ResponseType = unknown>(
|
||||||
runAddressLaneAttemptRuntimeSafe(
|
runAddressLaneAttemptRuntimeSafe(
|
||||||
buildAssistantAddressLaneAttemptRuntimeInput({
|
buildAssistantAddressLaneAttemptRuntimeInput({
|
||||||
messageUsed,
|
messageUsed,
|
||||||
|
rawUserMessage: input.userMessage,
|
||||||
carryMeta,
|
carryMeta,
|
||||||
analysisDateHint,
|
analysisDateHint,
|
||||||
llmSemanticHints,
|
llmSemanticHints,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import type { RunAssistantAddressLaneAttemptRuntimeInput } from "./assistantAddr
|
||||||
|
|
||||||
export interface BuildAssistantAddressLaneAttemptRuntimeInputInput {
|
export interface BuildAssistantAddressLaneAttemptRuntimeInputInput {
|
||||||
messageUsed: RunAssistantAddressLaneAttemptRuntimeInput["messageUsed"];
|
messageUsed: RunAssistantAddressLaneAttemptRuntimeInput["messageUsed"];
|
||||||
|
rawUserMessage: RunAssistantAddressLaneAttemptRuntimeInput["rawUserMessage"];
|
||||||
carryMeta: RunAssistantAddressLaneAttemptRuntimeInput["carryMeta"];
|
carryMeta: RunAssistantAddressLaneAttemptRuntimeInput["carryMeta"];
|
||||||
analysisDateHint: RunAssistantAddressLaneAttemptRuntimeInput["analysisDateHint"];
|
analysisDateHint: RunAssistantAddressLaneAttemptRuntimeInput["analysisDateHint"];
|
||||||
llmSemanticHints: RunAssistantAddressLaneAttemptRuntimeInput["llmSemanticHints"];
|
llmSemanticHints: RunAssistantAddressLaneAttemptRuntimeInput["llmSemanticHints"];
|
||||||
|
|
@ -17,6 +18,7 @@ export function buildAssistantAddressLaneAttemptRuntimeInput(
|
||||||
): RunAssistantAddressLaneAttemptRuntimeInput {
|
): RunAssistantAddressLaneAttemptRuntimeInput {
|
||||||
return {
|
return {
|
||||||
messageUsed: input.messageUsed,
|
messageUsed: input.messageUsed,
|
||||||
|
rawUserMessage: input.rawUserMessage,
|
||||||
carryMeta: input.carryMeta,
|
carryMeta: input.carryMeta,
|
||||||
analysisDateHint: input.analysisDateHint,
|
analysisDateHint: input.analysisDateHint,
|
||||||
llmSemanticHints: input.llmSemanticHints,
|
llmSemanticHints: input.llmSemanticHints,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ export function resolveAssistantAddressLaneAttemptFollowupContext(
|
||||||
|
|
||||||
export interface BuildAssistantAddressLaneAttemptQueryOptionsInput {
|
export interface BuildAssistantAddressLaneAttemptQueryOptionsInput {
|
||||||
analysisDateHint: RunAssistantAddressLaneAttemptRuntimeInput["analysisDateHint"];
|
analysisDateHint: RunAssistantAddressLaneAttemptRuntimeInput["analysisDateHint"];
|
||||||
|
rawUserMessage: RunAssistantAddressLaneAttemptRuntimeInput["rawUserMessage"];
|
||||||
scopedFollowupContext: Record<string, unknown> | null;
|
scopedFollowupContext: Record<string, unknown> | null;
|
||||||
llmSemanticHints: RunAssistantAddressLaneAttemptRuntimeInput["llmSemanticHints"];
|
llmSemanticHints: RunAssistantAddressLaneAttemptRuntimeInput["llmSemanticHints"];
|
||||||
activeOrganization: RunAssistantAddressLaneAttemptRuntimeInput["activeOrganization"];
|
activeOrganization: RunAssistantAddressLaneAttemptRuntimeInput["activeOrganization"];
|
||||||
|
|
@ -20,7 +21,8 @@ export function buildAssistantAddressLaneAttemptQueryOptions(
|
||||||
input: BuildAssistantAddressLaneAttemptQueryOptionsInput
|
input: BuildAssistantAddressLaneAttemptQueryOptionsInput
|
||||||
): Parameters<RunAssistantAddressLaneAttemptRuntimeInput["runAddressQueryTryHandle"]>[1] {
|
): Parameters<RunAssistantAddressLaneAttemptRuntimeInput["runAddressQueryTryHandle"]>[1] {
|
||||||
const base = {
|
const base = {
|
||||||
analysisDateHint: input.analysisDateHint
|
analysisDateHint: input.analysisDateHint,
|
||||||
|
rawUserMessage: input.rawUserMessage ?? undefined
|
||||||
} as Parameters<RunAssistantAddressLaneAttemptRuntimeInput["runAddressQueryTryHandle"]>[1];
|
} as Parameters<RunAssistantAddressLaneAttemptRuntimeInput["runAddressQueryTryHandle"]>[1];
|
||||||
if (input.scopedFollowupContext) {
|
if (input.scopedFollowupContext) {
|
||||||
base.followupContext = input.scopedFollowupContext;
|
base.followupContext = input.scopedFollowupContext;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
|
|
||||||
export interface RunAssistantAddressLaneAttemptRuntimeInput {
|
export interface RunAssistantAddressLaneAttemptRuntimeInput {
|
||||||
messageUsed: string;
|
messageUsed: string;
|
||||||
|
rawUserMessage?: string | null;
|
||||||
carryMeta: AssistantAddressCarryoverLike | null;
|
carryMeta: AssistantAddressCarryoverLike | null;
|
||||||
analysisDateHint: string | null;
|
analysisDateHint: string | null;
|
||||||
llmSemanticHints?: Record<string, unknown> | null;
|
llmSemanticHints?: Record<string, unknown> | null;
|
||||||
|
|
@ -24,6 +25,7 @@ export interface RunAssistantAddressLaneAttemptRuntimeInput {
|
||||||
llmSemanticHints?: Record<string, unknown> | null;
|
llmSemanticHints?: Record<string, unknown> | null;
|
||||||
activeOrganization?: string | null;
|
activeOrganization?: string | null;
|
||||||
knownOrganizations?: string[];
|
knownOrganizations?: string[];
|
||||||
|
rawUserMessage?: string | null;
|
||||||
}
|
}
|
||||||
) => Promise<AssistantAddressLaneLike | null>;
|
) => Promise<AssistantAddressLaneLike | null>;
|
||||||
}
|
}
|
||||||
|
|
@ -43,7 +45,8 @@ export async function runAssistantAddressLaneAttemptRuntime(
|
||||||
scopedFollowupContext,
|
scopedFollowupContext,
|
||||||
llmSemanticHints: input.llmSemanticHints ?? null,
|
llmSemanticHints: input.llmSemanticHints ?? null,
|
||||||
activeOrganization: input.activeOrganization,
|
activeOrganization: input.activeOrganization,
|
||||||
knownOrganizations: input.knownOrganizations
|
knownOrganizations: input.knownOrganizations,
|
||||||
|
rawUserMessage: input.rawUserMessage ?? input.messageUsed
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,16 @@ function requestsCounterpartyLeaderSurface(
|
||||||
return /(?:(?:кто|как\p{L}*|покаж\p{L}*|назов\p{L}*|раскро\p{L}*)[\s\S]{0,100}(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*|топ)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:топ[-\s]*(?:клиент|заказчик|поставщик|получател))|(?:клиент|поставщик)[\s\S]{0,80}(?:главн|крупнейш|основн|ведущ|топ))/iu.test(text);
|
return /(?:(?:кто|как\p{L}*|покаж\p{L}*|назов\p{L}*|раскро\p{L}*)[\s\S]{0,100}(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*|топ)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:топ[-\s]*(?:клиент|заказчик|поставщик|получател))|(?:клиент|поставщик)[\s\S]{0,80}(?:главн|крупнейш|основн|ведущ|топ))/iu.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function requestsBusinessOverviewNextChecks(
|
||||||
|
turnMeaning: Record<string, unknown> | null,
|
||||||
|
graph: Record<string, unknown> | null
|
||||||
|
): boolean {
|
||||||
|
const text = normalizedTurnAndGraphText(turnMeaning, graph);
|
||||||
|
return /(?:что\s+(?:дальше\s+)?провер\w*|что\s+провер\w*\s+дальше|следующ\w*\s+шаг|в\s+первую\s+очередь\s+провер\w*|next\s+checks?|next\s+steps?)/iu.test(
|
||||||
|
text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function requestsCompactCashflowAnswer(
|
function requestsCompactCashflowAnswer(
|
||||||
turnMeaning: Record<string, unknown> | null,
|
turnMeaning: Record<string, unknown> | null,
|
||||||
graph: Record<string, unknown> | null
|
graph: Record<string, unknown> | null
|
||||||
|
|
@ -527,6 +537,14 @@ function businessOverviewPeriodText(overview: Record<string, unknown>): string {
|
||||||
return period ? `за ${period}` : "за все доступное проверенное окно";
|
return period ? `за ${period}` : "за все доступное проверенное окно";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function businessOverviewPeriodPhrase(value: string | null | undefined): string {
|
||||||
|
const normalized = toNonEmptyString(value);
|
||||||
|
if (!normalized) {
|
||||||
|
return "за все доступное проверенное окно";
|
||||||
|
}
|
||||||
|
return /^за\s+/iu.test(normalized) ? normalized : `за ${normalized}`;
|
||||||
|
}
|
||||||
|
|
||||||
function strongestIncomingYear(overview: Record<string, unknown>): Record<string, unknown> | null {
|
function strongestIncomingYear(overview: Record<string, unknown>): Record<string, unknown> | null {
|
||||||
const years = Array.isArray(overview.yearly_breakdown) ? overview.yearly_breakdown : [];
|
const years = Array.isArray(overview.yearly_breakdown) ? overview.yearly_breakdown : [];
|
||||||
const sorted = years
|
const sorted = years
|
||||||
|
|
@ -1031,6 +1049,7 @@ function buildCompactBusinessOverviewReply(
|
||||||
const compactCashflowRequested = directMoneyOnlyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
|
const compactCashflowRequested = directMoneyOnlyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
|
||||||
const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph);
|
const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph);
|
||||||
const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph);
|
const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph);
|
||||||
|
const nextChecksRequested = requestsBusinessOverviewNextChecks(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(
|
const earningsAmountRequested = /(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0434\u043e\u0445\u043e\u0434\p{L}*)/iu.test(
|
||||||
normalizeQuestionText(rawMessage ?? "")
|
normalizeQuestionText(rawMessage ?? "")
|
||||||
|
|
@ -1268,10 +1287,13 @@ function buildCompactBusinessOverviewReply(
|
||||||
? `${vendorProcurementQuality.top_non_financial_supplier_share_pct}%`
|
? `${vendorProcurementQuality.top_non_financial_supplier_share_pct}%`
|
||||||
: null;
|
: null;
|
||||||
const periodScope = toNonEmptyString(vendorProcurementQuality.period_scope) ?? period;
|
const periodScope = toNonEmptyString(vendorProcurementQuality.period_scope) ?? period;
|
||||||
const totalText = totalOutgoing ? `; всего исходящих платежей в проверенном срезе ${totalOutgoing}` : "";
|
const periodPhrase = businessOverviewPeriodPhrase(periodScope);
|
||||||
|
const totalText = totalOutgoing
|
||||||
|
? `; всего исходящих платежей в проверенном срезе ${sentenceAmount(totalOutgoing)}`
|
||||||
|
: "";
|
||||||
if (status === "financial_institution_leads_outgoing_cash") {
|
if (status === "financial_institution_leads_outgoing_cash") {
|
||||||
lines.push(
|
lines.push(
|
||||||
`Коротко: проверка концентрации закупок/исходящих платежей за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`
|
`Коротко: проверка концентрации закупок/исходящих платежей ${periodPhrase} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`
|
||||||
);
|
);
|
||||||
const financialHintText = financialFlowHintTextRuFromRecord(topOutgoingRecord);
|
const financialHintText = financialFlowHintTextRuFromRecord(topOutgoingRecord);
|
||||||
if (financialHintText) {
|
if (financialHintText) {
|
||||||
|
|
@ -1284,11 +1306,11 @@ function buildCompactBusinessOverviewReply(
|
||||||
}
|
}
|
||||||
} else if (status === "reviewed_procurement_concentration") {
|
} else if (status === "reviewed_procurement_concentration") {
|
||||||
lines.push(
|
lines.push(
|
||||||
`Коротко: точный риск зависимости от одного поставщика не подтвержден полностью; проверка концентрации закупок/исходящих платежей за ${periodScope} нашла крупнейшего получателя исходящего потока: ${topOutgoingName ?? nonFinancialName ?? "получатель не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : nonFinancialShare ? ` держит около ${nonFinancialShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}${totalText}.`
|
`Коротко: точный риск зависимости от одного поставщика не подтвержден полностью; проверка концентрации закупок/исходящих платежей ${periodPhrase} нашла крупнейшего получателя исходящего потока: ${topOutgoingName ?? nonFinancialName ?? "получатель не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : nonFinancialShare ? ` держит около ${nonFinancialShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}${totalText}.`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
lines.push(
|
lines.push(
|
||||||
`Коротко: проверка концентрации закупок/исходящих платежей за ${periodScope} выполнена, но надежной небанковской концентрации поставщика по найденным исходящим платежам не хватает${totalText}.`
|
`Коротко: проверка концентрации закупок/исходящих платежей ${periodPhrase} выполнена, но надежной небанковской концентрации поставщика по найденным исходящим платежам не хватает${totalText}.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const contractText =
|
const contractText =
|
||||||
|
|
@ -1298,7 +1320,7 @@ function buildCompactBusinessOverviewReply(
|
||||||
: ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts} договоров.`
|
: ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts} договоров.`
|
||||||
: "";
|
: "";
|
||||||
lines.push(
|
lines.push(
|
||||||
`Что не доказано этим срезом: надежность поставщика, качество поставок, договорные условия, назначение каждого платежа и полная структура всех расходов.${contractText}`
|
`Это не аудит поставщиков: срез не доказывает надежность поставщика, качество поставок, договорные условия, назначение каждого платежа и полную структуру всех расходов.${contractText}`
|
||||||
);
|
);
|
||||||
return joinBusinessReplyLines(lines);
|
return joinBusinessReplyLines(lines);
|
||||||
}
|
}
|
||||||
|
|
@ -1352,6 +1374,26 @@ function buildCompactBusinessOverviewReply(
|
||||||
return joinBusinessReplyLines(lines);
|
return joinBusinessReplyLines(lines);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!separateSubject &&
|
||||||
|
!crossScopeExecutiveSummary &&
|
||||||
|
!counterpartyLeaderSurfaceRequested &&
|
||||||
|
!rankingNeed &&
|
||||||
|
nextChecksRequested
|
||||||
|
) {
|
||||||
|
const subject = organizationScope ? `по компании ${organizationScope}` : "по компании";
|
||||||
|
lines.push(
|
||||||
|
`В первую очередь ${subject} надо проверить чистую прибыль через закрытие 90/91/99, зависимость от топ-3 клиентов и поставщиков, долги/зависшие расчеты, складские остатки и НДС-позицию.`
|
||||||
|
);
|
||||||
|
lines.push(
|
||||||
|
"Смысл порядка: сначала отделить бухгалтерский финрезультат от денежного нетто, затем проверить концентрацию потоков и только после этого углубляться в склад, долги и налоговую позицию."
|
||||||
|
);
|
||||||
|
if (limitLine) {
|
||||||
|
lines.push(limitLine);
|
||||||
|
}
|
||||||
|
return joinBusinessReplyLines(lines);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!separateSubject &&
|
!separateSubject &&
|
||||||
!crossScopeExecutiveSummary &&
|
!crossScopeExecutiveSummary &&
|
||||||
|
|
|
||||||
|
|
@ -903,6 +903,22 @@ function hasDebtDueDateBoundaryFollowupSignal(text: string): boolean {
|
||||||
return hasDueDateCue && hasFollowupShape;
|
return hasDueDateCue && hasFollowupShape;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasSupplierDependencyFollowupSignal(text: string): boolean {
|
||||||
|
if (!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const normalized = compactLower(text);
|
||||||
|
const hasSupplierOrOutgoingCue =
|
||||||
|
/(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0437\u0430\u043a\u0443\u043f|\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u043f\u043b\u0430\u0442[\u0435\u0451]\u0436|\u0437\u0430\u043f\u043b\u0430\u0442|supplier|vendor|procurement|outgoing|payout)/iu.test(
|
||||||
|
normalized
|
||||||
|
);
|
||||||
|
const hasDependencyCue =
|
||||||
|
/(?:\u0437\u0430\u0432\u0438\u0441\u0438\u043c|\u0437\u0430\u0432\u044f\u0437\u0430\u043d|\u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446|\u043a\u043b\u044e\u0447\u0435\u0432|\u043e\u0441\u043d\u043e\u0432\u043d|\u043a\u0440\u0438\u0442\u0438\u0447|dependency|concentration|key|critical)/iu.test(
|
||||||
|
normalized
|
||||||
|
);
|
||||||
|
return hasSupplierOrOutgoingCue && hasDependencyCue;
|
||||||
|
}
|
||||||
|
|
||||||
function hasOrganizationLevelDebtDueDateOverviewSignal(text: string): boolean {
|
function hasOrganizationLevelDebtDueDateOverviewSignal(text: string): boolean {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -1082,7 +1098,7 @@ function hasBusinessOverviewContinuationSignal(text: string): boolean {
|
||||||
normalized
|
normalized
|
||||||
);
|
);
|
||||||
const hasAnalystContinuationCue =
|
const hasAnalystContinuationCue =
|
||||||
/(?:можно\s+ли|если\s+нет|proxy|прокси|аудит|оцен|что\s+думаешь|нормальн\p{L}*\s+прибыл|прибыл|марж|рентаб|ликвидн|просроч|качество\s+долг|риск|налогов\p{L}*\s+вывод)/iu.test(
|
/(?:можно\s+ли|если\s+нет|proxy|прокси|аудит|оцен|что\s+думаешь|нормальн\p{L}*\s+прибыл|прибыл|марж|рентаб|ликвидн|просроч|качество\s+долг|риск|зависим|завязан|концентрац|налогов\p{L}*\s+вывод)/iu.test(
|
||||||
normalized
|
normalized
|
||||||
);
|
);
|
||||||
const hasTaxContinuationCue =
|
const hasTaxContinuationCue =
|
||||||
|
|
@ -1868,7 +1884,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
hasOrganizationLevelDebtDueDateOverviewSignal(rawText)) ||
|
hasOrganizationLevelDebtDueDateOverviewSignal(rawText)) ||
|
||||||
debtDueDateFollowupBusinessOverviewSignal;
|
debtDueDateFollowupBusinessOverviewSignal;
|
||||||
const supplierQualityBusinessOverviewSignal =
|
const supplierQualityBusinessOverviewSignal =
|
||||||
(rawBusinessOverviewSignal || seededBusinessOverviewSignal) && hasOrganizationLevelSupplierQualityOverviewSignal(rawText);
|
((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
||||||
|
(hasOrganizationLevelSupplierQualityOverviewSignal(rawText) || hasSupplierDependencyFollowupSignal(rawText))) ||
|
||||||
|
(businessOverviewContinuationSignal && hasSupplierDependencyFollowupSignal(rawText));
|
||||||
const profitMarginFollowupBusinessOverviewSignal =
|
const profitMarginFollowupBusinessOverviewSignal =
|
||||||
businessOverviewContinuationSignal && hasProfitMarginBoundaryFollowupSignal(rawText);
|
businessOverviewContinuationSignal && hasProfitMarginBoundaryFollowupSignal(rawText);
|
||||||
const profitMarginBusinessOverviewSignal =
|
const profitMarginBusinessOverviewSignal =
|
||||||
|
|
@ -3086,6 +3104,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
if (debtDueDateFollowupBusinessOverviewSignal) {
|
if (debtDueDateFollowupBusinessOverviewSignal) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_business_overview_debt_due_date_followup_boundary");
|
pushReason(reasonCodes, "mcp_discovery_business_overview_debt_due_date_followup_boundary");
|
||||||
}
|
}
|
||||||
|
if (supplierQualityBusinessOverviewSignal && hasSupplierDependencyFollowupSignal(rawText)) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_business_overview_supplier_dependency_followup_boundary");
|
||||||
|
}
|
||||||
if (explicitVatSuppressesBusinessOverviewContinuation) {
|
if (explicitVatSuppressesBusinessOverviewContinuation) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_suppressed_by_explicit_vat_question");
|
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_suppressed_by_explicit_vat_question");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -426,6 +426,25 @@ function detectBroadBusinessEvaluation(text) {
|
||||||
family: "broad_business_evaluation"
|
family: "broad_business_evaluation"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
/(?:что\s+(?:дальше\s+)?провер\w*|что\s+провер\w*\s+дальше|следующ\w*\s+шаг|в\s+первую\s+очередь\s+провер\w*|next\s+checks?|next\s+steps?)/iu.test(
|
||||||
|
normalized
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
family: "broad_business_evaluation"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
/(?:поставщик|закуп|исходящ|плат[её]ж|supplier|vendor|procurement|outgoing|payout)/iu.test(normalized) &&
|
||||||
|
/(?:зависим|завязан|концентрац|ключев|основн|критич|dependency|concentration|key|critical)/iu.test(
|
||||||
|
normalized
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
family: "broad_business_evaluation"
|
||||||
|
};
|
||||||
|
}
|
||||||
if (hasOrganizationLevelEarningsOverviewSignal(normalized)) {
|
if (hasOrganizationLevelEarningsOverviewSignal(normalized)) {
|
||||||
return {
|
return {
|
||||||
family: "broad_business_evaluation"
|
family: "broad_business_evaluation"
|
||||||
|
|
|
||||||
|
|
@ -2949,6 +2949,12 @@ describe("address intent resolver expansion (M2.3a)", () => {
|
||||||
expect(result.reasons).toContain("unicode_business_overview_supplier_quality_deferred_to_discovery");
|
expect(result.reasons).toContain("unicode_business_overview_supplier_quality_deferred_to_discovery");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("defers supplier dependency follow-up wording to business overview discovery instead of bank ops by pseudo-counterparty", () => {
|
||||||
|
const result = resolveAddressIntent("а по поставщикам и исходящим платежам от кого зависимость?");
|
||||||
|
expect(result.intent).toBe("unknown");
|
||||||
|
expect(result.reasons).toContain("unicode_business_overview_supplier_quality_deferred_to_discovery");
|
||||||
|
});
|
||||||
|
|
||||||
it("routes reconciliation mismatch wording into open contracts intent", () => {
|
it("routes reconciliation mismatch wording into open contracts intent", () => {
|
||||||
const result = resolveAddressIntent(
|
const result = resolveAddressIntent(
|
||||||
"Покажи контрагентов, по которым сальдо скорее всего не совпадет с их актом сверки. Может, стоит поторопиться и запросить сверку?"
|
"Покажи контрагентов, по которым сальдо скорее всего не совпадет с их актом сверки. Может, стоит поторопиться и запросить сверку?"
|
||||||
|
|
|
||||||
|
|
@ -465,6 +465,64 @@ describe("assistant MCP discovery response candidate", () => {
|
||||||
expect(candidate.reply_text).not.toContain("business_overview_route_template_v1");
|
expect(candidate.reply_text).not.toContain("business_overview_route_template_v1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("formats reviewed supplier dependency answers without duplicated period or punctuation", () => {
|
||||||
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
|
entryPoint({
|
||||||
|
turn_input: {
|
||||||
|
adapter_status: "ready",
|
||||||
|
turn_meaning_ref: {
|
||||||
|
asked_domain_family: "business_overview",
|
||||||
|
asked_action_family: "vendor_risk_procurement_boundary",
|
||||||
|
unsupported_but_understood_family: "vendor_risk_procurement_boundary"
|
||||||
|
},
|
||||||
|
data_need_graph: {
|
||||||
|
business_fact_family: "business_overview",
|
||||||
|
ranking_need: null,
|
||||||
|
reason_codes: ["data_need_graph_family_business_overview"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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: {
|
||||||
|
vendor_procurement_quality: {
|
||||||
|
period_scope: "за все доступное проверенное окно",
|
||||||
|
total_outgoing_amount_human_ru: "137 963 720,37 руб.",
|
||||||
|
top_outgoing_counterparty: {
|
||||||
|
axis_value: "ГТК-Интер, ООО",
|
||||||
|
total_amount_human_ru: "44 943 460 руб."
|
||||||
|
},
|
||||||
|
top_outgoing_share_pct: 32.58,
|
||||||
|
used_contracts: 291,
|
||||||
|
total_contracts: 394,
|
||||||
|
used_contract_share_pct: 73.86,
|
||||||
|
evidence_status: "reviewed_procurement_concentration"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference",
|
||||||
|
headline: "Vendor route.",
|
||||||
|
confirmed_lines: [],
|
||||||
|
inference_lines: [],
|
||||||
|
unknown_lines: [],
|
||||||
|
limitation_lines: [],
|
||||||
|
next_step_line: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(candidate.reply_text).toContain("ГТК-Интер, ООО держит около 32.58% (44 943 460 руб.)");
|
||||||
|
expect(candidate.reply_text).toContain("Это не аудит поставщиков");
|
||||||
|
expect(candidate.reply_text).not.toContain("за за");
|
||||||
|
expect(candidate.reply_text).not.toContain("руб..");
|
||||||
|
});
|
||||||
|
|
||||||
it("uses a compact direct answer for business-overview top year questions", () => {
|
it("uses a compact direct answer for business-overview top year questions", () => {
|
||||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
entryPoint({
|
entryPoint({
|
||||||
|
|
@ -1013,6 +1071,67 @@ describe("assistant MCP discovery response candidate", () => {
|
||||||
expect(candidate.reply_text).not.toContain("wide overview");
|
expect(candidate.reply_text).not.toContain("wide overview");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("answers business overview next-check follow-ups with a concrete analyst plan first", () => {
|
||||||
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
|
entryPoint({
|
||||||
|
turn_input: {
|
||||||
|
adapter_status: "ready",
|
||||||
|
turn_meaning_ref: {
|
||||||
|
raw_message: "что дальше проверять в первую очередь?",
|
||||||
|
asked_action_family: "broad_evaluation",
|
||||||
|
unsupported_but_understood_family: "broad_business_evaluation",
|
||||||
|
explicit_organization_scope: "ООО Альтернатива Плюс"
|
||||||
|
},
|
||||||
|
data_need_graph: {
|
||||||
|
business_fact_family: "business_overview",
|
||||||
|
ranking_need: null,
|
||||||
|
reason_codes: ["data_need_graph_family_business_overview"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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: {
|
||||||
|
incoming_customer_revenue: {
|
||||||
|
total_amount_human_ru: "285 819 547,57 руб.",
|
||||||
|
coverage_limited_by_probe_limit: true
|
||||||
|
},
|
||||||
|
outgoing_supplier_payout: {
|
||||||
|
total_amount_human_ru: "137 963 720,37 руб.",
|
||||||
|
coverage_limited_by_probe_limit: true
|
||||||
|
},
|
||||||
|
net_amount_human_ru: "147 855 827,20 руб.",
|
||||||
|
net_direction: "net_incoming",
|
||||||
|
top_customers: [{ axis_value: "Комитет государственных услуг г. Москвы", total_amount_human_ru: "133 839 880,82 руб." }],
|
||||||
|
top_suppliers: [{ axis_value: "ГТК-Интер, ООО", total_amount_human_ru: "44 943 460 руб." }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference",
|
||||||
|
headline: "generic advice should not leak",
|
||||||
|
confirmed_lines: [],
|
||||||
|
inference_lines: [],
|
||||||
|
unknown_lines: [],
|
||||||
|
limitation_lines: [],
|
||||||
|
next_step_line: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const firstLine = candidate.reply_text?.split("\n")[0] ?? "";
|
||||||
|
expect(firstLine).toContain("чистую прибыль через закрытие 90/91/99");
|
||||||
|
expect(firstLine).toContain("топ-3");
|
||||||
|
expect(firstLine).toContain("долги");
|
||||||
|
expect(firstLine).toContain("складские остатки");
|
||||||
|
expect(candidate.reply_text).toContain("не считаю полным бухгалтерским оборотом");
|
||||||
|
expect(candidate.reply_text).not.toContain("generic advice");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps counterparty leaders for money followups instead of compact cashflow", () => {
|
it("keeps counterparty leaders for money followups instead of compact cashflow", () => {
|
||||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
entryPoint({
|
entryPoint({
|
||||||
|
|
|
||||||
|
|
@ -2637,6 +2637,70 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||||
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
|
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps supplier dependency follow-ups in business overview instead of pseudo-counterparty bank operations", () => {
|
||||||
|
const orgName = "ООО Альтернатива Плюс";
|
||||||
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
|
userMessage: "а по поставщикам и исходящим платежам от кого зависимость?",
|
||||||
|
assistantTurnMeaning: {
|
||||||
|
asked_domain_family: "counterparty",
|
||||||
|
asked_action_family: "list_movements",
|
||||||
|
explicit_intent_candidate: "bank_operations_by_counterparty",
|
||||||
|
explicit_entity_candidates: ["поставщикам"]
|
||||||
|
},
|
||||||
|
followupContext: {
|
||||||
|
previous_discovery_pilot_scope: "business_overview_route_template_v1",
|
||||||
|
previous_discovery_loop_selected_chain_id: "business_overview",
|
||||||
|
previous_filters: {
|
||||||
|
organization: orgName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.adapter_status).toBe("ready");
|
||||||
|
expect(result.should_run_discovery).toBe(true);
|
||||||
|
expect(result.semantic_data_need).toBe("business overview evidence with bounded analyst interpretation");
|
||||||
|
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
|
||||||
|
expect(result.turn_meaning_ref).toMatchObject({
|
||||||
|
asked_domain_family: "business_overview",
|
||||||
|
asked_action_family: "vendor_risk_procurement_boundary",
|
||||||
|
explicit_organization_scope: orgName,
|
||||||
|
unsupported_but_understood_family: "vendor_risk_procurement_boundary",
|
||||||
|
stale_replay_forbidden: true
|
||||||
|
});
|
||||||
|
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||||
|
expect(result.reason_codes).toContain("mcp_discovery_business_overview_supplier_dependency_followup_boundary");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps supplier dependency follow-ups in the procurement boundary after broad-business meaning normalization", () => {
|
||||||
|
const orgName = "ООО Альтернатива Плюс";
|
||||||
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
|
userMessage: "а по поставщикам и исходящим платежам от кого зависимость?",
|
||||||
|
assistantTurnMeaning: {
|
||||||
|
asked_domain_family: "business_overview",
|
||||||
|
asked_action_family: "broad_evaluation",
|
||||||
|
explicit_organization_scope: orgName,
|
||||||
|
unsupported_but_understood_family: "broad_business_evaluation"
|
||||||
|
},
|
||||||
|
followupContext: {
|
||||||
|
previous_discovery_pilot_scope: "business_overview_route_template_v1",
|
||||||
|
previous_filters: {
|
||||||
|
organization: orgName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.adapter_status).toBe("ready");
|
||||||
|
expect(result.should_run_discovery).toBe(true);
|
||||||
|
expect(result.turn_meaning_ref).toMatchObject({
|
||||||
|
asked_domain_family: "business_overview",
|
||||||
|
asked_action_family: "vendor_risk_procurement_boundary",
|
||||||
|
explicit_organization_scope: orgName,
|
||||||
|
unsupported_but_understood_family: "vendor_risk_procurement_boundary",
|
||||||
|
stale_replay_forbidden: true
|
||||||
|
});
|
||||||
|
expect(result.reason_codes).toContain("mcp_discovery_business_overview_supplier_dependency_followup_boundary");
|
||||||
|
});
|
||||||
|
|
||||||
it("resumes an open-scope total clarification loop from saved state when the user resolves the pending period with all-time wording", () => {
|
it("resumes an open-scope total clarification loop from saved state when the user resolves the pending period with all-time wording", () => {
|
||||||
const orgName = "ООО Альтернатива Плюс";
|
const orgName = "ООО Альтернатива Плюс";
|
||||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
|
|
|
||||||
|
|
@ -303,6 +303,22 @@ describe("assistantTurnMeaningPolicy", () => {
|
||||||
expect(supplierQuality.asked_action_family).toBe("broad_evaluation");
|
expect(supplierQuality.asked_action_family).toBe("broad_evaluation");
|
||||||
expect(supplierQuality.unsupported_but_understood_family).toBe("broad_business_evaluation");
|
expect(supplierQuality.unsupported_but_understood_family).toBe("broad_business_evaluation");
|
||||||
expect(supplierQuality.reason_codes).toContain("broad_business_evaluation_current_turn_signal");
|
expect(supplierQuality.reason_codes).toContain("broad_business_evaluation_current_turn_signal");
|
||||||
|
|
||||||
|
const supplierDependencyFollowup = policy.resolveAssistantTurnMeaning({
|
||||||
|
rawUserMessage: "а по поставщикам и исходящим платежам от кого зависимость?"
|
||||||
|
});
|
||||||
|
expect(supplierDependencyFollowup.explicit_intent_candidate).toBeNull();
|
||||||
|
expect(supplierDependencyFollowup.asked_domain_family).toBe("business_summary");
|
||||||
|
expect(supplierDependencyFollowup.asked_action_family).toBe("broad_evaluation");
|
||||||
|
expect(supplierDependencyFollowup.unsupported_but_understood_family).toBe("broad_business_evaluation");
|
||||||
|
|
||||||
|
const nextChecks = policy.resolveAssistantTurnMeaning({
|
||||||
|
rawUserMessage: "что дальше проверять в первую очередь?"
|
||||||
|
});
|
||||||
|
expect(nextChecks.explicit_intent_candidate).toBeNull();
|
||||||
|
expect(nextChecks.asked_domain_family).toBe("business_summary");
|
||||||
|
expect(nextChecks.asked_action_family).toBe("broad_evaluation");
|
||||||
|
expect(nextChecks.unsupported_but_understood_family).toBe("broad_business_evaluation");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps selected-object profitability as an inventory exact intent instead of company overview", () => {
|
it("keeps selected-object profitability as an inventory exact intent instead of company overview", () => {
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,40 @@ describe("counterparty analytics reply builders", () => {
|
||||||
expect(reply.text).not.toContain("1. 2020 |");
|
expect(reply.text).not.toContain("1. 2020 |");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("preserves raw dependency wording when the address lane receives a canonicalized inflow ranking", () => {
|
||||||
|
const reply = composeFactualReply(
|
||||||
|
"customer_revenue_and_payments",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
period: "2020-03-01T00:00:00Z",
|
||||||
|
registrator: "Поступление 1",
|
||||||
|
account_dt: "",
|
||||||
|
account_kt: "",
|
||||||
|
amount: 500,
|
||||||
|
analytics: ["Малый клиент", "Договор 1"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
period: "2020-03-02T00:00:00Z",
|
||||||
|
registrator: "Поступление 2",
|
||||||
|
account_dt: "",
|
||||||
|
account_kt: "",
|
||||||
|
amount: 1200,
|
||||||
|
analytics: ["Главный клиент", "Договор 2"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
rawUserMessage: "от кого мы больше всего зависим по входящим деньгам?",
|
||||||
|
userMessage: "определить контрагентов с наибольшим объемом входящих денежных потоков"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(reply.responseType).toBe("FACTUAL_LIST");
|
||||||
|
expect(reply.text.split("\n")[0]).toContain("Главный сигнал зависимости по входящим деньгам");
|
||||||
|
expect(reply.text.split("\n")[0]).toContain("Главный клиент");
|
||||||
|
expect(reply.text).toContain("сигнал концентрации");
|
||||||
|
expect(reply.text).toContain("не чистая прибыль");
|
||||||
|
});
|
||||||
|
|
||||||
it("explains organization activity age as 1C activity rather than legal age", () => {
|
it("explains organization activity age as 1C activity rather than legal age", () => {
|
||||||
const reply = composeFactualReply(
|
const reply = composeFactualReply(
|
||||||
"counterparty_activity_lifecycle",
|
"counterparty_activity_lifecycle",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue