Закрепить reviewed routes и защиту бизнес-ответов
This commit is contained in:
parent
b625f9af5b
commit
4fcf349894
|
|
@ -77,7 +77,7 @@ exports.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1 = toBooleanFlag(process.env.FEAT
|
|||
exports.ASSISTANT_MCP_PROXY_URL = (process.env.ASSISTANT_MCP_PROXY_URL ?? "http://127.0.0.1:6003").replace(/\/+$/, "");
|
||||
exports.ASSISTANT_MCP_CHANNEL = process.env.ASSISTANT_MCP_CHANNEL ?? "default";
|
||||
exports.ASSISTANT_MCP_TIMEOUT_MS = toNumberFlag(process.env.ASSISTANT_MCP_TIMEOUT_MS, 6000);
|
||||
exports.ASSISTANT_MCP_LIVE_LIMIT = Math.max(1, Math.trunc(toNumberFlag(process.env.ASSISTANT_MCP_LIVE_LIMIT, 24)));
|
||||
exports.ASSISTANT_MCP_LIVE_LIMIT = Math.max(1, Math.trunc(toNumberFlag(process.env.ASSISTANT_MCP_LIVE_LIMIT, 128)));
|
||||
exports.VAT_PAYABLE_68_PREFIXES = toStringListFlag(process.env.VAT_PAYABLE_68_PREFIXES, ["68.02"]);
|
||||
exports.VAT_PAYABLE_19_PREFIXES = toStringListFlag(process.env.VAT_PAYABLE_19_PREFIXES, ["19"]);
|
||||
exports.DATA_DIR = process.env.DATA_DIR ?? path_1.default.resolve(exports.MODULE_ROOT, "data");
|
||||
|
|
|
|||
|
|
@ -1733,8 +1733,7 @@ function extractAddressFilters(userMessage, intent) {
|
|||
const periodWasDerivedHeuristically = warnings.includes("period_derived_from_month_phrase") ||
|
||||
warnings.includes("period_derived_from_year_range_phrase") ||
|
||||
warnings.includes("period_derived_from_year_phrase");
|
||||
const preserveDerivedPeriodWindow = usesAsOfPrimaryWindow(intent) ||
|
||||
intent === "inventory_on_hand_as_of_date" ||
|
||||
const preserveDerivedPeriodWindow = intent === "inventory_on_hand_as_of_date" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date";
|
||||
if (periodWasDerivedHeuristically && !warnings.includes("exact_historical_period_window_requested")) {
|
||||
warnings.push("exact_historical_period_window_requested");
|
||||
|
|
|
|||
|
|
@ -1672,7 +1672,8 @@ function hasVatPeriodInspectionBridgeSignal(text) {
|
|||
const hasPeriodCue = /(?:\b(?:19|20)\d{2}\b|за\s+(?:\d{4}|год|период|квартал|месяц|январ|феврал|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр)|\b[1-4]\s*(?:кв|квартал))/iu.test(normalized);
|
||||
const hasInspectionCue = /(?:что\s+с|позици|основан|не\s+хватает|налогов[а-яё]*\s+вывод|вывод|декларац|книга\s+(?:продаж|покупок)|расшифр|разбор)/iu.test(normalized);
|
||||
const forecastOnlyCue = /(?:прогноз|план|примерн|ориентировочн)/iu.test(normalized) && !hasInspectionCue;
|
||||
return hasPeriodCue && hasInspectionCue && !forecastOnlyCue;
|
||||
const hasVatMovementInspectionCue = /(?:покаж|движен|операц|по\s+сч(?:е|ё)т|покаж|движен|операц|РїРѕ\s+СЃС‡(?:Рµ|С‘)С‚|show|movement|movements|operation|operations|account)/iu.test(normalized);
|
||||
return hasPeriodCue && (hasInspectionCue || hasVatMovementInspectionCue) && !forecastOnlyCue;
|
||||
}
|
||||
function resolveUnicodeAddressIntentBridge(text) {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
|
|
@ -2044,6 +2045,16 @@ function resolveAddressIntent(userMessage) {
|
|||
reasons
|
||||
};
|
||||
}
|
||||
const hasExplicitVatLiabilityPeriodBridge = /(?:\u043d\u0434\u0441|vat)/iu.test(text) &&
|
||||
/(?:\b(?:19|20)\d{2}\b|\u0437\u0430\s+(?:\d{4}|\u0433\u043e\u0434|\u043f\u0435\u0440\u0438\u043e\u0434|\u043a\u0432\u0430\u0440\u0442\u0430\u043b|\u043c\u0435\u0441\u044f\u0446))/iu.test(text) &&
|
||||
/(?:\u043a\u0430\u043a\u043e\u0439|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u043d\u0430\u0447\u0438\u0441\u043b|\u0443\u043f\u043b\u0430\u0447|\u0443\u043f\u043b\u0430\u0442|\u043f\u0440\u043e\u0434\u0430\u0436|\u043f\u043e\u043a\u0443\u043f|\u0432\u044b\u0447\u0435\u0442|\u043a\s+\u0443\u043f\u043b\u0430\u0442|\u043a\s+\u0432\u043e\u0437\u043c\u0435\u0449|\u043f\u043e\u0437\u0438\u0446|liability|payable|charged|paid|sales|purchase|deduction|position)/iu.test(text);
|
||||
if (hasExplicitVatLiabilityPeriodBridge) {
|
||||
return {
|
||||
intent: "vat_liability_confirmed_for_tax_period",
|
||||
confidence: "high",
|
||||
reasons: ["vat_liability_explicit_period_bridge_signal_detected"]
|
||||
};
|
||||
}
|
||||
const hasLooseVatPayableBridge = /(?:\u043d\u0434\u0441|vat)/iu.test(text) &&
|
||||
/(?:\u043a\u0430\u043a\u043e\u0439\s+\u043d\u0434\u0441\s+(?:(?:\u043d\u0430\u043c|(?:\u043c\u044b\s+)?\u0434\u043e\u043b\u0436\u043d\u044b)\s+)?(?:\u043d\u0430\u0434\u043e|\u043d\u0443\u0436\u043d\u043e|\u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e)|(?:\u043d\u0430\u043c|\u043c\u044b\s+)?\u043d\u0430\u0434\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|(?:\u043d\u0430\u043c|\u043c\u044b\s+)?\u043d\u0443\u0436\u043d\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043d\u0434\u0441\s+\u043a\s+\u0443\u043f\u043b\u0430\u0442\u0435)/iu.test(text) &&
|
||||
/(?:\u0437\u0430\s+(?:\d{4}|(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440)\S*(?:\s+(?:19|20)\d{2})?)|\u043d\u0430\s+(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440)\S*(?:\s+(?:19|20)\d{2})?|\u0432\s+(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440)\S*(?:\s+(?:19|20)\d{2})?|\b[1-4]\s*(?:\u043a\u0432\u0430\u0440\u0442\u0430\u043b|\u043a\u0432\.?)\b)/iu.test(text);
|
||||
|
|
|
|||
|
|
@ -197,6 +197,51 @@ const OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
|||
УПОРЯДОЧИТЬ ПО
|
||||
Сумма __ORDER_DIRECTION__
|
||||
`;
|
||||
const DEBT_DUE_DATE_AGING_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
__AS_OF_EXPR__ КАК Период,
|
||||
"DUE_DATE_OPEN_BALANCE" КАК Регистратор,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетДт,
|
||||
"" КАК СчетКт,
|
||||
Остатки.СуммаРазвернутыйОстатокДт КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоДт1,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоДт2,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоДт3,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК ДокументРасчетов,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация,
|
||||
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).Дата КАК ДатаДоговора,
|
||||
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).УстановленСрокОплаты КАК УстановленСрокОплаты,
|
||||
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).СрокОплаты КАК СрокОплаты,
|
||||
"debit_open_balance" КАК НаправлениеОстатка
|
||||
ИЗ
|
||||
РегистрБухгалтерии.Хозрасчетный.Остатки(__AS_OF_EXPR__, , , ) КАК Остатки
|
||||
__WHERE_DT__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
__AS_OF_EXPR__ КАК Период,
|
||||
"DUE_DATE_OPEN_BALANCE" КАК Регистратор,
|
||||
"" КАК СчетДт,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетКт,
|
||||
Остатки.СуммаРазвернутыйОстатокКт КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоДт1,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоДт2,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоДт3,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК ДокументРасчетов,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация,
|
||||
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).Дата КАК ДатаДоговора,
|
||||
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).УстановленСрокОплаты КАК УстановленСрокОплаты,
|
||||
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).СрокОплаты КАК СрокОплаты,
|
||||
"credit_open_balance" КАК НаправлениеОстатка
|
||||
ИЗ
|
||||
РегистрБухгалтерии.Хозрасчетный.Остатки(__AS_OF_EXPR__, , , ) КАК Остатки
|
||||
__WHERE_KT__
|
||||
УПОРЯДОЧИТЬ ПО
|
||||
Сумма __ORDER_DIRECTION__
|
||||
`;
|
||||
const VAT_PAYABLE_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
__AS_OF_EXPR__ КАК Период,
|
||||
|
|
@ -723,7 +768,7 @@ const BASE_RECIPES = [
|
|||
purpose: "Build customer value ranking and incoming deal profile from bank inflow docs",
|
||||
required_filters: [],
|
||||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
||||
default_limit: 20,
|
||||
default_limit: 200,
|
||||
account_scope_mode: "preferred",
|
||||
query_template: "customer_revenue_profile"
|
||||
},
|
||||
|
|
@ -733,7 +778,7 @@ const BASE_RECIPES = [
|
|||
purpose: "Build supplier payout ranking and outgoing deal profile from bank outflow docs",
|
||||
required_filters: [],
|
||||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
||||
default_limit: 20,
|
||||
default_limit: 200,
|
||||
account_scope_mode: "preferred",
|
||||
query_template: "supplier_payout_profile"
|
||||
},
|
||||
|
|
@ -778,6 +823,17 @@ const BASE_RECIPES = [
|
|||
account_scope_mode: "preferred",
|
||||
query_template: "vat_liability_confirmed_tax_period_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_accounting_financial_result_for_organization_v1",
|
||||
intent: "accounting_financial_result_for_organization",
|
||||
purpose: "Build reviewed accounting financial-result aggregate from 90/91/99 period-close movements",
|
||||
required_filters: ["period_from", "period_to"],
|
||||
optional_filters: ["organization", "limit", "sort"],
|
||||
default_limit: 32,
|
||||
account_scope: ["90", "91", "99"],
|
||||
account_scope_mode: "strict",
|
||||
query_template: "accounting_financial_result_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_inventory_on_hand_as_of_date_v1",
|
||||
intent: "inventory_on_hand_as_of_date",
|
||||
|
|
@ -888,6 +944,17 @@ const BASE_RECIPES = [
|
|||
account_scope_mode: "strict",
|
||||
query_template: "open_contracts_confirmed_as_of_balance_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_debt_due_date_aging_for_organization_v1",
|
||||
intent: "debt_due_date_aging_for_organization",
|
||||
purpose: "Check open 60/62/76 settlements against contract payment-term fields and settlement document dates before claiming overdue debt",
|
||||
required_filters: ["as_of_date"],
|
||||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
||||
default_limit: 400,
|
||||
account_scope: ["60", "62", "76"],
|
||||
account_scope_mode: "strict",
|
||||
query_template: "debt_due_date_aging_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_contracts_by_counterparty_v1",
|
||||
intent: "list_contracts_by_counterparty",
|
||||
|
|
@ -1093,6 +1160,32 @@ function buildContractValueWhereClause(filters, fieldPath, contractFieldPath) {
|
|||
`${contractFieldPath} <> ЗНАЧЕНИЕ(Справочник.ДоговорыКонтрагентов.ПустаяСсылка)`
|
||||
]);
|
||||
}
|
||||
function buildContractReferenceCondition(filters, fieldPaths) {
|
||||
const contract = typeof filters.contract === "string" ? filters.contract.trim() : "";
|
||||
if (!contract) {
|
||||
return null;
|
||||
}
|
||||
const contractTokens = Array.from(new Set(contract
|
||||
.split(/[^A-Za-zА-Яа-яЁё0-9]+/u)
|
||||
.map((token) => token.trim())
|
||||
.filter((token) => token.length >= 3)
|
||||
.filter((token) => !["договор", "дог"].includes(token.toLowerCase()))));
|
||||
const tokens = contractTokens.length > 0 ? contractTokens : [contract];
|
||||
const clauses = fieldPaths
|
||||
.map((fieldPath) => String(fieldPath ?? "").trim())
|
||||
.filter((fieldPath) => fieldPath.length > 0)
|
||||
.map((fieldPath) => {
|
||||
const tokenConditions = tokens.map((token) => {
|
||||
const escapedToken = toQueryStringLiteral(token);
|
||||
return `${fieldPath}.Наименование ПОДОБНО "%${escapedToken}%"`;
|
||||
});
|
||||
return tokenConditions.length === 1 ? tokenConditions[0] : `(${tokenConditions.join(" И ")})`;
|
||||
});
|
||||
if (clauses.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
||||
}
|
||||
function normalizeAccountTokenForQuery(value) {
|
||||
const source = String(value ?? "").trim().replace(",", ".");
|
||||
const match = source.match(/^(\d{2})(?:\.(\d{1,2}))?/);
|
||||
|
|
@ -1178,6 +1271,35 @@ function buildAccountPrefixPredicate(fieldPath, prefixes) {
|
|||
const clauses = normalizedPrefixes.map((prefix) => `ПОДСТРОКА(ЕСТЬNULL(${fieldPath}.Код, ""), 1, ${prefix.length}) = "${prefix}"`);
|
||||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
||||
}
|
||||
function buildDebtDueDateAgingWhereClause(filters, amountFieldPath, accountPredicate) {
|
||||
const conditions = [
|
||||
`${amountFieldPath} > 0`,
|
||||
`(${accountPredicate})`,
|
||||
buildOrganizationReferenceCondition(filters, ["Остатки.Организация"]),
|
||||
buildCounterpartyReferenceCondition(filters, ["Остатки.Субконто1"]),
|
||||
buildContractReferenceCondition(filters, ["Остатки.Субконто2"])
|
||||
].filter((item) => Boolean(item));
|
||||
return `ГДЕ\n ${conditions.join("\n И ")}`;
|
||||
}
|
||||
function buildDebtDueDateAgingQuery(filters, resolvedLimit) {
|
||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||
? toDateTimeExpr(filters.as_of_date, true)
|
||||
: null) ??
|
||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_to, true)
|
||||
: null) ??
|
||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ТЕКУЩАЯДАТА()";
|
||||
const accountPredicate = buildAccountPrefixPredicate("Остатки.Счет", ["60", "62", "76"]);
|
||||
return DEBT_DUE_DATE_AGING_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__WHERE_DT__", buildDebtDueDateAgingWhereClause(filters, "Остатки.СуммаРазвернутыйОстатокДт", accountPredicate))
|
||||
.replaceAll("__WHERE_KT__", buildDebtDueDateAgingWhereClause(filters, "Остатки.СуммаРазвернутыйОстатокКт", accountPredicate))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
}
|
||||
function buildInventoryMovementQuery(filters, resolvedLimit, side) {
|
||||
const debitPredicate = buildAccountPrefixPredicate("Движения.СчетДт", ["41.01"]);
|
||||
const creditPredicate = buildAccountPrefixPredicate("Движения.СчетКт", ["41.01"]);
|
||||
|
|
@ -1246,6 +1368,148 @@ function buildCounterpartyReferenceCondition(filters, fieldPaths) {
|
|||
}
|
||||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
||||
}
|
||||
const ORGANIZATION_REFERENCE_STOP_WORDS = new Set([
|
||||
"ооо",
|
||||
"зао",
|
||||
"оао",
|
||||
"ао",
|
||||
"пао",
|
||||
"ип",
|
||||
"на",
|
||||
"за",
|
||||
"по",
|
||||
"конец",
|
||||
"начало",
|
||||
"год",
|
||||
"года",
|
||||
"период",
|
||||
"можно",
|
||||
"точно",
|
||||
"понять",
|
||||
"какая",
|
||||
"какой",
|
||||
"какие",
|
||||
"какую",
|
||||
"компания",
|
||||
"компании",
|
||||
"организация",
|
||||
"организации",
|
||||
"дебиторка",
|
||||
"дебиторки",
|
||||
"кредиторка",
|
||||
"кредиторки",
|
||||
"просрочена",
|
||||
"просроченные",
|
||||
"просрочка",
|
||||
"срок",
|
||||
"оплаты",
|
||||
"прибыль",
|
||||
"маржа",
|
||||
"ндс"
|
||||
]);
|
||||
const ORGANIZATION_REFERENCE_BOUNDARY_WORDS = new Set([
|
||||
"на",
|
||||
"за",
|
||||
"конец",
|
||||
"начало",
|
||||
"можно",
|
||||
"точно",
|
||||
"понять",
|
||||
"какая",
|
||||
"какой",
|
||||
"какие",
|
||||
"какую",
|
||||
"дебиторка",
|
||||
"дебиторки",
|
||||
"кредиторка",
|
||||
"кредиторки",
|
||||
"просрочена",
|
||||
"просроченные",
|
||||
"просрочка",
|
||||
"прибыль",
|
||||
"маржа",
|
||||
"ндс"
|
||||
]);
|
||||
function organizationReferenceTokens(organization) {
|
||||
const rawTokens = organization
|
||||
.split(/[^A-Za-zА-Яа-яЁё0-9]+/u)
|
||||
.map((token) => token.trim())
|
||||
.filter((token) => token.length > 0);
|
||||
const boundaryIndex = rawTokens.findIndex((token) => {
|
||||
const lower = token.toLowerCase();
|
||||
return /^\d+$/.test(token) || ORGANIZATION_REFERENCE_BOUNDARY_WORDS.has(lower);
|
||||
});
|
||||
const scopedTokens = boundaryIndex > 0 ? rawTokens.slice(0, boundaryIndex) : rawTokens;
|
||||
return Array.from(new Set(scopedTokens
|
||||
.filter((token) => token.length >= 3)
|
||||
.filter((token) => !/^\d+$/.test(token))
|
||||
.filter((token) => !ORGANIZATION_REFERENCE_STOP_WORDS.has(token.toLowerCase())))).slice(0, 4);
|
||||
}
|
||||
function buildOrganizationReferenceCondition(filters, fieldPaths) {
|
||||
const organization = typeof filters.organization === "string" ? filters.organization.trim() : "";
|
||||
if (!organization) {
|
||||
return null;
|
||||
}
|
||||
const organizationTokens = organizationReferenceTokens(organization);
|
||||
const tokens = organizationTokens.length > 0 ? organizationTokens : [organization];
|
||||
const clauses = fieldPaths
|
||||
.map((fieldPath) => String(fieldPath ?? "").trim())
|
||||
.filter((fieldPath) => fieldPath.length > 0)
|
||||
.map((fieldPath) => {
|
||||
const tokenConditions = tokens.map((token) => {
|
||||
const escapedToken = toQueryStringLiteral(token);
|
||||
return `(Организации.Наименование ПОДОБНО "%${escapedToken}%" ИЛИ Организации.НаименованиеПолное ПОДОБНО "%${escapedToken}%")`;
|
||||
});
|
||||
const referenceSubquery = `(ВЫБРАТЬ Организации.Ссылка ИЗ Справочник.Организации КАК Организации ` +
|
||||
`ГДЕ ${tokenConditions.length === 1 ? tokenConditions[0] : tokenConditions.join(" И ")})`;
|
||||
return `${fieldPath} В ${referenceSubquery}`;
|
||||
});
|
||||
if (clauses.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
||||
}
|
||||
function buildAccountingFinancialResultAggregateSelect(filters, marker, debitLabel, creditLabel, debitPrefixes, creditPrefixes) {
|
||||
const whereClause = buildWhereClause(filters, "Движения.Период", [
|
||||
debitPrefixes.length > 0 ? buildAccountPrefixPredicate("Движения.СчетДт", debitPrefixes) : null,
|
||||
creditPrefixes.length > 0 ? buildAccountPrefixPredicate("Движения.СчетКт", creditPrefixes) : null,
|
||||
buildOrganizationReferenceCondition(filters, ["Движения.Организация"])
|
||||
].filter((item) => Boolean(item)));
|
||||
return `
|
||||
ВЫБРАТЬ
|
||||
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||||
"${marker}" КАК Регистратор,
|
||||
"${debitLabel}" КАК СчетДт,
|
||||
"${creditLabel}" КАК СчетКт,
|
||||
ЕСТЬNULL(СУММА(Движения.Сумма), 0) КАК Сумма,
|
||||
"" КАК СубконтоДт1,
|
||||
"" КАК СубконтоДт2,
|
||||
"" КАК СубконтоДт3,
|
||||
"" КАК СубконтоКт1,
|
||||
"" КАК СубконтоКт2,
|
||||
"" КАК СубконтоКт3,
|
||||
"" КАК Организация
|
||||
ИЗ
|
||||
РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто КАК Движения
|
||||
${whereClause}`;
|
||||
}
|
||||
function buildAccountingFinancialResultQuery(filters) {
|
||||
const rows = [
|
||||
["ACC90_REVENUE_KT", "ANY", "90.01", [], ["90.01"]],
|
||||
["ACC90_COST_DT", "90.02", "ANY", ["90.02"], []],
|
||||
["ACC90_SELLING_DT", "90.07", "ANY", ["90.07"], []],
|
||||
["ACC90_ADMIN_DT", "90.08", "ANY", ["90.08"], []],
|
||||
["ACC90_RESULT_TO_99_PROFIT", "90.09", "99", ["90.09"], ["99"]],
|
||||
["ACC90_RESULT_FROM_99_LOSS", "99", "90.09", ["99"], ["90.09"]],
|
||||
["ACC91_RESULT_TO_99_PROFIT", "91.09", "99", ["91.09"], ["99"]],
|
||||
["ACC91_RESULT_FROM_99_LOSS", "99", "91.09", ["99"], ["91.09"]],
|
||||
["ACC99_TO84_PROFIT_TRANSFER", "99", "84", ["99"], ["84"]],
|
||||
["ACC84_TO99_LOSS_TRANSFER", "84", "99", ["84"], ["99"]]
|
||||
];
|
||||
return rows
|
||||
.map(([marker, debitLabel, creditLabel, debitPrefixes, creditPrefixes]) => buildAccountingFinancialResultAggregateSelect(filters, marker, debitLabel, creditLabel, [...debitPrefixes], [...creditPrefixes]).trim())
|
||||
.join("\nОБЪЕДИНИТЬ ВСЕ\n");
|
||||
}
|
||||
function buildInventorySaleDocumentQuery(filters, resolvedLimit) {
|
||||
const itemCondition = buildInventoryItemReferenceCondition(filters, ["Товары.Номенклатура"]);
|
||||
return INVENTORY_SALE_DOCUMENTS_QUERY_TEMPLATE
|
||||
|
|
@ -1324,6 +1588,8 @@ function maxLimitForIntent(intent) {
|
|||
intent === "contract_usage_and_value" ||
|
||||
intent === "vat_payable_forecast" ||
|
||||
intent === "vat_liability_confirmed_for_tax_period" ||
|
||||
intent === "accounting_financial_result_for_organization" ||
|
||||
intent === "debt_due_date_aging_for_organization" ||
|
||||
intent === "inventory_on_hand_as_of_date" ||
|
||||
intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
|
|
@ -1374,7 +1640,8 @@ function buildAddressRecipePlan(recipe, filters) {
|
|||
recipe.query_template === "counterparty_roles_profile" ||
|
||||
recipe.query_template === "contract_usage_profile" ||
|
||||
recipe.query_template === "vat_payable_forecast_profile" ||
|
||||
recipe.query_template === "vat_liability_confirmed_tax_period_profile";
|
||||
recipe.query_template === "vat_liability_confirmed_tax_period_profile" ||
|
||||
recipe.query_template === "accounting_financial_result_profile";
|
||||
const baseLimit = typeof filters.limit === "number" && Number.isFinite(filters.limit)
|
||||
? Math.max(1, Math.min(maxLimit, Math.trunc(filters.limit)))
|
||||
: recipe.default_limit;
|
||||
|
|
@ -1467,6 +1734,10 @@ function buildAddressRecipePlan(recipe, filters) {
|
|||
.replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период"))
|
||||
.replaceAll("__PERIOD_TO_EXPR__", periodToExpr);
|
||||
})()
|
||||
: recipe.query_template === "accounting_financial_result_profile"
|
||||
? buildAccountingFinancialResultQuery(filters)
|
||||
: recipe.query_template === "debt_due_date_aging_profile"
|
||||
? buildDebtDueDateAgingQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "vat_payable_confirmed_as_of_balance_profile"
|
||||
? (() => {
|
||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|||
exports.contractCandidatesFromRows = contractCandidatesFromRows;
|
||||
exports.composeFactualReply = composeFactualReply;
|
||||
exports.inferReplyType = inferReplyType;
|
||||
const assistantOrganizationMatcher_1 = require("../assistantOrganizationMatcher");
|
||||
const replyPackaging_1 = require("./replyPackaging");
|
||||
const counterpartyAnalyticsReplyBuilders_1 = require("./counterpartyAnalyticsReplyBuilders");
|
||||
const inventoryReplyBuilders_1 = require("./inventoryReplyBuilders");
|
||||
|
|
@ -515,10 +516,12 @@ function detectValueRankingFocus(userMessage) {
|
|||
if (asksTotalMoneyEarned) {
|
||||
return "total_flow";
|
||||
}
|
||||
const hasCounterpartyRankingSubject = /(?:клиент|заказчик|покупател|контрагент|customer|client|counterpart|\u043a\u043b\u0438\u0435\u043d\u0442|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442)/iu.test(text);
|
||||
const asksExplicitYearBreakdown = /(?:по\s+годам|за\s+какие\s+годы|динамик\w*\s+по\s+год|yearly\s+breakdown|by\s+year|\u043f\u043e\s+\u0433\u043e\u0434\u0430\u043c|\u0437\u0430\s+\u043a\u0430\u043a\u0438\u0435\s+\u0433\u043e\u0434\u044b|\u0434\u0438\u043d\u0430\u043c\u0438\u043a\w*\s+\u043f\u043e\s+\u0433\u043e\u0434)/iu.test(text);
|
||||
const asksYearlyRevenueRanking = /(?:доходн|выручк|оборот|прибыл|деньг|денег|revenue|turnover|income)/iu.test(text) &&
|
||||
/(?:год|года|годы|year|years|по\s+годам)/iu.test(text) &&
|
||||
/(?:сам(?:ый|ая|ое|ые)|топ|луч|best|max|наибольш|больше)/iu.test(text);
|
||||
if (asksYearlyRevenueRanking) {
|
||||
if (asksYearlyRevenueRanking && (!hasCounterpartyRankingSubject || asksExplicitYearBreakdown)) {
|
||||
return "top_years_by_total";
|
||||
}
|
||||
if (/(?:сам(?:ый|ая|ое|ые)\s+высок[а-яё]*|highest|largest)\s+чек|(?:max\s+check|чек\s+макс)/iu.test(text)) {
|
||||
|
|
@ -2516,7 +2519,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||
const nonErrorProbeRows = orderedProbeRows.filter((item) => item.status !== "error");
|
||||
const visibleProbeRows = (nonErrorProbeRows.length > 0 ? nonErrorProbeRows : orderedProbeRows).slice(0, 6);
|
||||
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
|
||||
lines.push("", "Покрытие VAT-источников через MCP:", `- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`, `- Источников с ошибкой запроса: ${formatNumberWithDots(erroredSources)}.`);
|
||||
lines.push("", "Покрытие VAT-источников в 1С:", `- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`, `- Источников с ошибкой запроса: ${formatNumberWithDots(erroredSources)}.`);
|
||||
if (visibleProbeRows.length > 0) {
|
||||
lines.push(...visibleProbeRows.map((item, index) => {
|
||||
const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName;
|
||||
|
|
@ -2536,7 +2539,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||
lines.push("- Сумма прогноза выше рассчитана строго по оборотам 68.02*/19*; прямые VAT-источники показаны для проверки покрытия.");
|
||||
}
|
||||
else if (vatProbe && vatProbe.status === "error") {
|
||||
lines.push("", "Покрытие VAT-источников через MCP: probe завершился ошибкой, поэтому использован только базовый контур 68.02*/19*.");
|
||||
lines.push("", "Покрытие VAT-источников в 1С: дополнительная проверка завершилась ошибкой, поэтому использован только базовый контур 68.02*/19*.");
|
||||
}
|
||||
if (!vatActivityDetected) {
|
||||
lines.push(`В выбранном окне не найдено движений по НДС-субсчетам 68.02*/19*; поэтому оперативный прогноз к уплате равен ${formatForecastMoney(0)}.`);
|
||||
|
|
@ -2588,12 +2591,15 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||
const periodWindowLabel = options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null;
|
||||
const formatConfirmedMoney = (value) => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
||||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
||||
const organizationLabel = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.organizationHint);
|
||||
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
|
||||
const lines = [
|
||||
`Коротко: подтвержденный НДС к уплате за налоговый период — ${formatConfirmedMoney(vatToPay)}.`,
|
||||
`Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`,
|
||||
`Если смотреть на возможный перенос или переплату, получается ${formatConfirmedMoney(carryoverOrOverpayment)}.`,
|
||||
"Это подтвержденный расчет по регистрам книг продаж и покупок, без surrogate-формулы 68/19.",
|
||||
"",
|
||||
"Что вошло в расчет:",
|
||||
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
|
||||
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
|
||||
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
|
||||
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
|
||||
|
|
@ -2602,14 +2608,14 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||
if (vatProbe && vatProbe.status === "ok") {
|
||||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
||||
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
|
||||
lines.push("", "Покрытие VAT-источников через MCP:", `- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`, `- Источников с ошибкой запроса: ${formatNumberWithDots(erroredSources)}.`);
|
||||
lines.push("", "Покрытие VAT-источников в 1С:", `- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`, `- Источников с ошибкой запроса: ${formatNumberWithDots(erroredSources)}.`);
|
||||
if (vatProbe.errors.length > 0) {
|
||||
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
||||
}
|
||||
lines.push("- Сумма расчета выше получена по книгам продаж/покупок; probe использован для контроля полноты VAT-источников.");
|
||||
lines.push("- Сумма расчета выше получена по книгам продаж/покупок; дополнительная проверка использована для контроля полноты VAT-источников.");
|
||||
}
|
||||
else if (vatProbe && vatProbe.status === "error") {
|
||||
lines.push("", "Покрытие VAT-источников через MCP: дополнительный probe недоступен (например, timeout metadata).", "Итоговая сумма НДС выше рассчитана по основному маршруту книг продаж/покупок; probe влияет только на диагностику покрытия.");
|
||||
lines.push("", "Покрытие VAT-источников в 1С: дополнительная проверка недоступна, поэтому использован основной бухгалтерский срез.", "Итоговая сумма НДС выше рассчитана по основному маршруту книг продаж/покупок; probe влияет только на диагностику покрытия.");
|
||||
if (vatProbe.errors.length > 0) {
|
||||
lines.push(`- Детали probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
||||
}
|
||||
|
|
@ -2679,7 +2685,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
||||
if (vatProbe && vatProbe.status === "ok") {
|
||||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
||||
lines.push("", "Блок 2.1. MCP-проверка VAT-источников", `- VAT-объектов в метаданных 1С: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Пробных прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`);
|
||||
lines.push("", "Блок 2.1. Проверка VAT-источников в 1С", `- VAT-объектов в метаданных 1С: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Пробных прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`);
|
||||
if (vatProbe.probedSources.length > 0) {
|
||||
lines.push(...vatProbe.probedSources.slice(0, 4).map((item, index) => {
|
||||
const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName;
|
||||
|
|
@ -2696,7 +2702,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||
}
|
||||
}
|
||||
else if (vatProbe && vatProbe.status === "error") {
|
||||
lines.push("", "Блок 2.1. MCP-проверка VAT-источников", "- Probe VAT-источников завершился ошибкой, поэтому срез подтвержден по доступному бухгалтерскому источнику (68*).");
|
||||
lines.push("", "Блок 2.1. Проверка VAT-источников в 1С", "- Дополнительная проверка VAT-источников завершилась ошибкой, поэтому срез подтвержден по доступному бухгалтерскому источнику (68*).");
|
||||
}
|
||||
lines.push("", "Блок 3. Сводка", `- Строк в выборке: ${formatNumberWithDots(rows.length)}.`, `- Подтвержденных позиций по НДС: ${formatNumberWithDots(accountRows.length)}.`, "", "Блок 4. Подтвержденные позиции");
|
||||
if (accountRows.length > 0) {
|
||||
|
|
|
|||
|
|
@ -48,9 +48,9 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
|||
const includeTotal = focus === "full_profile" || focus === "total_only";
|
||||
const includeRoles = focus === "full_profile" || focus === "roles_only";
|
||||
const directLead = focus === "suppliers_only"
|
||||
? `Поставщиков (только supplier-роль): ${supplierOnly}.`
|
||||
? `Поставщиков с ролью поставщика: ${supplierOnly}.`
|
||||
: focus === "customers_only"
|
||||
? `Заказчиков (только customer-роль): ${customerOnly}.`
|
||||
? `Заказчиков с ролью покупателя: ${customerOnly}.`
|
||||
: focus === "mixed_only"
|
||||
? `Контрагентов со смешанной ролью: ${mixedActive}.`
|
||||
: includeTotal && totalCounterparties > 0
|
||||
|
|
@ -74,9 +74,9 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
|||
}
|
||||
if (includeRoles) {
|
||||
if (resolvedActive > 0 || activeCounterparties > 0) {
|
||||
lines.push("Роли контрагентов по активности:");
|
||||
lines.push(`Заказчики (только customer-роль): ${customerOnly}.`);
|
||||
lines.push(`Поставщики (только supplier-роль): ${supplierOnly}.`);
|
||||
lines.push("Распределение ролей по активности:");
|
||||
lines.push(`Заказчики с ролью покупателя: ${customerOnly}.`);
|
||||
lines.push(`Поставщики с ролью поставщика: ${supplierOnly}.`);
|
||||
lines.push(`Смешанные (и покупатель, и поставщик): ${mixedActive}.`);
|
||||
lines.push(`4. Всего активных контрагентов: ${activeCounterparties}.`);
|
||||
if (otherCounterparties !== null) {
|
||||
|
|
@ -88,10 +88,10 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
|||
}
|
||||
}
|
||||
if (focus === "suppliers_only") {
|
||||
lines.push(`Поставщиков (только supplier-роль): ${supplierOnly}.`);
|
||||
lines.push(`Поставщиков с ролью поставщика: ${supplierOnly}.`);
|
||||
}
|
||||
if (focus === "customers_only") {
|
||||
lines.push(`Заказчиков (только customer-роль): ${customerOnly}.`);
|
||||
lines.push(`Заказчиков с ролью покупателя: ${customerOnly}.`);
|
||||
}
|
||||
if (focus === "mixed_only") {
|
||||
lines.push(`Контрагентов со смешанной ролью: ${mixedActive}.`);
|
||||
|
|
@ -387,6 +387,11 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
|||
const limit = deps.detectRankingLimit(options.userMessage, 20);
|
||||
const minOpsForAvgCheck = deps.detectMinOpsForAvgCheck(options.userMessage);
|
||||
const normalizedQuestion = deps.normalizeQuestionText(options.userMessage);
|
||||
const asksSingleBestCounterparty = focus === "top_by_total" &&
|
||||
/(?:какой|кто|which|who|какой|кто)/iu.test(normalizedQuestion) &&
|
||||
/(?:больше\s+всего|сам(?:ый|ая|ое|ые)|наибольш|прин[её]с|highest|most|больше\s+всего|сам(?:ый|ая|РѕРµ|ые)|наибол|РїСЂРёРЅ[её]СЃ)/iu.test(normalizedQuestion) &&
|
||||
!/(?:\btop\b|топ|рейтинг|список|первые|покажи\s+топ|дай\s+топ|покаж\w*\s+топ|дай\s+топ)/iu.test(normalizedQuestion);
|
||||
const effectiveLimit = asksSingleBestCounterparty ? 1 : limit;
|
||||
const byCounterparty = new Map();
|
||||
const byYear = new Map();
|
||||
const deals = [];
|
||||
|
|
@ -554,7 +559,7 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
|||
? `Топ-${visible.length} поставщиков по максимальной разовой выплате:`
|
||||
: `Топ-${visible.length} заказчиков по максимальной сумме одной входящей операции:`;
|
||||
lines.unshift(heading);
|
||||
lines.push(...visible.map((item, index) => `${index + 1}. ${item.name} | max single: ${item.maxSingle} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops}`));
|
||||
lines.push(...visible.map((item, index) => `${index + 1}. ${item.name} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops}`));
|
||||
return (0, replyContracts_1.buildFactualListReply)(lines);
|
||||
}
|
||||
if (focus === "top_by_avg_check_min_ops") {
|
||||
|
|
@ -592,8 +597,11 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
|||
lines.push(...visible.map((item, index) => `${index + 1}. ${formatOptionalDate(item.period, deps.formatDateRu)} | ${item.counterparty} | ${item.registrator} | ${deps.formatMoneyRub(item.amount)}`));
|
||||
return (0, replyContracts_1.buildFactualListReply)(lines);
|
||||
}
|
||||
const visible = rankedByTotal.slice(0, limit);
|
||||
const visible = rankedByTotal.slice(0, effectiveLimit);
|
||||
const singleCandidateOnly = rankedByTotal.length === 1;
|
||||
const rankingPeriodLabel = options.periodFrom && options.periodTo
|
||||
? `за период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)}`
|
||||
: "за доступное время";
|
||||
const heading = singleCandidateOnly
|
||||
? isSupplier
|
||||
? "Найденный поставщик по сумме выплат:"
|
||||
|
|
@ -603,14 +611,17 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
|||
: `Топ-${visible.length} заказчиков по сумме поступлений:`;
|
||||
const leadingCounterparty = visible[0] ?? null;
|
||||
lines.unshift(heading);
|
||||
if (options.periodFrom && options.periodTo) {
|
||||
lines.push(`Период рейтинга: ${rankingPeriodLabel}.`);
|
||||
}
|
||||
if (leadingCounterparty) {
|
||||
const directAnswerLine = singleCandidateOnly
|
||||
? isSupplier
|
||||
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
||||
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг; сумма является денежным потоком, а не чистой прибылью.`
|
||||
: isSupplier
|
||||
? `Крупнейший поставщик по подтвержденным выплатам за доступное время: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||
: `Самый доходный клиент за доступное время по подтвержденным поступлениям: ${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} операциям). Это денежный поток, а не чистая прибыль.`;
|
||||
lines.unshift(directAnswerLine);
|
||||
}
|
||||
lines.push(...visible.map((item, index) => {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION = void 0;
|
||||
exports.buildAssistantMcpDiscoveryAnswerDraft = buildAssistantMcpDiscoveryAnswerDraft;
|
||||
const counterpartyRoleHeuristics_1 = require("./counterpartyRoleHeuristics");
|
||||
exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION = "assistant_mcp_discovery_answer_draft_v1";
|
||||
function normalizeReasonCode(value) {
|
||||
const normalized = value
|
||||
|
|
@ -371,6 +372,26 @@ function metadataRouteFamilyLabelRu(routeFamily) {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
function isInventoryReserveBoundaryTurn(pilot) {
|
||||
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
|
||||
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
|
||||
return action === "inventory_reserve_boundary" || unsupported === "inventory_reserve_liquidation_boundary";
|
||||
}
|
||||
function isProfitMarginBoundaryTurn(pilot) {
|
||||
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
|
||||
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
|
||||
return action === "profit_margin_boundary" || unsupported === "profit_margin_boundary";
|
||||
}
|
||||
function isDebtDueDateBoundaryTurn(pilot) {
|
||||
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
|
||||
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
|
||||
return action === "debt_due_date_boundary" || unsupported === "debt_due_date_boundary";
|
||||
}
|
||||
function isVendorRiskBoundaryTurn(pilot) {
|
||||
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
|
||||
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
|
||||
return action === "vendor_risk_procurement_boundary" || unsupported === "vendor_risk_procurement_boundary";
|
||||
}
|
||||
function businessOverviewInventoryUnknownLabel(overview) {
|
||||
if (overview.inventory_staleness_risk_proxy) {
|
||||
return "резервы/списания/ликвидационная стоимость склада";
|
||||
|
|
@ -433,6 +454,67 @@ function inlineBusinessOverviewAmount(value) {
|
|||
.replace(/\s*руб\.$/u, " рублей")
|
||||
.replace(/[\s.]+$/u, "");
|
||||
}
|
||||
function isFinancialInstitutionBucket(bucket) {
|
||||
if (!bucket) {
|
||||
return false;
|
||||
}
|
||||
return (bucket.counterparty_role_hint === "bank_or_financial_institution" ||
|
||||
(0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(bucket.axis_value));
|
||||
}
|
||||
function firstNonFinancialInstitutionBucket(buckets) {
|
||||
return (buckets ?? []).find((bucket) => !isFinancialInstitutionBucket(bucket)) ?? null;
|
||||
}
|
||||
function rankedBucketAmountLabel(bucket) {
|
||||
return `${bucket.axis_value} — ${bucket.total_amount_human_ru}`;
|
||||
}
|
||||
function businessOverviewIncomingLeaderLine(overview) {
|
||||
const leader = overview.top_customers[0];
|
||||
if (!leader) {
|
||||
return null;
|
||||
}
|
||||
if (!isFinancialInstitutionBucket(leader)) {
|
||||
return `Самый крупный подтвержденный клиент в проверенном срезе: ${rankedBucketAmountLabel(leader)}.`;
|
||||
}
|
||||
const nonFinancial = firstNonFinancialInstitutionBucket(overview.top_customers.slice(1));
|
||||
const nonFinancialText = nonFinancial
|
||||
? ` Крупнейший небанковский входящий контрагент в этом же срезе: ${rankedBucketAmountLabel(nonFinancial)}.`
|
||||
: "";
|
||||
return (`Крупнейший входящий денежный источник в проверенном срезе: ${rankedBucketAmountLabel(leader)}. ` +
|
||||
"По названию это банк/финансовая организация, поэтому без проверки назначения платежа не называю это клиентской выручкой или бизнес-заказчиком." +
|
||||
nonFinancialText);
|
||||
}
|
||||
function businessOverviewOutgoingLeaderLine(overview) {
|
||||
const leader = overview.top_suppliers?.[0];
|
||||
if (!leader) {
|
||||
return null;
|
||||
}
|
||||
if (!isFinancialInstitutionBucket(leader)) {
|
||||
return `Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${rankedBucketAmountLabel(leader)}.`;
|
||||
}
|
||||
const nonFinancial = firstNonFinancialInstitutionBucket(overview.top_suppliers.slice(1));
|
||||
const nonFinancialText = nonFinancial
|
||||
? ` Крупнейший небанковский получатель исходящих денег в этом же срезе: ${rankedBucketAmountLabel(nonFinancial)}.`
|
||||
: "";
|
||||
return (`Крупнейший получатель исходящих денег в проверенном срезе: ${rankedBucketAmountLabel(leader)}. ` +
|
||||
"По названию это банк/финансовая организация, поэтому без назначения платежа/договора не считаю это обычным поставщиком." +
|
||||
nonFinancialText);
|
||||
}
|
||||
function businessOverviewSupplierBoundaryBasis(overview) {
|
||||
const leader = overview.top_suppliers?.[0] ?? null;
|
||||
if (!leader) {
|
||||
return "есть только общий срез исходящих платежей без надежного vendor-risk профиля";
|
||||
}
|
||||
const share = percentText(leader.total_amount, overview.outgoing_supplier_payout.total_amount);
|
||||
if (isFinancialInstitutionBucket(leader)) {
|
||||
const base = share
|
||||
? `крупнейший получатель исходящих денег ${leader.axis_value} держит около ${share} проверенного исходящего потока (${leader.total_amount_human_ru})`
|
||||
: `крупнейший получатель исходящих денег: ${rankedBucketAmountLabel(leader)}`;
|
||||
return `${base}; по названию это банк/финансовая организация, поэтому этот факт нельзя считать доказанной зависимостью от одного обычного поставщика`;
|
||||
}
|
||||
return share
|
||||
? `крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенного исходящего потока (${leader.total_amount_human_ru})`
|
||||
: `крупнейший подтвержденный поставщик/получатель исходящих платежей: ${rankedBucketAmountLabel(leader)}`;
|
||||
}
|
||||
function businessOverviewHeadlineMetricsLine(overview) {
|
||||
const parts = [];
|
||||
if (overview.incoming_customer_revenue.rows_with_amount > 0) {
|
||||
|
|
@ -444,14 +526,72 @@ function businessOverviewHeadlineMetricsLine(overview) {
|
|||
if (overview.incoming_customer_revenue.rows_with_amount > 0 || overview.outgoing_supplier_payout.rows_with_amount > 0) {
|
||||
parts.push(`расчетное операционное нетто ${inlineBusinessOverviewAmount(overview.net_amount_human_ru)}`);
|
||||
}
|
||||
if (overview.accounting_financial_result) {
|
||||
const result = overview.accounting_financial_result;
|
||||
const direction = result.final_result_direction === "profit"
|
||||
? "учетная прибыль"
|
||||
: result.final_result_direction === "loss"
|
||||
? "учетный убыток"
|
||||
: "нулевой учетный финрезультат";
|
||||
const amount = result.final_result_direction === "loss"
|
||||
? `минус ${inlineBusinessOverviewAmount(result.final_result_amount_human_ru)}`
|
||||
: inlineBusinessOverviewAmount(result.final_result_amount_human_ru);
|
||||
const margin = result.net_margin_to_revenue_pct === null
|
||||
? "маржа к выручке 90.01 не рассчитана"
|
||||
: `маржа к выручке 90.01 ${result.net_margin_to_revenue_pct}%`;
|
||||
parts.push(`${direction} 90/91/99 ${amount}; ${margin}`);
|
||||
}
|
||||
const strongestIncomingYear = businessOverviewStrongestIncomingYear(overview);
|
||||
if (strongestIncomingYear) {
|
||||
parts.push(`самый сильный год по подтвержденным входящим поступлениям ${strongestIncomingYear.year_bucket}: ${inlineBusinessOverviewAmount(strongestIncomingYear.incoming_total_amount_human_ru)}`);
|
||||
}
|
||||
return parts.length > 0
|
||||
? `${parts.join("; ")}. Это operating-flow proxy по найденным строкам, не бухгалтерская прибыль и не финрезультат`
|
||||
? overview.accounting_financial_result
|
||||
? `${parts.join("; ")}. Финрезультат ограничен найденными строками 1С и не является внешним аудитом или юридически подтвержденной отчетностью`
|
||||
: `${parts.join("; ")}. Это operating-flow proxy по найденным строкам, не бухгалтерская прибыль и не финрезультат`
|
||||
: null;
|
||||
}
|
||||
function businessOverviewAccountingFinancialResultText(overview) {
|
||||
const result = overview.accounting_financial_result;
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
const direction = result.final_result_direction === "profit"
|
||||
? "учетная прибыль"
|
||||
: result.final_result_direction === "loss"
|
||||
? "учетный убыток"
|
||||
: "нулевой учетный финрезультат";
|
||||
const signedAmount = result.final_result_direction === "loss"
|
||||
? `минус ${result.final_result_amount_human_ru}`
|
||||
: result.final_result_amount_human_ru;
|
||||
const marginText = result.net_margin_to_revenue_pct === null
|
||||
? "маржа к выручке 90.01 не рассчитана"
|
||||
: `маржа к выручке 90.01 ${result.net_margin_to_revenue_pct}%`;
|
||||
const basis = result.final_transfer_basis === "account_99_to_84_period_close"
|
||||
? "по закрытию 99 на 84"
|
||||
: "по закрытию 90/91 на 99";
|
||||
return `По бухгалтерскому маршруту 90/91/99 за ${result.period_scope} подтвержден ${direction}: ${signedAmount}; ${marginText}. Основа: ${basis}, ${result.period_close_rows_with_amount} строк(и) закрытия периода с суммой. Это учетный финрезультат по найденным строкам 1С, не внешний аудит и не юридически подтвержденная отчетность.`;
|
||||
}
|
||||
function businessOverviewDebtDueDateAgingText(overview) {
|
||||
const aging = overview.debt_due_date_aging;
|
||||
if (!aging) {
|
||||
return null;
|
||||
}
|
||||
if (aging.evidence_status === "confirmed_overdue") {
|
||||
const top = aging.top_overdue_items?.[0] ?? null;
|
||||
const topText = top
|
||||
? ` Самая старая строка: due date ${top.due_date}, просрочка ${top.overdue_days} дн., ${top.amount_human_ru}${top.contract ? ` по договору ${top.contract}` : ""}.`
|
||||
: "";
|
||||
return `Due-date aging на ${aging.as_of_date} проверен по срокам оплаты договоров и датам расчетных документов: подтверждено просроченных строк ${aging.overdue_rows}, сумма ${aging.overdue_amount_human_ru}.${topText}`;
|
||||
}
|
||||
if (aging.evidence_status === "no_payment_terms_configured") {
|
||||
return `Due-date aging на ${aging.as_of_date} проверен по открытым расчетам: брутто ${aging.gross_open_amount_human_ru}, строк с суммой ${aging.rows_with_amount}, но в проверенных договорах срок оплаты не установлен. Подтвержденной просрочки по договорным срокам оплаты нет.`;
|
||||
}
|
||||
if (aging.evidence_status === "insufficient_due_date_basis") {
|
||||
return `Due-date aging на ${aging.as_of_date} запускался, но по строкам с установленным сроком оплаты не хватило даты расчетного документа для честного расчета due date. Просрочка не подтверждена.`;
|
||||
}
|
||||
return `Due-date aging на ${aging.as_of_date} проверен: строк с установленным сроком оплаты ${aging.rows_with_payment_terms}, подтвержденной просрочки не найдено; не просрочено по расчету ${aging.not_yet_due_amount_human_ru}.`;
|
||||
}
|
||||
function headlineFor(mode, pilot) {
|
||||
const askedMonthlyBreakdown = pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
|
||||
pilot.derived_value_flow?.aggregation_axis === "month";
|
||||
|
|
@ -469,6 +609,35 @@ function headlineFor(mode, pilot) {
|
|||
}
|
||||
if (isBusinessOverviewPilot(pilot) && pilot.derived_business_overview && mode === "confirmed_with_bounded_inference") {
|
||||
const overview = pilot.derived_business_overview;
|
||||
if (isProfitMarginBoundaryTurn(pilot)) {
|
||||
const accountingFinancialResultText = businessOverviewAccountingFinancialResultText(overview);
|
||||
if (accountingFinancialResultText) {
|
||||
return accountingFinancialResultText;
|
||||
}
|
||||
return "Нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только bounded operating-flow/trading-margin proxy, не P&L и не бухгалтерский финрезультат.";
|
||||
}
|
||||
if (isDebtDueDateBoundaryTurn(pilot)) {
|
||||
const dueDateText = businessOverviewDebtDueDateAgingText(overview);
|
||||
if (dueDateText) {
|
||||
return dueDateText;
|
||||
}
|
||||
return "Нельзя точно определить, какая дебиторка просрочена, по текущему срезу 1С; есть только debt-quality proxy, но нет проверенного due-date маршрута по договорам, срокам оплаты и погашению расчетов.";
|
||||
}
|
||||
if (isInventoryReserveBoundaryTurn(pilot)) {
|
||||
const inventoryBasis = overview.inventory_staleness_risk_proxy
|
||||
? "есть только складской staleness-risk proxy по найденным строкам"
|
||||
: overview.inventory_position || overview.inventory_turnover_proxy
|
||||
? "есть только ограниченные складские proxy-сигналы по найденным строкам"
|
||||
: "нет отдельного складского среза на дату и проверки учетной политики резервов";
|
||||
return `Коротко: точно подтвердить резерв под неликвиды по текущим данным нельзя; ${inventoryBasis}. Можно честно говорить только о необходимости отдельной проверки склада, списаний/резервов и ликвидационной стоимости, не превращая proxy в доказанный факт резерва.`;
|
||||
}
|
||||
if (isVendorRiskBoundaryTurn(pilot)) {
|
||||
const supplierLeader = overview.top_suppliers?.[0] ?? null;
|
||||
const proxyLabel = isFinancialInstitutionBucket(supplierLeader)
|
||||
? "outgoing cash concentration proxy"
|
||||
: "procurement concentration proxy";
|
||||
return `Коротко: точный риск зависимости от одного поставщика по текущим данным не подтвержден; есть только ${proxyLabel}: ${businessOverviewSupplierBoundaryBasis(overview)}. Это сигнал концентрации исходящих платежей, а не полный аудит надежности поставщиков, условий, качества и структуры всех расходов.`;
|
||||
}
|
||||
const families = [];
|
||||
if (overview.incoming_customer_revenue.rows_with_amount > 0 ||
|
||||
overview.outgoing_supplier_payout.rows_with_amount > 0) {
|
||||
|
|
@ -492,6 +661,9 @@ function headlineFor(mode, pilot) {
|
|||
if (overview.tax_position) {
|
||||
families.push("НДС-позиция");
|
||||
}
|
||||
if (overview.accounting_financial_result) {
|
||||
families.push("учетный финрезультат 90/91/99");
|
||||
}
|
||||
if (overview.trading_margin_proxy) {
|
||||
families.push("торговый margin proxy");
|
||||
}
|
||||
|
|
@ -507,6 +679,9 @@ function headlineFor(mode, pilot) {
|
|||
if (overview.debt_staleness_risk_proxy) {
|
||||
families.push("staleness risk proxy открытых расчетов");
|
||||
}
|
||||
if (overview.debt_due_date_aging) {
|
||||
families.push("due-date aging открытых расчетов");
|
||||
}
|
||||
if (overview.inventory_position) {
|
||||
families.push("складской срез на дату");
|
||||
}
|
||||
|
|
@ -516,18 +691,22 @@ function headlineFor(mode, pilot) {
|
|||
if (overview.inventory_staleness_risk_proxy) {
|
||||
families.push("staleness risk proxy склада");
|
||||
}
|
||||
const unknownFamilies = [overview.trading_margin_proxy ? "чистая прибыль/точная маржа" : "прибыль/маржа"];
|
||||
const unknownFamilies = overview.accounting_financial_result
|
||||
? ["аудированная/юридически подтвержденная прибыль"]
|
||||
: [overview.trading_margin_proxy ? "чистая прибыль/точная маржа" : "прибыль/маржа"];
|
||||
if (!overview.tax_position) {
|
||||
unknownFamilies.push("НДС");
|
||||
}
|
||||
if (!overview.debt_position) {
|
||||
unknownFamilies.push("долговой срез");
|
||||
}
|
||||
if (!overview.debt_due_date_aging) {
|
||||
unknownFamilies.push(overview.debt_staleness_risk_proxy
|
||||
? "договорные сроки оплаты/due-date просрочка"
|
||||
: overview.debt_open_settlement_quality
|
||||
? "due-date просрочка"
|
||||
: "качество открытых расчетов");
|
||||
}
|
||||
unknownFamilies.push(businessOverviewInventoryUnknownLabel(overview));
|
||||
const metricLead = businessOverviewHeadlineMetricsLine(overview);
|
||||
if (metricLead) {
|
||||
|
|
@ -725,9 +904,14 @@ function buildMustNotClaim(pilot) {
|
|||
claims.push("Do not present a debt-position snapshot as debt aging, overdue debt, or credit-quality analysis.");
|
||||
claims.push("Do not present open-settlement concentration as contractual due-date aging or confirmed overdue debt.");
|
||||
claims.push("Do not present business overview debt staleness risk proxy as confirmed overdue debt, contractual delinquency, credit risk, or due-date aging.");
|
||||
claims.push("Do not claim contractual overdue debt unless the due-date aging route found configured payment terms and enough settlement-date evidence.");
|
||||
claims.push("Do not present an inventory snapshot or purchase-date aging signal as turnover, obsolescence, liquidation value, or full inventory health.");
|
||||
claims.push("Do not present business overview inventory turnover proxy as full inventory liquidity, FIFO turnover, obsolescence analysis, or liquidation value.");
|
||||
claims.push("Do not present business overview inventory staleness risk proxy as confirmed obsolete stock, reserve, write-off, or liquidation value.");
|
||||
if (pilot.derived_business_overview?.top_customers?.some(isFinancialInstitutionBucket) ||
|
||||
pilot.derived_business_overview?.top_suppliers?.some(isFinancialInstitutionBucket)) {
|
||||
claims.push("Do not present bank-like counterparties as ordinary customers, suppliers, revenue, procurement dependency, or business quality evidence without payment-purpose/contract proof.");
|
||||
}
|
||||
if (pilot.derived_business_overview?.missing_proof_families?.length) {
|
||||
claims.push("Do not present business overview missing proof families as checked, executed, or confirmed routes.");
|
||||
}
|
||||
|
|
@ -736,6 +920,9 @@ function buildMustNotClaim(pilot) {
|
|||
if (pilot.derived_ranked_value_flow) {
|
||||
claims.push("Do not present a bounded ranking as a complete all-time ranking outside the checked period and organization.");
|
||||
claims.push("Do not imply the top-ranked counterparty is globally final when probe-limit or scope boundaries still exist.");
|
||||
if (pilot.derived_ranked_value_flow.ranked_values.some(isFinancialInstitutionBucket)) {
|
||||
claims.push("Do not present bank-like counterparties as ordinary customers, suppliers, revenue, procurement dependency, or business quality evidence without payment-purpose/contract proof.");
|
||||
}
|
||||
}
|
||||
if (isDocumentPilot(pilot)) {
|
||||
claims.push("Do not claim full document history outside the checked period.");
|
||||
|
|
@ -885,18 +1072,32 @@ function derivedRankedValueFlowConfirmedLine(pilot) {
|
|||
return null;
|
||||
}
|
||||
const leader = ranking.ranked_values[0];
|
||||
const leaderLooksFinancial = isFinancialInstitutionBucket(leader);
|
||||
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
|
||||
const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне";
|
||||
const roleCaveat = leaderLooksFinancial
|
||||
? ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? " По названию это банк/финансовая организация, поэтому без назначения платежа/договора не называю это обычным поставщиком."
|
||||
: " По названию это банк/финансовая организация, поэтому без назначения платежа не называю это клиентской выручкой или бизнес-заказчиком."
|
||||
: "";
|
||||
if (ranking.ranked_values.length === 1) {
|
||||
const singleLead = ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
const singleLead = leaderLooksFinancial
|
||||
? ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "В проверенных исходящих платежах найден один банковский/финансовый получатель"
|
||||
: "В проверенных входящих поступлениях найден один банковский/финансовый источник"
|
||||
: ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "В проверенных исходящих платежах найден один контрагент"
|
||||
: "В проверенных входящих поступлениях найден один контрагент";
|
||||
const limitCaveat = ranking.coverage_limited_by_probe_limit
|
||||
? " Лимит строк проверки достигнут; сравнение с другими контрагентами может быть неполным."
|
||||
: " Других контрагентов в этом проверенном срезе не найдено, поэтому это не полноценный сравнительный рейтинг.";
|
||||
return `${singleLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${limitCaveat}`;
|
||||
return `${singleLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${roleCaveat}${limitCaveat}`;
|
||||
}
|
||||
const directionLead = ranking.ranking_need === "bottom_asc"
|
||||
const directionLead = leaderLooksFinancial
|
||||
? ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "Крупнейший получатель исходящих денег"
|
||||
: "Крупнейший входящий денежный источник"
|
||||
: ranking.ranking_need === "bottom_asc"
|
||||
? ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "Меньше всего заплатили контрагенту"
|
||||
: "Меньше всего денег принёс контрагент"
|
||||
|
|
@ -911,7 +1112,7 @@ function derivedRankedValueFlowConfirmedLine(pilot) {
|
|||
const limitCaveat = ranking.coverage_limited_by_probe_limit
|
||||
? " Лимит строк проверки достигнут; рейтинг может быть неполным."
|
||||
: "";
|
||||
return `${directionLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${trail}${limitCaveat}`;
|
||||
return `${directionLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${roleCaveat}${trail}${limitCaveat}`;
|
||||
}
|
||||
function derivedValueFlowConfirmedLine(pilot) {
|
||||
const flow = pilot.derived_value_flow;
|
||||
|
|
@ -1070,13 +1271,13 @@ function derivedBusinessOverviewConfirmedLines(pilot) {
|
|||
if (strongestIncomingYear) {
|
||||
lines.push(`Самый сильный год по подтвержденным входящим поступлениям: ${strongestIncomingYear.year_bucket} — ${strongestIncomingYear.incoming_total_amount_human_ru} по ${strongestIncomingYear.incoming_rows_with_amount} строкам с суммой. Это не бухгалтерская прибыль.`);
|
||||
}
|
||||
const leader = overview.top_customers[0];
|
||||
if (leader) {
|
||||
lines.push(`Самый крупный подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`);
|
||||
const incomingLeaderLine = businessOverviewIncomingLeaderLine(overview);
|
||||
if (incomingLeaderLine) {
|
||||
lines.push(incomingLeaderLine);
|
||||
}
|
||||
const supplierLeader = overview.top_suppliers?.[0];
|
||||
if (supplierLeader) {
|
||||
lines.push(`Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value} — ${supplierLeader.total_amount_human_ru}.`);
|
||||
const outgoingLeaderLine = businessOverviewOutgoingLeaderLine(overview);
|
||||
if (outgoingLeaderLine) {
|
||||
lines.push(outgoingLeaderLine);
|
||||
}
|
||||
if (overview.yearly_breakdown?.length) {
|
||||
lines.push(`Годовая раскладка операционного денежного потока построена по подтвержденным строкам 1С за ${yearCountHumanRu(overview.yearly_breakdown.length)}.`);
|
||||
|
|
@ -1124,6 +1325,12 @@ function derivedBusinessOverviewConfirmedLines(pilot) {
|
|||
: "сбалансирован";
|
||||
lines.push(`НДС-позиция за ${overview.tax_position.period_scope}: книга продаж ${overview.tax_position.sales_vat_amount_human_ru}, книга покупок/вычеты ${overview.tax_position.purchase_vat_amount_human_ru}, нетто ${taxDirection} ${overview.tax_position.net_vat_amount_human_ru}.`);
|
||||
}
|
||||
if (overview.accounting_financial_result) {
|
||||
const accountingFinancialResultText = businessOverviewAccountingFinancialResultText(overview);
|
||||
if (accountingFinancialResultText) {
|
||||
lines.push(accountingFinancialResultText);
|
||||
}
|
||||
}
|
||||
if (overview.trading_margin_proxy) {
|
||||
const proxy = overview.trading_margin_proxy;
|
||||
const marginText = proxy.margin_to_revenue_pct === null ? "не рассчитана" : `${proxy.margin_to_revenue_pct}%`;
|
||||
|
|
@ -1156,6 +1363,10 @@ function derivedBusinessOverviewConfirmedLines(pilot) {
|
|||
const counterpartyText = proxy.top_contract_counterparty ? ` / ${proxy.top_contract_counterparty}` : "";
|
||||
lines.push(`Staleness risk proxy открытых расчетов на ${proxy.as_of_date}: самый старый договорный сигнал ${proxy.oldest_contract_start_date}, возраст ${proxy.max_contract_age_days} дн.; старейший крупный договор ${proxy.top_contract}${counterpartyText} держит ${proxy.top_contract_amount_human_ru} (${proxy.top_contract_share_pct}% брутто открытых остатков); оценка ${debtStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная просрочка, не кредитный риск и не due-date aging.`);
|
||||
}
|
||||
const dueDateText = businessOverviewDebtDueDateAgingText(overview);
|
||||
if (dueDateText) {
|
||||
lines.push(dueDateText);
|
||||
}
|
||||
if (overview.inventory_position) {
|
||||
const leader = overview.inventory_position.top_items[0];
|
||||
const leaderText = leader
|
||||
|
|
@ -1203,6 +1414,13 @@ function businessOverviewCustomerConcentrationLine(overview) {
|
|||
return null;
|
||||
}
|
||||
const share = percentText(leader.total_amount, overview.incoming_customer_revenue.total_amount);
|
||||
if (isFinancialInstitutionBucket(leader)) {
|
||||
const base = share
|
||||
? `Крупнейший входящий денежный источник ${leader.axis_value} дает около ${share} проверенных входящих поступлений (${leader.total_amount_human_ru})`
|
||||
: `Крупнейший входящий денежный источник в проверенном срезе: ${rankedBucketAmountLabel(leader)}`;
|
||||
const nonFinancial = firstNonFinancialInstitutionBucket(overview.top_customers.slice(1));
|
||||
return `${base}. По названию это банк/финансовая организация, поэтому это не доказывает клиентскую выручку или зависимость от клиента.${nonFinancial ? ` Крупнейший небанковский входящий контрагент: ${rankedBucketAmountLabel(nonFinancial)}.` : ""}`;
|
||||
}
|
||||
return share
|
||||
? `Концентрация входящего потока: крупнейший подтвержденный клиент ${leader.axis_value} дает около ${share} проверенных входящих поступлений (${leader.total_amount_human_ru}). Это сигнал зависимости от клиента, а не полный customer-risk аудит.`
|
||||
: `Крупнейший подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`;
|
||||
|
|
@ -1213,6 +1431,13 @@ function businessOverviewSupplierConcentrationLine(overview) {
|
|||
return null;
|
||||
}
|
||||
const share = percentText(leader.total_amount, overview.outgoing_supplier_payout.total_amount);
|
||||
if (isFinancialInstitutionBucket(leader)) {
|
||||
const base = share
|
||||
? `Концентрация исходящего потока: крупнейший получатель исходящих денег ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru})`
|
||||
: `Крупнейший получатель исходящих денег в проверенном срезе: ${rankedBucketAmountLabel(leader)}`;
|
||||
const nonFinancial = firstNonFinancialInstitutionBucket(overview.top_suppliers.slice(1));
|
||||
return `${base}. По названию это банк/финансовая организация, поэтому это не доказательство зависимости от обычного поставщика без проверки назначения платежа/договора.${nonFinancial ? ` Крупнейший небанковский получатель исходящих денег: ${rankedBucketAmountLabel(nonFinancial)}.` : ""}`;
|
||||
}
|
||||
return share
|
||||
? `Концентрация исходящего потока: крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru}). Это сигнал procurement concentration по найденным строкам, а не полный vendor-risk аудит или структура всех расходов.`
|
||||
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`;
|
||||
|
|
@ -1257,6 +1482,18 @@ function businessOverviewRiskSynthesisLine(overview) {
|
|||
: `маржинальность proxy ${overview.trading_margin_proxy.margin_to_revenue_pct}%`;
|
||||
signals.push(`торговый спред proxy ${overview.trading_margin_proxy.gross_spread_proxy_human_ru}, ${marginText}`);
|
||||
}
|
||||
if (overview.accounting_financial_result) {
|
||||
const result = overview.accounting_financial_result;
|
||||
const direction = result.final_result_direction === "profit"
|
||||
? "учетная прибыль"
|
||||
: result.final_result_direction === "loss"
|
||||
? "учетный убыток"
|
||||
: "нулевой учетный финрезультат";
|
||||
const marginText = result.net_margin_to_revenue_pct === null
|
||||
? "маржа к выручке 90.01 не рассчитана"
|
||||
: `маржа к выручке 90.01 ${result.net_margin_to_revenue_pct}%`;
|
||||
signals.push(`${direction} 90/91/99 ${result.final_result_amount_human_ru}, ${marginText}`);
|
||||
}
|
||||
if (overview.debt_position) {
|
||||
const debtDirection = overview.debt_position.net_debt_position_direction === "net_receivable"
|
||||
? `дебиторка больше кредиторки на ${overview.debt_position.net_debt_position_amount_human_ru}`
|
||||
|
|
@ -1275,6 +1512,16 @@ function businessOverviewRiskSynthesisLine(overview) {
|
|||
if (overview.debt_staleness_risk_proxy) {
|
||||
signals.push(`staleness risk proxy открытых расчетов: ${debtStalenessRiskBandRu(overview.debt_staleness_risk_proxy.risk_band)}, возраст ${overview.debt_staleness_risk_proxy.max_contract_age_days} дн., концентрация старейшего крупного договора ${overview.debt_staleness_risk_proxy.top_contract_share_pct}%`);
|
||||
}
|
||||
if (overview.debt_due_date_aging) {
|
||||
const aging = overview.debt_due_date_aging;
|
||||
signals.push(aging.evidence_status === "confirmed_overdue"
|
||||
? `due-date aging: подтвержденная просрочка ${aging.overdue_amount_human_ru}, строк ${aging.overdue_rows}`
|
||||
: aging.evidence_status === "no_payment_terms_configured"
|
||||
? "due-date aging: проверено, но сроки оплаты в договорах не установлены; подтвержденной просрочки нет"
|
||||
: aging.evidence_status === "insufficient_due_date_basis"
|
||||
? "due-date aging: не хватило даты расчетного документа для честного расчета просрочки"
|
||||
: `due-date aging: проверено, подтвержденной просрочки не найдено`);
|
||||
}
|
||||
if (overview.document_activity_profile) {
|
||||
const topDocument = overview.document_activity_profile.top_document_types[0];
|
||||
const topSection = overview.document_activity_profile.top_account_sections[0];
|
||||
|
|
@ -1411,6 +1658,9 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
|||
if (pilot.derived_business_overview?.tax_position) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_tax_position");
|
||||
}
|
||||
if (pilot.derived_business_overview?.accounting_financial_result) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_accounting_financial_result");
|
||||
}
|
||||
if (pilot.derived_business_overview?.trading_margin_proxy) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_trading_margin_proxy");
|
||||
}
|
||||
|
|
@ -1441,6 +1691,10 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
|||
if (pilot.derived_business_overview?.debt_staleness_risk_proxy) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_debt_staleness_risk_proxy");
|
||||
}
|
||||
if (pilot.derived_business_overview?.debt_due_date_aging) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_debt_due_date_aging");
|
||||
pushReason(reasonCodes, `answer_contains_business_overview_debt_due_date_aging_${pilot.derived_business_overview.debt_due_date_aging.evidence_status}`);
|
||||
}
|
||||
if (pilot.derived_business_overview?.inventory_position) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_inventory_position");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,11 @@ function isMcpDiscoveryEntryPointContract(value) {
|
|||
return (record?.schema_version === "assistant_mcp_discovery_runtime_entry_point_v1" &&
|
||||
record?.policy_owner === "assistantMcpDiscoveryRuntimeEntryPoint");
|
||||
}
|
||||
function isRouteCandidateContract(value) {
|
||||
const record = toRecordObject(value);
|
||||
return (record?.schema_version === "assistant_mcp_route_candidate_v1" &&
|
||||
record?.policy_owner === "assistantMcpDiscoveryRuntimeBridge");
|
||||
}
|
||||
function resolveEntryPoint(input) {
|
||||
if (isMcpDiscoveryEntryPointContract(input.entryPoint)) {
|
||||
return input.entryPoint;
|
||||
|
|
@ -47,6 +52,7 @@ function buildAssistantMcpDiscoveryDebugAttachmentFields(input) {
|
|||
const bridge = toRecordObject(entryPoint?.bridge);
|
||||
const planner = toRecordObject(bridge?.planner);
|
||||
const chainAlignment = toRecordObject(planner?.catalog_chain_template_alignment);
|
||||
const routeCandidate = isRouteCandidateContract(bridge?.route_candidate) ? bridge.route_candidate : null;
|
||||
const answerDraft = toRecordObject(bridge?.answer_draft);
|
||||
return {
|
||||
assistant_mcp_discovery_entry_point_v1: entryPoint,
|
||||
|
|
@ -59,6 +65,16 @@ function buildAssistantMcpDiscoveryDebugAttachmentFields(input) {
|
|||
mcp_discovery_catalog_chain_alignment_status: toNonEmptyString(chainAlignment?.alignment_status),
|
||||
mcp_discovery_catalog_chain_top_match: toNonEmptyString(chainAlignment?.top_chain_template_match),
|
||||
mcp_discovery_catalog_chain_selected_matches_top: chainAlignment?.selected_chain_matches_top === true,
|
||||
mcp_discovery_route_candidate_v1: routeCandidate,
|
||||
mcp_discovery_route_candidate_status: toNonEmptyString(routeCandidate?.candidate_status),
|
||||
mcp_discovery_route_candidate_fact_family: toNonEmptyString(routeCandidate?.business_fact_family),
|
||||
mcp_discovery_route_candidate_action_family: toNonEmptyString(routeCandidate?.action_family),
|
||||
mcp_discovery_route_candidate_proof_expectation: toNonEmptyString(routeCandidate?.proof_expectation),
|
||||
mcp_discovery_route_candidate_missing_axes: toStringArray(routeCandidate?.missing_axes),
|
||||
mcp_discovery_route_candidate_provided_axes: toStringArray(routeCandidate?.provided_axes),
|
||||
mcp_discovery_route_candidate_executable_now: routeCandidate?.executable_now === true,
|
||||
mcp_discovery_route_candidate_enablement_reason: toNonEmptyString(routeCandidate?.enablement_reason),
|
||||
mcp_discovery_route_candidate_next_action: toNonEmptyString(routeCandidate?.recommended_next_action),
|
||||
mcp_discovery_answer_mode: toNonEmptyString(answerDraft?.answer_mode),
|
||||
mcp_discovery_business_fact_answer_allowed: bridge?.business_fact_answer_allowed === true,
|
||||
mcp_discovery_user_facing_response_allowed: bridge?.user_facing_response_allowed === true,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const addressMcpClient_1 = require("./addressMcpClient");
|
|||
const assistantMcpDiscoveryRuntimeAdapter_1 = require("./assistantMcpDiscoveryRuntimeAdapter");
|
||||
const assistantMcpDiscoveryPolicy_1 = require("./assistantMcpDiscoveryPolicy");
|
||||
const addressRecipeCatalog_1 = require("./addressRecipeCatalog");
|
||||
const counterpartyRoleHeuristics_1 = require("./counterpartyRoleHeuristics");
|
||||
exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION = "assistant_mcp_discovery_pilot_executor_v1";
|
||||
const DEFAULT_DEPS = {
|
||||
executeAddressMcpQuery: addressMcpClient_1.executeAddressMcpQuery,
|
||||
|
|
@ -200,6 +201,16 @@ function buildBusinessOverviewDebtFilters(planner) {
|
|||
sort: "period_asc"
|
||||
};
|
||||
}
|
||||
function shouldRunDebtDueDateAgingProbe(planner) {
|
||||
const actionFamily = toNonEmptyString(planner.data_need_graph?.action_family);
|
||||
const turnActionFamily = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.asked_action_family);
|
||||
const unsupportedFamily = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.unsupported_but_understood_family);
|
||||
const proofExpectation = toNonEmptyString(planner.data_need_graph?.proof_expectation);
|
||||
const combined = [actionFamily, turnActionFamily, unsupportedFamily, proofExpectation]
|
||||
.filter((item) => Boolean(item))
|
||||
.join(" ");
|
||||
return /(?:debt_due_date_boundary|due[-_ ]?date|overdue|aging|просроч|срок\s+оплат|дебиторк|кредиторск)/iu.test(combined);
|
||||
}
|
||||
function buildBusinessOverviewInventoryFilters(planner) {
|
||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||
const organization = toNonEmptyString(meaning?.explicit_organization_scope);
|
||||
|
|
@ -231,6 +242,17 @@ function buildBusinessOverviewTradingMarginFilters(planner) {
|
|||
sort: "period_asc"
|
||||
};
|
||||
}
|
||||
function buildBusinessOverviewAccountingFinancialResultFilters(planner) {
|
||||
const filters = buildBusinessOverviewTradingMarginFilters(planner);
|
||||
if (!filters) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...filters,
|
||||
limit: Math.max(32, planner.discovery_plan.execution_budget.max_rows_per_probe),
|
||||
sort: "period_asc"
|
||||
};
|
||||
}
|
||||
function buildInventoryExactFilters(planner) {
|
||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||
const subject = firstEntityCandidate(planner);
|
||||
|
|
@ -715,7 +737,8 @@ async function executeCoverageAwareValueFlowQuery(input) {
|
|||
});
|
||||
executedProbeCount += 1;
|
||||
probeResults.push(queryResultToProbeResult(input.primitiveId, broadResult));
|
||||
const broadCoverageLimited = !broadResult.error && broadResult.matched_rows >= input.maxRowsPerProbe;
|
||||
const broadLimitThreshold = Math.max(1, Math.min(input.maxRowsPerProbe, broadRecipePlan.limit));
|
||||
const broadCoverageLimited = !broadResult.error && broadResult.matched_rows >= broadLimitThreshold;
|
||||
if (broadResult.error) {
|
||||
pushUnique(queryLimitations, broadResult.error);
|
||||
return {
|
||||
|
|
@ -772,7 +795,8 @@ async function executeCoverageAwareValueFlowQuery(input) {
|
|||
pushUnique(queryLimitations, chunkResult.error);
|
||||
continue;
|
||||
}
|
||||
if (chunkResult.matched_rows >= input.maxRowsPerProbe) {
|
||||
const chunkLimitThreshold = Math.max(1, Math.min(input.maxRowsPerProbe, chunkPlan.limit));
|
||||
if (chunkResult.matched_rows >= chunkLimitThreshold) {
|
||||
anyChunkLimited = true;
|
||||
}
|
||||
chunkResults.push(chunkResult);
|
||||
|
|
@ -1604,6 +1628,13 @@ function extractContractDateFromText(value) {
|
|||
if (!text || !/(?:договор|contract|дог\.)/iu.test(text)) {
|
||||
return null;
|
||||
}
|
||||
return extractAnyDateFromText(text);
|
||||
}
|
||||
function extractAnyDateFromText(value) {
|
||||
const text = toNonEmptyString(value);
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
const isoLikeMatch = text.match(/(\d{4})[-./](\d{1,2})[-./](\d{1,2})/);
|
||||
if (isoLikeMatch) {
|
||||
return normalizeDateParts(isoLikeMatch[1], isoLikeMatch[2], isoLikeMatch[3]);
|
||||
|
|
@ -1614,6 +1645,59 @@ function extractContractDateFromText(value) {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
function rowContractDateValue(row) {
|
||||
const explicit = rowTextValue(row, ["ДатаДоговора", "ContractDate", "contract_date"]);
|
||||
return extractAnyDateFromText(explicit) ?? rowOpenSettlementContractStartDateValue(row);
|
||||
}
|
||||
function rowSettlementDocumentDateValue(row) {
|
||||
const explicit = rowTextValue(row, [
|
||||
"ДатаДокументаРасчетов",
|
||||
"SettlementDocumentDate",
|
||||
"settlement_document_date"
|
||||
]);
|
||||
const settlementDocument = rowTextValue(row, [
|
||||
"ДокументРасчетов",
|
||||
"SettlementDocument",
|
||||
"settlement_document"
|
||||
]);
|
||||
return extractAnyDateFromText(explicit) ?? extractAnyDateFromText(settlementDocument) ?? extractAnyDateFromText(rowDocumentValue(row));
|
||||
}
|
||||
function rowPaymentTermIsSetValue(row) {
|
||||
const candidate = rowTextValue(row, ["УстановленСрокОплаты", "PaymentTermIsSet", "payment_term_is_set"]);
|
||||
if (typeof row["УстановленСрокОплаты"] === "boolean") {
|
||||
return row["УстановленСрокОплаты"] === true;
|
||||
}
|
||||
if (typeof row["PaymentTermIsSet"] === "boolean") {
|
||||
return row["PaymentTermIsSet"] === true;
|
||||
}
|
||||
if (!candidate) {
|
||||
return false;
|
||||
}
|
||||
return /^(?:true|истина|да|yes|1)$/iu.test(candidate.trim());
|
||||
}
|
||||
function rowPaymentTermDaysValue(row) {
|
||||
const value = rowNumberValue(row, ["СрокОплаты", "PaymentTermDays", "payment_term_days"]);
|
||||
if (value === null || !Number.isFinite(value) || value <= 0) {
|
||||
return null;
|
||||
}
|
||||
return Math.trunc(value);
|
||||
}
|
||||
function addDaysToIsoDate(isoDate, days) {
|
||||
const match = isoDate.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (!match || !Number.isFinite(days)) {
|
||||
return null;
|
||||
}
|
||||
const date = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])));
|
||||
date.setUTCDate(date.getUTCDate() + Math.trunc(days));
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return null;
|
||||
}
|
||||
return [
|
||||
String(date.getUTCFullYear()).padStart(4, "0"),
|
||||
String(date.getUTCMonth() + 1).padStart(2, "0"),
|
||||
String(date.getUTCDate()).padStart(2, "0")
|
||||
].join("-");
|
||||
}
|
||||
function earlierIsoDate(left, right) {
|
||||
if (!left) {
|
||||
return right;
|
||||
|
|
@ -2038,7 +2122,8 @@ function deriveRankedValueFlow(result, input) {
|
|||
axis_value: axisValue,
|
||||
rows_with_amount: bucket.rows_with_amount,
|
||||
total_amount: bucket.total_amount,
|
||||
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount)
|
||||
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount),
|
||||
counterparty_role_hint: (0, counterpartyRoleHeuristics_1.counterpartyRoleHintForName)(axisValue)
|
||||
}))
|
||||
.sort((left, right) => {
|
||||
const amountDelta = right.total_amount - left.total_amount;
|
||||
|
|
@ -2199,6 +2284,99 @@ function deriveBusinessOverviewTaxPosition(result, periodScope) {
|
|||
inference_basis: "sales_book_minus_purchase_book_confirmed_1c_vat_rows"
|
||||
};
|
||||
}
|
||||
function accountingFinancialResultMarkerAmount(result, marker) {
|
||||
let total = 0;
|
||||
for (const row of result.rows) {
|
||||
if (String(rowDocumentValue(row) ?? "") !== marker) {
|
||||
continue;
|
||||
}
|
||||
const amount = rowAmountValue(row);
|
||||
if (amount !== null && Number.isFinite(amount)) {
|
||||
total += amount;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
function accountingFinancialResultNonZeroCount(values) {
|
||||
return values.filter((value) => Math.abs(value) > 0).length;
|
||||
}
|
||||
function deriveBusinessOverviewAccountingFinancialResult(result, periodScope) {
|
||||
if (!result || result.error || result.matched_rows <= 0 || !periodScope) {
|
||||
return null;
|
||||
}
|
||||
const salesRevenueAccounting = accountingFinancialResultMarkerAmount(result, "ACC90_REVENUE_KT");
|
||||
const costOfSalesAccounting = accountingFinancialResultMarkerAmount(result, "ACC90_COST_DT");
|
||||
const sellingExpensesAccounting = accountingFinancialResultMarkerAmount(result, "ACC90_SELLING_DT");
|
||||
const adminExpensesAccounting = accountingFinancialResultMarkerAmount(result, "ACC90_ADMIN_DT");
|
||||
const salesProfitTo99 = accountingFinancialResultMarkerAmount(result, "ACC90_RESULT_TO_99_PROFIT");
|
||||
const salesLossFrom99 = accountingFinancialResultMarkerAmount(result, "ACC90_RESULT_FROM_99_LOSS");
|
||||
const otherProfitTo99 = accountingFinancialResultMarkerAmount(result, "ACC91_RESULT_TO_99_PROFIT");
|
||||
const otherLossFrom99 = accountingFinancialResultMarkerAmount(result, "ACC91_RESULT_FROM_99_LOSS");
|
||||
const profitTransferTo84 = accountingFinancialResultMarkerAmount(result, "ACC99_TO84_PROFIT_TRANSFER");
|
||||
const lossTransferFrom84 = accountingFinancialResultMarkerAmount(result, "ACC84_TO99_LOSS_TRANSFER");
|
||||
const amountSignals = [
|
||||
salesRevenueAccounting,
|
||||
costOfSalesAccounting,
|
||||
sellingExpensesAccounting,
|
||||
adminExpensesAccounting,
|
||||
salesProfitTo99,
|
||||
salesLossFrom99,
|
||||
otherProfitTo99,
|
||||
otherLossFrom99,
|
||||
profitTransferTo84,
|
||||
lossTransferFrom84
|
||||
];
|
||||
const rowsWithAmount = accountingFinancialResultNonZeroCount(amountSignals);
|
||||
if (rowsWithAmount <= 0) {
|
||||
return null;
|
||||
}
|
||||
const salesResultAmount = salesProfitTo99 - salesLossFrom99;
|
||||
const otherResultAmount = otherProfitTo99 - otherLossFrom99;
|
||||
const hasFinalTransfer = profitTransferTo84 > 0 || lossTransferFrom84 > 0;
|
||||
const finalResultAmount = hasFinalTransfer
|
||||
? profitTransferTo84 - lossTransferFrom84
|
||||
: salesResultAmount + otherResultAmount;
|
||||
const finalResultDirection = finalResultAmount > 0
|
||||
? "profit"
|
||||
: finalResultAmount < 0
|
||||
? "loss"
|
||||
: "balanced";
|
||||
const netMarginToRevenuePct = salesRevenueAccounting > 0 ? percentageOfTotal(finalResultAmount, salesRevenueAccounting) : null;
|
||||
const periodCloseRowsWithAmount = accountingFinancialResultNonZeroCount([
|
||||
salesProfitTo99,
|
||||
salesLossFrom99,
|
||||
otherProfitTo99,
|
||||
otherLossFrom99,
|
||||
profitTransferTo84,
|
||||
lossTransferFrom84
|
||||
]);
|
||||
return {
|
||||
period_scope: periodScope,
|
||||
rows_matched: result.matched_rows,
|
||||
rows_with_amount: rowsWithAmount,
|
||||
sales_revenue_accounting: salesRevenueAccounting,
|
||||
sales_revenue_accounting_human_ru: formatAmountHumanRu(salesRevenueAccounting),
|
||||
cost_of_sales_accounting: costOfSalesAccounting,
|
||||
cost_of_sales_accounting_human_ru: formatAmountHumanRu(costOfSalesAccounting),
|
||||
selling_expenses_accounting: sellingExpensesAccounting,
|
||||
selling_expenses_accounting_human_ru: formatAmountHumanRu(sellingExpensesAccounting),
|
||||
admin_expenses_accounting: adminExpensesAccounting,
|
||||
admin_expenses_accounting_human_ru: formatAmountHumanRu(adminExpensesAccounting),
|
||||
sales_result_amount: salesResultAmount,
|
||||
sales_result_amount_human_ru: formatAmountHumanRu(Math.abs(salesResultAmount)),
|
||||
other_result_amount: otherResultAmount,
|
||||
other_result_amount_human_ru: formatAmountHumanRu(Math.abs(otherResultAmount)),
|
||||
final_result_amount: finalResultAmount,
|
||||
final_result_amount_human_ru: formatAmountHumanRu(Math.abs(finalResultAmount)),
|
||||
final_result_direction: finalResultDirection,
|
||||
net_margin_to_revenue_pct: netMarginToRevenuePct,
|
||||
final_transfer_basis: hasFinalTransfer
|
||||
? "account_99_to_84_period_close"
|
||||
: "account_90_91_to_99_period_close",
|
||||
period_close_rows_with_amount: periodCloseRowsWithAmount,
|
||||
inference_basis: "account_90_91_99_period_close_aggregate_confirmed_1c_rows"
|
||||
};
|
||||
}
|
||||
function deriveBusinessOverviewTradingMarginProxy(result, periodScope) {
|
||||
if (!result || result.error || result.matched_rows <= 0 || !periodScope) {
|
||||
return null;
|
||||
|
|
@ -2554,6 +2732,121 @@ function deriveBusinessOverviewDebtStalenessRiskProxy(quality) {
|
|||
inference_basis: "contract_date_age_and_open_balance_concentration_confirmed_1c_rows"
|
||||
};
|
||||
}
|
||||
function deriveBusinessOverviewDebtDueDateAging(input) {
|
||||
if (!input.debtAsOfDate || !input.dueDateResult || input.dueDateResult.error || input.dueDateResult.matched_rows <= 0) {
|
||||
return null;
|
||||
}
|
||||
const overdueItems = [];
|
||||
let rowsWithAmount = 0;
|
||||
let grossOpenAmount = 0;
|
||||
let rowsWithPaymentTerms = 0;
|
||||
let rowsWithoutPaymentTerms = 0;
|
||||
let rowsWithoutDocumentDate = 0;
|
||||
let overdueAmount = 0;
|
||||
let notYetDueAmount = 0;
|
||||
let notYetDueRows = 0;
|
||||
for (const row of input.dueDateResult.rows) {
|
||||
const amount = rowAmountValue(row);
|
||||
if (amount === null) {
|
||||
continue;
|
||||
}
|
||||
const absAmount = Math.abs(amount);
|
||||
if (absAmount <= 0) {
|
||||
continue;
|
||||
}
|
||||
rowsWithAmount += 1;
|
||||
grossOpenAmount += absAmount;
|
||||
const paymentTermIsSet = rowPaymentTermIsSetValue(row);
|
||||
const paymentTermDays = rowPaymentTermDaysValue(row);
|
||||
if (!paymentTermIsSet || paymentTermDays === null) {
|
||||
rowsWithoutPaymentTerms += 1;
|
||||
continue;
|
||||
}
|
||||
rowsWithPaymentTerms += 1;
|
||||
const documentDate = rowSettlementDocumentDateValue(row) ?? rowContractDateValue(row);
|
||||
if (!documentDate) {
|
||||
rowsWithoutDocumentDate += 1;
|
||||
continue;
|
||||
}
|
||||
const dueDate = addDaysToIsoDate(documentDate, paymentTermDays);
|
||||
if (!dueDate) {
|
||||
rowsWithoutDocumentDate += 1;
|
||||
continue;
|
||||
}
|
||||
const overdueDays = dueDate < input.debtAsOfDate ? daysBetweenIsoDates(dueDate, input.debtAsOfDate) : null;
|
||||
if (overdueDays !== null && overdueDays > 0) {
|
||||
overdueAmount += absAmount;
|
||||
overdueItems.push({
|
||||
counterparty: rowCounterpartyValue(row),
|
||||
contract: rowContractValue(row),
|
||||
settlement_document: rowTextValue(row, [
|
||||
"ДокументРасчетов",
|
||||
"SettlementDocument",
|
||||
"settlement_document"
|
||||
]) ?? rowDocumentValue(row),
|
||||
document_date: documentDate,
|
||||
due_date: dueDate,
|
||||
payment_term_days: paymentTermDays,
|
||||
overdue_days: overdueDays,
|
||||
amount: absAmount,
|
||||
amount_human_ru: formatAmountHumanRu(absAmount)
|
||||
});
|
||||
}
|
||||
else {
|
||||
notYetDueRows += 1;
|
||||
notYetDueAmount += absAmount;
|
||||
}
|
||||
}
|
||||
if (rowsWithAmount <= 0) {
|
||||
return null;
|
||||
}
|
||||
const topOverdueItems = overdueItems
|
||||
.sort((left, right) => {
|
||||
const daysDelta = right.overdue_days - left.overdue_days;
|
||||
if (daysDelta !== 0) {
|
||||
return daysDelta;
|
||||
}
|
||||
const amountDelta = right.amount - left.amount;
|
||||
return amountDelta !== 0 ? amountDelta : String(left.contract ?? "").localeCompare(String(right.contract ?? ""), "ru");
|
||||
})
|
||||
.slice(0, 5)
|
||||
.map((item) => ({
|
||||
...item,
|
||||
share_of_overdue_amount_pct: percentageOfTotal(item.amount, overdueAmount)
|
||||
}));
|
||||
const oldestDueDate = overdueItems
|
||||
.map((item) => item.due_date)
|
||||
.sort()[0] ?? null;
|
||||
const maxOverdueDays = overdueItems.reduce((max, item) => max === null ? item.overdue_days : Math.max(max, item.overdue_days), null);
|
||||
const evidenceStatus = overdueItems.length > 0
|
||||
? "confirmed_overdue"
|
||||
: rowsWithPaymentTerms <= 0
|
||||
? "no_payment_terms_configured"
|
||||
: rowsWithoutDocumentDate >= rowsWithPaymentTerms
|
||||
? "insufficient_due_date_basis"
|
||||
: "no_overdue_found";
|
||||
return {
|
||||
as_of_date: input.debtAsOfDate,
|
||||
rows_matched: input.dueDateResult.matched_rows,
|
||||
rows_with_amount: rowsWithAmount,
|
||||
gross_open_amount: grossOpenAmount,
|
||||
gross_open_amount_human_ru: formatAmountHumanRu(grossOpenAmount),
|
||||
rows_with_payment_terms: rowsWithPaymentTerms,
|
||||
rows_without_payment_terms: rowsWithoutPaymentTerms,
|
||||
rows_without_document_date: rowsWithoutDocumentDate,
|
||||
overdue_rows: overdueItems.length,
|
||||
overdue_amount: overdueAmount,
|
||||
overdue_amount_human_ru: formatAmountHumanRu(overdueAmount),
|
||||
not_yet_due_rows: notYetDueRows,
|
||||
not_yet_due_amount: notYetDueAmount,
|
||||
not_yet_due_amount_human_ru: formatAmountHumanRu(notYetDueAmount),
|
||||
oldest_due_date: oldestDueDate,
|
||||
max_overdue_days: maxOverdueDays,
|
||||
top_overdue_items: topOverdueItems,
|
||||
evidence_status: evidenceStatus,
|
||||
inference_basis: "contract_payment_terms_and_settlement_document_dates_from_open_balance_rows"
|
||||
};
|
||||
}
|
||||
function debtStalenessRiskBandRu(riskBand) {
|
||||
if (riskBand === "high") {
|
||||
return "высокая зона внимания";
|
||||
|
|
@ -2748,7 +3041,7 @@ function buildBusinessOverviewMissingProofFamilies(input) {
|
|||
items.push(item);
|
||||
}
|
||||
};
|
||||
if (missing.has("profit_margin") || missing.has("accounting_profit_margin")) {
|
||||
if ((missing.has("profit_margin") || missing.has("accounting_profit_margin")) && !input.accountingFinancialResult) {
|
||||
pushUnique({
|
||||
family: "accounting_profit_margin",
|
||||
current_status: input.tradingMarginProxy ? "proxy_only_currently" : "reviewed_route_not_wired",
|
||||
|
|
@ -2759,7 +3052,7 @@ function buildBusinessOverviewMissingProofFamilies(input) {
|
|||
must_not_claim: "clean_profit_accounting_margin_or_full_pnl"
|
||||
});
|
||||
}
|
||||
if (missing.has("debt_due_date_aging_quality") || missing.has("debt_open_settlement_quality")) {
|
||||
if ((missing.has("debt_due_date_aging_quality") || missing.has("debt_open_settlement_quality")) && !input.debtDueDateAging) {
|
||||
pushUnique({
|
||||
family: "debt_due_date_aging_quality",
|
||||
current_status: input.debtStalenessRiskProxy
|
||||
|
|
@ -2830,6 +3123,7 @@ function deriveBusinessOverview(input) {
|
|||
});
|
||||
const activityPeriod = deriveActivityPeriod(input.lifecycleResult);
|
||||
const taxPosition = deriveBusinessOverviewTaxPosition(input.taxResult, input.periodScope);
|
||||
const accountingFinancialResult = deriveBusinessOverviewAccountingFinancialResult(input.accountingFinancialResultResult, input.periodScope);
|
||||
const tradingMarginProxy = deriveBusinessOverviewTradingMarginProxy(input.tradingMarginResult, input.periodScope);
|
||||
const debtPosition = deriveBusinessOverviewDebtPosition({
|
||||
receivablesResult: input.receivablesResult,
|
||||
|
|
@ -2844,6 +3138,10 @@ function deriveBusinessOverview(input) {
|
|||
const counterpartyProfile = deriveBusinessOverviewCounterpartyProfile(input.counterpartyProfileResult, input.periodScope);
|
||||
const contractUsageProfile = deriveBusinessOverviewContractUsageProfile(input.contractUsageProfileResult, input.periodScope);
|
||||
const debtStalenessRiskProxy = deriveBusinessOverviewDebtStalenessRiskProxy(debtOpenSettlementQuality);
|
||||
const debtDueDateAging = deriveBusinessOverviewDebtDueDateAging({
|
||||
dueDateResult: input.dueDateAgingResult,
|
||||
debtAsOfDate: input.debtAsOfDate
|
||||
});
|
||||
const inventoryPosition = deriveBusinessOverviewInventoryPosition({
|
||||
inventoryOnHandResult: input.inventoryOnHandResult,
|
||||
inventoryAgingResult: input.inventoryAgingResult,
|
||||
|
|
@ -2862,10 +3160,12 @@ function deriveBusinessOverview(input) {
|
|||
outgoing.rows_with_amount > 0,
|
||||
Boolean(activityPeriod),
|
||||
Boolean(taxPosition),
|
||||
Boolean(accountingFinancialResult),
|
||||
Boolean(tradingMarginProxy),
|
||||
Boolean(debtPosition),
|
||||
Boolean(debtOpenSettlementQuality),
|
||||
Boolean(debtStalenessRiskProxy),
|
||||
Boolean(debtDueDateAging),
|
||||
Boolean(documentActivityProfile),
|
||||
Boolean(counterpartyProfile),
|
||||
Boolean(contractUsageProfile),
|
||||
|
|
@ -2879,9 +3179,9 @@ function deriveBusinessOverview(input) {
|
|||
const netAmount = incoming.total_amount - outgoing.total_amount;
|
||||
const hasBusinessOverviewProfileSignal = Boolean(documentActivityProfile || counterpartyProfile || contractUsageProfile);
|
||||
const missingSignalFamilies = [
|
||||
tradingMarginProxy ? "accounting_profit_margin" : "profit_margin",
|
||||
accountingFinancialResult ? null : tradingMarginProxy ? "accounting_profit_margin" : "profit_margin",
|
||||
debtPosition ? null : "debt_position",
|
||||
debtOpenSettlementQuality ? "debt_due_date_aging_quality" : "debt_open_settlement_quality",
|
||||
debtDueDateAging ? null : debtOpenSettlementQuality ? "debt_due_date_aging_quality" : "debt_open_settlement_quality",
|
||||
taxPosition ? null : "tax_position",
|
||||
inventoryPosition
|
||||
? inventoryStalenessRiskProxy
|
||||
|
|
@ -2894,9 +3194,11 @@ function deriveBusinessOverview(input) {
|
|||
].filter((item) => Boolean(item));
|
||||
const missingProofFamilies = buildBusinessOverviewMissingProofFamilies({
|
||||
missingSignalFamilies,
|
||||
accountingFinancialResult,
|
||||
tradingMarginProxy,
|
||||
debtOpenSettlementQuality,
|
||||
debtStalenessRiskProxy,
|
||||
debtDueDateAging,
|
||||
inventoryPosition,
|
||||
inventoryTurnoverProxy,
|
||||
inventoryStalenessRiskProxy,
|
||||
|
|
@ -2915,10 +3217,12 @@ function deriveBusinessOverview(input) {
|
|||
yearly_breakdown: yearlyBreakdown,
|
||||
activity_period: activityPeriod,
|
||||
tax_position: taxPosition,
|
||||
accounting_financial_result: accountingFinancialResult,
|
||||
trading_margin_proxy: tradingMarginProxy,
|
||||
debt_position: debtPosition,
|
||||
debt_open_settlement_quality: debtOpenSettlementQuality,
|
||||
debt_staleness_risk_proxy: debtStalenessRiskProxy,
|
||||
debt_due_date_aging: debtDueDateAging,
|
||||
inventory_position: inventoryPosition,
|
||||
inventory_turnover_proxy: inventoryTurnoverProxy,
|
||||
inventory_staleness_risk_proxy: inventoryStalenessRiskProxy,
|
||||
|
|
@ -2929,9 +3233,9 @@ function deriveBusinessOverview(input) {
|
|||
checked_signal_count: checkedSignalCount,
|
||||
missing_signal_families: missingSignalFamilies,
|
||||
missing_proof_families: missingProofFamilies,
|
||||
inference_basis: hasBusinessOverviewProfileSignal || inventoryPosition
|
||||
inference_basis: hasBusinessOverviewProfileSignal || inventoryPosition || accountingFinancialResult
|
||||
? "business_overview_from_confirmed_1c_multi_family_rows"
|
||||
: debtOpenSettlementQuality
|
||||
: debtOpenSettlementQuality || debtDueDateAging
|
||||
? "business_overview_from_confirmed_1c_multi_family_rows"
|
||||
: taxPosition && debtPosition
|
||||
? "business_overview_from_confirmed_1c_money_activity_tax_and_debt_rows"
|
||||
|
|
@ -2956,6 +3260,9 @@ function summarizeBusinessOverviewRows(input) {
|
|||
if (input.taxResult && !input.taxResult.error) {
|
||||
parts.push(`${input.taxResult.fetched_rows} VAT/tax rows fetched, ${input.taxResult.matched_rows} matched`);
|
||||
}
|
||||
if (input.accountingFinancialResultResult && !input.accountingFinancialResultResult.error) {
|
||||
parts.push(`${input.accountingFinancialResultResult.fetched_rows} accounting financial-result aggregate rows fetched, ${input.accountingFinancialResultResult.matched_rows} matched`);
|
||||
}
|
||||
if (input.tradingMarginResult && !input.tradingMarginResult.error) {
|
||||
parts.push(`${input.tradingMarginResult.fetched_rows} trading-margin document rows fetched, ${input.tradingMarginResult.matched_rows} matched`);
|
||||
}
|
||||
|
|
@ -2968,6 +3275,9 @@ function summarizeBusinessOverviewRows(input) {
|
|||
if (input.openContractsResult && !input.openContractsResult.error) {
|
||||
parts.push(`${input.openContractsResult.fetched_rows} open-contract rows fetched, ${input.openContractsResult.matched_rows} matched`);
|
||||
}
|
||||
if (input.dueDateAgingResult && !input.dueDateAgingResult.error) {
|
||||
parts.push(`${input.dueDateAgingResult.fetched_rows} due-date aging rows fetched, ${input.dueDateAgingResult.matched_rows} matched`);
|
||||
}
|
||||
if (input.documentActivityProfileResult && !input.documentActivityProfileResult.error) {
|
||||
parts.push(`${input.documentActivityProfileResult.fetched_rows} document/account-section profile rows fetched, ${input.documentActivityProfileResult.matched_rows} matched`);
|
||||
}
|
||||
|
|
@ -3000,12 +3310,30 @@ function buildBusinessOverviewConfirmedFacts(derived) {
|
|||
}
|
||||
if (derived.top_customers.length > 0) {
|
||||
const leader = derived.top_customers[0];
|
||||
if ((0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(leader.axis_value)) {
|
||||
facts.push(`Крупнейший входящий денежный источник в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}. По названию это банк/финансовая организация, поэтому без назначения платежа это не доказанный клиент или бизнес-выручка.`);
|
||||
const nonFinancialLeader = derived.top_customers.slice(1).find((item) => !(0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(item.axis_value));
|
||||
if (nonFinancialLeader) {
|
||||
facts.push(`Крупнейший небанковский входящий контрагент в проверенном срезе: ${nonFinancialLeader.axis_value} — ${nonFinancialLeader.total_amount_human_ru}.`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
facts.push(`Самый крупный подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`);
|
||||
}
|
||||
}
|
||||
if (derived.top_suppliers.length > 0) {
|
||||
const leader = derived.top_suppliers[0];
|
||||
if ((0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(leader.axis_value)) {
|
||||
facts.push(`Крупнейший получатель исходящих денег в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}. По названию это банк/финансовая организация, поэтому без назначения платежа/договора это не доказанный обычный поставщик.`);
|
||||
const nonFinancialLeader = derived.top_suppliers.slice(1).find((item) => !(0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(item.axis_value));
|
||||
if (nonFinancialLeader) {
|
||||
facts.push(`Крупнейший небанковский получатель исходящих денег в проверенном срезе: ${nonFinancialLeader.axis_value} — ${nonFinancialLeader.total_amount_human_ru}.`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
facts.push(`Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`);
|
||||
}
|
||||
}
|
||||
if (derived.yearly_breakdown.length > 0) {
|
||||
facts.push(`Годовая раскладка операционного денежного потока построена по подтвержденным строкам 1С за ${yearCountHumanRu(derived.yearly_breakdown.length)}.`);
|
||||
}
|
||||
|
|
@ -3087,6 +3415,25 @@ function buildBusinessOverviewConfirmedFacts(derived) {
|
|||
const counterpartyText = proxy.top_contract_counterparty ? ` / ${proxy.top_contract_counterparty}` : "";
|
||||
facts.push(`Staleness risk proxy открытых расчетов на ${proxy.as_of_date}: самый старый договорный сигнал ${proxy.oldest_contract_start_date}, возраст ${proxy.max_contract_age_days} дн.; старейший крупный договор ${proxy.top_contract}${counterpartyText} держит ${proxy.top_contract_amount_human_ru} (${proxy.top_contract_share_pct}% брутто открытых остатков); оценка ${debtStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная просрочка, не кредитный риск и не due-date aging.`);
|
||||
}
|
||||
if (derived.debt_due_date_aging) {
|
||||
const aging = derived.debt_due_date_aging;
|
||||
if (aging.evidence_status === "confirmed_overdue") {
|
||||
const top = aging.top_overdue_items[0];
|
||||
const topText = top
|
||||
? ` Самая старая просрочка: ${top.due_date}, ${top.overdue_days} дн., ${top.amount_human_ru}${top.contract ? ` по договору ${top.contract}` : ""}.`
|
||||
: "";
|
||||
facts.push(`Due-date aging открытых расчетов на ${aging.as_of_date} проверен по срокам оплаты договоров и датам расчетных документов: подтверждено просроченных строк ${aging.overdue_rows}, сумма ${aging.overdue_amount_human_ru}.${topText}`);
|
||||
}
|
||||
else if (aging.evidence_status === "no_payment_terms_configured") {
|
||||
facts.push(`Due-date aging открытых расчетов на ${aging.as_of_date} проверен по ${aging.rows_with_amount} строкам с суммой: брутто открытых остатков ${aging.gross_open_amount_human_ru}, но в проверенных договорах срок оплаты не установлен. Подтвержденной просрочки по договорным срокам оплаты нет.`);
|
||||
}
|
||||
else if (aging.evidence_status === "insufficient_due_date_basis") {
|
||||
facts.push(`Due-date aging открытых расчетов на ${aging.as_of_date} запускался по ${aging.rows_with_payment_terms} строкам с установленным сроком оплаты, но в найденных строках не хватило даты расчетного документа для честного расчета due date. Просрочка не подтверждена.`);
|
||||
}
|
||||
else {
|
||||
facts.push(`Due-date aging открытых расчетов на ${aging.as_of_date} проверен: строк с установленным сроком оплаты ${aging.rows_with_payment_terms}, подтвержденной просрочки не найдено; не просрочено по расчету ${aging.not_yet_due_amount_human_ru}.`);
|
||||
}
|
||||
}
|
||||
if (derived.inventory_position) {
|
||||
const leader = derived.inventory_position.top_items[0];
|
||||
const leaderText = leader
|
||||
|
|
@ -3133,6 +3480,9 @@ function buildBusinessOverviewInferredFacts(derived) {
|
|||
const supplierSharePct = supplierLeader
|
||||
? percentageOfTotal(supplierLeader.total_amount, derived.outgoing_supplier_payout.total_amount)
|
||||
: null;
|
||||
const supplierLeaderIsFinancial = supplierLeader
|
||||
? (0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(supplierLeader.axis_value)
|
||||
: false;
|
||||
const strongestIncomingYear = [...derived.yearly_breakdown]
|
||||
.filter((bucket) => bucket.incoming_total_amount > 0)
|
||||
.sort((left, right) => right.incoming_total_amount - left.incoming_total_amount || left.year_bucket.localeCompare(right.year_bucket))[0];
|
||||
|
|
@ -3142,7 +3492,11 @@ function buildBusinessOverviewInferredFacts(derived) {
|
|||
return [
|
||||
`Расчетное нетто по найденным строкам: ${derived.net_amount_human_ru}; ${direction}.`,
|
||||
supplierLeader
|
||||
? supplierLeaderIsFinancial
|
||||
? supplierSharePct !== null
|
||||
? `Крупнейший получатель исходящих денег ${supplierLeader.axis_value} держит около ${supplierSharePct}% проверенного исходящего потока (${supplierLeader.total_amount_human_ru}). По названию это банк/финансовая организация, поэтому это outgoing cash concentration proxy, а не доказанный vendor-risk по обычному поставщику.`
|
||||
: `Крупнейший получатель исходящих денег в проверенном срезе: ${supplierLeader.axis_value} — ${supplierLeader.total_amount_human_ru}. По названию это банк/финансовая организация, поэтому это не доказанный обычный поставщик.`
|
||||
: supplierSharePct !== null
|
||||
? `Крупнейший подтвержденный поставщик/получатель исходящих платежей ${supplierLeader.axis_value} держит около ${supplierSharePct}% проверенного исходящего потока (${supplierLeader.total_amount_human_ru}). Это procurement concentration proxy по найденным строкам, а не полный vendor-risk аудит.`
|
||||
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value} — ${supplierLeader.total_amount_human_ru}.`
|
||||
: null,
|
||||
|
|
@ -3803,10 +4157,12 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
let outgoingResult = null;
|
||||
let lifecycleResult = null;
|
||||
let taxResult = null;
|
||||
let accountingFinancialResultResult = null;
|
||||
let tradingMarginResult = null;
|
||||
let receivablesResult = null;
|
||||
let payablesResult = null;
|
||||
let openContractsResult = null;
|
||||
let dueDateAgingResult = null;
|
||||
let documentActivityProfileResult = null;
|
||||
let counterpartyProfileResult = null;
|
||||
let contractUsageProfileResult = null;
|
||||
|
|
@ -3816,8 +4172,10 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
const lifecycleFilters = buildLifecycleFilters(planner);
|
||||
const profileFilters = buildBusinessOverviewProfileFilters(planner);
|
||||
const taxFilters = buildBusinessOverviewTaxFilters(planner);
|
||||
const accountingFinancialResultFilters = buildBusinessOverviewAccountingFinancialResultFilters(planner);
|
||||
const tradingMarginFilters = buildBusinessOverviewTradingMarginFilters(planner);
|
||||
const debtFilters = buildBusinessOverviewDebtFilters(planner);
|
||||
const debtDueDateAgingProbeEnabled = shouldRunDebtDueDateAgingProbe(planner);
|
||||
const inventoryFilters = buildBusinessOverviewInventoryFilters(planner);
|
||||
const debtAsOfDate = toNonEmptyString(debtFilters?.as_of_date);
|
||||
const inventoryAsOfDate = toNonEmptyString(inventoryFilters?.as_of_date);
|
||||
|
|
@ -3830,6 +4188,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
const taxSelection = taxFilters
|
||||
? (0, addressRecipeCatalog_1.selectAddressRecipe)("vat_liability_confirmed_for_tax_period", taxFilters)
|
||||
: null;
|
||||
const accountingFinancialResultSelection = accountingFinancialResultFilters
|
||||
? (0, addressRecipeCatalog_1.selectAddressRecipe)("accounting_financial_result_for_organization", accountingFinancialResultFilters)
|
||||
: null;
|
||||
const tradingMarginSelection = tradingMarginFilters
|
||||
? (0, addressRecipeCatalog_1.selectAddressRecipe)("inventory_trading_margin_proxy_for_organization", tradingMarginFilters)
|
||||
: null;
|
||||
|
|
@ -3842,6 +4203,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
const openContractsSelection = debtFilters
|
||||
? (0, addressRecipeCatalog_1.selectAddressRecipe)("open_contracts_confirmed_as_of_date", debtFilters)
|
||||
: null;
|
||||
const dueDateAgingSelection = debtFilters && debtDueDateAgingProbeEnabled
|
||||
? (0, addressRecipeCatalog_1.selectAddressRecipe)("debt_due_date_aging_for_organization", debtFilters)
|
||||
: null;
|
||||
const inventoryOnHandSelection = inventoryFilters
|
||||
? (0, addressRecipeCatalog_1.selectAddressRecipe)("inventory_on_hand_as_of_date", inventoryFilters)
|
||||
: null;
|
||||
|
|
@ -3909,6 +4273,16 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
pushReason(reasonCodes, "pilot_business_overview_tax_recipe_not_available");
|
||||
pushUnique(queryLimitations, "Business overview VAT/tax probe requires an executable tax-period recipe");
|
||||
}
|
||||
if (accountingFinancialResultSelection?.selected_recipe) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_accounting_financial_result_recipe_selected");
|
||||
}
|
||||
else if (!accountingFinancialResultFilters) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_accounting_financial_result_probe_skipped_without_explicit_period");
|
||||
}
|
||||
else {
|
||||
pushReason(reasonCodes, "pilot_business_overview_accounting_financial_result_recipe_not_available");
|
||||
pushUnique(queryLimitations, "Business overview accounting financial-result probe requires an executable 90/91/99 period-close recipe");
|
||||
}
|
||||
if (tradingMarginSelection?.selected_recipe) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_trading_margin_recipe_selected");
|
||||
}
|
||||
|
|
@ -3939,6 +4313,19 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
pushReason(reasonCodes, "pilot_business_overview_open_contracts_recipe_not_available");
|
||||
pushUnique(queryLimitations, "Business overview open-settlement quality probe requires executable open-contracts as-of-date recipe");
|
||||
}
|
||||
if (dueDateAgingSelection?.selected_recipe) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_debt_due_date_aging_recipe_selected");
|
||||
}
|
||||
else if (!debtFilters) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_debt_due_date_aging_probe_skipped_without_explicit_as_of_date");
|
||||
}
|
||||
else if (!debtDueDateAgingProbeEnabled) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_debt_due_date_aging_probe_skipped_without_boundary_need");
|
||||
}
|
||||
else {
|
||||
pushReason(reasonCodes, "pilot_business_overview_debt_due_date_aging_recipe_not_available");
|
||||
pushUnique(queryLimitations, "Business overview due-date aging probe requires executable contract payment-term/open-balance recipe");
|
||||
}
|
||||
if (inventoryOnHandSelection?.selected_recipe) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_inventory_on_hand_recipe_selected");
|
||||
if (inventoryAgingSelection?.selected_recipe) {
|
||||
|
|
@ -3982,6 +4369,14 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
account_scope: taxPlan.account_scope
|
||||
});
|
||||
}
|
||||
if (accountingFinancialResultSelection?.selected_recipe) {
|
||||
const accountingFinancialResultPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(accountingFinancialResultSelection.selected_recipe, accountingFinancialResultFilters);
|
||||
accountingFinancialResultResult = await runtimeDeps.executeAddressMcpQuery({
|
||||
query: accountingFinancialResultPlan.query,
|
||||
limit: accountingFinancialResultPlan.limit,
|
||||
account_scope: accountingFinancialResultPlan.account_scope
|
||||
});
|
||||
}
|
||||
if (receivablesSelection?.selected_recipe && payablesSelection?.selected_recipe) {
|
||||
const receivablesPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(receivablesSelection.selected_recipe, debtFilters);
|
||||
receivablesResult = await runtimeDeps.executeAddressMcpQuery({
|
||||
|
|
@ -4004,6 +4399,14 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
account_scope: openContractsPlan.account_scope
|
||||
});
|
||||
}
|
||||
if (dueDateAgingSelection?.selected_recipe) {
|
||||
const dueDateAgingPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(dueDateAgingSelection.selected_recipe, debtFilters);
|
||||
dueDateAgingResult = await runtimeDeps.executeAddressMcpQuery({
|
||||
query: dueDateAgingPlan.query,
|
||||
limit: dueDateAgingPlan.limit,
|
||||
account_scope: dueDateAgingPlan.account_scope
|
||||
});
|
||||
}
|
||||
if (inventoryOnHandSelection?.selected_recipe) {
|
||||
const inventoryOnHandPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(inventoryOnHandSelection.selected_recipe, inventoryFilters);
|
||||
inventoryOnHandResult = await runtimeDeps.executeAddressMcpQuery({
|
||||
|
|
@ -4025,6 +4428,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
if (taxResult) {
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, taxResult));
|
||||
}
|
||||
if (accountingFinancialResultResult) {
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, accountingFinancialResultResult));
|
||||
}
|
||||
if (receivablesResult) {
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, receivablesResult));
|
||||
}
|
||||
|
|
@ -4034,6 +4440,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
if (openContractsResult) {
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, openContractsResult));
|
||||
}
|
||||
if (dueDateAgingResult) {
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, dueDateAgingResult));
|
||||
}
|
||||
if (inventoryOnHandResult) {
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, inventoryOnHandResult));
|
||||
}
|
||||
|
|
@ -4059,6 +4468,13 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
else if (taxResult) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_tax_query_mcp_executed");
|
||||
}
|
||||
if (accountingFinancialResultResult?.error) {
|
||||
pushUnique(queryLimitations, accountingFinancialResultResult.error);
|
||||
pushReason(reasonCodes, "pilot_business_overview_accounting_financial_result_query_mcp_error");
|
||||
}
|
||||
else if (accountingFinancialResultResult) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_accounting_financial_result_query_mcp_executed");
|
||||
}
|
||||
if (receivablesResult?.error) {
|
||||
pushUnique(queryLimitations, receivablesResult.error);
|
||||
pushReason(reasonCodes, "pilot_business_overview_receivables_query_mcp_error");
|
||||
|
|
@ -4084,6 +4500,13 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
else if (openContractsResult) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_open_contracts_query_mcp_executed");
|
||||
}
|
||||
if (dueDateAgingResult?.error) {
|
||||
pushUnique(queryLimitations, dueDateAgingResult.error);
|
||||
pushReason(reasonCodes, "pilot_business_overview_debt_due_date_aging_query_mcp_error");
|
||||
}
|
||||
else if (dueDateAgingResult) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_debt_due_date_aging_query_mcp_executed");
|
||||
}
|
||||
if (inventoryOnHandResult?.error) {
|
||||
pushUnique(queryLimitations, inventoryOnHandResult.error);
|
||||
pushReason(reasonCodes, "pilot_business_overview_inventory_on_hand_query_mcp_error");
|
||||
|
|
@ -4194,10 +4617,12 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
outgoingResult,
|
||||
lifecycleResult,
|
||||
taxResult,
|
||||
accountingFinancialResultResult,
|
||||
tradingMarginResult,
|
||||
receivablesResult,
|
||||
payablesResult,
|
||||
openContractsResult,
|
||||
dueDateAgingResult,
|
||||
documentActivityProfileResult,
|
||||
counterpartyProfileResult,
|
||||
contractUsageProfileResult,
|
||||
|
|
@ -4234,6 +4659,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
if (derivedBusinessOverview.tax_position) {
|
||||
pushReason(reasonCodes, "pilot_derived_business_overview_tax_position_from_confirmed_rows");
|
||||
}
|
||||
if (derivedBusinessOverview.accounting_financial_result) {
|
||||
pushReason(reasonCodes, "pilot_derived_business_overview_accounting_financial_result_from_confirmed_rows");
|
||||
}
|
||||
if (derivedBusinessOverview.trading_margin_proxy) {
|
||||
pushReason(reasonCodes, "pilot_derived_business_overview_trading_margin_proxy_from_confirmed_rows");
|
||||
}
|
||||
|
|
@ -4249,6 +4677,10 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
if (derivedBusinessOverview.debt_staleness_risk_proxy) {
|
||||
pushReason(reasonCodes, "pilot_derived_business_overview_debt_staleness_risk_proxy_from_confirmed_rows");
|
||||
}
|
||||
if (derivedBusinessOverview.debt_due_date_aging) {
|
||||
pushReason(reasonCodes, "pilot_derived_business_overview_debt_due_date_aging_from_confirmed_rows");
|
||||
pushReason(reasonCodes, `pilot_derived_business_overview_debt_due_date_aging_${derivedBusinessOverview.debt_due_date_aging.evidence_status}`);
|
||||
}
|
||||
if (derivedBusinessOverview.inventory_position) {
|
||||
pushReason(reasonCodes, "pilot_derived_business_overview_inventory_position_from_confirmed_rows");
|
||||
}
|
||||
|
|
@ -4267,10 +4699,12 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
outgoingResult,
|
||||
lifecycleResult,
|
||||
taxResult,
|
||||
accountingFinancialResultResult,
|
||||
tradingMarginResult,
|
||||
receivablesResult,
|
||||
payablesResult,
|
||||
openContractsResult,
|
||||
dueDateAgingResult,
|
||||
documentActivityProfileResult,
|
||||
counterpartyProfileResult,
|
||||
contractUsageProfileResult,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ exports.ASSISTANT_MCP_DISCOVERY_PRIMITIVES = [
|
|||
];
|
||||
const DEFAULT_DISCOVERY_BUDGET = {
|
||||
max_probe_count: 3,
|
||||
max_rows_per_probe: 100
|
||||
max_rows_per_probe: 200
|
||||
};
|
||||
const MAX_PROBE_COUNT = 36;
|
||||
const MAX_ROWS_PER_PROBE = 500;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION = void 0;
|
||||
exports.buildAssistantMcpDiscoveryResponseCandidate = buildAssistantMcpDiscoveryResponseCandidate;
|
||||
const counterpartyRoleHeuristics_1 = require("./counterpartyRoleHeuristics");
|
||||
exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION = "assistant_mcp_discovery_response_candidate_v1";
|
||||
function toRecordObject(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
|
|
@ -67,7 +68,26 @@ function hasInternalMechanics(value) {
|
|||
function userFacingLines(values) {
|
||||
return uniqueStrings(values).filter((line) => !hasInternalMechanics(line));
|
||||
}
|
||||
function sanitizeUserFacingMechanics(value) {
|
||||
return String(value ?? "").replace(/MCP-срез(?:ом|у|е|а)?/giu, (match) => {
|
||||
const normalized = match.toLowerCase();
|
||||
if (normalized.endsWith("ом")) {
|
||||
return "срезом 1С";
|
||||
}
|
||||
if (normalized.endsWith("у")) {
|
||||
return "срезу 1С";
|
||||
}
|
||||
if (normalized.endsWith("е")) {
|
||||
return "срезе 1С";
|
||||
}
|
||||
if (normalized.endsWith("а")) {
|
||||
return "среза 1С";
|
||||
}
|
||||
return "срез 1С";
|
||||
});
|
||||
}
|
||||
function localizeLine(value) {
|
||||
const sanitizedValue = sanitizeUserFacingMechanics(value);
|
||||
if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) {
|
||||
return "В 1С найдены строки активности в запрошенном срезе.";
|
||||
}
|
||||
|
|
@ -88,7 +108,7 @@ function localizeLine(value) {
|
|||
return `В 1С проверены входящие и исходящие денежные строки в запрошенном срезе: ${incoming}, ${outgoing}.`;
|
||||
}
|
||||
if (/^Requested period hit the MCP row limit, but the approved monthly recovery probe budget is smaller than the required subperiod count$/i.test(value)) {
|
||||
return "Запрошенный период уперся в лимит строк MCP; доступного бюджета помесячных дозапросов не хватило, чтобы покрыть все подпериоды.";
|
||||
return "Запрошенный период достиг лимита строк; доступного бюджета помесячных дозапросов не хватило, чтобы покрыть все подпериоды.";
|
||||
}
|
||||
const counterpartyMatch = value.match(/^1C activity rows were found for counterparty\s+(.+)$/i);
|
||||
if (counterpartyMatch) {
|
||||
|
|
@ -113,10 +133,10 @@ function localizeLine(value) {
|
|||
}
|
||||
const movementRowsMatch = value.match(/^1C movement rows were found for counterparty\s+(.+)$/i);
|
||||
if (movementRowsMatch) {
|
||||
return `В 1С найдены строки движений по контрагенту ${movementRowsMatch[1]}.`;
|
||||
return `В 1С найдены строки движений по контрагенту ${movementRowsMatch[1]}.`;
|
||||
}
|
||||
if (/^1C movement rows were found for the requested scope$/i.test(value)) {
|
||||
return "В 1С найдены строки движений по запрошенному контуру.";
|
||||
return "В 1С найдены строки движений по запрошенному контуру.";
|
||||
}
|
||||
const supplierPayoutMatch = value.match(/^1C supplier-payout rows were found for counterparty\s+(.+)$/i);
|
||||
if (supplierPayoutMatch) {
|
||||
|
|
@ -144,7 +164,7 @@ function localizeLine(value) {
|
|||
return "Срез документов ограничен только подтвержденными строками документов в проверенном окне.";
|
||||
}
|
||||
if (/^Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope$/i.test(value)) {
|
||||
return "Срез движений ограничен только подтвержденными строками движений в проверенном окне.";
|
||||
return "Срез движений ограничен только подтвержденными строками движений в проверенном окне.";
|
||||
}
|
||||
if (/^Counterparty value-flow total was calculated from confirmed 1C movement rows$/i.test(value)) {
|
||||
return "Сумма входящих поступлений рассчитана только по подтвержденным строкам поступлений в 1С.";
|
||||
|
|
@ -227,10 +247,10 @@ function localizeLine(value) {
|
|||
return "Полный срез документов без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
if (/^Full movement history outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||
return "Полный исторический срез движений вне проверенного периода этим поиском не подтвержден.";
|
||||
return "Полный исторический срез движений вне проверенного периода этим поиском не подтвержден.";
|
||||
}
|
||||
if (/^Full movement history is not proven without an explicit checked period$/i.test(value)) {
|
||||
return "Полный срез движений без явно проверенного периода не подтвержден.";
|
||||
return "Полный срез движений без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
if (/^Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||
return "Полный объем исходящих платежей вне проверенного периода этим поиском не подтвержден.";
|
||||
|
|
@ -245,10 +265,10 @@ function localizeLine(value) {
|
|||
return "Полный двусторонний денежный поток за все время без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
if (/^Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit$/i.test(value)) {
|
||||
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С после того, как общая выборка уперлась в лимит строк.";
|
||||
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С после того, как общая выборка достигла лимита строк.";
|
||||
}
|
||||
if (/^Requested period coverage for bidirectional value-flow was recovered through monthly 1C side probes after a broad probe hit the row limit$/i.test(value)) {
|
||||
return "Покрытие запрошенного периода по двустороннему денежному потоку восстановлено помесячными проверками 1С после того, как общая выборка уперлась в лимит строк хотя бы по одной стороне.";
|
||||
return "Покрытие запрошенного периода по двустороннему денежному потоку восстановлено помесячными проверками 1С после того, как общая выборка достигла лимита строк хотя бы по одной стороне.";
|
||||
}
|
||||
if (/^Requested period coverage was recovered through monthly 1C value-flow probes$/i.test(value)) {
|
||||
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С.";
|
||||
|
|
@ -268,7 +288,7 @@ function localizeLine(value) {
|
|||
if (/^Complete requested-period coverage for bidirectional value-flow is not proven by the available checked rows$/i.test(value)) {
|
||||
return "Полное покрытие запрошенного периода по двустороннему денежному потоку не подтверждено доступными проверенными строками.";
|
||||
}
|
||||
return value;
|
||||
return sanitizedValue;
|
||||
}
|
||||
function section(title, lines) {
|
||||
const clean = userFacingLines(lines.map(localizeLine));
|
||||
|
|
@ -345,7 +365,7 @@ function businessOverviewCoverageLimitLine(overview) {
|
|||
limited.push("исходящие");
|
||||
}
|
||||
return limited.length > 0
|
||||
? `Важно: ${limited.join(" и ")} уперлись в лимит выборки MCP, поэтому это проверенный срез найденных строк, а не гарантированно полный бухгалтерский оборот.`
|
||||
? `Важно: по направлению ${limited.join(" и ")} проверка достигла лимита строк; это расширенный проверенный срез найденных строк, но не гарантия полного бухгалтерского оборота без отдельной полной выгрузки.`
|
||||
: null;
|
||||
}
|
||||
function businessOverviewYearRowsLine(overview) {
|
||||
|
|
@ -371,6 +391,30 @@ function firstOverviewAxisLabel(rows, amountKey = "total_amount_human_ru") {
|
|||
const amount = moneyText(first?.[amountKey]);
|
||||
return label && amount ? `${label} — ${sentenceAmount(amount) ?? amount}` : null;
|
||||
}
|
||||
function firstNonFinancialOverviewAxisLabel(rows, amountKey = "total_amount_human_ru") {
|
||||
if (!Array.isArray(rows)) {
|
||||
return null;
|
||||
}
|
||||
for (const row of rows) {
|
||||
const item = toRecordObject(row);
|
||||
const label = toNonEmptyString(item?.axis_value);
|
||||
if (!label || (0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(label)) {
|
||||
continue;
|
||||
}
|
||||
const amount = moneyText(item?.[amountKey]);
|
||||
if (amount) {
|
||||
return `${label} — ${sentenceAmount(amount) ?? amount}`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function overviewAxisLooksFinancial(row) {
|
||||
if (!row) {
|
||||
return false;
|
||||
}
|
||||
return (row.counterparty_role_hint === "bank_or_financial_institution" ||
|
||||
(0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(row.axis_value));
|
||||
}
|
||||
function businessOverviewTaxLine(overview) {
|
||||
const tax = toRecordObject(overview.tax_position);
|
||||
if (!tax) {
|
||||
|
|
@ -487,7 +531,7 @@ function buildCompactBidirectionalValueFlowReply(entryPoint, draft) {
|
|||
lines.push(`Основа: ${basis.join("; ")}.`);
|
||||
}
|
||||
if (flow.coverage_limited_by_probe_limit === true) {
|
||||
lines.push("Важно: часть проверки уперлась в лимит строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");
|
||||
lines.push("Важно: часть проверки достигла лимита строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");
|
||||
}
|
||||
lines.push("Метод: нетто рассчитано как подтвержденные входящие строки 1С минус подтвержденные исходящие строки; это не полное бухгалтерское сальдо вне проверенного окна.");
|
||||
const fallbackNextStep = toNonEmptyString(draft.next_step_line);
|
||||
|
|
@ -610,16 +654,158 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
|||
const topCustomer = toRecordObject(Array.isArray(overview.top_customers) ? overview.top_customers[0] : null);
|
||||
const customerName = toNonEmptyString(topCustomer?.axis_value);
|
||||
const customerAmount = moneyText(topCustomer?.total_amount_human_ru);
|
||||
const topCustomerLooksFinancial = overviewAxisLooksFinancial(topCustomer);
|
||||
const nonFinancialCustomer = firstNonFinancialOverviewAxisLabel(topCustomerLooksFinancial ? overview.top_customers : []);
|
||||
const topCustomerLead = customerName && customerAmount
|
||||
? `; крупнейший источник входящих денег: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}`
|
||||
? topCustomerLooksFinancial
|
||||
? `; крупнейший входящий денежный источник: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount} (похоже на банк/финорганизацию, не называю это клиентской выручкой без назначения платежа)${nonFinancialCustomer ? `; крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}` : ""}`
|
||||
: `; крупнейший источник входящих денег: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}`
|
||||
: "";
|
||||
const topSupplierRecord = toRecordObject(Array.isArray(overview.top_suppliers) ? overview.top_suppliers[0] : null);
|
||||
const topSupplier = firstOverviewAxisLabel(overview.top_suppliers);
|
||||
const topSupplierLead = topSupplier ? `; крупнейший получатель исходящих денег: ${topSupplier}` : "";
|
||||
const topSupplierLooksFinancial = overviewAxisLooksFinancial(topSupplierRecord);
|
||||
const nonFinancialSupplier = firstNonFinancialOverviewAxisLabel(topSupplierLooksFinancial ? overview.top_suppliers : []);
|
||||
const topSupplierLead = topSupplier
|
||||
? topSupplierLooksFinancial
|
||||
? `; крупнейший получатель исходящих денег: ${topSupplier} (похоже на банк/финорганизацию, не называю это обычным поставщиком без назначения платежа/договора)${nonFinancialSupplier ? `; крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}` : ""}`
|
||||
: `; крупнейший получатель исходящих денег: ${topSupplier}`
|
||||
: "";
|
||||
const roleBoundaryLead = topCustomer || topSupplier ? "; клиент/поставщик как бизнес-роли этим денежным срезом не подтверждены" : "";
|
||||
const graphReasonCodes = toStringList(graph?.reason_codes);
|
||||
const directMoneyAnswer = graphReasonCodes.includes("data_need_graph_business_overview_direct_money_answer");
|
||||
const crossScopeExecutiveSummary = Boolean(separateSubject && previousCounterpartySummary);
|
||||
const lines = [];
|
||||
const actionFamily = toNonEmptyString(turnMeaning?.asked_action_family);
|
||||
const unsupportedFamily = toNonEmptyString(turnMeaning?.unsupported_but_understood_family);
|
||||
const profitMarginBoundary = actionFamily === "profit_margin_boundary" || unsupportedFamily === "profit_margin_boundary";
|
||||
const debtDueDateBoundary = actionFamily === "debt_due_date_boundary" || unsupportedFamily === "debt_due_date_boundary";
|
||||
const vendorRiskBoundary = actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary";
|
||||
const inventoryReserveBoundary = actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary";
|
||||
if (profitMarginBoundary) {
|
||||
const accountingFinancialResult = toRecordObject(overview.accounting_financial_result);
|
||||
if (accountingFinancialResult) {
|
||||
const direction = toNonEmptyString(accountingFinancialResult.final_result_direction);
|
||||
const amount = moneyText(accountingFinancialResult.final_result_amount_human_ru);
|
||||
const periodScope = toNonEmptyString(accountingFinancialResult.period_scope) ?? period;
|
||||
const marginPct = typeof accountingFinancialResult.net_margin_to_revenue_pct === "number" &&
|
||||
Number.isFinite(accountingFinancialResult.net_margin_to_revenue_pct)
|
||||
? `${accountingFinancialResult.net_margin_to_revenue_pct}%`
|
||||
: null;
|
||||
const directionText = direction === "profit"
|
||||
? "учетная прибыль"
|
||||
: direction === "loss"
|
||||
? "учетный убыток"
|
||||
: "нулевой учетный финрезультат";
|
||||
const amountText = amount
|
||||
? direction === "loss"
|
||||
? `минус ${amount}`
|
||||
: amount
|
||||
: "сумма не распознана";
|
||||
lines.push(`Коротко: по бухгалтерскому маршруту 90/91/99 за ${periodScope} подтвержден ${directionText}: ${amountText}${marginPct ? `; маржа к выручке 90.01 ${marginPct}` : "; маржа к выручке 90.01 не рассчитана"}.`);
|
||||
lines.push("Это учетный финрезультат по найденным строкам закрытия периода в 1С, а не внешний аудит и не юридически подтвержденная отчетность.");
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
}
|
||||
const headline = toNonEmptyString(draft.headline);
|
||||
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
|
||||
lines.push(cleanHeadline
|
||||
? `Коротко: ${localizeLine(cleanHeadline)}`
|
||||
: "Коротко: нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только bounded operating-flow/trading-margin proxy, не P&L и не бухгалтерский финансовый результат.");
|
||||
const boundaryLines = userFacingLines([
|
||||
...toStringList(draft.confirmed_lines),
|
||||
...toStringList(draft.inference_lines),
|
||||
...toStringList(draft.unknown_lines)
|
||||
])
|
||||
.filter((line) => /(?:прибыл|марж|финанс|p\s*&\s*l|p&l|расход|себестоим|закрыт|profit|margin|financial)/iu.test(line))
|
||||
.slice(0, 2);
|
||||
if (boundaryLines.length > 0) {
|
||||
lines.push(...boundaryLines.map(localizeLine));
|
||||
}
|
||||
lines.push("Для точного P&L нужны отдельный маршрут по себестоимости, расходам, закрытию периода и финрезультату; текущий proxy нельзя выдавать за подтвержденную чистую прибыль или маржу.");
|
||||
if (limitLine) {
|
||||
lines.push(limitLine);
|
||||
}
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
}
|
||||
if (debtDueDateBoundary) {
|
||||
const dueDateAging = toRecordObject(overview.debt_due_date_aging);
|
||||
if (dueDateAging) {
|
||||
const status = toNonEmptyString(dueDateAging.evidence_status);
|
||||
const asOfDate = toNonEmptyString(dueDateAging.as_of_date) ?? "проверенную дату";
|
||||
const overdueAmount = moneyText(dueDateAging.overdue_amount_human_ru);
|
||||
const grossAmount = moneyText(dueDateAging.gross_open_amount_human_ru);
|
||||
const rowsWithPaymentTerms = typeof dueDateAging.rows_with_payment_terms === "number" && Number.isFinite(dueDateAging.rows_with_payment_terms)
|
||||
? dueDateAging.rows_with_payment_terms
|
||||
: null;
|
||||
const rowsWithAmount = typeof dueDateAging.rows_with_amount === "number" && Number.isFinite(dueDateAging.rows_with_amount)
|
||||
? dueDateAging.rows_with_amount
|
||||
: null;
|
||||
if (status === "confirmed_overdue") {
|
||||
lines.push(`Коротко: на ${asOfDate} подтвержденная просрочка есть: ${overdueAmount ?? "сумма не распознана"} по ${dueDateAging.overdue_rows ?? "найденным"} строкам.`);
|
||||
lines.push("Основа ответа: открытые расчеты 60/62/76, договорный срок оплаты и дата расчетного документа; это уже due-date route, не старение договора как proxy.");
|
||||
}
|
||||
else if (status === "no_payment_terms_configured") {
|
||||
lines.push(`Коротко: на ${asOfDate} подтвержденной просрочки нет: открытые расчеты проверены${grossAmount ? ` на ${grossAmount}` : ""}, но в найденных договорах срок оплаты не установлен.`);
|
||||
lines.push(rowsWithAmount !== null
|
||||
? `Проверено строк с суммой: ${rowsWithAmount}. Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой.`
|
||||
: "Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой.");
|
||||
}
|
||||
else if (status === "insufficient_due_date_basis") {
|
||||
lines.push(`Коротко: due-date route запущен на ${asOfDate}, но просрочка не подтверждена: по строкам с установленным сроком оплаты не хватило даты расчетного документа.`);
|
||||
if (rowsWithPaymentTerms !== null) {
|
||||
lines.push(`Строк с установленным сроком оплаты: ${rowsWithPaymentTerms}; нужен документ-основание с датой для расчета due date.`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
lines.push(`Коротко: due-date route на ${asOfDate} проверен, подтвержденной просрочки не найдено${rowsWithPaymentTerms !== null ? `; строк с установленным сроком оплаты ${rowsWithPaymentTerms}` : ""}.`);
|
||||
}
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
}
|
||||
const headline = toNonEmptyString(draft.headline);
|
||||
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
|
||||
lines.push(cleanHeadline
|
||||
? `Коротко: ${localizeLine(cleanHeadline)}`
|
||||
: "Коротко: нельзя точно определить, какая дебиторка просрочена, по текущему срезу 1С; есть только debt-quality proxy, но нет проверенного due-date маршрута.");
|
||||
lines.push("Проверить нужно отдельно: договоры, сроки оплаты, погашение и закрытие задолженности; без этого нельзя доказать overdue/due-date aging.");
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
}
|
||||
if (vendorRiskBoundary) {
|
||||
const supplierBasis = topSupplier
|
||||
? topSupplierLooksFinancial
|
||||
? `крупнейший получатель исходящих денег: ${topSupplier}; по названию это банк/финансовая организация, поэтому это не доказанная зависимость от обычного поставщика${nonFinancialSupplier ? `; крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}` : ""}`
|
||||
: `крупнейший подтвержденный поставщик/получатель исходящих платежей: ${topSupplier}`
|
||||
: outgoingAmount
|
||||
? `исходящие платежи/закупочный поток в проверенном срезе: ${outgoingAmount}`
|
||||
: "есть только ограниченный срез исходящих платежей без полного vendor-risk профиля";
|
||||
const proxyLabel = topSupplierLooksFinancial ? "outgoing cash concentration proxy" : "procurement concentration proxy";
|
||||
lines.push(`Коротко: точный риск зависимости от одного поставщика по текущим данным не подтвержден; есть только ${proxyLabel}: ${supplierBasis}.`);
|
||||
lines.push("Это сигнал концентрации закупок/исходящих платежей, а не полный аудит надежности поставщиков, условий, качества и структуры всех расходов.");
|
||||
lines.push("Для точного вывода нужен отдельный reviewed vendor-risk route: поставщики, договорные условия, качество поставок, сроки, доля в закупках и полная структура расходов.");
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
}
|
||||
if (inventoryReserveBoundary) {
|
||||
const headline = toNonEmptyString(draft.headline);
|
||||
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
|
||||
lines.push(cleanHeadline
|
||||
? `Коротко: ${localizeLine(cleanHeadline)}`
|
||||
: "Коротко: точно подтвердить резерв под неликвиды по текущим данным нельзя.");
|
||||
const boundaryLines = userFacingLines([
|
||||
...toStringList(draft.unknown_lines),
|
||||
...toStringList(draft.limitation_lines)
|
||||
])
|
||||
.filter((line) => /(?:резерв|неликвид|склад|товар|reserve|obsolete|inventory|stock)/iu.test(line))
|
||||
.slice(0, 2);
|
||||
if (boundaryLines.length > 0) {
|
||||
lines.push(...boundaryLines.map(localizeLine));
|
||||
}
|
||||
lines.push("Проверить нужно отдельно: складской срез на дату, учетную политику резервов, списания и ликвидационную стоимость; proxy-сигналы нельзя выдавать за доказанный факт резерва.");
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
}
|
||||
if (crossScopeExecutiveSummary && separateSubject && previousCounterpartySummary && (incomingAmount || outgoingAmount || netAmount)) {
|
||||
lines.push(`Коротко: по компании ${organizationScope ?? "в выбранном контуре"} ${period} подтвержден денежный срез: получили ${incomingAmount ?? "0 руб."}, исходящие платежи/списания ${outgoingAmount ?? "0 руб."}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}${previousCounterpartySummary.lead}; можно утверждать только эти подтвержденные срезы, нельзя называть это чистой прибылью, полным оборотом или доказанной ролью главного клиента/поставщика.`);
|
||||
lines.push(previousCounterpartySummary.line);
|
||||
|
|
@ -640,7 +826,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
|||
if (!leaderYear || !leaderAmount) {
|
||||
return null;
|
||||
}
|
||||
lines.push(`Коротко: в доступном проверенном MCP-срезе по входящим денежным строкам лидирует ${leaderYear}: ${leaderAmount}${Number.isFinite(leaderRows) && leaderRows > 0 ? ` по ${leaderRows} строкам с суммой` : ""}; это не полный бухгалтерский рейтинг доходности.`);
|
||||
lines.push(`Коротко: в доступном проверенном срезе 1С по входящим денежным строкам лидирует ${leaderYear}: ${leaderAmount}${Number.isFinite(leaderRows) && leaderRows > 0 ? ` по ${leaderRows} строкам с суммой` : ""}; это не полный бухгалтерский рейтинг доходности.`);
|
||||
const netYear = toNonEmptyString(netLeader?.year_bucket);
|
||||
const netYearAmount = moneyText(netLeader?.net_amount_human_ru);
|
||||
if (netYear && netYearAmount) {
|
||||
|
|
@ -660,7 +846,9 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
|||
lines.push(`Коротко: ${organizationPrefix}${period} по подтвержденным строкам 1С получили ${incomingAmount ?? "0 руб."}; исходящие платежи/списания ${outgoingAmount ?? "0 руб."}; ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб"}${topCustomerLead}${topSupplierLead}${roleBoundaryLead}${separateSubjectLead}.`);
|
||||
lines.push('Метод: "заработали" здесь считаю как денежный operating-flow proxy по 1С; это не чистая прибыль и не финрезультат.');
|
||||
if (!directMoneyAnswer && customerName && customerAmount) {
|
||||
lines.push(`Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}.`);
|
||||
lines.push(topCustomerLooksFinancial
|
||||
? `Крупнейший входящий денежный источник в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}. По названию это банк/финансовая организация, поэтому без назначения платежа не называю это клиентской выручкой.${nonFinancialCustomer ? ` Крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}.` : ""}`
|
||||
: `Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}.`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
|
@ -671,10 +859,14 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
|||
`Отдельно по контрагенту ${separateSubject}: этот итог не переносит суммы компании на контрагента. Можно утверждать только разделение контура; нельзя делать вывод о выручке, долге или прибыльности ${separateSubject} без отдельного контрагентского среза документов и движений.`);
|
||||
}
|
||||
if (!directMoneyAnswer && topSupplier) {
|
||||
lines.push(`Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`);
|
||||
lines.push(topSupplierLooksFinancial
|
||||
? `Крупнейший получатель исходящих денег: ${topSupplier}. По названию это банк/финансовая организация, поэтому без назначения платежа/договора не считаю это обычным поставщиком.${nonFinancialSupplier ? ` Крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}.` : ""}`
|
||||
: `Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`);
|
||||
}
|
||||
if (!directMoneyAnswer && (topCustomer || topSupplier)) {
|
||||
lines.push("Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль.");
|
||||
lines.push(topCustomerLooksFinancial || topSupplierLooksFinancial
|
||||
? "Важно по ролям: текущий денежный срез подтверждает источники и получателей денег, но банковские контрагенты требуют проверки назначения платежа/счетов и не доказывают роль клиента или поставщика."
|
||||
: "Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль.");
|
||||
}
|
||||
if (!directMoneyAnswer) {
|
||||
lines.push(`Что подтверждено: денежный срез по компании${organizationScope ? ` ${organizationScope}` : ""}${period ? ` ${period}` : ""}${topCustomer ? ", крупнейший источник входящих денег" : ""}${topSupplier ? ", крупнейший получатель исходящих денег" : ""}.`);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION = exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION = void 0;
|
||||
exports.ASSISTANT_MCP_ROUTE_CANDIDATE_SCHEMA_VERSION = exports.ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION = exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION = void 0;
|
||||
exports.runAssistantMcpDiscoveryRuntimeBridge = runAssistantMcpDiscoveryRuntimeBridge;
|
||||
const assistantMcpDiscoveryAnswerAdapter_1 = require("./assistantMcpDiscoveryAnswerAdapter");
|
||||
const assistantMcpDiscoveryPilotExecutor_1 = require("./assistantMcpDiscoveryPilotExecutor");
|
||||
const assistantMcpDiscoveryPlanner_1 = require("./assistantMcpDiscoveryPlanner");
|
||||
exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION = "assistant_mcp_discovery_runtime_bridge_v1";
|
||||
exports.ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION = "assistant_mcp_discovery_loop_state_v1";
|
||||
exports.ASSISTANT_MCP_ROUTE_CANDIDATE_SCHEMA_VERSION = "assistant_mcp_route_candidate_v1";
|
||||
function normalizeReasonCode(value) {
|
||||
const normalized = value
|
||||
.trim()
|
||||
|
|
@ -58,6 +59,21 @@ function loopStatusFor(bridgeStatus) {
|
|||
}
|
||||
return "ready_for_next_hop";
|
||||
}
|
||||
function routeCandidateStatusFor(bridgeStatus, pilot, missingProofFamily) {
|
||||
if (bridgeStatus === "blocked" || pilot.pilot_status === "blocked") {
|
||||
return "blocked";
|
||||
}
|
||||
if (bridgeStatus === "needs_clarification" || pilot.pilot_status === "skipped_needs_clarification") {
|
||||
return "needs_user_scope";
|
||||
}
|
||||
if (bridgeStatus === "unsupported" || pilot.pilot_status === "unsupported") {
|
||||
return "needs_route_enablement";
|
||||
}
|
||||
if (missingProofFamily) {
|
||||
return "needs_route_enablement";
|
||||
}
|
||||
return "ready_for_reviewed_execution";
|
||||
}
|
||||
function flattenAxes(pilot, source) {
|
||||
const result = [];
|
||||
for (const step of pilot.dry_run.execution_steps) {
|
||||
|
|
@ -83,6 +99,119 @@ function entityCandidatesFromPlanner(planner) {
|
|||
const values = planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? [];
|
||||
return uniqueStrings(values);
|
||||
}
|
||||
function firstNonEmpty(values) {
|
||||
for (const value of values) {
|
||||
const text = String(value ?? "").trim();
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function routeCandidateProofFamiliesFor(actionFamily, proofExpectation) {
|
||||
const combined = `${actionFamily ?? ""} ${proofExpectation ?? ""}`.trim().toLowerCase();
|
||||
const result = [];
|
||||
const add = (family) => {
|
||||
if (!result.includes(family)) {
|
||||
result.push(family);
|
||||
}
|
||||
};
|
||||
if (!combined || combined === "broad_evaluation bounded_inference") {
|
||||
return result;
|
||||
}
|
||||
if (/(?:inventory|stock|warehouse|reserve|liquidation|write[-_ ]?off|obsolete|obsolescence)/iu.test(combined)) {
|
||||
add("inventory_reserve_liquidation_quality");
|
||||
}
|
||||
if (/(?:debt|due[-_ ]?date|overdue|aging|credit[-_ ]?risk)/iu.test(combined)) {
|
||||
add("debt_due_date_aging_quality");
|
||||
}
|
||||
if (/(?:vendor|supplier|procurement|sourcing)/iu.test(combined)) {
|
||||
add("vendor_risk_procurement_quality");
|
||||
}
|
||||
if (/(?:profit|margin|pnl|p&l|financial[-_ ]?result)/iu.test(combined)) {
|
||||
add("accounting_profit_margin");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function routeCandidateMissingProofFamily(planner, pilot) {
|
||||
if (planner.data_need_graph?.business_fact_family !== "business_overview") {
|
||||
return null;
|
||||
}
|
||||
const wantedFamilies = routeCandidateProofFamiliesFor(planner.data_need_graph?.action_family ?? null, planner.data_need_graph?.proof_expectation ?? null);
|
||||
if (wantedFamilies.length <= 0) {
|
||||
return null;
|
||||
}
|
||||
const missingProofFamilies = pilot.derived_business_overview?.missing_proof_families ?? [];
|
||||
return missingProofFamilies.find((item) => wantedFamilies.includes(item.family)) ?? null;
|
||||
}
|
||||
function routeCandidateEnablementReason(status, pilot, missingAxes, missingProofFamily) {
|
||||
if (status === "ready_for_reviewed_execution") {
|
||||
return null;
|
||||
}
|
||||
if (status === "needs_user_scope") {
|
||||
return missingAxes.length > 0
|
||||
? `Missing scope axes: ${missingAxes.join(", ")}`
|
||||
: "Selected chain needs user clarification before MCP execution";
|
||||
}
|
||||
if (missingProofFamily) {
|
||||
return [
|
||||
`Missing reviewed proof family: ${missingProofFamily.family}`,
|
||||
`next_required_evidence=${missingProofFamily.next_required_evidence}`,
|
||||
missingProofFamily.current_supported_evidence
|
||||
? `current_supported_evidence=${missingProofFamily.current_supported_evidence}`
|
||||
: null,
|
||||
`must_not_claim=${missingProofFamily.must_not_claim}`
|
||||
]
|
||||
.filter((item) => Boolean(item))
|
||||
.join("; ");
|
||||
}
|
||||
return firstNonEmpty([
|
||||
...pilot.query_limitations,
|
||||
...pilot.evidence.unknown_facts,
|
||||
"Selected chain is not safely executable by the reviewed MCP runtime yet"
|
||||
]);
|
||||
}
|
||||
function routeCandidateNextAction(status) {
|
||||
if (status === "ready_for_reviewed_execution") {
|
||||
return "Execute through the reviewed runtime bridge and truth gate.";
|
||||
}
|
||||
if (status === "needs_user_scope") {
|
||||
return "Ask the user for the missing scope axes before MCP execution.";
|
||||
}
|
||||
if (status === "needs_route_enablement") {
|
||||
return "Create or wire a reviewed exact route for the selected chain before treating the fact as answerable.";
|
||||
}
|
||||
return "Do not execute until the blocking reason is resolved.";
|
||||
}
|
||||
function buildRouteCandidate(planner, pilot, bridgeStatus) {
|
||||
const plannerClarificationGaps = planner.discovery_plan.clarification_gaps ?? [];
|
||||
const providedAxes = flattenAxes(pilot, "provided_axes");
|
||||
const missingAxes = plannerClarificationGaps.length > 0 ? plannerClarificationGaps : flattenAxes(pilot, "missing_axis_options");
|
||||
const missingProofFamily = routeCandidateMissingProofFamily(planner, pilot);
|
||||
const candidateStatus = routeCandidateStatusFor(bridgeStatus, pilot, missingProofFamily);
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_ROUTE_CANDIDATE_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
|
||||
candidate_status: candidateStatus,
|
||||
selected_chain_id: planner.selected_chain_id,
|
||||
selected_chain_summary: planner.selected_chain_summary,
|
||||
nearest_catalog_chain_template: planner.catalog_chain_template_alignment.top_chain_template_match,
|
||||
catalog_alignment_status: planner.catalog_chain_template_alignment.alignment_status,
|
||||
business_fact_family: planner.data_need_graph?.business_fact_family ?? null,
|
||||
action_family: planner.data_need_graph?.action_family ?? null,
|
||||
proof_expectation: planner.data_need_graph?.proof_expectation ?? null,
|
||||
required_axes: [...planner.required_axes],
|
||||
provided_axes: providedAxes,
|
||||
missing_axes: missingAxes,
|
||||
executable_now: candidateStatus === "ready_for_reviewed_execution",
|
||||
enablement_reason: routeCandidateEnablementReason(candidateStatus, pilot, missingAxes, missingProofFamily),
|
||||
recommended_next_action: routeCandidateNextAction(candidateStatus),
|
||||
forbidden_overclaim_flags: uniqueStrings([
|
||||
...(planner.data_need_graph?.forbidden_overclaim_flags ?? []),
|
||||
...(missingProofFamily ? [missingProofFamily.must_not_claim] : [])
|
||||
])
|
||||
};
|
||||
}
|
||||
function buildLoopState(planner, pilot, bridgeStatus) {
|
||||
const plannerClarificationGaps = planner.discovery_plan.clarification_gaps ?? [];
|
||||
return {
|
||||
|
|
@ -120,10 +249,13 @@ async function runAssistantMcpDiscoveryRuntimeBridge(input) {
|
|||
const answerDraft = (0, assistantMcpDiscoveryAnswerAdapter_1.buildAssistantMcpDiscoveryAnswerDraft)(pilot);
|
||||
const bridgeStatus = bridgeStatusFor(pilot, answerDraft);
|
||||
const loopState = buildLoopState(planner, pilot, bridgeStatus);
|
||||
const routeCandidate = buildRouteCandidate(planner, pilot, bridgeStatus);
|
||||
const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]);
|
||||
pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`);
|
||||
pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer");
|
||||
pushReason(reasonCodes, `runtime_bridge_loop_state_${loopState.loop_status}`);
|
||||
pushReason(reasonCodes, "runtime_bridge_route_candidate_built");
|
||||
pushReason(reasonCodes, `runtime_bridge_route_candidate_${routeCandidate.candidate_status}`);
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
|
||||
|
|
@ -133,6 +265,7 @@ async function runAssistantMcpDiscoveryRuntimeBridge(input) {
|
|||
pilot,
|
||||
answer_draft: answerDraft,
|
||||
loop_state: loopState,
|
||||
route_candidate: routeCandidate,
|
||||
user_facing_response_allowed: bridgeStatus !== "blocked",
|
||||
business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft),
|
||||
requires_user_clarification: bridgeStatus === "needs_clarification",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,11 @@ function isGarbageSemanticAnchorCandidate(value) {
|
|||
"всему",
|
||||
"всей",
|
||||
"всем",
|
||||
"год",
|
||||
"года",
|
||||
"году",
|
||||
"годом",
|
||||
"годы",
|
||||
"выводу",
|
||||
"выводам",
|
||||
"аудиту",
|
||||
|
|
@ -626,6 +631,30 @@ function hasOrganizationLevelEarningsOverviewSignal(text) {
|
|||
/(?:\u043a\u0430\u043a\w*|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|\u043f\u043e\u043a\u0430\u0436|\u0434\u0430\u0439|\u0443\s+\u043d\u0430\u0441|\u043d\u0430\u0448\w*|\u043c\u044b\b|\u043a\u043e\u043c\u043f\u0430\u043d|\u0431\u0438\u0437\u043d\u0435\u0441|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0432\s+\u0446\u0435\u043b\u043e\u043c|\u0432\u043e\u043e\u0431\u0449\u0435|(?:19|20)\d{2}|all\s+time|what|which|how\s+much|show|give|company|business|organization|our|we|us)/iu.test(text);
|
||||
return hasYearRankingCue || hasCompanyEarningsCue || hasCompanyProfitMarginCue;
|
||||
}
|
||||
function hasOrganizationLevelProfitMarginBoundaryOverviewSignal(text) {
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
const hasProfitMarginCue = /(?:\u043f\u0440\u0438\u0431\u044b\u043b\w*|\u043c\u0430\u0440\u0436\w*|\u0440\u0435\u043d\u0442\u0430\u0431\w*|\u0444\u0438\u043d(?:\u0430\u043d\u0441\w*)?\s*[- ]?\s*\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442|p\s*&\s*l|profit(?:ability)?|margin|financial\s+result)/iu.test(text);
|
||||
const hasCompanyScopeCue = /(?:\u0443\s+\u043d\u0430\u0441|\u043d\u0430\u0448\w*|\u043c\u044b\b|\u043f\u043e\s+\u043a\u043e\u043c\u043f\u0430\u043d|\u043a\u043e\u043c\u043f\u0430\u043d|\u0431\u0438\u0437\u043d\u0435\u0441|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0432\s+\u0446\u0435\u043b\u043e\u043c|\b(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e|\u043e\u0430\u043e)\b|(?:19|20)\d{2}|company|business|organization|our|we|us)/iu.test(text);
|
||||
return hasProfitMarginCue && hasCompanyScopeCue;
|
||||
}
|
||||
function hasProfitMarginBoundaryFollowupSignal(text) {
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
const hasProfitOrResultCue = /(?:\u043f\u0440\u0438\u0431\u044b\u043b\w*|\u0443\u0431\u044b\u0442\w*|\u043c\u0430\u0440\u0436\w*|\u0440\u0435\u043d\u0442\u0430\u0431\w*|\u0444\u0438\u043d(?:\u0430\u043d\u0441\w*)?\s*[- ]?\s*\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442|p\s*&\s*l|profit|loss|margin|financial\s+result)/iu.test(text);
|
||||
const hasFollowupShape = /(?:\u044d\u0442\u043e|\u0438\u0442\u043e\u0433|\u0438\u0442\u043e\u0433\u043e|\u043f\u043e\u043b\u0443\u0447\w*|\u043a\u043e\u0440\u043e\u0442\w*|\u0432\s+\u0438\u0442\u043e\u0433\u0435|\u043c\u043e\u0436\u043d\u043e\s+(?:\u043b\u0438\s+)?\u0441\u043a\u0430\u0437\u0430\u0442\u044c|is\s+it|result|short|brief)/iu.test(text);
|
||||
return hasProfitOrResultCue && hasFollowupShape;
|
||||
}
|
||||
function hasDebtDueDateBoundaryFollowupSignal(text) {
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
const hasDueDateCue = /(?:\u043f\u0440\u043e\u0441\u0440\u043e\u0447\w*|\u0441\u0440\u043e\u043a\w*\s+\u043e\u043f\u043b\u0430\u0442|\u0434\u043e\u043a\u0430\u0437\w*|\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\w*|due[-\s]?date|overdue|debt\s+aging|aging)/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;
|
||||
}
|
||||
function hasOrganizationLevelDebtDueDateOverviewSignal(text) {
|
||||
if (!text) {
|
||||
return false;
|
||||
|
|
@ -711,6 +740,12 @@ function hasExplicitVatQuestionSignal(text) {
|
|||
return (/(?:\u043d\u0434\u0441|vat)/iu.test(text) &&
|
||||
/(?:\u0437\u0430|\u043d\u0430|\u043f\u0435\u0440\u0438\u043e\u0434|\u043f\u043e\u0437\u0438\u0446|\u043a\s+\u0443\u043f\u043b\u0430\u0442|\u043a\s+\u0432\u043e\u0437\u043c\u0435\u0449|\u043e\u0441\u043d\u043e\u0432\u0430\u043d|\u043d\u0430\u043b\u043e\u0433\u043e\u0432\p{L}*\s+\u0432\u044b\u0432\u043e\u0434|tax\s+period|tax\s+position)/iu.test(text));
|
||||
}
|
||||
function hasExplicitVatMovementEvidenceSignal(text) {
|
||||
if (!/(?:\u043d\u0434\u0441|vat)/iu.test(text)) {
|
||||
return false;
|
||||
}
|
||||
return hasMovementEvidenceFollowupSignal(text);
|
||||
}
|
||||
function hasBusinessOverviewSeparateCounterpartySignal(text) {
|
||||
if (!text) {
|
||||
return false;
|
||||
|
|
@ -1102,6 +1137,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
const rawReferentialDocumentExclusionSignal = hasReferentialDocumentExclusionFollowupSignal(repairedUserText ?? rawUserText ?? "");
|
||||
const rawPrimaryBusinessOverviewSignal = hasBusinessOverviewSignal(rawText);
|
||||
const explicitVatQuestionSignal = hasExplicitVatQuestionSignal(rawText);
|
||||
const explicitVatMovementEvidenceSignal = hasExplicitVatMovementEvidenceSignal(rawText);
|
||||
const explicitVatSuppressesBusinessOverviewContinuation = Boolean(explicitVatQuestionSignal && !rawPrimaryBusinessOverviewSignal);
|
||||
const businessOverviewContinuationSignal = hasBusinessOverviewFollowupSeed(followupSeed) &&
|
||||
hasBusinessOverviewContinuationSignal(rawText) &&
|
||||
|
|
@ -1114,6 +1150,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
(hasValueFlowSignal(rawText) || hasValueRankingSignal(rawText) || rawBidirectionalValueFlowSignal);
|
||||
const rawMetadataSignal = !rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
!explicitVatMovementEvidenceSignal &&
|
||||
!rawReferentialDocumentExclusionSignal &&
|
||||
hasMetadataSignal(rawText);
|
||||
const rawEntityResolutionSignal = !rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
|
||||
|
|
@ -1128,7 +1165,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(dateScopeSignalText);
|
||||
const relativeCurrentDateHintDetected = hasRelativeCurrentDateHint(rawText);
|
||||
const rawDateScope = collectDateScopeFromRawText(dateScopeSignalText);
|
||||
const rawMetadataScopeHint = rawMetadataSignal ? metadataScopeHintFromRawText(rawText) : null;
|
||||
const rawMetadataScopeHint = rawMetadataSignal || explicitVatMovementEvidenceSignal ? metadataScopeHintFromRawText(rawText) : null;
|
||||
const rawTopicSwitchSignal = hasExplicitTopicSwitchSignal(rawText);
|
||||
const rawEntityCandidate = rawEntityResolutionSignal ? rawEntityResolutionCandidate(rawEntitySourceText) : null;
|
||||
const rawScopedEntityCandidate = !predecomposeEntities.counterparty &&
|
||||
|
|
@ -1147,11 +1184,41 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis);
|
||||
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
|
||||
const broadBusinessEvaluationUnsupported = unsupported === "broad_business_evaluation";
|
||||
const businessOverviewSignal = rawBusinessOverviewSignal ||
|
||||
broadBusinessEvaluationUnsupported ||
|
||||
const seededBusinessOverviewSignal = broadBusinessEvaluationUnsupported ||
|
||||
rawDomain === "business_summary" ||
|
||||
rawDomain === "business_overview" ||
|
||||
rawAction === "broad_evaluation";
|
||||
const inventoryReserveBusinessOverviewSignal = (rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
||||
hasOrganizationLevelInventoryReserveLiquidationOverviewSignal(rawText);
|
||||
const debtDueDateFollowupBusinessOverviewSignal = businessOverviewContinuationSignal && hasDebtDueDateBoundaryFollowupSignal(rawText);
|
||||
const debtDueDateBusinessOverviewSignal = ((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
||||
hasOrganizationLevelDebtDueDateOverviewSignal(rawText)) ||
|
||||
debtDueDateFollowupBusinessOverviewSignal;
|
||||
const supplierQualityBusinessOverviewSignal = (rawBusinessOverviewSignal || seededBusinessOverviewSignal) && hasOrganizationLevelSupplierQualityOverviewSignal(rawText);
|
||||
const profitMarginFollowupBusinessOverviewSignal = businessOverviewContinuationSignal && hasProfitMarginBoundaryFollowupSignal(rawText);
|
||||
const profitMarginBusinessOverviewSignal = ((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
||||
hasOrganizationLevelProfitMarginBoundaryOverviewSignal(rawText)) ||
|
||||
profitMarginFollowupBusinessOverviewSignal;
|
||||
const businessOverviewActionFamily = inventoryReserveBusinessOverviewSignal
|
||||
? "inventory_reserve_boundary"
|
||||
: debtDueDateBusinessOverviewSignal
|
||||
? "debt_due_date_boundary"
|
||||
: supplierQualityBusinessOverviewSignal
|
||||
? "vendor_risk_procurement_boundary"
|
||||
: profitMarginBusinessOverviewSignal
|
||||
? "profit_margin_boundary"
|
||||
: "broad_evaluation";
|
||||
const businessOverviewUnsupportedFamily = inventoryReserveBusinessOverviewSignal
|
||||
? "inventory_reserve_liquidation_boundary"
|
||||
: debtDueDateBusinessOverviewSignal
|
||||
? "debt_due_date_boundary"
|
||||
: supplierQualityBusinessOverviewSignal
|
||||
? "vendor_risk_procurement_boundary"
|
||||
: profitMarginBusinessOverviewSignal
|
||||
? "profit_margin_boundary"
|
||||
: "broad_business_evaluation";
|
||||
const businessOverviewSignal = rawBusinessOverviewSignal ||
|
||||
seededBusinessOverviewSignal;
|
||||
const businessOverviewSeparateCounterpartySignal = Boolean(businessOverviewSignal && hasBusinessOverviewSeparateCounterpartySignal(rawText));
|
||||
const businessOverviewSeparateCounterpartyCandidate = businessOverviewSeparateCounterpartySignal
|
||||
? businessOverviewSeparateCounterpartyCandidateFromText(rawText)
|
||||
|
|
@ -1176,7 +1243,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
: rawAssistantTurnMeaningOrganizationScope;
|
||||
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
|
||||
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
|
||||
const currentTurnFreshOrganizationScope = rawOrganizationScope ?? predecomposeEntities.organization;
|
||||
const currentTurnFreshOrganizationScope = predecomposeEntities.organization ?? rawOrganizationScope;
|
||||
const currentTurnOrganizationScope = currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope;
|
||||
const followupCounterpartyIsMetadataOrganizationScope = Boolean(followupSeed.subjectResolutionOptional &&
|
||||
followupSeed.counterparty &&
|
||||
|
|
@ -1500,9 +1567,21 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
const semanticDataNeed = metadataAmbiguityLaneClarificationApplicable
|
||||
? "metadata lane clarification"
|
||||
: semanticNeedFor({
|
||||
domain: businessOverviewSignal ? "business_overview" : rawDomain ?? seededDomain,
|
||||
action: businessOverviewSignal ? "broad_evaluation" : rawAction ?? seededAction,
|
||||
unsupported: businessOverviewSignal ? "broad_business_evaluation" : unsupported ?? seededUnsupported,
|
||||
domain: explicitVatMovementEvidenceSignal
|
||||
? "movements"
|
||||
: businessOverviewSignal
|
||||
? "business_overview"
|
||||
: rawDomain ?? seededDomain,
|
||||
action: explicitVatMovementEvidenceSignal
|
||||
? "list_movements"
|
||||
: businessOverviewSignal
|
||||
? businessOverviewActionFamily
|
||||
: rawAction ?? seededAction,
|
||||
unsupported: explicitVatMovementEvidenceSignal
|
||||
? "movement_evidence"
|
||||
: businessOverviewSignal
|
||||
? businessOverviewUnsupportedFamily
|
||||
: unsupported ?? seededUnsupported,
|
||||
lifecycleSignal,
|
||||
valueFlowSignal,
|
||||
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
|
||||
|
|
@ -1530,7 +1609,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
followupSeed.discoveryEntity ??
|
||||
followupSeed.metadataSelectedEntitySet ??
|
||||
null;
|
||||
const metadataScopedLaneWithoutSubject = Boolean((metadataGroundedMovementLaneApplicable || metadataGroundedDocumentLaneApplicable) &&
|
||||
const metadataScopedLaneWithoutSubject = Boolean((metadataGroundedMovementLaneApplicable ||
|
||||
metadataGroundedDocumentLaneApplicable ||
|
||||
explicitVatMovementEvidenceSignal) &&
|
||||
!effectiveFollowupCounterparty &&
|
||||
metadataLaneCarryoverAvailable);
|
||||
const groundedFollowupEntity = metadataScopedLaneWithoutSubject
|
||||
|
|
@ -1711,6 +1792,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
? "counterparty_lifecycle"
|
||||
: valueFlowSignal
|
||||
? "counterparty_value"
|
||||
: explicitVatMovementEvidenceSignal
|
||||
? "movements"
|
||||
: metadataGroundedMovementLaneApplicable
|
||||
? "movements"
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
|
|
@ -1721,7 +1804,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
? "metadata"
|
||||
: rawDomain ?? seededDomain,
|
||||
asked_action_family: businessOverviewSignal
|
||||
? "broad_evaluation"
|
||||
? businessOverviewActionFamily
|
||||
: lifecycleSignal
|
||||
? "activity_duration"
|
||||
: valueFlowSignal
|
||||
|
|
@ -1730,6 +1813,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
: payoutSignal
|
||||
? "payout"
|
||||
: rawAction ?? seededAction ?? "turnover"
|
||||
: explicitVatMovementEvidenceSignal
|
||||
? "list_movements"
|
||||
: metadataGroundedMovementLaneApplicable
|
||||
? "list_movements"
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
|
|
@ -1757,7 +1842,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
explicit_date_scope: explicitDateScope,
|
||||
subject_resolution_optional: metadataScopedLaneWithoutSubject || undefined,
|
||||
unsupported_but_understood_family: businessOverviewSignal
|
||||
? "broad_business_evaluation"
|
||||
? businessOverviewUnsupportedFamily
|
||||
: unsupported ??
|
||||
(lifecycleSignal
|
||||
? "counterparty_lifecycle"
|
||||
|
|
@ -1771,6 +1856,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
? "movement_evidence"
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
? "document_evidence"
|
||||
: explicitVatMovementEvidenceSignal
|
||||
? "movement_evidence"
|
||||
: metadataAmbiguityLaneClarificationApplicable
|
||||
? "metadata_lane_choice_clarification"
|
||||
: entityResolutionSignal
|
||||
|
|
@ -1785,6 +1872,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
unsupported ||
|
||||
lifecycleSignal ||
|
||||
valueFlowSignal ||
|
||||
explicitVatMovementEvidenceSignal ||
|
||||
metadataGroundedMovementLaneApplicable ||
|
||||
metadataGroundedDocumentLaneApplicable ||
|
||||
metadataAmbiguityLaneClarificationApplicable ||
|
||||
|
|
@ -1841,11 +1929,15 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
}
|
||||
const currentTurnValueFlowExactOverrideApplicable = Boolean(valueFlowSignal &&
|
||||
explicitIntentCandidate &&
|
||||
rawValueFlowAggregateQuestionSignal &&
|
||||
(rawValueFlowAggregateQuestionSignal || hasValueRankingSignal(rawText)) &&
|
||||
semanticDataNeed &&
|
||||
(entityCandidates.length > 0 || explicitOrganizationScope || openScopeValueFlowWithoutResolvedCounterparty));
|
||||
const runDiscovery = shouldRunDiscovery({
|
||||
unsupported: businessOverviewSignal ? "broad_business_evaluation" : unsupported ?? seededUnsupported,
|
||||
unsupported: explicitVatMovementEvidenceSignal
|
||||
? "movement_evidence"
|
||||
: businessOverviewSignal
|
||||
? "broad_business_evaluation"
|
||||
: unsupported ?? seededUnsupported,
|
||||
lifecycleSignal,
|
||||
valueFlowSignal,
|
||||
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
|
||||
|
|
@ -1860,6 +1952,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
metadataGroundedDocumentLaneApplicable ||
|
||||
groundedValueFlowFollowupApplicable,
|
||||
forceDiscoveryOverExplicitIntent: businessOverviewSignal ||
|
||||
explicitVatMovementEvidenceSignal ||
|
||||
Boolean(entityResolutionClarificationCandidate) ||
|
||||
organizationClarificationFollowupApplicable ||
|
||||
periodClarificationFollowupApplicable ||
|
||||
|
|
@ -1883,6 +1976,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
? "followup_context"
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
? "followup_context"
|
||||
: explicitVatMovementEvidenceSignal
|
||||
? "raw_text"
|
||||
: predecomposeContract
|
||||
? "predecompose_contract"
|
||||
: lifecycleSignal
|
||||
|
|
@ -1903,6 +1998,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
if (rawMetadataSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_metadata_signal_detected");
|
||||
}
|
||||
if (explicitVatMovementEvidenceSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_vat_movement_evidence_signal_detected");
|
||||
}
|
||||
if (entityResolutionSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_entity_resolution_signal_detected");
|
||||
}
|
||||
|
|
@ -2026,6 +2124,12 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
if (businessOverviewContinuationSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_from_followup_context");
|
||||
}
|
||||
if (profitMarginFollowupBusinessOverviewSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_business_overview_profit_margin_followup_boundary");
|
||||
}
|
||||
if (debtDueDateFollowupBusinessOverviewSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_business_overview_debt_due_date_followup_boundary");
|
||||
}
|
||||
if (explicitVatSuppressesBusinessOverviewContinuation) {
|
||||
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_suppressed_by_explicit_vat_question");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -313,6 +313,13 @@ function createAssistantRoutePolicy(deps) {
|
|||
const hasTemporalCue = /(?:на\s+эту\s+же\s+дат[ауеы]|на\s+тот\s+же\s+период|за\s+этот\s+же\s+период|за\s+этот\s+период|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр|\b(?:19|20)\d{2}\b)/iu.test(normalized);
|
||||
return hasRequestCue && hasTemporalCue;
|
||||
}
|
||||
function hasOrganizationClarificationTextCue(text) {
|
||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return /(?<!\p{L})(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e)(?!\p{L})|(?:\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u043a\u043e\u043c\u043f\u0430\u043d|llc|company|organization)/iu.test(normalized);
|
||||
}
|
||||
function resolveAssistantOrchestrationDecision(input) {
|
||||
const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? "");
|
||||
const effectiveAddressUserMessage = String(input?.effectiveAddressUserMessage ?? rawUserMessage);
|
||||
|
|
@ -475,6 +482,27 @@ function createAssistantRoutePolicy(deps) {
|
|||
const followupPreviousFilters = followupContext?.previous_filters && typeof followupContext.previous_filters === "object"
|
||||
? followupContext.previous_filters
|
||||
: null;
|
||||
const followupLoopStatus = toNonEmptyString(followupContext?.previous_discovery_loop_status);
|
||||
const followupLoopSelectedChainId = toNonEmptyString(followupContext?.previous_discovery_loop_selected_chain_id);
|
||||
const followupLoopPendingAxes = Array.isArray(followupContext?.previous_discovery_loop_pending_axes)
|
||||
? followupContext.previous_discovery_loop_pending_axes.map((item) => toNonEmptyString(item)).filter(Boolean)
|
||||
: [];
|
||||
const currentTurnPredecomposeOrganization = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.entities?.organization) ??
|
||||
(toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.semantics?.anchor_kind) === "organization"
|
||||
? toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.semantics?.anchor_value)
|
||||
: null);
|
||||
const routeCandidateOrganizationClarificationDetected = Boolean(followupContext &&
|
||||
followupLoopStatus === "awaiting_clarification" &&
|
||||
followupLoopSelectedChainId &&
|
||||
followupLoopPendingAxes.includes("organization") &&
|
||||
(currentTurnPredecomposeOrganization ||
|
||||
explicitOrganizationClarificationSelection ||
|
||||
[
|
||||
rawUserMessage,
|
||||
repairedRawUserMessage,
|
||||
effectiveAddressUserMessage,
|
||||
repairedEffectiveAddressUserMessage
|
||||
].some((message) => hasOrganizationClarificationTextCue(message))));
|
||||
const protectedInventoryShortFollowup = Boolean(followupContext &&
|
||||
(isInventorySelectedObjectIntent(followupPreviousIntent) ||
|
||||
(followupPreviousIntent === "inventory_on_hand_as_of_date" &&
|
||||
|
|
@ -524,6 +552,7 @@ function createAssistantRoutePolicy(deps) {
|
|||
"net_value_flow"
|
||||
].includes(String(toNonEmptyString(assistantTurnMeaning?.asked_action_family) ?? "")) ||
|
||||
/(?:нетто|сальдо|сколько\s+мы\s+(?:получили|заплатили)|incoming|outgoing)/iu.test(analyticsSample)));
|
||||
const effectiveGroundedValueFlowFollowupContextDetected = groundedValueFlowFollowupContextDetected || routeCandidateOrganizationClarificationDetected;
|
||||
const baseToolGatePreservesAddressLane = Boolean(baseToolGate?.runAddressLane &&
|
||||
[
|
||||
"address_intent_resolver_detected",
|
||||
|
|
@ -533,14 +562,15 @@ function createAssistantRoutePolicy(deps) {
|
|||
].includes(String(baseToolGate?.reason ?? ""))) ||
|
||||
Boolean(baseToolGate?.runAddressLane &&
|
||||
String(baseToolGate?.reason ?? "") === "followup_context_detected" &&
|
||||
groundedValueFlowFollowupContextDetected);
|
||||
effectiveGroundedValueFlowFollowupContextDetected);
|
||||
const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate &&
|
||||
deterministicNonDomainGuard &&
|
||||
(llmFirstUnsupportedCandidate || llmContractMode === null) &&
|
||||
!baseToolGatePreservesAddressLane &&
|
||||
!groundedValueFlowFollowupContextDetected &&
|
||||
!effectiveGroundedValueFlowFollowupContextDetected &&
|
||||
!protectedInventoryShortFollowup &&
|
||||
!organizationClarificationContinuationDetected);
|
||||
!organizationClarificationContinuationDetected &&
|
||||
!routeCandidateOrganizationClarificationDetected);
|
||||
const lastAddressAssistantDebug = sessionItems
|
||||
? findLastAddressAssistantItem(sessionItems)?.debug ?? null
|
||||
: null;
|
||||
|
|
@ -583,7 +613,7 @@ function createAssistantRoutePolicy(deps) {
|
|||
!turnMeaningIntentCandidate &&
|
||||
!dataScopeMetaQuery &&
|
||||
!dangerOrCoercionSignal &&
|
||||
!groundedValueFlowFollowupContextDetected &&
|
||||
!effectiveGroundedValueFlowFollowupContextDetected &&
|
||||
!organizationClarificationContinuationDetected);
|
||||
const hardMetaMode = resolveHardMetaMode({
|
||||
dataScopeMetaQuery,
|
||||
|
|
@ -748,7 +778,7 @@ function createAssistantRoutePolicy(deps) {
|
|||
!dataScopeMetaQuery &&
|
||||
!capabilityMetaQuery &&
|
||||
!dangerOrCoercionSignal &&
|
||||
!groundedValueFlowFollowupContextDetected &&
|
||||
!effectiveGroundedValueFlowFollowupContextDetected &&
|
||||
!organizationClarificationContinuationDetected);
|
||||
if (unsupportedCurrentTurnMeaningBoundary) {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -2123,6 +2123,9 @@ function isAddressLaneDebugPayload(debug) {
|
|||
if (typeof debug.mcp_call_status === "string" && debug.mcp_call_status.trim().length > 0) {
|
||||
return true;
|
||||
}
|
||||
if (debug.mcp_discovery_response_applied === true && debug.assistant_mcp_discovery_entry_point_v1) {
|
||||
return true;
|
||||
}
|
||||
if (typeof debug.anchor_type === "string" && debug.anchor_type.trim().length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,6 +120,22 @@ function createAssistantTransitionPolicy(deps) {
|
|||
const samples = [userMessage, alternateMessage].map((item) => normalizeFollowupText(item)).filter(Boolean);
|
||||
return samples.some((sample) => /(?:итог|summary|резюм|вывод|что\s+(?:мы\s+)?подтверд|что\s+понят|что\s+можно|что\s+нельзя|собери\s+коротк)/iu.test(sample) && /(?:контрагент|группа\s+свк|свк|отдельн)/iu.test(sample));
|
||||
}
|
||||
function hasBusinessOverviewBoundaryFollowupCue(text) {
|
||||
const normalized = normalizeFollowupText(text);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const hasBoundaryCue = /(?:\u043f\u0440\u043e\u0441\u0440\u043e\u0447|\u0441\u0440\u043e\u043a\w*\s+\u043e\u043f\u043b\u0430\u0442|\u0434\u043e\u043a\u0430\u0437|\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434|\u043f\u0440\u0438\u0431\u044b\u043b|\u0443\u0431\u044b\u0442|\u043c\u0430\u0440\u0436|\u0440\u0435\u0437\u0435\u0440\u0432|\u043d\u0435\u043b\u0438\u043a\u0432\u0438\u0434|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0432\u0435\u043d\u0434\u043e\u0440|due[-\s]?date|overdue|aging|profit|loss|margin|vendor|risk)/iu.test(normalized);
|
||||
const hasFollowupShape = /(?:\u0442\u043e\s+\u0435\u0441\u0442\u044c|\u043f\u043e\u0447\u0435\u043c\u0443|\u043a\u043e\u0440\u043e\u0442\u043a|\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|\u0438\u0442\u043e\u0433|why|short|brief|so)/iu.test(normalized);
|
||||
return hasBoundaryCue && hasFollowupShape;
|
||||
}
|
||||
function hasOrganizationClarificationTextCue(text) {
|
||||
const normalized = deps.compactWhitespace(deps.repairAddressMojibake(String(text ?? "")).toLowerCase());
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return /(?<!\p{L})(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e)(?!\p{L})|(?:\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u043a\u043e\u043c\u043f\u0430\u043d|llc|company|organization)/iu.test(normalized);
|
||||
}
|
||||
function parseDmyDateToIso(value) {
|
||||
const match = String(value ?? "").trim().match(/^(\d{2})\.(\d{2})\.(\d{4})$/);
|
||||
if (!match) {
|
||||
|
|
@ -433,7 +449,11 @@ function createAssistantTransitionPolicy(deps) {
|
|||
(deps.toNonEmptyString(alternateMessage)
|
||||
? deps.hasDataRetrievalRequestSignal(String(alternateMessage ?? ""))
|
||||
: false);
|
||||
if (rawCapabilityMetaQuery && !rawDataRetrievalSignal) {
|
||||
const rawBusinessOverviewBoundaryFollowupCue = hasBusinessOverviewBoundaryFollowupCue(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
? hasBusinessOverviewBoundaryFollowupCue(String(alternateMessage ?? ""))
|
||||
: false);
|
||||
if (rawCapabilityMetaQuery && !rawDataRetrievalSignal && !rawBusinessOverviewBoundaryFollowupCue) {
|
||||
return null;
|
||||
}
|
||||
const assistantTurnMeaning = typeof deps.resolveAssistantTurnMeaning === "function"
|
||||
|
|
@ -492,10 +512,25 @@ function createAssistantTransitionPolicy(deps) {
|
|||
: false));
|
||||
const sourceIntentHint = (0, assistantContinuityPolicy_1.readAddressDebugIntent)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryPilotScopeHint = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryPilotScope)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopStatusHint = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopStatus)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopSelectedChainIdHint = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopSelectedChainId)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopPendingAxesHint = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopPendingAxes)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopProvidedAxesHint = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopProvidedAxes)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const currentTurnPredecomposeOrganization = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.entities?.organization) ??
|
||||
(deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.semantics?.anchor_kind) === "organization"
|
||||
? deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.semantics?.anchor_value)
|
||||
: null);
|
||||
const mcpDiscoveryOrganizationClarificationContinuation = Boolean(sourceDiscoveryLoopStatusHint === "awaiting_clarification" &&
|
||||
sourceDiscoveryLoopSelectedChainIdHint &&
|
||||
sourceDiscoveryLoopPendingAxesHint.includes("organization") &&
|
||||
(currentTurnPredecomposeOrganization ||
|
||||
explicitOrganizationClarificationSelection ||
|
||||
[userMessage, alternateMessage].some((message) => hasOrganizationClarificationTextCue(message))));
|
||||
const hasValueFlowCarryoverSourceHint = sourceIntentHint === "customer_revenue_and_payments" ||
|
||||
sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" ||
|
||||
sourceDiscoveryPilotScopeHint === "counterparty_supplier_payout_query_movements_v1" ||
|
||||
sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1";
|
||||
const hasBusinessOverviewCarryoverSourceHint = sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
|
||||
const navigationSessionState = (0, assistantContinuityPolicy_1.resolveNavigationSessionContextState)(addressNavigationState, deps.toNonEmptyString, deps.normalizeOrganizationScopeValue);
|
||||
const navigationFocusObjectHint = navigationSessionState.focusObject;
|
||||
const hasNavigationInventoryItemFocusHint = Boolean(deps.toNonEmptyString(navigationFocusObjectHint?.label) &&
|
||||
|
|
@ -521,20 +556,28 @@ function createAssistantTransitionPolicy(deps) {
|
|||
const shortValueFlowRetargetAlternate = hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
|
||||
? hasShortValueFlowRetargetCue(String(alternateMessage ?? ""))
|
||||
: false;
|
||||
const businessOverviewBoundaryFollowupPrimary = hasBusinessOverviewCarryoverSourceHint && hasBusinessOverviewBoundaryFollowupCue(userMessage);
|
||||
const businessOverviewBoundaryFollowupAlternate = hasBusinessOverviewCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
|
||||
? hasBusinessOverviewBoundaryFollowupCue(String(alternateMessage ?? ""))
|
||||
: false;
|
||||
const explicitSummaryBundleReuseSignal = hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage);
|
||||
let hasPrimaryFollowupSignal = deps.hasAddressFollowupContextSignal(userMessage) ||
|
||||
Boolean(debtRoleSwapPrimary) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
explicitSummaryBundleReuseSignal;
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation;
|
||||
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||
Boolean(debtRoleSwapAlternate) ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
explicitSummaryBundleReuseSignal
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation
|
||||
: false;
|
||||
const hasPrimaryIndexReferenceSignal = deps.extractDisplayedEntityIndexMention(userMessage) !== null;
|
||||
const hasAlternateIndexReferenceSignal = deps.toNonEmptyString(alternateMessage)
|
||||
|
|
@ -557,6 +600,7 @@ function createAssistantTransitionPolicy(deps) {
|
|||
let hasStrongFollowupReference = hasPrimaryIndexReferenceSignal ||
|
||||
hasAlternateIndexReferenceSignal ||
|
||||
hasOrganizationClarificationContinuation ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation ||
|
||||
hasImplicitContinuationSignal ||
|
||||
hasSuggestedIntentPivotSignal ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
|
|
@ -570,6 +614,8 @@ function createAssistantTransitionPolicy(deps) {
|
|||
Boolean(debtRoleSwapIntent) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
|
|
@ -579,6 +625,7 @@ function createAssistantTransitionPolicy(deps) {
|
|||
const hasConcreteFollowupReference = hasPrimaryIndexReferenceSignal ||
|
||||
hasAlternateIndexReferenceSignal ||
|
||||
hasOrganizationClarificationContinuation ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
hasInventoryRootTemporalFollowupPrimary ||
|
||||
|
|
@ -590,6 +637,8 @@ function createAssistantTransitionPolicy(deps) {
|
|||
Boolean(debtRoleSwapIntent) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
|
|
@ -617,6 +666,7 @@ function createAssistantTransitionPolicy(deps) {
|
|||
!hasImplicitContinuationSignal &&
|
||||
!hasSuggestedIntentPivotSignal &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
!mcpDiscoveryOrganizationClarificationContinuation &&
|
||||
!hasIndexReferenceSignal &&
|
||||
!explicitSummaryBundleReuseSignal) {
|
||||
return null;
|
||||
|
|
@ -632,6 +682,7 @@ function createAssistantTransitionPolicy(deps) {
|
|||
!hasImplicitContinuationSignal &&
|
||||
!hasSuggestedIntentPivotSignal &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
!mcpDiscoveryOrganizationClarificationContinuation &&
|
||||
!hasIndexReferenceSignal &&
|
||||
!explicitSummaryBundleReuseSignal) {
|
||||
return null;
|
||||
|
|
@ -650,10 +701,10 @@ function createAssistantTransitionPolicy(deps) {
|
|||
const sourceDiscoveryMetadataAmbiguityEntitySets = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataAmbiguityEntitySets)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryEntityResolutionStatus = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityResolutionStatus)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryEntityCandidates = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityCandidates)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopStatus = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopStatus)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopSelectedChainId = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopSelectedChainId)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopPendingAxes = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopPendingAxes)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopProvidedAxes = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopProvidedAxes)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopStatus = sourceDiscoveryLoopStatusHint;
|
||||
const sourceDiscoveryLoopSelectedChainId = sourceDiscoveryLoopSelectedChainIdHint;
|
||||
const sourceDiscoveryLoopPendingAxes = sourceDiscoveryLoopPendingAxesHint;
|
||||
const sourceDiscoveryLoopProvidedAxes = sourceDiscoveryLoopProvidedAxesHint;
|
||||
const sourceDiscoveryLoopAskedDomainFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopAskedDomainFamily)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopAskedActionFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopAskedActionFamily)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopUnsupportedFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopUnsupportedFamily)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
|
|
@ -689,6 +740,7 @@ function createAssistantTransitionPolicy(deps) {
|
|||
explicitIntentFamily &&
|
||||
sourceIntentFamily !== explicitIntentFamily &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
!mcpDiscoveryOrganizationClarificationContinuation &&
|
||||
!hasImplicitContinuationSignal &&
|
||||
!hasIndexReferenceSignal &&
|
||||
!hasInventoryRootTemporalFollowupPrimary &&
|
||||
|
|
@ -697,6 +749,8 @@ function createAssistantTransitionPolicy(deps) {
|
|||
!hasInventoryRootRestatementAlternate &&
|
||||
!inventoryShortFollowupPrimary &&
|
||||
!inventoryShortFollowupAlternate &&
|
||||
!businessOverviewBoundaryFollowupPrimary &&
|
||||
!businessOverviewBoundaryFollowupAlternate &&
|
||||
!foreignAccountingPivotOverInventory &&
|
||||
!deps.hasFollowupMarker(userMessage) &&
|
||||
!deps.hasReferentialPointer(userMessage) &&
|
||||
|
|
@ -753,24 +807,29 @@ function createAssistantTransitionPolicy(deps) {
|
|||
hasSuggestedIntentPivotSignal ||
|
||||
Boolean(debtRoleSwapPrimary) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
hasInventoryRootTemporalFollowupPrimary;
|
||||
hasInventoryRootTemporalFollowupPrimary ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation;
|
||||
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||
hasSuggestedIntentPivotSignal ||
|
||||
Boolean(debtRoleSwapAlternate) ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
hasInventoryRootTemporalFollowupAlternate
|
||||
hasInventoryRootTemporalFollowupAlternate ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation
|
||||
: false;
|
||||
hasStrongFollowupReference =
|
||||
hasPrimaryIndexReferenceSignal ||
|
||||
hasAlternateIndexReferenceSignal ||
|
||||
hasOrganizationClarificationContinuation ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation ||
|
||||
hasSuggestedIntentPivotSignal ||
|
||||
hasImplicitContinuationSignal ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
|
|
@ -782,6 +841,8 @@ function createAssistantTransitionPolicy(deps) {
|
|||
Boolean(debtRoleSwapIntent) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.isLikelyFinancialInstitutionCounterparty = isLikelyFinancialInstitutionCounterparty;
|
||||
exports.counterpartyRoleHintForName = counterpartyRoleHintForName;
|
||||
const FINANCIAL_INSTITUTION_PATTERNS = [
|
||||
/(?:^|[\s"«(,-])банк(?:$|[\s"»),.-])/u,
|
||||
/сбербанк/u,
|
||||
/(?:^|[\s"«(,-])сбер(?:$|[\s"»),.-])/u,
|
||||
/(?:^|[\s"«(,-])втб(?:$|[\s"»),.-])/u,
|
||||
/альфа[\s-]*банк/u,
|
||||
/тинькофф/u,
|
||||
/(?:^|[\s"«(,-])т[\s-]*банк(?:$|[\s"»),.-])/u,
|
||||
/газпромбанк/u,
|
||||
/росбанк/u,
|
||||
/райффайзен/u,
|
||||
/совкомбанк/u,
|
||||
/промсвязьбанк/u,
|
||||
/(?:^|[\s"«(,-])псб(?:$|[\s"»),.-])/u,
|
||||
/(?:^|[\s"«(,-])мкб(?:$|[\s"»),.-])/u,
|
||||
/ак[\s-]*барс/u,
|
||||
/уралсиб/u,
|
||||
/юникредит/u,
|
||||
/почта[\s-]*банк/u,
|
||||
/(?:^|[\s"«(,-])открытие(?:$|[\s"»),.-])/u,
|
||||
/кредитн(?:ая|ый|ое|ые)\s+организац/u
|
||||
];
|
||||
function normalizeCounterpartyRoleText(value) {
|
||||
return String(value ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/ё/g, "е")
|
||||
.replace(/[._]+/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
function isLikelyFinancialInstitutionCounterparty(value) {
|
||||
const normalized = normalizeCounterpartyRoleText(value);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return FINANCIAL_INSTITUTION_PATTERNS.some((pattern) => pattern.test(normalized));
|
||||
}
|
||||
function counterpartyRoleHintForName(value) {
|
||||
return isLikelyFinancialInstitutionCounterparty(value)
|
||||
? "bank_or_financial_institution"
|
||||
: "ordinary_counterparty";
|
||||
}
|
||||
|
|
@ -174,7 +174,7 @@ export const ASSISTANT_MCP_PROXY_URL = (process.env.ASSISTANT_MCP_PROXY_URL ?? "
|
|||
);
|
||||
export const ASSISTANT_MCP_CHANNEL = process.env.ASSISTANT_MCP_CHANNEL ?? "default";
|
||||
export const ASSISTANT_MCP_TIMEOUT_MS = toNumberFlag(process.env.ASSISTANT_MCP_TIMEOUT_MS, 6000);
|
||||
export const ASSISTANT_MCP_LIVE_LIMIT = Math.max(1, Math.trunc(toNumberFlag(process.env.ASSISTANT_MCP_LIVE_LIMIT, 24)));
|
||||
export const ASSISTANT_MCP_LIVE_LIMIT = Math.max(1, Math.trunc(toNumberFlag(process.env.ASSISTANT_MCP_LIVE_LIMIT, 128)));
|
||||
export const VAT_PAYABLE_68_PREFIXES = toStringListFlag(process.env.VAT_PAYABLE_68_PREFIXES, ["68.02"]);
|
||||
export const VAT_PAYABLE_19_PREFIXES = toStringListFlag(process.env.VAT_PAYABLE_19_PREFIXES, ["19"]);
|
||||
|
||||
|
|
|
|||
|
|
@ -2014,7 +2014,6 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
|||
warnings.includes("period_derived_from_year_range_phrase") ||
|
||||
warnings.includes("period_derived_from_year_phrase");
|
||||
const preserveDerivedPeriodWindow =
|
||||
usesAsOfPrimaryWindow(intent) ||
|
||||
intent === "inventory_on_hand_as_of_date" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date";
|
||||
if (periodWasDerivedHeuristically && !warnings.includes("exact_historical_period_window_requested")) {
|
||||
|
|
|
|||
|
|
@ -2166,7 +2166,11 @@ function hasVatPeriodInspectionBridgeSignal(text: string): boolean {
|
|||
normalized
|
||||
);
|
||||
const forecastOnlyCue = /(?:прогноз|план|примерн|ориентировочн)/iu.test(normalized) && !hasInspectionCue;
|
||||
return hasPeriodCue && hasInspectionCue && !forecastOnlyCue;
|
||||
const hasVatMovementInspectionCue =
|
||||
/(?:покаж|движен|операц|по\s+сч(?:е|ё)т|покаж|движен|операц|РїРѕ\s+СЃС‡(?:Рµ|С‘)С‚|show|movement|movements|operation|operations|account)/iu.test(
|
||||
normalized
|
||||
);
|
||||
return hasPeriodCue && (hasInspectionCue || hasVatMovementInspectionCue) && !forecastOnlyCue;
|
||||
}
|
||||
|
||||
function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolution | null {
|
||||
|
|
@ -3034,6 +3038,22 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
|
|||
};
|
||||
}
|
||||
|
||||
const hasExplicitVatLiabilityPeriodBridge =
|
||||
/(?:\u043d\u0434\u0441|vat)/iu.test(text) &&
|
||||
/(?:\b(?:19|20)\d{2}\b|\u0437\u0430\s+(?:\d{4}|\u0433\u043e\u0434|\u043f\u0435\u0440\u0438\u043e\u0434|\u043a\u0432\u0430\u0440\u0442\u0430\u043b|\u043c\u0435\u0441\u044f\u0446))/iu.test(
|
||||
text
|
||||
) &&
|
||||
/(?:\u043a\u0430\u043a\u043e\u0439|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u043d\u0430\u0447\u0438\u0441\u043b|\u0443\u043f\u043b\u0430\u0447|\u0443\u043f\u043b\u0430\u0442|\u043f\u0440\u043e\u0434\u0430\u0436|\u043f\u043e\u043a\u0443\u043f|\u0432\u044b\u0447\u0435\u0442|\u043a\s+\u0443\u043f\u043b\u0430\u0442|\u043a\s+\u0432\u043e\u0437\u043c\u0435\u0449|\u043f\u043e\u0437\u0438\u0446|liability|payable|charged|paid|sales|purchase|deduction|position)/iu.test(
|
||||
text
|
||||
);
|
||||
if (hasExplicitVatLiabilityPeriodBridge) {
|
||||
return {
|
||||
intent: "vat_liability_confirmed_for_tax_period",
|
||||
confidence: "high",
|
||||
reasons: ["vat_liability_explicit_period_bridge_signal_detected"]
|
||||
};
|
||||
}
|
||||
|
||||
const hasLooseVatPayableBridge =
|
||||
/(?:\u043d\u0434\u0441|vat)/iu.test(text) &&
|
||||
/(?:\u043a\u0430\u043a\u043e\u0439\s+\u043d\u0434\u0441\s+(?:(?:\u043d\u0430\u043c|(?:\u043c\u044b\s+)?\u0434\u043e\u043b\u0436\u043d\u044b)\s+)?(?:\u043d\u0430\u0434\u043e|\u043d\u0443\u0436\u043d\u043e|\u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e)|(?:\u043d\u0430\u043c|\u043c\u044b\s+)?\u043d\u0430\u0434\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|(?:\u043d\u0430\u043c|\u043c\u044b\s+)?\u043d\u0443\u0436\u043d\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043d\u0434\u0441\s+\u043a\s+\u0443\u043f\u043b\u0430\u0442\u0435)/iu.test(
|
||||
|
|
|
|||
|
|
@ -207,6 +207,52 @@ const OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
|||
Сумма __ORDER_DIRECTION__
|
||||
`;
|
||||
|
||||
const DEBT_DUE_DATE_AGING_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
__AS_OF_EXPR__ КАК Период,
|
||||
"DUE_DATE_OPEN_BALANCE" КАК Регистратор,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетДт,
|
||||
"" КАК СчетКт,
|
||||
Остатки.СуммаРазвернутыйОстатокДт КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоДт1,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоДт2,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоДт3,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК ДокументРасчетов,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация,
|
||||
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).Дата КАК ДатаДоговора,
|
||||
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).УстановленСрокОплаты КАК УстановленСрокОплаты,
|
||||
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).СрокОплаты КАК СрокОплаты,
|
||||
"debit_open_balance" КАК НаправлениеОстатка
|
||||
ИЗ
|
||||
РегистрБухгалтерии.Хозрасчетный.Остатки(__AS_OF_EXPR__, , , ) КАК Остатки
|
||||
__WHERE_DT__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
__AS_OF_EXPR__ КАК Период,
|
||||
"DUE_DATE_OPEN_BALANCE" КАК Регистратор,
|
||||
"" КАК СчетДт,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетКт,
|
||||
Остатки.СуммаРазвернутыйОстатокКт КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоДт1,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоДт2,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоДт3,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК ДокументРасчетов,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация,
|
||||
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).Дата КАК ДатаДоговора,
|
||||
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).УстановленСрокОплаты КАК УстановленСрокОплаты,
|
||||
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).СрокОплаты КАК СрокОплаты,
|
||||
"credit_open_balance" КАК НаправлениеОстатка
|
||||
ИЗ
|
||||
РегистрБухгалтерии.Хозрасчетный.Остатки(__AS_OF_EXPR__, , , ) КАК Остатки
|
||||
__WHERE_KT__
|
||||
УПОРЯДОЧИТЬ ПО
|
||||
Сумма __ORDER_DIRECTION__
|
||||
`;
|
||||
|
||||
const VAT_PAYABLE_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
__AS_OF_EXPR__ КАК Период,
|
||||
|
|
@ -747,7 +793,7 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
|
|||
purpose: "Build customer value ranking and incoming deal profile from bank inflow docs",
|
||||
required_filters: [],
|
||||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
||||
default_limit: 20,
|
||||
default_limit: 200,
|
||||
account_scope_mode: "preferred",
|
||||
query_template: "customer_revenue_profile"
|
||||
},
|
||||
|
|
@ -757,7 +803,7 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
|
|||
purpose: "Build supplier payout ranking and outgoing deal profile from bank outflow docs",
|
||||
required_filters: [],
|
||||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
||||
default_limit: 20,
|
||||
default_limit: 200,
|
||||
account_scope_mode: "preferred",
|
||||
query_template: "supplier_payout_profile"
|
||||
},
|
||||
|
|
@ -802,6 +848,17 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
|
|||
account_scope_mode: "preferred",
|
||||
query_template: "vat_liability_confirmed_tax_period_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_accounting_financial_result_for_organization_v1",
|
||||
intent: "accounting_financial_result_for_organization",
|
||||
purpose: "Build reviewed accounting financial-result aggregate from 90/91/99 period-close movements",
|
||||
required_filters: ["period_from", "period_to"],
|
||||
optional_filters: ["organization", "limit", "sort"],
|
||||
default_limit: 32,
|
||||
account_scope: ["90", "91", "99"],
|
||||
account_scope_mode: "strict",
|
||||
query_template: "accounting_financial_result_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_inventory_on_hand_as_of_date_v1",
|
||||
intent: "inventory_on_hand_as_of_date",
|
||||
|
|
@ -912,6 +969,17 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
|
|||
account_scope_mode: "strict",
|
||||
query_template: "open_contracts_confirmed_as_of_balance_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_debt_due_date_aging_for_organization_v1",
|
||||
intent: "debt_due_date_aging_for_organization",
|
||||
purpose: "Check open 60/62/76 settlements against contract payment-term fields and settlement document dates before claiming overdue debt",
|
||||
required_filters: ["as_of_date"],
|
||||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
||||
default_limit: 400,
|
||||
account_scope: ["60", "62", "76"],
|
||||
account_scope_mode: "strict",
|
||||
query_template: "debt_due_date_aging_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_contracts_by_counterparty_v1",
|
||||
intent: "list_contracts_by_counterparty",
|
||||
|
|
@ -1146,6 +1214,37 @@ function buildContractValueWhereClause(filters: AddressFilterSet, fieldPath: str
|
|||
]);
|
||||
}
|
||||
|
||||
function buildContractReferenceCondition(filters: AddressFilterSet, fieldPaths: string[]): string | null {
|
||||
const contract = typeof filters.contract === "string" ? filters.contract.trim() : "";
|
||||
if (!contract) {
|
||||
return null;
|
||||
}
|
||||
const contractTokens = Array.from(
|
||||
new Set(
|
||||
contract
|
||||
.split(/[^A-Za-zА-Яа-яЁё0-9]+/u)
|
||||
.map((token) => token.trim())
|
||||
.filter((token) => token.length >= 3)
|
||||
.filter((token) => !["договор", "дог"].includes(token.toLowerCase()))
|
||||
)
|
||||
);
|
||||
const tokens = contractTokens.length > 0 ? contractTokens : [contract];
|
||||
const clauses = fieldPaths
|
||||
.map((fieldPath) => String(fieldPath ?? "").trim())
|
||||
.filter((fieldPath) => fieldPath.length > 0)
|
||||
.map((fieldPath) => {
|
||||
const tokenConditions = tokens.map((token) => {
|
||||
const escapedToken = toQueryStringLiteral(token);
|
||||
return `${fieldPath}.Наименование ПОДОБНО "%${escapedToken}%"`;
|
||||
});
|
||||
return tokenConditions.length === 1 ? tokenConditions[0] : `(${tokenConditions.join(" И ")})`;
|
||||
});
|
||||
if (clauses.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
||||
}
|
||||
|
||||
function normalizeAccountTokenForQuery(value: string): string {
|
||||
const source = String(value ?? "").trim().replace(",", ".");
|
||||
const match = source.match(/^(\d{2})(?:\.(\d{1,2}))?/);
|
||||
|
|
@ -1251,6 +1350,48 @@ function buildAccountPrefixPredicate(fieldPath: string, prefixes: string[]): str
|
|||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
||||
}
|
||||
|
||||
function buildDebtDueDateAgingWhereClause(
|
||||
filters: AddressFilterSet,
|
||||
amountFieldPath: string,
|
||||
accountPredicate: string
|
||||
): string {
|
||||
const conditions = [
|
||||
`${amountFieldPath} > 0`,
|
||||
`(${accountPredicate})`,
|
||||
buildOrganizationReferenceCondition(filters, ["Остатки.Организация"]),
|
||||
buildCounterpartyReferenceCondition(filters, ["Остатки.Субконто1"]),
|
||||
buildContractReferenceCondition(filters, ["Остатки.Субконто2"])
|
||||
].filter((item): item is string => Boolean(item));
|
||||
return `ГДЕ\n ${conditions.join("\n И ")}`;
|
||||
}
|
||||
|
||||
function buildDebtDueDateAgingQuery(filters: AddressFilterSet, resolvedLimit: number): string {
|
||||
const asOfExpr =
|
||||
(typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||
? toDateTimeExpr(filters.as_of_date, true)
|
||||
: null) ??
|
||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_to, true)
|
||||
: null) ??
|
||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ТЕКУЩАЯДАТА()";
|
||||
const accountPredicate = buildAccountPrefixPredicate("Остатки.Счет", ["60", "62", "76"]);
|
||||
return DEBT_DUE_DATE_AGING_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll(
|
||||
"__WHERE_DT__",
|
||||
buildDebtDueDateAgingWhereClause(filters, "Остатки.СуммаРазвернутыйОстатокДт", accountPredicate)
|
||||
)
|
||||
.replaceAll(
|
||||
"__WHERE_KT__",
|
||||
buildDebtDueDateAgingWhereClause(filters, "Остатки.СуммаРазвернутыйОстатокКт", accountPredicate)
|
||||
)
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
}
|
||||
|
||||
function buildInventoryMovementQuery(
|
||||
filters: AddressFilterSet,
|
||||
resolvedLimit: number,
|
||||
|
|
@ -1340,6 +1481,179 @@ function buildCounterpartyReferenceCondition(filters: AddressFilterSet, fieldPat
|
|||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
||||
}
|
||||
|
||||
const ORGANIZATION_REFERENCE_STOP_WORDS = new Set([
|
||||
"ооо",
|
||||
"зао",
|
||||
"оао",
|
||||
"ао",
|
||||
"пао",
|
||||
"ип",
|
||||
"на",
|
||||
"за",
|
||||
"по",
|
||||
"конец",
|
||||
"начало",
|
||||
"год",
|
||||
"года",
|
||||
"период",
|
||||
"можно",
|
||||
"точно",
|
||||
"понять",
|
||||
"какая",
|
||||
"какой",
|
||||
"какие",
|
||||
"какую",
|
||||
"компания",
|
||||
"компании",
|
||||
"организация",
|
||||
"организации",
|
||||
"дебиторка",
|
||||
"дебиторки",
|
||||
"кредиторка",
|
||||
"кредиторки",
|
||||
"просрочена",
|
||||
"просроченные",
|
||||
"просрочка",
|
||||
"срок",
|
||||
"оплаты",
|
||||
"прибыль",
|
||||
"маржа",
|
||||
"ндс"
|
||||
]);
|
||||
|
||||
const ORGANIZATION_REFERENCE_BOUNDARY_WORDS = new Set([
|
||||
"на",
|
||||
"за",
|
||||
"конец",
|
||||
"начало",
|
||||
"можно",
|
||||
"точно",
|
||||
"понять",
|
||||
"какая",
|
||||
"какой",
|
||||
"какие",
|
||||
"какую",
|
||||
"дебиторка",
|
||||
"дебиторки",
|
||||
"кредиторка",
|
||||
"кредиторки",
|
||||
"просрочена",
|
||||
"просроченные",
|
||||
"просрочка",
|
||||
"прибыль",
|
||||
"маржа",
|
||||
"ндс"
|
||||
]);
|
||||
|
||||
function organizationReferenceTokens(organization: string): string[] {
|
||||
const rawTokens = organization
|
||||
.split(/[^A-Za-zА-Яа-яЁё0-9]+/u)
|
||||
.map((token) => token.trim())
|
||||
.filter((token) => token.length > 0);
|
||||
const boundaryIndex = rawTokens.findIndex((token) => {
|
||||
const lower = token.toLowerCase();
|
||||
return /^\d+$/.test(token) || ORGANIZATION_REFERENCE_BOUNDARY_WORDS.has(lower);
|
||||
});
|
||||
const scopedTokens = boundaryIndex > 0 ? rawTokens.slice(0, boundaryIndex) : rawTokens;
|
||||
return Array.from(
|
||||
new Set(
|
||||
scopedTokens
|
||||
.filter((token) => token.length >= 3)
|
||||
.filter((token) => !/^\d+$/.test(token))
|
||||
.filter((token) => !ORGANIZATION_REFERENCE_STOP_WORDS.has(token.toLowerCase()))
|
||||
)
|
||||
).slice(0, 4);
|
||||
}
|
||||
|
||||
function buildOrganizationReferenceCondition(filters: AddressFilterSet, fieldPaths: string[]): string | null {
|
||||
const organization = typeof filters.organization === "string" ? filters.organization.trim() : "";
|
||||
if (!organization) {
|
||||
return null;
|
||||
}
|
||||
const organizationTokens = organizationReferenceTokens(organization);
|
||||
const tokens = organizationTokens.length > 0 ? organizationTokens : [organization];
|
||||
const clauses = fieldPaths
|
||||
.map((fieldPath) => String(fieldPath ?? "").trim())
|
||||
.filter((fieldPath) => fieldPath.length > 0)
|
||||
.map((fieldPath) => {
|
||||
const tokenConditions = tokens.map((token) => {
|
||||
const escapedToken = toQueryStringLiteral(token);
|
||||
return `(Организации.Наименование ПОДОБНО "%${escapedToken}%" ИЛИ Организации.НаименованиеПолное ПОДОБНО "%${escapedToken}%")`;
|
||||
});
|
||||
const referenceSubquery =
|
||||
`(ВЫБРАТЬ Организации.Ссылка ИЗ Справочник.Организации КАК Организации ` +
|
||||
`ГДЕ ${tokenConditions.length === 1 ? tokenConditions[0] : tokenConditions.join(" И ")})`;
|
||||
return `${fieldPath} В ${referenceSubquery}`;
|
||||
});
|
||||
if (clauses.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
||||
}
|
||||
|
||||
function buildAccountingFinancialResultAggregateSelect(
|
||||
filters: AddressFilterSet,
|
||||
marker: string,
|
||||
debitLabel: string,
|
||||
creditLabel: string,
|
||||
debitPrefixes: string[],
|
||||
creditPrefixes: string[]
|
||||
): string {
|
||||
const whereClause = buildWhereClause(
|
||||
filters,
|
||||
"Движения.Период",
|
||||
[
|
||||
debitPrefixes.length > 0 ? buildAccountPrefixPredicate("Движения.СчетДт", debitPrefixes) : null,
|
||||
creditPrefixes.length > 0 ? buildAccountPrefixPredicate("Движения.СчетКт", creditPrefixes) : null,
|
||||
buildOrganizationReferenceCondition(filters, ["Движения.Организация"])
|
||||
].filter((item): item is string => Boolean(item))
|
||||
);
|
||||
return `
|
||||
ВЫБРАТЬ
|
||||
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||||
"${marker}" КАК Регистратор,
|
||||
"${debitLabel}" КАК СчетДт,
|
||||
"${creditLabel}" КАК СчетКт,
|
||||
ЕСТЬNULL(СУММА(Движения.Сумма), 0) КАК Сумма,
|
||||
"" КАК СубконтоДт1,
|
||||
"" КАК СубконтоДт2,
|
||||
"" КАК СубконтоДт3,
|
||||
"" КАК СубконтоКт1,
|
||||
"" КАК СубконтоКт2,
|
||||
"" КАК СубконтоКт3,
|
||||
"" КАК Организация
|
||||
ИЗ
|
||||
РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто КАК Движения
|
||||
${whereClause}`;
|
||||
}
|
||||
|
||||
function buildAccountingFinancialResultQuery(filters: AddressFilterSet): string {
|
||||
const rows = [
|
||||
["ACC90_REVENUE_KT", "ANY", "90.01", [], ["90.01"]],
|
||||
["ACC90_COST_DT", "90.02", "ANY", ["90.02"], []],
|
||||
["ACC90_SELLING_DT", "90.07", "ANY", ["90.07"], []],
|
||||
["ACC90_ADMIN_DT", "90.08", "ANY", ["90.08"], []],
|
||||
["ACC90_RESULT_TO_99_PROFIT", "90.09", "99", ["90.09"], ["99"]],
|
||||
["ACC90_RESULT_FROM_99_LOSS", "99", "90.09", ["99"], ["90.09"]],
|
||||
["ACC91_RESULT_TO_99_PROFIT", "91.09", "99", ["91.09"], ["99"]],
|
||||
["ACC91_RESULT_FROM_99_LOSS", "99", "91.09", ["99"], ["91.09"]],
|
||||
["ACC99_TO84_PROFIT_TRANSFER", "99", "84", ["99"], ["84"]],
|
||||
["ACC84_TO99_LOSS_TRANSFER", "84", "99", ["84"], ["99"]]
|
||||
] as const;
|
||||
return rows
|
||||
.map(([marker, debitLabel, creditLabel, debitPrefixes, creditPrefixes]) =>
|
||||
buildAccountingFinancialResultAggregateSelect(
|
||||
filters,
|
||||
marker,
|
||||
debitLabel,
|
||||
creditLabel,
|
||||
[...debitPrefixes],
|
||||
[...creditPrefixes]
|
||||
).trim()
|
||||
)
|
||||
.join("\nОБЪЕДИНИТЬ ВСЕ\n");
|
||||
}
|
||||
|
||||
function buildInventorySaleDocumentQuery(filters: AddressFilterSet, resolvedLimit: number): string {
|
||||
const itemCondition = buildInventoryItemReferenceCondition(filters, ["Товары.Номенклатура"]);
|
||||
return INVENTORY_SALE_DOCUMENTS_QUERY_TEMPLATE
|
||||
|
|
@ -1458,6 +1772,8 @@ function maxLimitForIntent(intent: AddressIntent): number {
|
|||
intent === "contract_usage_and_value" ||
|
||||
intent === "vat_payable_forecast" ||
|
||||
intent === "vat_liability_confirmed_for_tax_period" ||
|
||||
intent === "accounting_financial_result_for_organization" ||
|
||||
intent === "debt_due_date_aging_for_organization" ||
|
||||
intent === "inventory_on_hand_as_of_date" ||
|
||||
intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
|
|
@ -1518,7 +1834,8 @@ export function buildAddressRecipePlan(
|
|||
recipe.query_template === "counterparty_roles_profile" ||
|
||||
recipe.query_template === "contract_usage_profile" ||
|
||||
recipe.query_template === "vat_payable_forecast_profile" ||
|
||||
recipe.query_template === "vat_liability_confirmed_tax_period_profile";
|
||||
recipe.query_template === "vat_liability_confirmed_tax_period_profile" ||
|
||||
recipe.query_template === "accounting_financial_result_profile";
|
||||
const baseLimit =
|
||||
typeof filters.limit === "number" && Number.isFinite(filters.limit)
|
||||
? Math.max(1, Math.min(maxLimit, Math.trunc(filters.limit)))
|
||||
|
|
@ -1659,6 +1976,10 @@ export function buildAddressRecipePlan(
|
|||
.replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период"))
|
||||
.replaceAll("__PERIOD_TO_EXPR__", periodToExpr);
|
||||
})()
|
||||
: recipe.query_template === "accounting_financial_result_profile"
|
||||
? buildAccountingFinancialResultQuery(filters)
|
||||
: recipe.query_template === "debt_due_date_aging_profile"
|
||||
? buildDebtDueDateAgingQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "vat_payable_confirmed_as_of_balance_profile"
|
||||
? (() => {
|
||||
const asOfExpr =
|
||||
|
|
|
|||
|
|
@ -705,11 +705,19 @@ function detectValueRankingFocus(userMessage: string | null | undefined): ValueR
|
|||
if (asksTotalMoneyEarned) {
|
||||
return "total_flow";
|
||||
}
|
||||
const hasCounterpartyRankingSubject =
|
||||
/(?:клиент|заказчик|покупател|контрагент|customer|client|counterpart|\u043a\u043b\u0438\u0435\u043d\u0442|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442)/iu.test(
|
||||
text
|
||||
);
|
||||
const asksExplicitYearBreakdown =
|
||||
/(?:по\s+годам|за\s+какие\s+годы|динамик\w*\s+по\s+год|yearly\s+breakdown|by\s+year|\u043f\u043e\s+\u0433\u043e\u0434\u0430\u043c|\u0437\u0430\s+\u043a\u0430\u043a\u0438\u0435\s+\u0433\u043e\u0434\u044b|\u0434\u0438\u043d\u0430\u043c\u0438\u043a\w*\s+\u043f\u043e\s+\u0433\u043e\u0434)/iu.test(
|
||||
text
|
||||
);
|
||||
const asksYearlyRevenueRanking =
|
||||
/(?:доходн|выручк|оборот|прибыл|деньг|денег|revenue|turnover|income)/iu.test(text) &&
|
||||
/(?:год|года|годы|year|years|по\s+годам)/iu.test(text) &&
|
||||
/(?:сам(?:ый|ая|ое|ые)|топ|луч|best|max|наибольш|больше)/iu.test(text);
|
||||
if (asksYearlyRevenueRanking) {
|
||||
if (asksYearlyRevenueRanking && (!hasCounterpartyRankingSubject || asksExplicitYearBreakdown)) {
|
||||
return "top_years_by_total";
|
||||
}
|
||||
if (/(?:сам(?:ый|ая|ое|ые)\s+высок[а-яё]*|highest|largest)\s+чек|(?:max\s+check|чек\s+макс)/iu.test(text)) {
|
||||
|
|
@ -3214,7 +3222,7 @@ function composeFactualReplyBody(
|
|||
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
|
||||
lines.push(
|
||||
"",
|
||||
"Покрытие VAT-источников через MCP:",
|
||||
"Покрытие VAT-источников в 1С:",
|
||||
`- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`,
|
||||
`- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`,
|
||||
`- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`,
|
||||
|
|
@ -3241,7 +3249,7 @@ function composeFactualReplyBody(
|
|||
}
|
||||
lines.push("- Сумма прогноза выше рассчитана строго по оборотам 68.02*/19*; прямые VAT-источники показаны для проверки покрытия.");
|
||||
} else if (vatProbe && vatProbe.status === "error") {
|
||||
lines.push("", "Покрытие VAT-источников через MCP: probe завершился ошибкой, поэтому использован только базовый контур 68.02*/19*.");
|
||||
lines.push("", "Покрытие VAT-источников в 1С: дополнительная проверка завершилась ошибкой, поэтому использован только базовый контур 68.02*/19*.");
|
||||
}
|
||||
|
||||
if (!vatActivityDetected) {
|
||||
|
|
@ -3328,13 +3336,16 @@ function composeFactualReplyBody(
|
|||
options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null;
|
||||
const formatConfirmedMoney = (value: number): string => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
||||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
||||
const organizationLabel = normalizeOrganizationScopeValue(options.organizationHint);
|
||||
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
|
||||
|
||||
const lines = [
|
||||
`Коротко: подтвержденный НДС к уплате за налоговый период — ${formatConfirmedMoney(vatToPay)}.`,
|
||||
`Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`,
|
||||
`Если смотреть на возможный перенос или переплату, получается ${formatConfirmedMoney(carryoverOrOverpayment)}.`,
|
||||
"Это подтвержденный расчет по регистрам книг продаж и покупок, без surrogate-формулы 68/19.",
|
||||
"",
|
||||
"Что вошло в расчет:",
|
||||
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
|
||||
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
|
||||
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
|
||||
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
|
||||
|
|
@ -3346,7 +3357,7 @@ function composeFactualReplyBody(
|
|||
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
|
||||
lines.push(
|
||||
"",
|
||||
"Покрытие VAT-источников через MCP:",
|
||||
"Покрытие VAT-источников в 1С:",
|
||||
`- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`,
|
||||
`- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`,
|
||||
`- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`,
|
||||
|
|
@ -3355,11 +3366,11 @@ function composeFactualReplyBody(
|
|||
if (vatProbe.errors.length > 0) {
|
||||
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
||||
}
|
||||
lines.push("- Сумма расчета выше получена по книгам продаж/покупок; probe использован для контроля полноты VAT-источников.");
|
||||
lines.push("- Сумма расчета выше получена по книгам продаж/покупок; дополнительная проверка использована для контроля полноты VAT-источников.");
|
||||
} else if (vatProbe && vatProbe.status === "error") {
|
||||
lines.push(
|
||||
"",
|
||||
"Покрытие VAT-источников через MCP: дополнительный probe недоступен (например, timeout metadata).",
|
||||
"Покрытие VAT-источников в 1С: дополнительная проверка недоступна, поэтому использован основной бухгалтерский срез.",
|
||||
"Итоговая сумма НДС выше рассчитана по основному маршруту книг продаж/покупок; probe влияет только на диагностику покрытия."
|
||||
);
|
||||
if (vatProbe.errors.length > 0) {
|
||||
|
|
@ -3453,7 +3464,7 @@ function composeFactualReplyBody(
|
|||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
||||
lines.push(
|
||||
"",
|
||||
"Блок 2.1. MCP-проверка VAT-источников",
|
||||
"Блок 2.1. Проверка VAT-источников в 1С",
|
||||
`- VAT-объектов в метаданных 1С: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`,
|
||||
`- Пробных прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`,
|
||||
`- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`
|
||||
|
|
@ -3478,8 +3489,8 @@ function composeFactualReplyBody(
|
|||
} else if (vatProbe && vatProbe.status === "error") {
|
||||
lines.push(
|
||||
"",
|
||||
"Блок 2.1. MCP-проверка VAT-источников",
|
||||
"- Probe VAT-источников завершился ошибкой, поэтому срез подтвержден по доступному бухгалтерскому источнику (68*)."
|
||||
"Блок 2.1. Проверка VAT-источников в 1С",
|
||||
"- Дополнительная проверка VAT-источников завершилась ошибкой, поэтому срез подтвержден по доступному бухгалтерскому источнику (68*)."
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -148,9 +148,9 @@ export function composeCounterpartyAnalyticsReply(
|
|||
const includeRoles = focus === "full_profile" || focus === "roles_only";
|
||||
const directLead =
|
||||
focus === "suppliers_only"
|
||||
? `Поставщиков (только supplier-роль): ${supplierOnly}.`
|
||||
? `Поставщиков с ролью поставщика: ${supplierOnly}.`
|
||||
: focus === "customers_only"
|
||||
? `Заказчиков (только customer-роль): ${customerOnly}.`
|
||||
? `Заказчиков с ролью покупателя: ${customerOnly}.`
|
||||
: focus === "mixed_only"
|
||||
? `Контрагентов со смешанной ролью: ${mixedActive}.`
|
||||
: includeTotal && totalCounterparties > 0
|
||||
|
|
@ -175,9 +175,9 @@ export function composeCounterpartyAnalyticsReply(
|
|||
|
||||
if (includeRoles) {
|
||||
if (resolvedActive > 0 || activeCounterparties > 0) {
|
||||
lines.push("Роли контрагентов по активности:");
|
||||
lines.push(`Заказчики (только customer-роль): ${customerOnly}.`);
|
||||
lines.push(`Поставщики (только supplier-роль): ${supplierOnly}.`);
|
||||
lines.push("Распределение ролей по активности:");
|
||||
lines.push(`Заказчики с ролью покупателя: ${customerOnly}.`);
|
||||
lines.push(`Поставщики с ролью поставщика: ${supplierOnly}.`);
|
||||
lines.push(`Смешанные (и покупатель, и поставщик): ${mixedActive}.`);
|
||||
lines.push(`4. Всего активных контрагентов: ${activeCounterparties}.`);
|
||||
if (otherCounterparties !== null) {
|
||||
|
|
@ -189,10 +189,10 @@ export function composeCounterpartyAnalyticsReply(
|
|||
}
|
||||
|
||||
if (focus === "suppliers_only") {
|
||||
lines.push(`Поставщиков (только supplier-роль): ${supplierOnly}.`);
|
||||
lines.push(`Поставщиков с ролью поставщика: ${supplierOnly}.`);
|
||||
}
|
||||
if (focus === "customers_only") {
|
||||
lines.push(`Заказчиков (только customer-роль): ${customerOnly}.`);
|
||||
lines.push(`Заказчиков с ролью покупателя: ${customerOnly}.`);
|
||||
}
|
||||
if (focus === "mixed_only") {
|
||||
lines.push(`Контрагентов со смешанной ролью: ${mixedActive}.`);
|
||||
|
|
@ -525,6 +525,16 @@ export function composeCounterpartyAnalyticsReply(
|
|||
const limit = deps.detectRankingLimit(options.userMessage, 20);
|
||||
const minOpsForAvgCheck = deps.detectMinOpsForAvgCheck(options.userMessage);
|
||||
const normalizedQuestion = deps.normalizeQuestionText(options.userMessage);
|
||||
const asksSingleBestCounterparty =
|
||||
focus === "top_by_total" &&
|
||||
/(?:какой|кто|which|who|какой|кто)/iu.test(normalizedQuestion) &&
|
||||
/(?:больше\s+всего|сам(?:ый|ая|ое|ые)|наибольш|прин[её]с|highest|most|больше\s+всего|сам(?:ый|ая|РѕРµ|ые)|наибол|РїСЂРёРЅ[её]СЃ)/iu.test(
|
||||
normalizedQuestion
|
||||
) &&
|
||||
!/(?:\btop\b|топ|рейтинг|список|первые|покажи\s+топ|дай\s+топ|покаж\w*\s+топ|дай\s+топ)/iu.test(
|
||||
normalizedQuestion
|
||||
);
|
||||
const effectiveLimit = asksSingleBestCounterparty ? 1 : limit;
|
||||
|
||||
const byCounterparty = new Map<string, CounterpartyValuePoint>();
|
||||
const byYear = new Map<number, CounterpartyYearPoint>();
|
||||
|
|
@ -728,7 +738,7 @@ export function composeCounterpartyAnalyticsReply(
|
|||
lines.push(
|
||||
...visible.map(
|
||||
(item, index) =>
|
||||
`${index + 1}. ${item.name} | max single: ${item.maxSingle} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops}`
|
||||
`${index + 1}. ${item.name} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops}`
|
||||
)
|
||||
);
|
||||
return buildFactualListReply(lines);
|
||||
|
|
@ -786,8 +796,12 @@ export function composeCounterpartyAnalyticsReply(
|
|||
return buildFactualListReply(lines);
|
||||
}
|
||||
|
||||
const visible = rankedByTotal.slice(0, limit);
|
||||
const visible = rankedByTotal.slice(0, effectiveLimit);
|
||||
const singleCandidateOnly = rankedByTotal.length === 1;
|
||||
const rankingPeriodLabel =
|
||||
options.periodFrom && options.periodTo
|
||||
? `за период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)}`
|
||||
: "за доступное время";
|
||||
const heading = singleCandidateOnly
|
||||
? isSupplier
|
||||
? "Найденный поставщик по сумме выплат:"
|
||||
|
|
@ -797,14 +811,17 @@ export function composeCounterpartyAnalyticsReply(
|
|||
: `Топ-${visible.length} заказчиков по сумме поступлений:`;
|
||||
const leadingCounterparty = visible[0] ?? null;
|
||||
lines.unshift(heading);
|
||||
if (options.periodFrom && options.periodTo) {
|
||||
lines.push(`Период рейтинга: ${rankingPeriodLabel}.`);
|
||||
}
|
||||
if (leadingCounterparty) {
|
||||
const directAnswerLine = singleCandidateOnly
|
||||
? isSupplier
|
||||
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
||||
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг; сумма является денежным потоком, а не чистой прибылью.`
|
||||
: isSupplier
|
||||
? `Крупнейший поставщик по подтвержденным выплатам за доступное время: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||
: `Самый доходный клиент за доступное время по подтвержденным поступлениям: ${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} операциям). Это денежный поток, а не чистая прибыль.`;
|
||||
lines.unshift(directAnswerLine);
|
||||
}
|
||||
lines.push(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { AssistantMcpDiscoveryPilotExecutionContract } from "./assistantMcpDiscoveryPilotExecutor";
|
||||
import { isLikelyFinancialInstitutionCounterparty } from "./counterpartyRoleHeuristics";
|
||||
|
||||
export const ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION =
|
||||
"assistant_mcp_discovery_answer_draft_v1" as const;
|
||||
|
|
@ -26,6 +27,7 @@ export interface AssistantMcpDiscoveryAnswerDraftContract {
|
|||
}
|
||||
|
||||
type BusinessOverview = NonNullable<AssistantMcpDiscoveryPilotExecutionContract["derived_business_overview"]>;
|
||||
type BusinessOverviewRankedBucket = BusinessOverview["top_customers"][number];
|
||||
|
||||
function normalizeReasonCode(value: string): string | null {
|
||||
const normalized = value
|
||||
|
|
@ -483,6 +485,30 @@ function metadataRouteFamilyLabelRu(
|
|||
return null;
|
||||
}
|
||||
|
||||
function isInventoryReserveBoundaryTurn(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
|
||||
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
|
||||
return action === "inventory_reserve_boundary" || unsupported === "inventory_reserve_liquidation_boundary";
|
||||
}
|
||||
|
||||
function isProfitMarginBoundaryTurn(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
|
||||
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
|
||||
return action === "profit_margin_boundary" || unsupported === "profit_margin_boundary";
|
||||
}
|
||||
|
||||
function isDebtDueDateBoundaryTurn(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
|
||||
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
|
||||
return action === "debt_due_date_boundary" || unsupported === "debt_due_date_boundary";
|
||||
}
|
||||
|
||||
function isVendorRiskBoundaryTurn(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
|
||||
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
|
||||
return action === "vendor_risk_procurement_boundary" || unsupported === "vendor_risk_procurement_boundary";
|
||||
}
|
||||
|
||||
function businessOverviewInventoryUnknownLabel(overview: BusinessOverview): string {
|
||||
if (overview.inventory_staleness_risk_proxy) {
|
||||
return "резервы/списания/ликвидационная стоимость склада";
|
||||
|
|
@ -543,6 +569,81 @@ function inlineBusinessOverviewAmount(value: string): string {
|
|||
.replace(/[\s.]+$/u, "");
|
||||
}
|
||||
|
||||
function isFinancialInstitutionBucket(bucket: BusinessOverviewRankedBucket | null | undefined): boolean {
|
||||
if (!bucket) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
bucket.counterparty_role_hint === "bank_or_financial_institution" ||
|
||||
isLikelyFinancialInstitutionCounterparty(bucket.axis_value)
|
||||
);
|
||||
}
|
||||
|
||||
function firstNonFinancialInstitutionBucket(
|
||||
buckets: BusinessOverviewRankedBucket[] | null | undefined
|
||||
): BusinessOverviewRankedBucket | null {
|
||||
return (buckets ?? []).find((bucket) => !isFinancialInstitutionBucket(bucket)) ?? null;
|
||||
}
|
||||
|
||||
function rankedBucketAmountLabel(bucket: BusinessOverviewRankedBucket): string {
|
||||
return `${bucket.axis_value} — ${bucket.total_amount_human_ru}`;
|
||||
}
|
||||
|
||||
function businessOverviewIncomingLeaderLine(overview: BusinessOverview): string | null {
|
||||
const leader = overview.top_customers[0];
|
||||
if (!leader) {
|
||||
return null;
|
||||
}
|
||||
if (!isFinancialInstitutionBucket(leader)) {
|
||||
return `Самый крупный подтвержденный клиент в проверенном срезе: ${rankedBucketAmountLabel(leader)}.`;
|
||||
}
|
||||
const nonFinancial = firstNonFinancialInstitutionBucket(overview.top_customers.slice(1));
|
||||
const nonFinancialText = nonFinancial
|
||||
? ` Крупнейший небанковский входящий контрагент в этом же срезе: ${rankedBucketAmountLabel(nonFinancial)}.`
|
||||
: "";
|
||||
return (
|
||||
`Крупнейший входящий денежный источник в проверенном срезе: ${rankedBucketAmountLabel(leader)}. ` +
|
||||
"По названию это банк/финансовая организация, поэтому без проверки назначения платежа не называю это клиентской выручкой или бизнес-заказчиком." +
|
||||
nonFinancialText
|
||||
);
|
||||
}
|
||||
|
||||
function businessOverviewOutgoingLeaderLine(overview: BusinessOverview): string | null {
|
||||
const leader = overview.top_suppliers?.[0];
|
||||
if (!leader) {
|
||||
return null;
|
||||
}
|
||||
if (!isFinancialInstitutionBucket(leader)) {
|
||||
return `Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${rankedBucketAmountLabel(leader)}.`;
|
||||
}
|
||||
const nonFinancial = firstNonFinancialInstitutionBucket(overview.top_suppliers.slice(1));
|
||||
const nonFinancialText = nonFinancial
|
||||
? ` Крупнейший небанковский получатель исходящих денег в этом же срезе: ${rankedBucketAmountLabel(nonFinancial)}.`
|
||||
: "";
|
||||
return (
|
||||
`Крупнейший получатель исходящих денег в проверенном срезе: ${rankedBucketAmountLabel(leader)}. ` +
|
||||
"По названию это банк/финансовая организация, поэтому без назначения платежа/договора не считаю это обычным поставщиком." +
|
||||
nonFinancialText
|
||||
);
|
||||
}
|
||||
|
||||
function businessOverviewSupplierBoundaryBasis(overview: BusinessOverview): string {
|
||||
const leader = overview.top_suppliers?.[0] ?? null;
|
||||
if (!leader) {
|
||||
return "есть только общий срез исходящих платежей без надежного vendor-risk профиля";
|
||||
}
|
||||
const share = percentText(leader.total_amount, overview.outgoing_supplier_payout.total_amount);
|
||||
if (isFinancialInstitutionBucket(leader)) {
|
||||
const base = share
|
||||
? `крупнейший получатель исходящих денег ${leader.axis_value} держит около ${share} проверенного исходящего потока (${leader.total_amount_human_ru})`
|
||||
: `крупнейший получатель исходящих денег: ${rankedBucketAmountLabel(leader)}`;
|
||||
return `${base}; по названию это банк/финансовая организация, поэтому этот факт нельзя считать доказанной зависимостью от одного обычного поставщика`;
|
||||
}
|
||||
return share
|
||||
? `крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенного исходящего потока (${leader.total_amount_human_ru})`
|
||||
: `крупнейший подтвержденный поставщик/получатель исходящих платежей: ${rankedBucketAmountLabel(leader)}`;
|
||||
}
|
||||
|
||||
function businessOverviewHeadlineMetricsLine(overview: BusinessOverview): string | null {
|
||||
const parts: string[] = [];
|
||||
if (overview.incoming_customer_revenue.rows_with_amount > 0) {
|
||||
|
|
@ -554,6 +655,24 @@ function businessOverviewHeadlineMetricsLine(overview: BusinessOverview): string
|
|||
if (overview.incoming_customer_revenue.rows_with_amount > 0 || overview.outgoing_supplier_payout.rows_with_amount > 0) {
|
||||
parts.push(`расчетное операционное нетто ${inlineBusinessOverviewAmount(overview.net_amount_human_ru)}`);
|
||||
}
|
||||
if (overview.accounting_financial_result) {
|
||||
const result = overview.accounting_financial_result;
|
||||
const direction =
|
||||
result.final_result_direction === "profit"
|
||||
? "учетная прибыль"
|
||||
: result.final_result_direction === "loss"
|
||||
? "учетный убыток"
|
||||
: "нулевой учетный финрезультат";
|
||||
const amount =
|
||||
result.final_result_direction === "loss"
|
||||
? `минус ${inlineBusinessOverviewAmount(result.final_result_amount_human_ru)}`
|
||||
: inlineBusinessOverviewAmount(result.final_result_amount_human_ru);
|
||||
const margin =
|
||||
result.net_margin_to_revenue_pct === null
|
||||
? "маржа к выручке 90.01 не рассчитана"
|
||||
: `маржа к выручке 90.01 ${result.net_margin_to_revenue_pct}%`;
|
||||
parts.push(`${direction} 90/91/99 ${amount}; ${margin}`);
|
||||
}
|
||||
const strongestIncomingYear = businessOverviewStrongestIncomingYear(overview);
|
||||
if (strongestIncomingYear) {
|
||||
parts.push(
|
||||
|
|
@ -561,10 +680,59 @@ function businessOverviewHeadlineMetricsLine(overview: BusinessOverview): string
|
|||
);
|
||||
}
|
||||
return parts.length > 0
|
||||
? `${parts.join("; ")}. Это operating-flow proxy по найденным строкам, не бухгалтерская прибыль и не финрезультат`
|
||||
? overview.accounting_financial_result
|
||||
? `${parts.join("; ")}. Финрезультат ограничен найденными строками 1С и не является внешним аудитом или юридически подтвержденной отчетностью`
|
||||
: `${parts.join("; ")}. Это operating-flow proxy по найденным строкам, не бухгалтерская прибыль и не финрезультат`
|
||||
: null;
|
||||
}
|
||||
|
||||
function businessOverviewAccountingFinancialResultText(overview: BusinessOverview): string | null {
|
||||
const result = overview.accounting_financial_result;
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
const direction =
|
||||
result.final_result_direction === "profit"
|
||||
? "учетная прибыль"
|
||||
: result.final_result_direction === "loss"
|
||||
? "учетный убыток"
|
||||
: "нулевой учетный финрезультат";
|
||||
const signedAmount =
|
||||
result.final_result_direction === "loss"
|
||||
? `минус ${result.final_result_amount_human_ru}`
|
||||
: result.final_result_amount_human_ru;
|
||||
const marginText =
|
||||
result.net_margin_to_revenue_pct === null
|
||||
? "маржа к выручке 90.01 не рассчитана"
|
||||
: `маржа к выручке 90.01 ${result.net_margin_to_revenue_pct}%`;
|
||||
const basis =
|
||||
result.final_transfer_basis === "account_99_to_84_period_close"
|
||||
? "по закрытию 99 на 84"
|
||||
: "по закрытию 90/91 на 99";
|
||||
return `По бухгалтерскому маршруту 90/91/99 за ${result.period_scope} подтвержден ${direction}: ${signedAmount}; ${marginText}. Основа: ${basis}, ${result.period_close_rows_with_amount} строк(и) закрытия периода с суммой. Это учетный финрезультат по найденным строкам 1С, не внешний аудит и не юридически подтвержденная отчетность.`;
|
||||
}
|
||||
|
||||
function businessOverviewDebtDueDateAgingText(overview: BusinessOverview): string | null {
|
||||
const aging = overview.debt_due_date_aging;
|
||||
if (!aging) {
|
||||
return null;
|
||||
}
|
||||
if (aging.evidence_status === "confirmed_overdue") {
|
||||
const top = aging.top_overdue_items?.[0] ?? null;
|
||||
const topText = top
|
||||
? ` Самая старая строка: due date ${top.due_date}, просрочка ${top.overdue_days} дн., ${top.amount_human_ru}${top.contract ? ` по договору ${top.contract}` : ""}.`
|
||||
: "";
|
||||
return `Due-date aging на ${aging.as_of_date} проверен по срокам оплаты договоров и датам расчетных документов: подтверждено просроченных строк ${aging.overdue_rows}, сумма ${aging.overdue_amount_human_ru}.${topText}`;
|
||||
}
|
||||
if (aging.evidence_status === "no_payment_terms_configured") {
|
||||
return `Due-date aging на ${aging.as_of_date} проверен по открытым расчетам: брутто ${aging.gross_open_amount_human_ru}, строк с суммой ${aging.rows_with_amount}, но в проверенных договорах срок оплаты не установлен. Подтвержденной просрочки по договорным срокам оплаты нет.`;
|
||||
}
|
||||
if (aging.evidence_status === "insufficient_due_date_basis") {
|
||||
return `Due-date aging на ${aging.as_of_date} запускался, но по строкам с установленным сроком оплаты не хватило даты расчетного документа для честного расчета due date. Просрочка не подтверждена.`;
|
||||
}
|
||||
return `Due-date aging на ${aging.as_of_date} проверен: строк с установленным сроком оплаты ${aging.rows_with_payment_terms}, подтвержденной просрочки не найдено; не просрочено по расчету ${aging.not_yet_due_amount_human_ru}.`;
|
||||
}
|
||||
|
||||
function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
|
||||
const askedMonthlyBreakdown =
|
||||
pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
|
||||
|
|
@ -583,6 +751,35 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
|||
}
|
||||
if (isBusinessOverviewPilot(pilot) && pilot.derived_business_overview && mode === "confirmed_with_bounded_inference") {
|
||||
const overview = pilot.derived_business_overview;
|
||||
if (isProfitMarginBoundaryTurn(pilot)) {
|
||||
const accountingFinancialResultText = businessOverviewAccountingFinancialResultText(overview);
|
||||
if (accountingFinancialResultText) {
|
||||
return accountingFinancialResultText;
|
||||
}
|
||||
return "Нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только bounded operating-flow/trading-margin proxy, не P&L и не бухгалтерский финрезультат.";
|
||||
}
|
||||
if (isDebtDueDateBoundaryTurn(pilot)) {
|
||||
const dueDateText = businessOverviewDebtDueDateAgingText(overview);
|
||||
if (dueDateText) {
|
||||
return dueDateText;
|
||||
}
|
||||
return "Нельзя точно определить, какая дебиторка просрочена, по текущему срезу 1С; есть только debt-quality proxy, но нет проверенного due-date маршрута по договорам, срокам оплаты и погашению расчетов.";
|
||||
}
|
||||
if (isInventoryReserveBoundaryTurn(pilot)) {
|
||||
const inventoryBasis = overview.inventory_staleness_risk_proxy
|
||||
? "есть только складской staleness-risk proxy по найденным строкам"
|
||||
: overview.inventory_position || overview.inventory_turnover_proxy
|
||||
? "есть только ограниченные складские proxy-сигналы по найденным строкам"
|
||||
: "нет отдельного складского среза на дату и проверки учетной политики резервов";
|
||||
return `Коротко: точно подтвердить резерв под неликвиды по текущим данным нельзя; ${inventoryBasis}. Можно честно говорить только о необходимости отдельной проверки склада, списаний/резервов и ликвидационной стоимости, не превращая proxy в доказанный факт резерва.`;
|
||||
}
|
||||
if (isVendorRiskBoundaryTurn(pilot)) {
|
||||
const supplierLeader = overview.top_suppliers?.[0] ?? null;
|
||||
const proxyLabel = isFinancialInstitutionBucket(supplierLeader)
|
||||
? "outgoing cash concentration proxy"
|
||||
: "procurement concentration proxy";
|
||||
return `Коротко: точный риск зависимости от одного поставщика по текущим данным не подтвержден; есть только ${proxyLabel}: ${businessOverviewSupplierBoundaryBasis(overview)}. Это сигнал концентрации исходящих платежей, а не полный аудит надежности поставщиков, условий, качества и структуры всех расходов.`;
|
||||
}
|
||||
const families: string[] = [];
|
||||
if (
|
||||
overview.incoming_customer_revenue.rows_with_amount > 0 ||
|
||||
|
|
@ -608,6 +805,9 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
|||
if (overview.tax_position) {
|
||||
families.push("НДС-позиция");
|
||||
}
|
||||
if (overview.accounting_financial_result) {
|
||||
families.push("учетный финрезультат 90/91/99");
|
||||
}
|
||||
if (overview.trading_margin_proxy) {
|
||||
families.push("торговый margin proxy");
|
||||
}
|
||||
|
|
@ -623,6 +823,9 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
|||
if (overview.debt_staleness_risk_proxy) {
|
||||
families.push("staleness risk proxy открытых расчетов");
|
||||
}
|
||||
if (overview.debt_due_date_aging) {
|
||||
families.push("due-date aging открытых расчетов");
|
||||
}
|
||||
if (overview.inventory_position) {
|
||||
families.push("складской срез на дату");
|
||||
}
|
||||
|
|
@ -632,13 +835,16 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
|||
if (overview.inventory_staleness_risk_proxy) {
|
||||
families.push("staleness risk proxy склада");
|
||||
}
|
||||
const unknownFamilies = [overview.trading_margin_proxy ? "чистая прибыль/точная маржа" : "прибыль/маржа"];
|
||||
const unknownFamilies = overview.accounting_financial_result
|
||||
? ["аудированная/юридически подтвержденная прибыль"]
|
||||
: [overview.trading_margin_proxy ? "чистая прибыль/точная маржа" : "прибыль/маржа"];
|
||||
if (!overview.tax_position) {
|
||||
unknownFamilies.push("НДС");
|
||||
}
|
||||
if (!overview.debt_position) {
|
||||
unknownFamilies.push("долговой срез");
|
||||
}
|
||||
if (!overview.debt_due_date_aging) {
|
||||
unknownFamilies.push(
|
||||
overview.debt_staleness_risk_proxy
|
||||
? "договорные сроки оплаты/due-date просрочка"
|
||||
|
|
@ -646,6 +852,7 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
|||
? "due-date просрочка"
|
||||
: "качество открытых расчетов"
|
||||
);
|
||||
}
|
||||
unknownFamilies.push(businessOverviewInventoryUnknownLabel(overview));
|
||||
const metricLead = businessOverviewHeadlineMetricsLine(overview);
|
||||
if (metricLead) {
|
||||
|
|
@ -851,9 +1058,16 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract):
|
|||
claims.push("Do not present a debt-position snapshot as debt aging, overdue debt, or credit-quality analysis.");
|
||||
claims.push("Do not present open-settlement concentration as contractual due-date aging or confirmed overdue debt.");
|
||||
claims.push("Do not present business overview debt staleness risk proxy as confirmed overdue debt, contractual delinquency, credit risk, or due-date aging.");
|
||||
claims.push("Do not claim contractual overdue debt unless the due-date aging route found configured payment terms and enough settlement-date evidence.");
|
||||
claims.push("Do not present an inventory snapshot or purchase-date aging signal as turnover, obsolescence, liquidation value, or full inventory health.");
|
||||
claims.push("Do not present business overview inventory turnover proxy as full inventory liquidity, FIFO turnover, obsolescence analysis, or liquidation value.");
|
||||
claims.push("Do not present business overview inventory staleness risk proxy as confirmed obsolete stock, reserve, write-off, or liquidation value.");
|
||||
if (
|
||||
pilot.derived_business_overview?.top_customers?.some(isFinancialInstitutionBucket) ||
|
||||
pilot.derived_business_overview?.top_suppliers?.some(isFinancialInstitutionBucket)
|
||||
) {
|
||||
claims.push("Do not present bank-like counterparties as ordinary customers, suppliers, revenue, procurement dependency, or business quality evidence without payment-purpose/contract proof.");
|
||||
}
|
||||
if (pilot.derived_business_overview?.missing_proof_families?.length) {
|
||||
claims.push("Do not present business overview missing proof families as checked, executed, or confirmed routes.");
|
||||
}
|
||||
|
|
@ -862,6 +1076,9 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract):
|
|||
if (pilot.derived_ranked_value_flow) {
|
||||
claims.push("Do not present a bounded ranking as a complete all-time ranking outside the checked period and organization.");
|
||||
claims.push("Do not imply the top-ranked counterparty is globally final when probe-limit or scope boundaries still exist.");
|
||||
if (pilot.derived_ranked_value_flow.ranked_values.some(isFinancialInstitutionBucket)) {
|
||||
claims.push("Do not present bank-like counterparties as ordinary customers, suppliers, revenue, procurement dependency, or business quality evidence without payment-purpose/contract proof.");
|
||||
}
|
||||
}
|
||||
if (isDocumentPilot(pilot)) {
|
||||
claims.push("Do not claim full document history outside the checked period.");
|
||||
|
|
@ -1028,20 +1245,34 @@ function derivedRankedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotEx
|
|||
return null;
|
||||
}
|
||||
const leader = ranking.ranked_values[0];
|
||||
const leaderLooksFinancial = isFinancialInstitutionBucket(leader);
|
||||
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
|
||||
const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне";
|
||||
const roleCaveat = leaderLooksFinancial
|
||||
? ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? " По названию это банк/финансовая организация, поэтому без назначения платежа/договора не называю это обычным поставщиком."
|
||||
: " По названию это банк/финансовая организация, поэтому без назначения платежа не называю это клиентской выручкой или бизнес-заказчиком."
|
||||
: "";
|
||||
if (ranking.ranked_values.length === 1) {
|
||||
const singleLead =
|
||||
ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
leaderLooksFinancial
|
||||
? ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "В проверенных исходящих платежах найден один банковский/финансовый получатель"
|
||||
: "В проверенных входящих поступлениях найден один банковский/финансовый источник"
|
||||
: ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "В проверенных исходящих платежах найден один контрагент"
|
||||
: "В проверенных входящих поступлениях найден один контрагент";
|
||||
const limitCaveat = ranking.coverage_limited_by_probe_limit
|
||||
? " Лимит строк проверки достигнут; сравнение с другими контрагентами может быть неполным."
|
||||
: " Других контрагентов в этом проверенном срезе не найдено, поэтому это не полноценный сравнительный рейтинг.";
|
||||
return `${singleLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${limitCaveat}`;
|
||||
return `${singleLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${roleCaveat}${limitCaveat}`;
|
||||
}
|
||||
const directionLead =
|
||||
ranking.ranking_need === "bottom_asc"
|
||||
leaderLooksFinancial
|
||||
? ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "Крупнейший получатель исходящих денег"
|
||||
: "Крупнейший входящий денежный источник"
|
||||
: ranking.ranking_need === "bottom_asc"
|
||||
? ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "Меньше всего заплатили контрагенту"
|
||||
: "Меньше всего денег принёс контрагент"
|
||||
|
|
@ -1056,7 +1287,7 @@ function derivedRankedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotEx
|
|||
const limitCaveat = ranking.coverage_limited_by_probe_limit
|
||||
? " Лимит строк проверки достигнут; рейтинг может быть неполным."
|
||||
: "";
|
||||
return `${directionLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${trail}${limitCaveat}`;
|
||||
return `${directionLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${roleCaveat}${trail}${limitCaveat}`;
|
||||
}
|
||||
|
||||
function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||
|
|
@ -1247,15 +1478,13 @@ function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilot
|
|||
`Самый сильный год по подтвержденным входящим поступлениям: ${strongestIncomingYear.year_bucket} — ${strongestIncomingYear.incoming_total_amount_human_ru} по ${strongestIncomingYear.incoming_rows_with_amount} строкам с суммой. Это не бухгалтерская прибыль.`
|
||||
);
|
||||
}
|
||||
const leader = overview.top_customers[0];
|
||||
if (leader) {
|
||||
lines.push(`Самый крупный подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`);
|
||||
const incomingLeaderLine = businessOverviewIncomingLeaderLine(overview);
|
||||
if (incomingLeaderLine) {
|
||||
lines.push(incomingLeaderLine);
|
||||
}
|
||||
const supplierLeader = overview.top_suppliers?.[0];
|
||||
if (supplierLeader) {
|
||||
lines.push(
|
||||
`Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value} — ${supplierLeader.total_amount_human_ru}.`
|
||||
);
|
||||
const outgoingLeaderLine = businessOverviewOutgoingLeaderLine(overview);
|
||||
if (outgoingLeaderLine) {
|
||||
lines.push(outgoingLeaderLine);
|
||||
}
|
||||
if (overview.yearly_breakdown?.length) {
|
||||
lines.push(
|
||||
|
|
@ -1316,6 +1545,12 @@ function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilot
|
|||
`НДС-позиция за ${overview.tax_position.period_scope}: книга продаж ${overview.tax_position.sales_vat_amount_human_ru}, книга покупок/вычеты ${overview.tax_position.purchase_vat_amount_human_ru}, нетто ${taxDirection} ${overview.tax_position.net_vat_amount_human_ru}.`
|
||||
);
|
||||
}
|
||||
if (overview.accounting_financial_result) {
|
||||
const accountingFinancialResultText = businessOverviewAccountingFinancialResultText(overview);
|
||||
if (accountingFinancialResultText) {
|
||||
lines.push(accountingFinancialResultText);
|
||||
}
|
||||
}
|
||||
if (overview.trading_margin_proxy) {
|
||||
const proxy = overview.trading_margin_proxy;
|
||||
const marginText = proxy.margin_to_revenue_pct === null ? "не рассчитана" : `${proxy.margin_to_revenue_pct}%`;
|
||||
|
|
@ -1359,6 +1594,10 @@ function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilot
|
|||
`Staleness risk proxy открытых расчетов на ${proxy.as_of_date}: самый старый договорный сигнал ${proxy.oldest_contract_start_date}, возраст ${proxy.max_contract_age_days} дн.; старейший крупный договор ${proxy.top_contract}${counterpartyText} держит ${proxy.top_contract_amount_human_ru} (${proxy.top_contract_share_pct}% брутто открытых остатков); оценка ${debtStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная просрочка, не кредитный риск и не due-date aging.`
|
||||
);
|
||||
}
|
||||
const dueDateText = businessOverviewDebtDueDateAgingText(overview);
|
||||
if (dueDateText) {
|
||||
lines.push(dueDateText);
|
||||
}
|
||||
if (overview.inventory_position) {
|
||||
const leader = overview.inventory_position.top_items[0];
|
||||
const leaderText = leader
|
||||
|
|
@ -1416,6 +1655,13 @@ function businessOverviewCustomerConcentrationLine(overview: BusinessOverview):
|
|||
return null;
|
||||
}
|
||||
const share = percentText(leader.total_amount, overview.incoming_customer_revenue.total_amount);
|
||||
if (isFinancialInstitutionBucket(leader)) {
|
||||
const base = share
|
||||
? `Крупнейший входящий денежный источник ${leader.axis_value} дает около ${share} проверенных входящих поступлений (${leader.total_amount_human_ru})`
|
||||
: `Крупнейший входящий денежный источник в проверенном срезе: ${rankedBucketAmountLabel(leader)}`;
|
||||
const nonFinancial = firstNonFinancialInstitutionBucket(overview.top_customers.slice(1));
|
||||
return `${base}. По названию это банк/финансовая организация, поэтому это не доказывает клиентскую выручку или зависимость от клиента.${nonFinancial ? ` Крупнейший небанковский входящий контрагент: ${rankedBucketAmountLabel(nonFinancial)}.` : ""}`;
|
||||
}
|
||||
return share
|
||||
? `Концентрация входящего потока: крупнейший подтвержденный клиент ${leader.axis_value} дает около ${share} проверенных входящих поступлений (${leader.total_amount_human_ru}). Это сигнал зависимости от клиента, а не полный customer-risk аудит.`
|
||||
: `Крупнейший подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`;
|
||||
|
|
@ -1427,6 +1673,13 @@ function businessOverviewSupplierConcentrationLine(overview: BusinessOverview):
|
|||
return null;
|
||||
}
|
||||
const share = percentText(leader.total_amount, overview.outgoing_supplier_payout.total_amount);
|
||||
if (isFinancialInstitutionBucket(leader)) {
|
||||
const base = share
|
||||
? `Концентрация исходящего потока: крупнейший получатель исходящих денег ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru})`
|
||||
: `Крупнейший получатель исходящих денег в проверенном срезе: ${rankedBucketAmountLabel(leader)}`;
|
||||
const nonFinancial = firstNonFinancialInstitutionBucket(overview.top_suppliers.slice(1));
|
||||
return `${base}. По названию это банк/финансовая организация, поэтому это не доказательство зависимости от обычного поставщика без проверки назначения платежа/договора.${nonFinancial ? ` Крупнейший небанковский получатель исходящих денег: ${rankedBucketAmountLabel(nonFinancial)}.` : ""}`;
|
||||
}
|
||||
return share
|
||||
? `Концентрация исходящего потока: крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru}). Это сигнал procurement concentration по найденным строкам, а не полный vendor-risk аудит или структура всех расходов.`
|
||||
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`;
|
||||
|
|
@ -1474,6 +1727,20 @@ function businessOverviewRiskSynthesisLine(overview: BusinessOverview): string |
|
|||
: `маржинальность proxy ${overview.trading_margin_proxy.margin_to_revenue_pct}%`;
|
||||
signals.push(`торговый спред proxy ${overview.trading_margin_proxy.gross_spread_proxy_human_ru}, ${marginText}`);
|
||||
}
|
||||
if (overview.accounting_financial_result) {
|
||||
const result = overview.accounting_financial_result;
|
||||
const direction =
|
||||
result.final_result_direction === "profit"
|
||||
? "учетная прибыль"
|
||||
: result.final_result_direction === "loss"
|
||||
? "учетный убыток"
|
||||
: "нулевой учетный финрезультат";
|
||||
const marginText =
|
||||
result.net_margin_to_revenue_pct === null
|
||||
? "маржа к выручке 90.01 не рассчитана"
|
||||
: `маржа к выручке 90.01 ${result.net_margin_to_revenue_pct}%`;
|
||||
signals.push(`${direction} 90/91/99 ${result.final_result_amount_human_ru}, ${marginText}`);
|
||||
}
|
||||
if (overview.debt_position) {
|
||||
const debtDirection =
|
||||
overview.debt_position.net_debt_position_direction === "net_receivable"
|
||||
|
|
@ -1495,6 +1762,18 @@ function businessOverviewRiskSynthesisLine(overview: BusinessOverview): string |
|
|||
`staleness risk proxy открытых расчетов: ${debtStalenessRiskBandRu(overview.debt_staleness_risk_proxy.risk_band)}, возраст ${overview.debt_staleness_risk_proxy.max_contract_age_days} дн., концентрация старейшего крупного договора ${overview.debt_staleness_risk_proxy.top_contract_share_pct}%`
|
||||
);
|
||||
}
|
||||
if (overview.debt_due_date_aging) {
|
||||
const aging = overview.debt_due_date_aging;
|
||||
signals.push(
|
||||
aging.evidence_status === "confirmed_overdue"
|
||||
? `due-date aging: подтвержденная просрочка ${aging.overdue_amount_human_ru}, строк ${aging.overdue_rows}`
|
||||
: aging.evidence_status === "no_payment_terms_configured"
|
||||
? "due-date aging: проверено, но сроки оплаты в договорах не установлены; подтвержденной просрочки нет"
|
||||
: aging.evidence_status === "insufficient_due_date_basis"
|
||||
? "due-date aging: не хватило даты расчетного документа для честного расчета просрочки"
|
||||
: `due-date aging: проверено, подтвержденной просрочки не найдено`
|
||||
);
|
||||
}
|
||||
if (overview.document_activity_profile) {
|
||||
const topDocument = overview.document_activity_profile.top_document_types[0];
|
||||
const topSection = overview.document_activity_profile.top_account_sections[0];
|
||||
|
|
@ -1648,6 +1927,9 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
|
|||
if (pilot.derived_business_overview?.tax_position) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_tax_position");
|
||||
}
|
||||
if (pilot.derived_business_overview?.accounting_financial_result) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_accounting_financial_result");
|
||||
}
|
||||
if (pilot.derived_business_overview?.trading_margin_proxy) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_trading_margin_proxy");
|
||||
}
|
||||
|
|
@ -1678,6 +1960,10 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
|
|||
if (pilot.derived_business_overview?.debt_staleness_risk_proxy) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_debt_staleness_risk_proxy");
|
||||
}
|
||||
if (pilot.derived_business_overview?.debt_due_date_aging) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_debt_due_date_aging");
|
||||
pushReason(reasonCodes, `answer_contains_business_overview_debt_due_date_aging_${pilot.derived_business_overview.debt_due_date_aging.evidence_status}`);
|
||||
}
|
||||
if (pilot.derived_business_overview?.inventory_position) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_inventory_position");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { AssistantMcpDiscoveryRuntimeEntryPointContract } from "./assistantMcpDiscoveryRuntimeEntryPoint";
|
||||
import type { AssistantMcpRouteCandidateContract } from "./assistantMcpDiscoveryRuntimeBridge";
|
||||
|
||||
export interface AssistantMcpDiscoveryDebugAttachmentFields {
|
||||
assistant_mcp_discovery_entry_point_v1: AssistantMcpDiscoveryRuntimeEntryPointContract | null;
|
||||
|
|
@ -11,6 +12,16 @@ export interface AssistantMcpDiscoveryDebugAttachmentFields {
|
|||
mcp_discovery_catalog_chain_alignment_status: string | null;
|
||||
mcp_discovery_catalog_chain_top_match: string | null;
|
||||
mcp_discovery_catalog_chain_selected_matches_top: boolean;
|
||||
mcp_discovery_route_candidate_v1: AssistantMcpRouteCandidateContract | null;
|
||||
mcp_discovery_route_candidate_status: string | null;
|
||||
mcp_discovery_route_candidate_fact_family: string | null;
|
||||
mcp_discovery_route_candidate_action_family: string | null;
|
||||
mcp_discovery_route_candidate_proof_expectation: string | null;
|
||||
mcp_discovery_route_candidate_missing_axes: string[];
|
||||
mcp_discovery_route_candidate_provided_axes: string[];
|
||||
mcp_discovery_route_candidate_executable_now: boolean;
|
||||
mcp_discovery_route_candidate_enablement_reason: string | null;
|
||||
mcp_discovery_route_candidate_next_action: string | null;
|
||||
mcp_discovery_answer_mode: string | null;
|
||||
mcp_discovery_business_fact_answer_allowed: boolean;
|
||||
mcp_discovery_user_facing_response_allowed: boolean;
|
||||
|
|
@ -59,6 +70,14 @@ function isMcpDiscoveryEntryPointContract(value: unknown): value is AssistantMcp
|
|||
);
|
||||
}
|
||||
|
||||
function isRouteCandidateContract(value: unknown): value is AssistantMcpRouteCandidateContract {
|
||||
const record = toRecordObject(value);
|
||||
return (
|
||||
record?.schema_version === "assistant_mcp_route_candidate_v1" &&
|
||||
record?.policy_owner === "assistantMcpDiscoveryRuntimeBridge"
|
||||
);
|
||||
}
|
||||
|
||||
function resolveEntryPoint(input: AttachAssistantMcpDiscoveryDebugInput): AssistantMcpDiscoveryRuntimeEntryPointContract | null {
|
||||
if (isMcpDiscoveryEntryPointContract(input.entryPoint)) {
|
||||
return input.entryPoint;
|
||||
|
|
@ -77,6 +96,7 @@ export function buildAssistantMcpDiscoveryDebugAttachmentFields(
|
|||
const bridge = toRecordObject(entryPoint?.bridge);
|
||||
const planner = toRecordObject(bridge?.planner);
|
||||
const chainAlignment = toRecordObject(planner?.catalog_chain_template_alignment);
|
||||
const routeCandidate = isRouteCandidateContract(bridge?.route_candidate) ? bridge.route_candidate : null;
|
||||
const answerDraft = toRecordObject(bridge?.answer_draft);
|
||||
|
||||
return {
|
||||
|
|
@ -90,6 +110,16 @@ export function buildAssistantMcpDiscoveryDebugAttachmentFields(
|
|||
mcp_discovery_catalog_chain_alignment_status: toNonEmptyString(chainAlignment?.alignment_status),
|
||||
mcp_discovery_catalog_chain_top_match: toNonEmptyString(chainAlignment?.top_chain_template_match),
|
||||
mcp_discovery_catalog_chain_selected_matches_top: chainAlignment?.selected_chain_matches_top === true,
|
||||
mcp_discovery_route_candidate_v1: routeCandidate,
|
||||
mcp_discovery_route_candidate_status: toNonEmptyString(routeCandidate?.candidate_status),
|
||||
mcp_discovery_route_candidate_fact_family: toNonEmptyString(routeCandidate?.business_fact_family),
|
||||
mcp_discovery_route_candidate_action_family: toNonEmptyString(routeCandidate?.action_family),
|
||||
mcp_discovery_route_candidate_proof_expectation: toNonEmptyString(routeCandidate?.proof_expectation),
|
||||
mcp_discovery_route_candidate_missing_axes: toStringArray(routeCandidate?.missing_axes),
|
||||
mcp_discovery_route_candidate_provided_axes: toStringArray(routeCandidate?.provided_axes),
|
||||
mcp_discovery_route_candidate_executable_now: routeCandidate?.executable_now === true,
|
||||
mcp_discovery_route_candidate_enablement_reason: toNonEmptyString(routeCandidate?.enablement_reason),
|
||||
mcp_discovery_route_candidate_next_action: toNonEmptyString(routeCandidate?.recommended_next_action),
|
||||
mcp_discovery_answer_mode: toNonEmptyString(answerDraft?.answer_mode),
|
||||
mcp_discovery_business_fact_answer_allowed: bridge?.business_fact_answer_allowed === true,
|
||||
mcp_discovery_user_facing_response_allowed: bridge?.user_facing_response_allowed === true,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ import {
|
|||
type AssistantMcpDiscoveryProbeResult
|
||||
} from "./assistantMcpDiscoveryPolicy";
|
||||
import { buildAddressRecipePlan, selectAddressRecipe } from "./addressRecipeCatalog";
|
||||
import {
|
||||
counterpartyRoleHintForName,
|
||||
isLikelyFinancialInstitutionCounterparty,
|
||||
type CounterpartyRoleHint
|
||||
} from "./counterpartyRoleHeuristics";
|
||||
import type { AddressFilterSet, AddressIntent } from "../types/addressQuery";
|
||||
|
||||
export const ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION =
|
||||
|
|
@ -83,6 +88,7 @@ export interface AssistantMcpDiscoveryRankedValueFlowBucket {
|
|||
rows_with_amount: number;
|
||||
total_amount: number;
|
||||
total_amount_human_ru: string;
|
||||
counterparty_role_hint?: CounterpartyRoleHint;
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryDerivedRankedValueFlow {
|
||||
|
|
@ -228,10 +234,12 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverview {
|
|||
yearly_breakdown: AssistantMcpDiscoveryBusinessOverviewYearBucket[];
|
||||
activity_period: AssistantMcpDiscoveryDerivedActivityPeriod | null;
|
||||
tax_position: AssistantMcpDiscoveryDerivedBusinessOverviewTaxPosition | null;
|
||||
accounting_financial_result: AssistantMcpDiscoveryDerivedBusinessOverviewAccountingFinancialResult | null;
|
||||
trading_margin_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewTradingMarginProxy | null;
|
||||
debt_position: AssistantMcpDiscoveryDerivedBusinessOverviewDebtPosition | null;
|
||||
debt_open_settlement_quality: AssistantMcpDiscoveryDerivedBusinessOverviewDebtOpenSettlementQuality | null;
|
||||
debt_staleness_risk_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewDebtStalenessRiskProxy | null;
|
||||
debt_due_date_aging: AssistantMcpDiscoveryDerivedBusinessOverviewDebtDueDateAging | null;
|
||||
inventory_position: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryPosition | null;
|
||||
inventory_turnover_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryTurnoverProxy | null;
|
||||
inventory_staleness_risk_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryStalenessRiskProxy | null;
|
||||
|
|
@ -264,6 +272,31 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverviewTaxPosition {
|
|||
inference_basis: "sales_book_minus_purchase_book_confirmed_1c_vat_rows";
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryDerivedBusinessOverviewAccountingFinancialResult {
|
||||
period_scope: string;
|
||||
rows_matched: number;
|
||||
rows_with_amount: number;
|
||||
sales_revenue_accounting: number;
|
||||
sales_revenue_accounting_human_ru: string;
|
||||
cost_of_sales_accounting: number;
|
||||
cost_of_sales_accounting_human_ru: string;
|
||||
selling_expenses_accounting: number;
|
||||
selling_expenses_accounting_human_ru: string;
|
||||
admin_expenses_accounting: number;
|
||||
admin_expenses_accounting_human_ru: string;
|
||||
sales_result_amount: number;
|
||||
sales_result_amount_human_ru: string;
|
||||
other_result_amount: number;
|
||||
other_result_amount_human_ru: string;
|
||||
final_result_amount: number;
|
||||
final_result_amount_human_ru: string;
|
||||
final_result_direction: "profit" | "loss" | "balanced";
|
||||
net_margin_to_revenue_pct: number | null;
|
||||
final_transfer_basis: "account_99_to_84_period_close" | "account_90_91_to_99_period_close";
|
||||
period_close_rows_with_amount: number;
|
||||
inference_basis: "account_90_91_99_period_close_aggregate_confirmed_1c_rows";
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryBusinessOverviewTradingItemBucket {
|
||||
item: string;
|
||||
sales_revenue: number;
|
||||
|
|
@ -373,6 +406,45 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverviewDebtStalenessRiskPr
|
|||
inference_basis: "contract_date_age_and_open_balance_concentration_confirmed_1c_rows";
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryBusinessOverviewDebtDueDateAgingBucket {
|
||||
counterparty: string | null;
|
||||
contract: string | null;
|
||||
settlement_document: string | null;
|
||||
document_date: string;
|
||||
due_date: string;
|
||||
payment_term_days: number;
|
||||
overdue_days: number;
|
||||
amount: number;
|
||||
amount_human_ru: string;
|
||||
share_of_overdue_amount_pct: number | null;
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryDerivedBusinessOverviewDebtDueDateAging {
|
||||
as_of_date: string;
|
||||
rows_matched: number;
|
||||
rows_with_amount: number;
|
||||
gross_open_amount: number;
|
||||
gross_open_amount_human_ru: string;
|
||||
rows_with_payment_terms: number;
|
||||
rows_without_payment_terms: number;
|
||||
rows_without_document_date: number;
|
||||
overdue_rows: number;
|
||||
overdue_amount: number;
|
||||
overdue_amount_human_ru: string;
|
||||
not_yet_due_rows: number;
|
||||
not_yet_due_amount: number;
|
||||
not_yet_due_amount_human_ru: string;
|
||||
oldest_due_date: string | null;
|
||||
max_overdue_days: number | null;
|
||||
top_overdue_items: AssistantMcpDiscoveryBusinessOverviewDebtDueDateAgingBucket[];
|
||||
evidence_status:
|
||||
| "confirmed_overdue"
|
||||
| "no_overdue_found"
|
||||
| "no_payment_terms_configured"
|
||||
| "insufficient_due_date_basis";
|
||||
inference_basis: "contract_payment_terms_and_settlement_document_dates_from_open_balance_rows";
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryBusinessOverviewInventoryItemBucket {
|
||||
item: string;
|
||||
rows_with_amount: number;
|
||||
|
|
@ -723,6 +795,17 @@ function buildBusinessOverviewDebtFilters(planner: AssistantMcpDiscoveryPlannerC
|
|||
};
|
||||
}
|
||||
|
||||
function shouldRunDebtDueDateAgingProbe(planner: AssistantMcpDiscoveryPlannerContract): boolean {
|
||||
const actionFamily = toNonEmptyString(planner.data_need_graph?.action_family);
|
||||
const turnActionFamily = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.asked_action_family);
|
||||
const unsupportedFamily = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.unsupported_but_understood_family);
|
||||
const proofExpectation = toNonEmptyString(planner.data_need_graph?.proof_expectation);
|
||||
const combined = [actionFamily, turnActionFamily, unsupportedFamily, proofExpectation]
|
||||
.filter((item): item is string => Boolean(item))
|
||||
.join(" ");
|
||||
return /(?:debt_due_date_boundary|due[-_ ]?date|overdue|aging|просроч|срок\s+оплат|дебиторк|кредиторск)/iu.test(combined);
|
||||
}
|
||||
|
||||
function buildBusinessOverviewInventoryFilters(planner: AssistantMcpDiscoveryPlannerContract): AddressFilterSet | null {
|
||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||
const organization = toNonEmptyString(meaning?.explicit_organization_scope);
|
||||
|
|
@ -756,6 +839,20 @@ function buildBusinessOverviewTradingMarginFilters(planner: AssistantMcpDiscover
|
|||
};
|
||||
}
|
||||
|
||||
function buildBusinessOverviewAccountingFinancialResultFilters(
|
||||
planner: AssistantMcpDiscoveryPlannerContract
|
||||
): AddressFilterSet | null {
|
||||
const filters = buildBusinessOverviewTradingMarginFilters(planner);
|
||||
if (!filters) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...filters,
|
||||
limit: Math.max(32, planner.discovery_plan.execution_budget.max_rows_per_probe),
|
||||
sort: "period_asc"
|
||||
};
|
||||
}
|
||||
|
||||
function buildInventoryExactFilters(planner: AssistantMcpDiscoveryPlannerContract): AddressFilterSet {
|
||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||
const subject = firstEntityCandidate(planner);
|
||||
|
|
@ -1359,7 +1456,8 @@ async function executeCoverageAwareValueFlowQuery(input: {
|
|||
});
|
||||
executedProbeCount += 1;
|
||||
probeResults.push(queryResultToProbeResult(input.primitiveId, broadResult));
|
||||
const broadCoverageLimited = !broadResult.error && broadResult.matched_rows >= input.maxRowsPerProbe;
|
||||
const broadLimitThreshold = Math.max(1, Math.min(input.maxRowsPerProbe, broadRecipePlan.limit));
|
||||
const broadCoverageLimited = !broadResult.error && broadResult.matched_rows >= broadLimitThreshold;
|
||||
|
||||
if (broadResult.error) {
|
||||
pushUnique(queryLimitations, broadResult.error);
|
||||
|
|
@ -1424,7 +1522,8 @@ async function executeCoverageAwareValueFlowQuery(input: {
|
|||
pushUnique(queryLimitations, chunkResult.error);
|
||||
continue;
|
||||
}
|
||||
if (chunkResult.matched_rows >= input.maxRowsPerProbe) {
|
||||
const chunkLimitThreshold = Math.max(1, Math.min(input.maxRowsPerProbe, chunkPlan.limit));
|
||||
if (chunkResult.matched_rows >= chunkLimitThreshold) {
|
||||
anyChunkLimited = true;
|
||||
}
|
||||
chunkResults.push(chunkResult);
|
||||
|
|
@ -2402,6 +2501,14 @@ function extractContractDateFromText(value: string | null): string | null {
|
|||
if (!text || !/(?:договор|contract|дог\.)/iu.test(text)) {
|
||||
return null;
|
||||
}
|
||||
return extractAnyDateFromText(text);
|
||||
}
|
||||
|
||||
function extractAnyDateFromText(value: string | null): string | null {
|
||||
const text = toNonEmptyString(value);
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
const isoLikeMatch = text.match(/(\d{4})[-./](\d{1,2})[-./](\d{1,2})/);
|
||||
if (isoLikeMatch) {
|
||||
return normalizeDateParts(isoLikeMatch[1], isoLikeMatch[2], isoLikeMatch[3]);
|
||||
|
|
@ -2413,6 +2520,64 @@ function extractContractDateFromText(value: string | null): string | null {
|
|||
return null;
|
||||
}
|
||||
|
||||
function rowContractDateValue(row: Record<string, unknown>): string | null {
|
||||
const explicit = rowTextValue(row, ["ДатаДоговора", "ContractDate", "contract_date"]);
|
||||
return extractAnyDateFromText(explicit) ?? rowOpenSettlementContractStartDateValue(row);
|
||||
}
|
||||
|
||||
function rowSettlementDocumentDateValue(row: Record<string, unknown>): string | null {
|
||||
const explicit = rowTextValue(row, [
|
||||
"ДатаДокументаРасчетов",
|
||||
"SettlementDocumentDate",
|
||||
"settlement_document_date"
|
||||
]);
|
||||
const settlementDocument = rowTextValue(row, [
|
||||
"ДокументРасчетов",
|
||||
"SettlementDocument",
|
||||
"settlement_document"
|
||||
]);
|
||||
return extractAnyDateFromText(explicit) ?? extractAnyDateFromText(settlementDocument) ?? extractAnyDateFromText(rowDocumentValue(row));
|
||||
}
|
||||
|
||||
function rowPaymentTermIsSetValue(row: Record<string, unknown>): boolean {
|
||||
const candidate = rowTextValue(row, ["УстановленСрокОплаты", "PaymentTermIsSet", "payment_term_is_set"]);
|
||||
if (typeof row["УстановленСрокОплаты"] === "boolean") {
|
||||
return row["УстановленСрокОплаты"] === true;
|
||||
}
|
||||
if (typeof row["PaymentTermIsSet"] === "boolean") {
|
||||
return row["PaymentTermIsSet"] === true;
|
||||
}
|
||||
if (!candidate) {
|
||||
return false;
|
||||
}
|
||||
return /^(?:true|истина|да|yes|1)$/iu.test(candidate.trim());
|
||||
}
|
||||
|
||||
function rowPaymentTermDaysValue(row: Record<string, unknown>): number | null {
|
||||
const value = rowNumberValue(row, ["СрокОплаты", "PaymentTermDays", "payment_term_days"]);
|
||||
if (value === null || !Number.isFinite(value) || value <= 0) {
|
||||
return null;
|
||||
}
|
||||
return Math.trunc(value);
|
||||
}
|
||||
|
||||
function addDaysToIsoDate(isoDate: string, days: number): string | null {
|
||||
const match = isoDate.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (!match || !Number.isFinite(days)) {
|
||||
return null;
|
||||
}
|
||||
const date = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])));
|
||||
date.setUTCDate(date.getUTCDate() + Math.trunc(days));
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return null;
|
||||
}
|
||||
return [
|
||||
String(date.getUTCFullYear()).padStart(4, "0"),
|
||||
String(date.getUTCMonth() + 1).padStart(2, "0"),
|
||||
String(date.getUTCDate()).padStart(2, "0")
|
||||
].join("-");
|
||||
}
|
||||
|
||||
function earlierIsoDate(left: string | null, right: string | null): string | null {
|
||||
if (!left) {
|
||||
return right;
|
||||
|
|
@ -2908,7 +3073,8 @@ function deriveRankedValueFlow(
|
|||
axis_value: axisValue,
|
||||
rows_with_amount: bucket.rows_with_amount,
|
||||
total_amount: bucket.total_amount,
|
||||
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount)
|
||||
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount),
|
||||
counterparty_role_hint: counterpartyRoleHintForName(axisValue)
|
||||
}))
|
||||
.sort((left, right) => {
|
||||
const amountDelta = right.total_amount - left.total_amount;
|
||||
|
|
@ -3097,6 +3263,116 @@ function deriveBusinessOverviewTaxPosition(
|
|||
};
|
||||
}
|
||||
|
||||
function accountingFinancialResultMarkerAmount(
|
||||
result: AddressMcpQueryExecutorResult,
|
||||
marker: string
|
||||
): number {
|
||||
let total = 0;
|
||||
for (const row of result.rows) {
|
||||
if (String(rowDocumentValue(row) ?? "") !== marker) {
|
||||
continue;
|
||||
}
|
||||
const amount = rowAmountValue(row);
|
||||
if (amount !== null && Number.isFinite(amount)) {
|
||||
total += amount;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
function accountingFinancialResultNonZeroCount(
|
||||
values: number[]
|
||||
): number {
|
||||
return values.filter((value) => Math.abs(value) > 0).length;
|
||||
}
|
||||
|
||||
function deriveBusinessOverviewAccountingFinancialResult(
|
||||
result: AddressMcpQueryExecutorResult | null,
|
||||
periodScope: string | null
|
||||
): AssistantMcpDiscoveryDerivedBusinessOverviewAccountingFinancialResult | null {
|
||||
if (!result || result.error || result.matched_rows <= 0 || !periodScope) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const salesRevenueAccounting = accountingFinancialResultMarkerAmount(result, "ACC90_REVENUE_KT");
|
||||
const costOfSalesAccounting = accountingFinancialResultMarkerAmount(result, "ACC90_COST_DT");
|
||||
const sellingExpensesAccounting = accountingFinancialResultMarkerAmount(result, "ACC90_SELLING_DT");
|
||||
const adminExpensesAccounting = accountingFinancialResultMarkerAmount(result, "ACC90_ADMIN_DT");
|
||||
const salesProfitTo99 = accountingFinancialResultMarkerAmount(result, "ACC90_RESULT_TO_99_PROFIT");
|
||||
const salesLossFrom99 = accountingFinancialResultMarkerAmount(result, "ACC90_RESULT_FROM_99_LOSS");
|
||||
const otherProfitTo99 = accountingFinancialResultMarkerAmount(result, "ACC91_RESULT_TO_99_PROFIT");
|
||||
const otherLossFrom99 = accountingFinancialResultMarkerAmount(result, "ACC91_RESULT_FROM_99_LOSS");
|
||||
const profitTransferTo84 = accountingFinancialResultMarkerAmount(result, "ACC99_TO84_PROFIT_TRANSFER");
|
||||
const lossTransferFrom84 = accountingFinancialResultMarkerAmount(result, "ACC84_TO99_LOSS_TRANSFER");
|
||||
|
||||
const amountSignals = [
|
||||
salesRevenueAccounting,
|
||||
costOfSalesAccounting,
|
||||
sellingExpensesAccounting,
|
||||
adminExpensesAccounting,
|
||||
salesProfitTo99,
|
||||
salesLossFrom99,
|
||||
otherProfitTo99,
|
||||
otherLossFrom99,
|
||||
profitTransferTo84,
|
||||
lossTransferFrom84
|
||||
];
|
||||
const rowsWithAmount = accountingFinancialResultNonZeroCount(amountSignals);
|
||||
if (rowsWithAmount <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const salesResultAmount = salesProfitTo99 - salesLossFrom99;
|
||||
const otherResultAmount = otherProfitTo99 - otherLossFrom99;
|
||||
const hasFinalTransfer = profitTransferTo84 > 0 || lossTransferFrom84 > 0;
|
||||
const finalResultAmount = hasFinalTransfer
|
||||
? profitTransferTo84 - lossTransferFrom84
|
||||
: salesResultAmount + otherResultAmount;
|
||||
const finalResultDirection =
|
||||
finalResultAmount > 0
|
||||
? "profit"
|
||||
: finalResultAmount < 0
|
||||
? "loss"
|
||||
: "balanced";
|
||||
const netMarginToRevenuePct =
|
||||
salesRevenueAccounting > 0 ? percentageOfTotal(finalResultAmount, salesRevenueAccounting) : null;
|
||||
const periodCloseRowsWithAmount = accountingFinancialResultNonZeroCount([
|
||||
salesProfitTo99,
|
||||
salesLossFrom99,
|
||||
otherProfitTo99,
|
||||
otherLossFrom99,
|
||||
profitTransferTo84,
|
||||
lossTransferFrom84
|
||||
]);
|
||||
|
||||
return {
|
||||
period_scope: periodScope,
|
||||
rows_matched: result.matched_rows,
|
||||
rows_with_amount: rowsWithAmount,
|
||||
sales_revenue_accounting: salesRevenueAccounting,
|
||||
sales_revenue_accounting_human_ru: formatAmountHumanRu(salesRevenueAccounting),
|
||||
cost_of_sales_accounting: costOfSalesAccounting,
|
||||
cost_of_sales_accounting_human_ru: formatAmountHumanRu(costOfSalesAccounting),
|
||||
selling_expenses_accounting: sellingExpensesAccounting,
|
||||
selling_expenses_accounting_human_ru: formatAmountHumanRu(sellingExpensesAccounting),
|
||||
admin_expenses_accounting: adminExpensesAccounting,
|
||||
admin_expenses_accounting_human_ru: formatAmountHumanRu(adminExpensesAccounting),
|
||||
sales_result_amount: salesResultAmount,
|
||||
sales_result_amount_human_ru: formatAmountHumanRu(Math.abs(salesResultAmount)),
|
||||
other_result_amount: otherResultAmount,
|
||||
other_result_amount_human_ru: formatAmountHumanRu(Math.abs(otherResultAmount)),
|
||||
final_result_amount: finalResultAmount,
|
||||
final_result_amount_human_ru: formatAmountHumanRu(Math.abs(finalResultAmount)),
|
||||
final_result_direction: finalResultDirection,
|
||||
net_margin_to_revenue_pct: netMarginToRevenuePct,
|
||||
final_transfer_basis: hasFinalTransfer
|
||||
? "account_99_to_84_period_close"
|
||||
: "account_90_91_to_99_period_close",
|
||||
period_close_rows_with_amount: periodCloseRowsWithAmount,
|
||||
inference_basis: "account_90_91_99_period_close_aggregate_confirmed_1c_rows"
|
||||
};
|
||||
}
|
||||
|
||||
function deriveBusinessOverviewTradingMarginProxy(
|
||||
result: AddressMcpQueryExecutorResult | null,
|
||||
periodScope: string | null
|
||||
|
|
@ -3505,6 +3781,135 @@ function deriveBusinessOverviewDebtStalenessRiskProxy(
|
|||
};
|
||||
}
|
||||
|
||||
function deriveBusinessOverviewDebtDueDateAging(input: {
|
||||
dueDateResult: AddressMcpQueryExecutorResult | null;
|
||||
debtAsOfDate: string | null;
|
||||
}): AssistantMcpDiscoveryDerivedBusinessOverviewDebtDueDateAging | null {
|
||||
if (!input.debtAsOfDate || !input.dueDateResult || input.dueDateResult.error || input.dueDateResult.matched_rows <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const overdueItems: Array<Omit<AssistantMcpDiscoveryBusinessOverviewDebtDueDateAgingBucket, "share_of_overdue_amount_pct">> = [];
|
||||
let rowsWithAmount = 0;
|
||||
let grossOpenAmount = 0;
|
||||
let rowsWithPaymentTerms = 0;
|
||||
let rowsWithoutPaymentTerms = 0;
|
||||
let rowsWithoutDocumentDate = 0;
|
||||
let overdueAmount = 0;
|
||||
let notYetDueAmount = 0;
|
||||
let notYetDueRows = 0;
|
||||
|
||||
for (const row of input.dueDateResult.rows) {
|
||||
const amount = rowAmountValue(row);
|
||||
if (amount === null) {
|
||||
continue;
|
||||
}
|
||||
const absAmount = Math.abs(amount);
|
||||
if (absAmount <= 0) {
|
||||
continue;
|
||||
}
|
||||
rowsWithAmount += 1;
|
||||
grossOpenAmount += absAmount;
|
||||
|
||||
const paymentTermIsSet = rowPaymentTermIsSetValue(row);
|
||||
const paymentTermDays = rowPaymentTermDaysValue(row);
|
||||
if (!paymentTermIsSet || paymentTermDays === null) {
|
||||
rowsWithoutPaymentTerms += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
rowsWithPaymentTerms += 1;
|
||||
const documentDate = rowSettlementDocumentDateValue(row) ?? rowContractDateValue(row);
|
||||
if (!documentDate) {
|
||||
rowsWithoutDocumentDate += 1;
|
||||
continue;
|
||||
}
|
||||
const dueDate = addDaysToIsoDate(documentDate, paymentTermDays);
|
||||
if (!dueDate) {
|
||||
rowsWithoutDocumentDate += 1;
|
||||
continue;
|
||||
}
|
||||
const overdueDays = dueDate < input.debtAsOfDate ? daysBetweenIsoDates(dueDate, input.debtAsOfDate) : null;
|
||||
if (overdueDays !== null && overdueDays > 0) {
|
||||
overdueAmount += absAmount;
|
||||
overdueItems.push({
|
||||
counterparty: rowCounterpartyValue(row),
|
||||
contract: rowContractValue(row),
|
||||
settlement_document: rowTextValue(row, [
|
||||
"ДокументРасчетов",
|
||||
"SettlementDocument",
|
||||
"settlement_document"
|
||||
]) ?? rowDocumentValue(row),
|
||||
document_date: documentDate,
|
||||
due_date: dueDate,
|
||||
payment_term_days: paymentTermDays,
|
||||
overdue_days: overdueDays,
|
||||
amount: absAmount,
|
||||
amount_human_ru: formatAmountHumanRu(absAmount)
|
||||
});
|
||||
} else {
|
||||
notYetDueRows += 1;
|
||||
notYetDueAmount += absAmount;
|
||||
}
|
||||
}
|
||||
|
||||
if (rowsWithAmount <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const topOverdueItems = overdueItems
|
||||
.sort((left, right) => {
|
||||
const daysDelta = right.overdue_days - left.overdue_days;
|
||||
if (daysDelta !== 0) {
|
||||
return daysDelta;
|
||||
}
|
||||
const amountDelta = right.amount - left.amount;
|
||||
return amountDelta !== 0 ? amountDelta : String(left.contract ?? "").localeCompare(String(right.contract ?? ""), "ru");
|
||||
})
|
||||
.slice(0, 5)
|
||||
.map((item) => ({
|
||||
...item,
|
||||
share_of_overdue_amount_pct: percentageOfTotal(item.amount, overdueAmount)
|
||||
}));
|
||||
const oldestDueDate = overdueItems
|
||||
.map((item) => item.due_date)
|
||||
.sort()[0] ?? null;
|
||||
const maxOverdueDays = overdueItems.reduce<number | null>(
|
||||
(max, item) => max === null ? item.overdue_days : Math.max(max, item.overdue_days),
|
||||
null
|
||||
);
|
||||
const evidenceStatus: AssistantMcpDiscoveryDerivedBusinessOverviewDebtDueDateAging["evidence_status"] =
|
||||
overdueItems.length > 0
|
||||
? "confirmed_overdue"
|
||||
: rowsWithPaymentTerms <= 0
|
||||
? "no_payment_terms_configured"
|
||||
: rowsWithoutDocumentDate >= rowsWithPaymentTerms
|
||||
? "insufficient_due_date_basis"
|
||||
: "no_overdue_found";
|
||||
|
||||
return {
|
||||
as_of_date: input.debtAsOfDate,
|
||||
rows_matched: input.dueDateResult.matched_rows,
|
||||
rows_with_amount: rowsWithAmount,
|
||||
gross_open_amount: grossOpenAmount,
|
||||
gross_open_amount_human_ru: formatAmountHumanRu(grossOpenAmount),
|
||||
rows_with_payment_terms: rowsWithPaymentTerms,
|
||||
rows_without_payment_terms: rowsWithoutPaymentTerms,
|
||||
rows_without_document_date: rowsWithoutDocumentDate,
|
||||
overdue_rows: overdueItems.length,
|
||||
overdue_amount: overdueAmount,
|
||||
overdue_amount_human_ru: formatAmountHumanRu(overdueAmount),
|
||||
not_yet_due_rows: notYetDueRows,
|
||||
not_yet_due_amount: notYetDueAmount,
|
||||
not_yet_due_amount_human_ru: formatAmountHumanRu(notYetDueAmount),
|
||||
oldest_due_date: oldestDueDate,
|
||||
max_overdue_days: maxOverdueDays,
|
||||
top_overdue_items: topOverdueItems,
|
||||
evidence_status: evidenceStatus,
|
||||
inference_basis: "contract_payment_terms_and_settlement_document_dates_from_open_balance_rows"
|
||||
};
|
||||
}
|
||||
|
||||
function debtStalenessRiskBandRu(
|
||||
riskBand: AssistantMcpDiscoveryDerivedBusinessOverviewDebtStalenessRiskProxy["risk_band"]
|
||||
): string {
|
||||
|
|
@ -3737,9 +4142,11 @@ function inventoryStalenessRiskBandRu(
|
|||
|
||||
function buildBusinessOverviewMissingProofFamilies(input: {
|
||||
missingSignalFamilies: string[];
|
||||
accountingFinancialResult: AssistantMcpDiscoveryDerivedBusinessOverviewAccountingFinancialResult | null;
|
||||
tradingMarginProxy: AssistantMcpDiscoveryDerivedBusinessOverviewTradingMarginProxy | null;
|
||||
debtOpenSettlementQuality: AssistantMcpDiscoveryDerivedBusinessOverviewDebtOpenSettlementQuality | null;
|
||||
debtStalenessRiskProxy: AssistantMcpDiscoveryDerivedBusinessOverviewDebtStalenessRiskProxy | null;
|
||||
debtDueDateAging: AssistantMcpDiscoveryDerivedBusinessOverviewDebtDueDateAging | null;
|
||||
inventoryPosition: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryPosition | null;
|
||||
inventoryTurnoverProxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryTurnoverProxy | null;
|
||||
inventoryStalenessRiskProxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryStalenessRiskProxy | null;
|
||||
|
|
@ -3753,7 +4160,7 @@ function buildBusinessOverviewMissingProofFamilies(input: {
|
|||
}
|
||||
};
|
||||
|
||||
if (missing.has("profit_margin") || missing.has("accounting_profit_margin")) {
|
||||
if ((missing.has("profit_margin") || missing.has("accounting_profit_margin")) && !input.accountingFinancialResult) {
|
||||
pushUnique({
|
||||
family: "accounting_profit_margin",
|
||||
current_status: input.tradingMarginProxy ? "proxy_only_currently" : "reviewed_route_not_wired",
|
||||
|
|
@ -3765,7 +4172,7 @@ function buildBusinessOverviewMissingProofFamilies(input: {
|
|||
});
|
||||
}
|
||||
|
||||
if (missing.has("debt_due_date_aging_quality") || missing.has("debt_open_settlement_quality")) {
|
||||
if ((missing.has("debt_due_date_aging_quality") || missing.has("debt_open_settlement_quality")) && !input.debtDueDateAging) {
|
||||
pushUnique({
|
||||
family: "debt_due_date_aging_quality",
|
||||
current_status: input.debtStalenessRiskProxy
|
||||
|
|
@ -3826,10 +4233,12 @@ function deriveBusinessOverview(input: {
|
|||
outgoingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null;
|
||||
lifecycleResult: AddressMcpQueryExecutorResult | null;
|
||||
taxResult: AddressMcpQueryExecutorResult | null;
|
||||
accountingFinancialResultResult: AddressMcpQueryExecutorResult | null;
|
||||
tradingMarginResult: AddressMcpQueryExecutorResult | null;
|
||||
receivablesResult: AddressMcpQueryExecutorResult | null;
|
||||
payablesResult: AddressMcpQueryExecutorResult | null;
|
||||
openContractsResult: AddressMcpQueryExecutorResult | null;
|
||||
dueDateAgingResult: AddressMcpQueryExecutorResult | null;
|
||||
documentActivityProfileResult: AddressMcpQueryExecutorResult | null;
|
||||
counterpartyProfileResult: AddressMcpQueryExecutorResult | null;
|
||||
contractUsageProfileResult: AddressMcpQueryExecutorResult | null;
|
||||
|
|
@ -3860,6 +4269,10 @@ function deriveBusinessOverview(input: {
|
|||
});
|
||||
const activityPeriod = deriveActivityPeriod(input.lifecycleResult);
|
||||
const taxPosition = deriveBusinessOverviewTaxPosition(input.taxResult, input.periodScope);
|
||||
const accountingFinancialResult = deriveBusinessOverviewAccountingFinancialResult(
|
||||
input.accountingFinancialResultResult,
|
||||
input.periodScope
|
||||
);
|
||||
const tradingMarginProxy = deriveBusinessOverviewTradingMarginProxy(input.tradingMarginResult, input.periodScope);
|
||||
const debtPosition = deriveBusinessOverviewDebtPosition({
|
||||
receivablesResult: input.receivablesResult,
|
||||
|
|
@ -3883,6 +4296,10 @@ function deriveBusinessOverview(input: {
|
|||
input.periodScope
|
||||
);
|
||||
const debtStalenessRiskProxy = deriveBusinessOverviewDebtStalenessRiskProxy(debtOpenSettlementQuality);
|
||||
const debtDueDateAging = deriveBusinessOverviewDebtDueDateAging({
|
||||
dueDateResult: input.dueDateAgingResult,
|
||||
debtAsOfDate: input.debtAsOfDate
|
||||
});
|
||||
const inventoryPosition = deriveBusinessOverviewInventoryPosition({
|
||||
inventoryOnHandResult: input.inventoryOnHandResult,
|
||||
inventoryAgingResult: input.inventoryAgingResult,
|
||||
|
|
@ -3901,10 +4318,12 @@ function deriveBusinessOverview(input: {
|
|||
outgoing.rows_with_amount > 0,
|
||||
Boolean(activityPeriod),
|
||||
Boolean(taxPosition),
|
||||
Boolean(accountingFinancialResult),
|
||||
Boolean(tradingMarginProxy),
|
||||
Boolean(debtPosition),
|
||||
Boolean(debtOpenSettlementQuality),
|
||||
Boolean(debtStalenessRiskProxy),
|
||||
Boolean(debtDueDateAging),
|
||||
Boolean(documentActivityProfile),
|
||||
Boolean(counterpartyProfile),
|
||||
Boolean(contractUsageProfile),
|
||||
|
|
@ -3921,9 +4340,9 @@ function deriveBusinessOverview(input: {
|
|||
documentActivityProfile || counterpartyProfile || contractUsageProfile
|
||||
);
|
||||
const missingSignalFamilies = [
|
||||
tradingMarginProxy ? "accounting_profit_margin" : "profit_margin",
|
||||
accountingFinancialResult ? null : tradingMarginProxy ? "accounting_profit_margin" : "profit_margin",
|
||||
debtPosition ? null : "debt_position",
|
||||
debtOpenSettlementQuality ? "debt_due_date_aging_quality" : "debt_open_settlement_quality",
|
||||
debtDueDateAging ? null : debtOpenSettlementQuality ? "debt_due_date_aging_quality" : "debt_open_settlement_quality",
|
||||
taxPosition ? null : "tax_position",
|
||||
inventoryPosition
|
||||
? inventoryStalenessRiskProxy
|
||||
|
|
@ -3936,9 +4355,11 @@ function deriveBusinessOverview(input: {
|
|||
].filter((item): item is string => Boolean(item));
|
||||
const missingProofFamilies = buildBusinessOverviewMissingProofFamilies({
|
||||
missingSignalFamilies,
|
||||
accountingFinancialResult,
|
||||
tradingMarginProxy,
|
||||
debtOpenSettlementQuality,
|
||||
debtStalenessRiskProxy,
|
||||
debtDueDateAging,
|
||||
inventoryPosition,
|
||||
inventoryTurnoverProxy,
|
||||
inventoryStalenessRiskProxy,
|
||||
|
|
@ -3957,10 +4378,12 @@ function deriveBusinessOverview(input: {
|
|||
yearly_breakdown: yearlyBreakdown,
|
||||
activity_period: activityPeriod,
|
||||
tax_position: taxPosition,
|
||||
accounting_financial_result: accountingFinancialResult,
|
||||
trading_margin_proxy: tradingMarginProxy,
|
||||
debt_position: debtPosition,
|
||||
debt_open_settlement_quality: debtOpenSettlementQuality,
|
||||
debt_staleness_risk_proxy: debtStalenessRiskProxy,
|
||||
debt_due_date_aging: debtDueDateAging,
|
||||
inventory_position: inventoryPosition,
|
||||
inventory_turnover_proxy: inventoryTurnoverProxy,
|
||||
inventory_staleness_risk_proxy: inventoryStalenessRiskProxy,
|
||||
|
|
@ -3973,9 +4396,9 @@ function deriveBusinessOverview(input: {
|
|||
missing_signal_families: missingSignalFamilies,
|
||||
missing_proof_families: missingProofFamilies,
|
||||
inference_basis:
|
||||
hasBusinessOverviewProfileSignal || inventoryPosition
|
||||
hasBusinessOverviewProfileSignal || inventoryPosition || accountingFinancialResult
|
||||
? "business_overview_from_confirmed_1c_multi_family_rows"
|
||||
: debtOpenSettlementQuality
|
||||
: debtOpenSettlementQuality || debtDueDateAging
|
||||
? "business_overview_from_confirmed_1c_multi_family_rows"
|
||||
: taxPosition && debtPosition
|
||||
? "business_overview_from_confirmed_1c_money_activity_tax_and_debt_rows"
|
||||
|
|
@ -3992,10 +4415,12 @@ function summarizeBusinessOverviewRows(input: {
|
|||
outgoingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null;
|
||||
lifecycleResult: AddressMcpQueryExecutorResult | null;
|
||||
taxResult: AddressMcpQueryExecutorResult | null;
|
||||
accountingFinancialResultResult: AddressMcpQueryExecutorResult | null;
|
||||
tradingMarginResult: AddressMcpQueryExecutorResult | null;
|
||||
receivablesResult: AddressMcpQueryExecutorResult | null;
|
||||
payablesResult: AddressMcpQueryExecutorResult | null;
|
||||
openContractsResult: AddressMcpQueryExecutorResult | null;
|
||||
dueDateAgingResult: AddressMcpQueryExecutorResult | null;
|
||||
documentActivityProfileResult: AddressMcpQueryExecutorResult | null;
|
||||
counterpartyProfileResult: AddressMcpQueryExecutorResult | null;
|
||||
contractUsageProfileResult: AddressMcpQueryExecutorResult | null;
|
||||
|
|
@ -4015,6 +4440,9 @@ function summarizeBusinessOverviewRows(input: {
|
|||
if (input.taxResult && !input.taxResult.error) {
|
||||
parts.push(`${input.taxResult.fetched_rows} VAT/tax rows fetched, ${input.taxResult.matched_rows} matched`);
|
||||
}
|
||||
if (input.accountingFinancialResultResult && !input.accountingFinancialResultResult.error) {
|
||||
parts.push(`${input.accountingFinancialResultResult.fetched_rows} accounting financial-result aggregate rows fetched, ${input.accountingFinancialResultResult.matched_rows} matched`);
|
||||
}
|
||||
if (input.tradingMarginResult && !input.tradingMarginResult.error) {
|
||||
parts.push(`${input.tradingMarginResult.fetched_rows} trading-margin document rows fetched, ${input.tradingMarginResult.matched_rows} matched`);
|
||||
}
|
||||
|
|
@ -4027,6 +4455,9 @@ function summarizeBusinessOverviewRows(input: {
|
|||
if (input.openContractsResult && !input.openContractsResult.error) {
|
||||
parts.push(`${input.openContractsResult.fetched_rows} open-contract rows fetched, ${input.openContractsResult.matched_rows} matched`);
|
||||
}
|
||||
if (input.dueDateAgingResult && !input.dueDateAgingResult.error) {
|
||||
parts.push(`${input.dueDateAgingResult.fetched_rows} due-date aging rows fetched, ${input.dueDateAgingResult.matched_rows} matched`);
|
||||
}
|
||||
if (input.documentActivityProfileResult && !input.documentActivityProfileResult.error) {
|
||||
parts.push(`${input.documentActivityProfileResult.fetched_rows} document/account-section profile rows fetched, ${input.documentActivityProfileResult.matched_rows} matched`);
|
||||
}
|
||||
|
|
@ -4064,16 +4495,40 @@ function buildBusinessOverviewConfirmedFacts(derived: AssistantMcpDiscoveryDeriv
|
|||
}
|
||||
if (derived.top_customers.length > 0) {
|
||||
const leader = derived.top_customers[0];
|
||||
if (isLikelyFinancialInstitutionCounterparty(leader.axis_value)) {
|
||||
facts.push(
|
||||
`Крупнейший входящий денежный источник в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}. По названию это банк/финансовая организация, поэтому без назначения платежа это не доказанный клиент или бизнес-выручка.`
|
||||
);
|
||||
const nonFinancialLeader = derived.top_customers.slice(1).find((item) => !isLikelyFinancialInstitutionCounterparty(item.axis_value));
|
||||
if (nonFinancialLeader) {
|
||||
facts.push(
|
||||
`Крупнейший небанковский входящий контрагент в проверенном срезе: ${nonFinancialLeader.axis_value} — ${nonFinancialLeader.total_amount_human_ru}.`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
facts.push(
|
||||
`Самый крупный подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (derived.top_suppliers.length > 0) {
|
||||
const leader = derived.top_suppliers[0];
|
||||
if (isLikelyFinancialInstitutionCounterparty(leader.axis_value)) {
|
||||
facts.push(
|
||||
`Крупнейший получатель исходящих денег в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}. По названию это банк/финансовая организация, поэтому без назначения платежа/договора это не доказанный обычный поставщик.`
|
||||
);
|
||||
const nonFinancialLeader = derived.top_suppliers.slice(1).find((item) => !isLikelyFinancialInstitutionCounterparty(item.axis_value));
|
||||
if (nonFinancialLeader) {
|
||||
facts.push(
|
||||
`Крупнейший небанковский получатель исходящих денег в проверенном срезе: ${nonFinancialLeader.axis_value} — ${nonFinancialLeader.total_amount_human_ru}.`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
facts.push(
|
||||
`Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (derived.yearly_breakdown.length > 0) {
|
||||
facts.push(
|
||||
`Годовая раскладка операционного денежного потока построена по подтвержденным строкам 1С за ${yearCountHumanRu(derived.yearly_breakdown.length)}.`
|
||||
|
|
@ -4179,6 +4634,30 @@ function buildBusinessOverviewConfirmedFacts(derived: AssistantMcpDiscoveryDeriv
|
|||
`Staleness risk proxy открытых расчетов на ${proxy.as_of_date}: самый старый договорный сигнал ${proxy.oldest_contract_start_date}, возраст ${proxy.max_contract_age_days} дн.; старейший крупный договор ${proxy.top_contract}${counterpartyText} держит ${proxy.top_contract_amount_human_ru} (${proxy.top_contract_share_pct}% брутто открытых остатков); оценка ${debtStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная просрочка, не кредитный риск и не due-date aging.`
|
||||
);
|
||||
}
|
||||
if (derived.debt_due_date_aging) {
|
||||
const aging = derived.debt_due_date_aging;
|
||||
if (aging.evidence_status === "confirmed_overdue") {
|
||||
const top = aging.top_overdue_items[0];
|
||||
const topText = top
|
||||
? ` Самая старая просрочка: ${top.due_date}, ${top.overdue_days} дн., ${top.amount_human_ru}${top.contract ? ` по договору ${top.contract}` : ""}.`
|
||||
: "";
|
||||
facts.push(
|
||||
`Due-date aging открытых расчетов на ${aging.as_of_date} проверен по срокам оплаты договоров и датам расчетных документов: подтверждено просроченных строк ${aging.overdue_rows}, сумма ${aging.overdue_amount_human_ru}.${topText}`
|
||||
);
|
||||
} else if (aging.evidence_status === "no_payment_terms_configured") {
|
||||
facts.push(
|
||||
`Due-date aging открытых расчетов на ${aging.as_of_date} проверен по ${aging.rows_with_amount} строкам с суммой: брутто открытых остатков ${aging.gross_open_amount_human_ru}, но в проверенных договорах срок оплаты не установлен. Подтвержденной просрочки по договорным срокам оплаты нет.`
|
||||
);
|
||||
} else if (aging.evidence_status === "insufficient_due_date_basis") {
|
||||
facts.push(
|
||||
`Due-date aging открытых расчетов на ${aging.as_of_date} запускался по ${aging.rows_with_payment_terms} строкам с установленным сроком оплаты, но в найденных строках не хватило даты расчетного документа для честного расчета due date. Просрочка не подтверждена.`
|
||||
);
|
||||
} else {
|
||||
facts.push(
|
||||
`Due-date aging открытых расчетов на ${aging.as_of_date} проверен: строк с установленным сроком оплаты ${aging.rows_with_payment_terms}, подтвержденной просрочки не найдено; не просрочено по расчету ${aging.not_yet_due_amount_human_ru}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (derived.inventory_position) {
|
||||
const leader = derived.inventory_position.top_items[0];
|
||||
const leaderText = leader
|
||||
|
|
@ -4237,6 +4716,9 @@ function buildBusinessOverviewInferredFacts(derived: AssistantMcpDiscoveryDerive
|
|||
const supplierSharePct = supplierLeader
|
||||
? percentageOfTotal(supplierLeader.total_amount, derived.outgoing_supplier_payout.total_amount)
|
||||
: null;
|
||||
const supplierLeaderIsFinancial = supplierLeader
|
||||
? isLikelyFinancialInstitutionCounterparty(supplierLeader.axis_value)
|
||||
: false;
|
||||
const strongestIncomingYear = [...derived.yearly_breakdown]
|
||||
.filter((bucket) => bucket.incoming_total_amount > 0)
|
||||
.sort((left, right) => right.incoming_total_amount - left.incoming_total_amount || left.year_bucket.localeCompare(right.year_bucket))[0];
|
||||
|
|
@ -4246,7 +4728,11 @@ function buildBusinessOverviewInferredFacts(derived: AssistantMcpDiscoveryDerive
|
|||
return [
|
||||
`Расчетное нетто по найденным строкам: ${derived.net_amount_human_ru}; ${direction}.`,
|
||||
supplierLeader
|
||||
? supplierLeaderIsFinancial
|
||||
? supplierSharePct !== null
|
||||
? `Крупнейший получатель исходящих денег ${supplierLeader.axis_value} держит около ${supplierSharePct}% проверенного исходящего потока (${supplierLeader.total_amount_human_ru}). По названию это банк/финансовая организация, поэтому это outgoing cash concentration proxy, а не доказанный vendor-risk по обычному поставщику.`
|
||||
: `Крупнейший получатель исходящих денег в проверенном срезе: ${supplierLeader.axis_value} — ${supplierLeader.total_amount_human_ru}. По названию это банк/финансовая организация, поэтому это не доказанный обычный поставщик.`
|
||||
: supplierSharePct !== null
|
||||
? `Крупнейший подтвержденный поставщик/получатель исходящих платежей ${supplierLeader.axis_value} держит около ${supplierSharePct}% проверенного исходящего потока (${supplierLeader.total_amount_human_ru}). Это procurement concentration proxy по найденным строкам, а не полный vendor-risk аудит.`
|
||||
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value} — ${supplierLeader.total_amount_human_ru}.`
|
||||
: null,
|
||||
|
|
@ -5032,10 +5518,12 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
let outgoingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null = null;
|
||||
let lifecycleResult: AddressMcpQueryExecutorResult | null = null;
|
||||
let taxResult: AddressMcpQueryExecutorResult | null = null;
|
||||
let accountingFinancialResultResult: AddressMcpQueryExecutorResult | null = null;
|
||||
let tradingMarginResult: AddressMcpQueryExecutorResult | null = null;
|
||||
let receivablesResult: AddressMcpQueryExecutorResult | null = null;
|
||||
let payablesResult: AddressMcpQueryExecutorResult | null = null;
|
||||
let openContractsResult: AddressMcpQueryExecutorResult | null = null;
|
||||
let dueDateAgingResult: AddressMcpQueryExecutorResult | null = null;
|
||||
let documentActivityProfileResult: AddressMcpQueryExecutorResult | null = null;
|
||||
let counterpartyProfileResult: AddressMcpQueryExecutorResult | null = null;
|
||||
let contractUsageProfileResult: AddressMcpQueryExecutorResult | null = null;
|
||||
|
|
@ -5045,8 +5533,10 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
const lifecycleFilters = buildLifecycleFilters(planner);
|
||||
const profileFilters = buildBusinessOverviewProfileFilters(planner);
|
||||
const taxFilters = buildBusinessOverviewTaxFilters(planner);
|
||||
const accountingFinancialResultFilters = buildBusinessOverviewAccountingFinancialResultFilters(planner);
|
||||
const tradingMarginFilters = buildBusinessOverviewTradingMarginFilters(planner);
|
||||
const debtFilters = buildBusinessOverviewDebtFilters(planner);
|
||||
const debtDueDateAgingProbeEnabled = shouldRunDebtDueDateAgingProbe(planner);
|
||||
const inventoryFilters = buildBusinessOverviewInventoryFilters(planner);
|
||||
const debtAsOfDate = toNonEmptyString(debtFilters?.as_of_date);
|
||||
const inventoryAsOfDate = toNonEmptyString(inventoryFilters?.as_of_date);
|
||||
|
|
@ -5059,6 +5549,9 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
const taxSelection = taxFilters
|
||||
? selectAddressRecipe("vat_liability_confirmed_for_tax_period", taxFilters)
|
||||
: null;
|
||||
const accountingFinancialResultSelection = accountingFinancialResultFilters
|
||||
? selectAddressRecipe("accounting_financial_result_for_organization", accountingFinancialResultFilters)
|
||||
: null;
|
||||
const tradingMarginSelection = tradingMarginFilters
|
||||
? selectAddressRecipe("inventory_trading_margin_proxy_for_organization", tradingMarginFilters)
|
||||
: null;
|
||||
|
|
@ -5071,6 +5564,9 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
const openContractsSelection = debtFilters
|
||||
? selectAddressRecipe("open_contracts_confirmed_as_of_date", debtFilters)
|
||||
: null;
|
||||
const dueDateAgingSelection = debtFilters && debtDueDateAgingProbeEnabled
|
||||
? selectAddressRecipe("debt_due_date_aging_for_organization", debtFilters)
|
||||
: null;
|
||||
const inventoryOnHandSelection = inventoryFilters
|
||||
? selectAddressRecipe("inventory_on_hand_as_of_date", inventoryFilters)
|
||||
: null;
|
||||
|
|
@ -5135,6 +5631,14 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
pushReason(reasonCodes, "pilot_business_overview_tax_recipe_not_available");
|
||||
pushUnique(queryLimitations, "Business overview VAT/tax probe requires an executable tax-period recipe");
|
||||
}
|
||||
if (accountingFinancialResultSelection?.selected_recipe) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_accounting_financial_result_recipe_selected");
|
||||
} else if (!accountingFinancialResultFilters) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_accounting_financial_result_probe_skipped_without_explicit_period");
|
||||
} else {
|
||||
pushReason(reasonCodes, "pilot_business_overview_accounting_financial_result_recipe_not_available");
|
||||
pushUnique(queryLimitations, "Business overview accounting financial-result probe requires an executable 90/91/99 period-close recipe");
|
||||
}
|
||||
if (tradingMarginSelection?.selected_recipe) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_trading_margin_recipe_selected");
|
||||
} else if (!tradingMarginFilters) {
|
||||
|
|
@ -5159,6 +5663,16 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
pushReason(reasonCodes, "pilot_business_overview_open_contracts_recipe_not_available");
|
||||
pushUnique(queryLimitations, "Business overview open-settlement quality probe requires executable open-contracts as-of-date recipe");
|
||||
}
|
||||
if (dueDateAgingSelection?.selected_recipe) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_debt_due_date_aging_recipe_selected");
|
||||
} else if (!debtFilters) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_debt_due_date_aging_probe_skipped_without_explicit_as_of_date");
|
||||
} else if (!debtDueDateAgingProbeEnabled) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_debt_due_date_aging_probe_skipped_without_boundary_need");
|
||||
} else {
|
||||
pushReason(reasonCodes, "pilot_business_overview_debt_due_date_aging_recipe_not_available");
|
||||
pushUnique(queryLimitations, "Business overview due-date aging probe requires executable contract payment-term/open-balance recipe");
|
||||
}
|
||||
if (inventoryOnHandSelection?.selected_recipe) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_inventory_on_hand_recipe_selected");
|
||||
if (inventoryAgingSelection?.selected_recipe) {
|
||||
|
|
@ -5200,6 +5714,17 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
account_scope: taxPlan.account_scope
|
||||
});
|
||||
}
|
||||
if (accountingFinancialResultSelection?.selected_recipe) {
|
||||
const accountingFinancialResultPlan = buildAddressRecipePlan(
|
||||
accountingFinancialResultSelection.selected_recipe,
|
||||
accountingFinancialResultFilters!
|
||||
);
|
||||
accountingFinancialResultResult = await runtimeDeps.executeAddressMcpQuery({
|
||||
query: accountingFinancialResultPlan.query,
|
||||
limit: accountingFinancialResultPlan.limit,
|
||||
account_scope: accountingFinancialResultPlan.account_scope
|
||||
});
|
||||
}
|
||||
if (receivablesSelection?.selected_recipe && payablesSelection?.selected_recipe) {
|
||||
const receivablesPlan = buildAddressRecipePlan(receivablesSelection.selected_recipe, debtFilters!);
|
||||
receivablesResult = await runtimeDeps.executeAddressMcpQuery({
|
||||
|
|
@ -5222,6 +5747,14 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
account_scope: openContractsPlan.account_scope
|
||||
});
|
||||
}
|
||||
if (dueDateAgingSelection?.selected_recipe) {
|
||||
const dueDateAgingPlan = buildAddressRecipePlan(dueDateAgingSelection.selected_recipe, debtFilters!);
|
||||
dueDateAgingResult = await runtimeDeps.executeAddressMcpQuery({
|
||||
query: dueDateAgingPlan.query,
|
||||
limit: dueDateAgingPlan.limit,
|
||||
account_scope: dueDateAgingPlan.account_scope
|
||||
});
|
||||
}
|
||||
if (inventoryOnHandSelection?.selected_recipe) {
|
||||
const inventoryOnHandPlan = buildAddressRecipePlan(inventoryOnHandSelection.selected_recipe, inventoryFilters!);
|
||||
inventoryOnHandResult = await runtimeDeps.executeAddressMcpQuery({
|
||||
|
|
@ -5243,6 +5776,9 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
if (taxResult) {
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, taxResult));
|
||||
}
|
||||
if (accountingFinancialResultResult) {
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, accountingFinancialResultResult));
|
||||
}
|
||||
if (receivablesResult) {
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, receivablesResult));
|
||||
}
|
||||
|
|
@ -5252,6 +5788,9 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
if (openContractsResult) {
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, openContractsResult));
|
||||
}
|
||||
if (dueDateAgingResult) {
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, dueDateAgingResult));
|
||||
}
|
||||
if (inventoryOnHandResult) {
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, inventoryOnHandResult));
|
||||
}
|
||||
|
|
@ -5276,6 +5815,12 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
} else if (taxResult) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_tax_query_mcp_executed");
|
||||
}
|
||||
if (accountingFinancialResultResult?.error) {
|
||||
pushUnique(queryLimitations, accountingFinancialResultResult.error);
|
||||
pushReason(reasonCodes, "pilot_business_overview_accounting_financial_result_query_mcp_error");
|
||||
} else if (accountingFinancialResultResult) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_accounting_financial_result_query_mcp_executed");
|
||||
}
|
||||
if (receivablesResult?.error) {
|
||||
pushUnique(queryLimitations, receivablesResult.error);
|
||||
pushReason(reasonCodes, "pilot_business_overview_receivables_query_mcp_error");
|
||||
|
|
@ -5300,6 +5845,12 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
} else if (openContractsResult) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_open_contracts_query_mcp_executed");
|
||||
}
|
||||
if (dueDateAgingResult?.error) {
|
||||
pushUnique(queryLimitations, dueDateAgingResult.error);
|
||||
pushReason(reasonCodes, "pilot_business_overview_debt_due_date_aging_query_mcp_error");
|
||||
} else if (dueDateAgingResult) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_debt_due_date_aging_query_mcp_executed");
|
||||
}
|
||||
if (inventoryOnHandResult?.error) {
|
||||
pushUnique(queryLimitations, inventoryOnHandResult.error);
|
||||
pushReason(reasonCodes, "pilot_business_overview_inventory_on_hand_query_mcp_error");
|
||||
|
|
@ -5411,10 +5962,12 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
outgoingResult,
|
||||
lifecycleResult,
|
||||
taxResult,
|
||||
accountingFinancialResultResult,
|
||||
tradingMarginResult,
|
||||
receivablesResult,
|
||||
payablesResult,
|
||||
openContractsResult,
|
||||
dueDateAgingResult,
|
||||
documentActivityProfileResult,
|
||||
counterpartyProfileResult,
|
||||
contractUsageProfileResult,
|
||||
|
|
@ -5451,6 +6004,9 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
if (derivedBusinessOverview.tax_position) {
|
||||
pushReason(reasonCodes, "pilot_derived_business_overview_tax_position_from_confirmed_rows");
|
||||
}
|
||||
if (derivedBusinessOverview.accounting_financial_result) {
|
||||
pushReason(reasonCodes, "pilot_derived_business_overview_accounting_financial_result_from_confirmed_rows");
|
||||
}
|
||||
if (derivedBusinessOverview.trading_margin_proxy) {
|
||||
pushReason(reasonCodes, "pilot_derived_business_overview_trading_margin_proxy_from_confirmed_rows");
|
||||
}
|
||||
|
|
@ -5466,6 +6022,10 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
if (derivedBusinessOverview.debt_staleness_risk_proxy) {
|
||||
pushReason(reasonCodes, "pilot_derived_business_overview_debt_staleness_risk_proxy_from_confirmed_rows");
|
||||
}
|
||||
if (derivedBusinessOverview.debt_due_date_aging) {
|
||||
pushReason(reasonCodes, "pilot_derived_business_overview_debt_due_date_aging_from_confirmed_rows");
|
||||
pushReason(reasonCodes, `pilot_derived_business_overview_debt_due_date_aging_${derivedBusinessOverview.debt_due_date_aging.evidence_status}`);
|
||||
}
|
||||
if (derivedBusinessOverview.inventory_position) {
|
||||
pushReason(reasonCodes, "pilot_derived_business_overview_inventory_position_from_confirmed_rows");
|
||||
}
|
||||
|
|
@ -5484,10 +6044,12 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
outgoingResult,
|
||||
lifecycleResult,
|
||||
taxResult,
|
||||
accountingFinancialResultResult,
|
||||
tradingMarginResult,
|
||||
receivablesResult,
|
||||
payablesResult,
|
||||
openContractsResult,
|
||||
dueDateAgingResult,
|
||||
documentActivityProfileResult,
|
||||
counterpartyProfileResult,
|
||||
contractUsageProfileResult,
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ export interface AssistantMcpDiscoveryEvidenceContract {
|
|||
|
||||
const DEFAULT_DISCOVERY_BUDGET: AssistantMcpDiscoveryExecutionBudget = {
|
||||
max_probe_count: 3,
|
||||
max_rows_per_probe: 100
|
||||
max_rows_per_probe: 200
|
||||
};
|
||||
|
||||
const MAX_PROBE_COUNT = 36;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { AssistantMcpDiscoveryRuntimeEntryPointContract } from "./assistantMcpDiscoveryRuntimeEntryPoint";
|
||||
import { isLikelyFinancialInstitutionCounterparty } from "./counterpartyRoleHeuristics";
|
||||
|
||||
export const ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION =
|
||||
"assistant_mcp_discovery_response_candidate_v1" as const;
|
||||
|
|
@ -97,7 +98,27 @@ function userFacingLines(values: string[]): string[] {
|
|||
return uniqueStrings(values).filter((line) => !hasInternalMechanics(line));
|
||||
}
|
||||
|
||||
function sanitizeUserFacingMechanics(value: string): string {
|
||||
return String(value ?? "").replace(/MCP-срез(?:ом|у|е|а)?/giu, (match) => {
|
||||
const normalized = match.toLowerCase();
|
||||
if (normalized.endsWith("ом")) {
|
||||
return "срезом 1С";
|
||||
}
|
||||
if (normalized.endsWith("у")) {
|
||||
return "срезу 1С";
|
||||
}
|
||||
if (normalized.endsWith("е")) {
|
||||
return "срезе 1С";
|
||||
}
|
||||
if (normalized.endsWith("а")) {
|
||||
return "среза 1С";
|
||||
}
|
||||
return "срез 1С";
|
||||
});
|
||||
}
|
||||
|
||||
function localizeLine(value: string): string {
|
||||
const sanitizedValue = sanitizeUserFacingMechanics(value);
|
||||
if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) {
|
||||
return "В 1С найдены строки активности в запрошенном срезе.";
|
||||
}
|
||||
|
|
@ -126,7 +147,7 @@ function localizeLine(value: string): string {
|
|||
value
|
||||
)
|
||||
) {
|
||||
return "Запрошенный период уперся в лимит строк MCP; доступного бюджета помесячных дозапросов не хватило, чтобы покрыть все подпериоды.";
|
||||
return "Запрошенный период достиг лимита строк; доступного бюджета помесячных дозапросов не хватило, чтобы покрыть все подпериоды.";
|
||||
}
|
||||
const counterpartyMatch = value.match(/^1C activity rows were found for counterparty\s+(.+)$/i);
|
||||
if (counterpartyMatch) {
|
||||
|
|
@ -151,10 +172,10 @@ function localizeLine(value: string): string {
|
|||
}
|
||||
const movementRowsMatch = value.match(/^1C movement rows were found for counterparty\s+(.+)$/i);
|
||||
if (movementRowsMatch) {
|
||||
return `В 1С найдены строки движений по контрагенту ${movementRowsMatch[1]}.`;
|
||||
return `В 1С найдены строки движений по контрагенту ${movementRowsMatch[1]}.`;
|
||||
}
|
||||
if (/^1C movement rows were found for the requested scope$/i.test(value)) {
|
||||
return "В 1С найдены строки движений по запрошенному контуру.";
|
||||
return "В 1С найдены строки движений по запрошенному контуру.";
|
||||
}
|
||||
const supplierPayoutMatch = value.match(/^1C supplier-payout rows were found for counterparty\s+(.+)$/i);
|
||||
if (supplierPayoutMatch) {
|
||||
|
|
@ -186,7 +207,7 @@ function localizeLine(value: string): string {
|
|||
return "Срез документов ограничен только подтвержденными строками документов в проверенном окне.";
|
||||
}
|
||||
if (/^Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope$/i.test(value)) {
|
||||
return "Срез движений ограничен только подтвержденными строками движений в проверенном окне.";
|
||||
return "Срез движений ограничен только подтвержденными строками движений в проверенном окне.";
|
||||
}
|
||||
if (/^Counterparty value-flow total was calculated from confirmed 1C movement rows$/i.test(value)) {
|
||||
return "Сумма входящих поступлений рассчитана только по подтвержденным строкам поступлений в 1С.";
|
||||
|
|
@ -274,10 +295,10 @@ function localizeLine(value: string): string {
|
|||
return "Полный срез документов без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
if (/^Full movement history outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||
return "Полный исторический срез движений вне проверенного периода этим поиском не подтвержден.";
|
||||
return "Полный исторический срез движений вне проверенного периода этим поиском не подтвержден.";
|
||||
}
|
||||
if (/^Full movement history is not proven without an explicit checked period$/i.test(value)) {
|
||||
return "Полный срез движений без явно проверенного периода не подтвержден.";
|
||||
return "Полный срез движений без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
if (/^Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||
return "Полный объем исходящих платежей вне проверенного периода этим поиском не подтвержден.";
|
||||
|
|
@ -296,14 +317,14 @@ function localizeLine(value: string): string {
|
|||
value
|
||||
)
|
||||
) {
|
||||
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С после того, как общая выборка уперлась в лимит строк.";
|
||||
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С после того, как общая выборка достигла лимита строк.";
|
||||
}
|
||||
if (
|
||||
/^Requested period coverage for bidirectional value-flow was recovered through monthly 1C side probes after a broad probe hit the row limit$/i.test(
|
||||
value
|
||||
)
|
||||
) {
|
||||
return "Покрытие запрошенного периода по двустороннему денежному потоку восстановлено помесячными проверками 1С после того, как общая выборка уперлась в лимит строк хотя бы по одной стороне.";
|
||||
return "Покрытие запрошенного периода по двустороннему денежному потоку восстановлено помесячными проверками 1С после того, как общая выборка достигла лимита строк хотя бы по одной стороне.";
|
||||
}
|
||||
if (/^Requested period coverage was recovered through monthly 1C value-flow probes$/i.test(value)) {
|
||||
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С.";
|
||||
|
|
@ -323,7 +344,7 @@ function localizeLine(value: string): string {
|
|||
if (/^Complete requested-period coverage for bidirectional value-flow is not proven by the available checked rows$/i.test(value)) {
|
||||
return "Полное покрытие запрошенного периода по двустороннему денежному потоку не подтверждено доступными проверенными строками.";
|
||||
}
|
||||
return value;
|
||||
return sanitizedValue;
|
||||
}
|
||||
|
||||
function section(title: string, lines: string[]): string | null {
|
||||
|
|
@ -408,7 +429,7 @@ function businessOverviewCoverageLimitLine(overview: Record<string, unknown>): s
|
|||
limited.push("исходящие");
|
||||
}
|
||||
return limited.length > 0
|
||||
? `Важно: ${limited.join(" и ")} уперлись в лимит выборки MCP, поэтому это проверенный срез найденных строк, а не гарантированно полный бухгалтерский оборот.`
|
||||
? `Важно: по направлению ${limited.join(" и ")} проверка достигла лимита строк; это расширенный проверенный срез найденных строк, но не гарантия полного бухгалтерского оборота без отдельной полной выгрузки.`
|
||||
: null;
|
||||
}
|
||||
|
||||
|
|
@ -437,6 +458,34 @@ function firstOverviewAxisLabel(rows: unknown, amountKey = "total_amount_human_r
|
|||
return label && amount ? `${label} — ${sentenceAmount(amount) ?? amount}` : null;
|
||||
}
|
||||
|
||||
function firstNonFinancialOverviewAxisLabel(rows: unknown, amountKey = "total_amount_human_ru"): string | null {
|
||||
if (!Array.isArray(rows)) {
|
||||
return null;
|
||||
}
|
||||
for (const row of rows) {
|
||||
const item = toRecordObject(row);
|
||||
const label = toNonEmptyString(item?.axis_value);
|
||||
if (!label || isLikelyFinancialInstitutionCounterparty(label)) {
|
||||
continue;
|
||||
}
|
||||
const amount = moneyText(item?.[amountKey]);
|
||||
if (amount) {
|
||||
return `${label} — ${sentenceAmount(amount) ?? amount}`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function overviewAxisLooksFinancial(row: Record<string, unknown> | null): boolean {
|
||||
if (!row) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
row.counterparty_role_hint === "bank_or_financial_institution" ||
|
||||
isLikelyFinancialInstitutionCounterparty(row.axis_value)
|
||||
);
|
||||
}
|
||||
|
||||
function businessOverviewTaxLine(overview: Record<string, unknown>): string | null {
|
||||
const tax = toRecordObject(overview.tax_position);
|
||||
if (!tax) {
|
||||
|
|
@ -568,7 +617,7 @@ function buildCompactBidirectionalValueFlowReply(
|
|||
lines.push(`Основа: ${basis.join("; ")}.`);
|
||||
}
|
||||
if (flow.coverage_limited_by_probe_limit === true) {
|
||||
lines.push("Важно: часть проверки уперлась в лимит строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");
|
||||
lines.push("Важно: часть проверки достигла лимита строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");
|
||||
}
|
||||
lines.push("Метод: нетто рассчитано как подтвержденные входящие строки 1С минус подтвержденные исходящие строки; это не полное бухгалтерское сальдо вне проверенного окна.");
|
||||
|
||||
|
|
@ -721,17 +770,202 @@ function buildCompactBusinessOverviewReply(
|
|||
const topCustomer = toRecordObject(Array.isArray(overview.top_customers) ? overview.top_customers[0] : null);
|
||||
const customerName = toNonEmptyString(topCustomer?.axis_value);
|
||||
const customerAmount = moneyText(topCustomer?.total_amount_human_ru);
|
||||
const topCustomerLooksFinancial = overviewAxisLooksFinancial(topCustomer);
|
||||
const nonFinancialCustomer = firstNonFinancialOverviewAxisLabel(
|
||||
topCustomerLooksFinancial ? overview.top_customers : []
|
||||
);
|
||||
const topCustomerLead =
|
||||
customerName && customerAmount
|
||||
? `; крупнейший источник входящих денег: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}`
|
||||
? topCustomerLooksFinancial
|
||||
? `; крупнейший входящий денежный источник: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount} (похоже на банк/финорганизацию, не называю это клиентской выручкой без назначения платежа)${nonFinancialCustomer ? `; крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}` : ""}`
|
||||
: `; крупнейший источник входящих денег: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}`
|
||||
: "";
|
||||
const topSupplierRecord = toRecordObject(Array.isArray(overview.top_suppliers) ? overview.top_suppliers[0] : null);
|
||||
const topSupplier = firstOverviewAxisLabel(overview.top_suppliers);
|
||||
const topSupplierLead = topSupplier ? `; крупнейший получатель исходящих денег: ${topSupplier}` : "";
|
||||
const topSupplierLooksFinancial = overviewAxisLooksFinancial(topSupplierRecord);
|
||||
const nonFinancialSupplier = firstNonFinancialOverviewAxisLabel(
|
||||
topSupplierLooksFinancial ? overview.top_suppliers : []
|
||||
);
|
||||
const topSupplierLead = topSupplier
|
||||
? topSupplierLooksFinancial
|
||||
? `; крупнейший получатель исходящих денег: ${topSupplier} (похоже на банк/финорганизацию, не называю это обычным поставщиком без назначения платежа/договора)${nonFinancialSupplier ? `; крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}` : ""}`
|
||||
: `; крупнейший получатель исходящих денег: ${topSupplier}`
|
||||
: "";
|
||||
const roleBoundaryLead = topCustomer || topSupplier ? "; клиент/поставщик как бизнес-роли этим денежным срезом не подтверждены" : "";
|
||||
const graphReasonCodes = toStringList(graph?.reason_codes);
|
||||
const directMoneyAnswer = graphReasonCodes.includes("data_need_graph_business_overview_direct_money_answer");
|
||||
const crossScopeExecutiveSummary = Boolean(separateSubject && previousCounterpartySummary);
|
||||
const lines: string[] = [];
|
||||
const actionFamily = toNonEmptyString(turnMeaning?.asked_action_family);
|
||||
const unsupportedFamily = toNonEmptyString(turnMeaning?.unsupported_but_understood_family);
|
||||
const profitMarginBoundary = actionFamily === "profit_margin_boundary" || unsupportedFamily === "profit_margin_boundary";
|
||||
const debtDueDateBoundary = actionFamily === "debt_due_date_boundary" || unsupportedFamily === "debt_due_date_boundary";
|
||||
const vendorRiskBoundary =
|
||||
actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary";
|
||||
const inventoryReserveBoundary =
|
||||
actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary";
|
||||
|
||||
if (profitMarginBoundary) {
|
||||
const accountingFinancialResult = toRecordObject(overview.accounting_financial_result);
|
||||
if (accountingFinancialResult) {
|
||||
const direction = toNonEmptyString(accountingFinancialResult.final_result_direction);
|
||||
const amount = moneyText(accountingFinancialResult.final_result_amount_human_ru);
|
||||
const periodScope = toNonEmptyString(accountingFinancialResult.period_scope) ?? period;
|
||||
const marginPct =
|
||||
typeof accountingFinancialResult.net_margin_to_revenue_pct === "number" &&
|
||||
Number.isFinite(accountingFinancialResult.net_margin_to_revenue_pct)
|
||||
? `${accountingFinancialResult.net_margin_to_revenue_pct}%`
|
||||
: null;
|
||||
const directionText =
|
||||
direction === "profit"
|
||||
? "учетная прибыль"
|
||||
: direction === "loss"
|
||||
? "учетный убыток"
|
||||
: "нулевой учетный финрезультат";
|
||||
const amountText = amount
|
||||
? direction === "loss"
|
||||
? `минус ${amount}`
|
||||
: amount
|
||||
: "сумма не распознана";
|
||||
lines.push(
|
||||
`Коротко: по бухгалтерскому маршруту 90/91/99 за ${periodScope} подтвержден ${directionText}: ${amountText}${marginPct ? `; маржа к выручке 90.01 ${marginPct}` : "; маржа к выручке 90.01 не рассчитана"}.`
|
||||
);
|
||||
lines.push(
|
||||
"Это учетный финрезультат по найденным строкам закрытия периода в 1С, а не внешний аудит и не юридически подтвержденная отчетность."
|
||||
);
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
}
|
||||
const headline = toNonEmptyString(draft.headline);
|
||||
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
|
||||
lines.push(
|
||||
cleanHeadline
|
||||
? `Коротко: ${localizeLine(cleanHeadline)}`
|
||||
: "Коротко: нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только bounded operating-flow/trading-margin proxy, не P&L и не бухгалтерский финансовый результат."
|
||||
);
|
||||
const boundaryLines = userFacingLines([
|
||||
...toStringList(draft.confirmed_lines),
|
||||
...toStringList(draft.inference_lines),
|
||||
...toStringList(draft.unknown_lines)
|
||||
])
|
||||
.filter((line) => /(?:прибыл|марж|финанс|p\s*&\s*l|p&l|расход|себестоим|закрыт|profit|margin|financial)/iu.test(line))
|
||||
.slice(0, 2);
|
||||
if (boundaryLines.length > 0) {
|
||||
lines.push(...boundaryLines.map(localizeLine));
|
||||
}
|
||||
lines.push(
|
||||
"Для точного P&L нужны отдельный маршрут по себестоимости, расходам, закрытию периода и финрезультату; текущий proxy нельзя выдавать за подтвержденную чистую прибыль или маржу."
|
||||
);
|
||||
if (limitLine) {
|
||||
lines.push(limitLine);
|
||||
}
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
}
|
||||
|
||||
if (debtDueDateBoundary) {
|
||||
const dueDateAging = toRecordObject(overview.debt_due_date_aging);
|
||||
if (dueDateAging) {
|
||||
const status = toNonEmptyString(dueDateAging.evidence_status);
|
||||
const asOfDate = toNonEmptyString(dueDateAging.as_of_date) ?? "проверенную дату";
|
||||
const overdueAmount = moneyText(dueDateAging.overdue_amount_human_ru);
|
||||
const grossAmount = moneyText(dueDateAging.gross_open_amount_human_ru);
|
||||
const rowsWithPaymentTerms =
|
||||
typeof dueDateAging.rows_with_payment_terms === "number" && Number.isFinite(dueDateAging.rows_with_payment_terms)
|
||||
? dueDateAging.rows_with_payment_terms
|
||||
: null;
|
||||
const rowsWithAmount =
|
||||
typeof dueDateAging.rows_with_amount === "number" && Number.isFinite(dueDateAging.rows_with_amount)
|
||||
? dueDateAging.rows_with_amount
|
||||
: null;
|
||||
if (status === "confirmed_overdue") {
|
||||
lines.push(
|
||||
`Коротко: на ${asOfDate} подтвержденная просрочка есть: ${overdueAmount ?? "сумма не распознана"} по ${dueDateAging.overdue_rows ?? "найденным"} строкам.`
|
||||
);
|
||||
lines.push("Основа ответа: открытые расчеты 60/62/76, договорный срок оплаты и дата расчетного документа; это уже due-date route, не старение договора как proxy.");
|
||||
} else if (status === "no_payment_terms_configured") {
|
||||
lines.push(
|
||||
`Коротко: на ${asOfDate} подтвержденной просрочки нет: открытые расчеты проверены${grossAmount ? ` на ${grossAmount}` : ""}, но в найденных договорах срок оплаты не установлен.`
|
||||
);
|
||||
lines.push(
|
||||
rowsWithAmount !== null
|
||||
? `Проверено строк с суммой: ${rowsWithAmount}. Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой.`
|
||||
: "Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой."
|
||||
);
|
||||
} else if (status === "insufficient_due_date_basis") {
|
||||
lines.push(
|
||||
`Коротко: due-date route запущен на ${asOfDate}, но просрочка не подтверждена: по строкам с установленным сроком оплаты не хватило даты расчетного документа.`
|
||||
);
|
||||
if (rowsWithPaymentTerms !== null) {
|
||||
lines.push(`Строк с установленным сроком оплаты: ${rowsWithPaymentTerms}; нужен документ-основание с датой для расчета due date.`);
|
||||
}
|
||||
} else {
|
||||
lines.push(
|
||||
`Коротко: due-date route на ${asOfDate} проверен, подтвержденной просрочки не найдено${rowsWithPaymentTerms !== null ? `; строк с установленным сроком оплаты ${rowsWithPaymentTerms}` : ""}.`
|
||||
);
|
||||
}
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
}
|
||||
const headline = toNonEmptyString(draft.headline);
|
||||
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
|
||||
lines.push(
|
||||
cleanHeadline
|
||||
? `Коротко: ${localizeLine(cleanHeadline)}`
|
||||
: "Коротко: нельзя точно определить, какая дебиторка просрочена, по текущему срезу 1С; есть только debt-quality proxy, но нет проверенного due-date маршрута."
|
||||
);
|
||||
lines.push(
|
||||
"Проверить нужно отдельно: договоры, сроки оплаты, погашение и закрытие задолженности; без этого нельзя доказать overdue/due-date aging."
|
||||
);
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
}
|
||||
|
||||
if (vendorRiskBoundary) {
|
||||
const supplierBasis = topSupplier
|
||||
? topSupplierLooksFinancial
|
||||
? `крупнейший получатель исходящих денег: ${topSupplier}; по названию это банк/финансовая организация, поэтому это не доказанная зависимость от обычного поставщика${nonFinancialSupplier ? `; крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}` : ""}`
|
||||
: `крупнейший подтвержденный поставщик/получатель исходящих платежей: ${topSupplier}`
|
||||
: outgoingAmount
|
||||
? `исходящие платежи/закупочный поток в проверенном срезе: ${outgoingAmount}`
|
||||
: "есть только ограниченный срез исходящих платежей без полного vendor-risk профиля";
|
||||
const proxyLabel = topSupplierLooksFinancial ? "outgoing cash concentration proxy" : "procurement concentration proxy";
|
||||
lines.push(
|
||||
`Коротко: точный риск зависимости от одного поставщика по текущим данным не подтвержден; есть только ${proxyLabel}: ${supplierBasis}.`
|
||||
);
|
||||
lines.push(
|
||||
"Это сигнал концентрации закупок/исходящих платежей, а не полный аудит надежности поставщиков, условий, качества и структуры всех расходов."
|
||||
);
|
||||
lines.push(
|
||||
"Для точного вывода нужен отдельный reviewed vendor-risk route: поставщики, договорные условия, качество поставок, сроки, доля в закупках и полная структура расходов."
|
||||
);
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
}
|
||||
|
||||
if (inventoryReserveBoundary) {
|
||||
const headline = toNonEmptyString(draft.headline);
|
||||
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
|
||||
lines.push(
|
||||
cleanHeadline
|
||||
? `Коротко: ${localizeLine(cleanHeadline)}`
|
||||
: "Коротко: точно подтвердить резерв под неликвиды по текущим данным нельзя."
|
||||
);
|
||||
const boundaryLines = userFacingLines([
|
||||
...toStringList(draft.unknown_lines),
|
||||
...toStringList(draft.limitation_lines)
|
||||
])
|
||||
.filter((line) => /(?:резерв|неликвид|склад|товар|reserve|obsolete|inventory|stock)/iu.test(line))
|
||||
.slice(0, 2);
|
||||
if (boundaryLines.length > 0) {
|
||||
lines.push(...boundaryLines.map(localizeLine));
|
||||
}
|
||||
lines.push(
|
||||
"Проверить нужно отдельно: складской срез на дату, учетную политику резервов, списания и ликвидационную стоимость; proxy-сигналы нельзя выдавать за доказанный факт резерва."
|
||||
);
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
}
|
||||
|
||||
if (crossScopeExecutiveSummary && separateSubject && previousCounterpartySummary && (incomingAmount || outgoingAmount || netAmount)) {
|
||||
lines.push(
|
||||
|
|
@ -761,7 +995,7 @@ function buildCompactBusinessOverviewReply(
|
|||
return null;
|
||||
}
|
||||
lines.push(
|
||||
`Коротко: в доступном проверенном MCP-срезе по входящим денежным строкам лидирует ${leaderYear}: ${leaderAmount}${Number.isFinite(leaderRows) && leaderRows > 0 ? ` по ${leaderRows} строкам с суммой` : ""}; это не полный бухгалтерский рейтинг доходности.`
|
||||
`Коротко: в доступном проверенном срезе 1С по входящим денежным строкам лидирует ${leaderYear}: ${leaderAmount}${Number.isFinite(leaderRows) && leaderRows > 0 ? ` по ${leaderRows} строкам с суммой` : ""}; это не полный бухгалтерский рейтинг доходности.`
|
||||
);
|
||||
const netYear = toNonEmptyString(netLeader?.year_bucket);
|
||||
const netYearAmount = moneyText(netLeader?.net_amount_human_ru);
|
||||
|
|
@ -783,7 +1017,11 @@ function buildCompactBusinessOverviewReply(
|
|||
);
|
||||
lines.push('Метод: "заработали" здесь считаю как денежный operating-flow proxy по 1С; это не чистая прибыль и не финрезультат.');
|
||||
if (!directMoneyAnswer && customerName && customerAmount) {
|
||||
lines.push(`Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}.`);
|
||||
lines.push(
|
||||
topCustomerLooksFinancial
|
||||
? `Крупнейший входящий денежный источник в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}. По названию это банк/финансовая организация, поэтому без назначения платежа не называю это клиентской выручкой.${nonFinancialCustomer ? ` Крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}.` : ""}`
|
||||
: `Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}.`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
|
|
@ -797,10 +1035,18 @@ function buildCompactBusinessOverviewReply(
|
|||
}
|
||||
|
||||
if (!directMoneyAnswer && topSupplier) {
|
||||
lines.push(`Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`);
|
||||
lines.push(
|
||||
topSupplierLooksFinancial
|
||||
? `Крупнейший получатель исходящих денег: ${topSupplier}. По названию это банк/финансовая организация, поэтому без назначения платежа/договора не считаю это обычным поставщиком.${nonFinancialSupplier ? ` Крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}.` : ""}`
|
||||
: `Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`
|
||||
);
|
||||
}
|
||||
if (!directMoneyAnswer && (topCustomer || topSupplier)) {
|
||||
lines.push("Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль.");
|
||||
lines.push(
|
||||
topCustomerLooksFinancial || topSupplierLooksFinancial
|
||||
? "Важно по ролям: текущий денежный срез подтверждает источники и получателей денег, но банковские контрагенты требуют проверки назначения платежа/счетов и не доказывают роль клиента или поставщика."
|
||||
: "Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль."
|
||||
);
|
||||
}
|
||||
if (!directMoneyAnswer) {
|
||||
lines.push(
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ export const ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION =
|
|||
"assistant_mcp_discovery_runtime_bridge_v1" as const;
|
||||
export const ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION =
|
||||
"assistant_mcp_discovery_loop_state_v1" as const;
|
||||
export const ASSISTANT_MCP_ROUTE_CANDIDATE_SCHEMA_VERSION =
|
||||
"assistant_mcp_route_candidate_v1" as const;
|
||||
|
||||
export type AssistantMcpDiscoveryRuntimeBridgeStatus =
|
||||
| "answer_draft_ready"
|
||||
|
|
@ -31,6 +33,11 @@ export type AssistantMcpDiscoveryLoopStatus =
|
|||
| "awaiting_clarification"
|
||||
| "ready_for_next_hop"
|
||||
| "blocked";
|
||||
export type AssistantMcpRouteCandidateStatus =
|
||||
| "ready_for_reviewed_execution"
|
||||
| "needs_user_scope"
|
||||
| "needs_route_enablement"
|
||||
| "blocked";
|
||||
|
||||
export interface AssistantMcpDiscoveryRuntimeBridgeInput {
|
||||
semanticDataNeed?: string | null;
|
||||
|
|
@ -61,6 +68,26 @@ export interface AssistantMcpDiscoveryLoopStateContract {
|
|||
explicit_date_scope: string | null;
|
||||
}
|
||||
|
||||
export interface AssistantMcpRouteCandidateContract {
|
||||
schema_version: typeof ASSISTANT_MCP_ROUTE_CANDIDATE_SCHEMA_VERSION;
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeBridge";
|
||||
candidate_status: AssistantMcpRouteCandidateStatus;
|
||||
selected_chain_id: AssistantMcpDiscoveryChainId;
|
||||
selected_chain_summary: string;
|
||||
nearest_catalog_chain_template: AssistantMcpDiscoveryPlannerContract["catalog_chain_template_alignment"]["top_chain_template_match"];
|
||||
catalog_alignment_status: AssistantMcpDiscoveryPlannerContract["catalog_chain_template_alignment"]["alignment_status"];
|
||||
business_fact_family: string | null;
|
||||
action_family: string | null;
|
||||
proof_expectation: string | null;
|
||||
required_axes: string[];
|
||||
provided_axes: string[];
|
||||
missing_axes: string[];
|
||||
executable_now: boolean;
|
||||
enablement_reason: string | null;
|
||||
recommended_next_action: string;
|
||||
forbidden_overclaim_flags: string[];
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryRuntimeBridgeContract {
|
||||
schema_version: typeof ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION;
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeBridge";
|
||||
|
|
@ -70,6 +97,7 @@ export interface AssistantMcpDiscoveryRuntimeBridgeContract {
|
|||
pilot: AssistantMcpDiscoveryPilotExecutionContract;
|
||||
answer_draft: AssistantMcpDiscoveryAnswerDraftContract;
|
||||
loop_state: AssistantMcpDiscoveryLoopStateContract;
|
||||
route_candidate: AssistantMcpRouteCandidateContract;
|
||||
user_facing_response_allowed: boolean;
|
||||
business_fact_answer_allowed: boolean;
|
||||
requires_user_clarification: boolean;
|
||||
|
|
@ -138,6 +166,26 @@ function loopStatusFor(
|
|||
return "ready_for_next_hop";
|
||||
}
|
||||
|
||||
function routeCandidateStatusFor(
|
||||
bridgeStatus: AssistantMcpDiscoveryRuntimeBridgeStatus,
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract,
|
||||
missingProofFamily: AssistantMcpDiscoveryBusinessOverviewMissingProofFamily | null
|
||||
): AssistantMcpRouteCandidateStatus {
|
||||
if (bridgeStatus === "blocked" || pilot.pilot_status === "blocked") {
|
||||
return "blocked";
|
||||
}
|
||||
if (bridgeStatus === "needs_clarification" || pilot.pilot_status === "skipped_needs_clarification") {
|
||||
return "needs_user_scope";
|
||||
}
|
||||
if (bridgeStatus === "unsupported" || pilot.pilot_status === "unsupported") {
|
||||
return "needs_route_enablement";
|
||||
}
|
||||
if (missingProofFamily) {
|
||||
return "needs_route_enablement";
|
||||
}
|
||||
return "ready_for_reviewed_execution";
|
||||
}
|
||||
|
||||
function flattenAxes(
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract,
|
||||
source: "provided_axes" | "missing_axis_options"
|
||||
|
|
@ -168,6 +216,144 @@ function entityCandidatesFromPlanner(planner: AssistantMcpDiscoveryPlannerContra
|
|||
return uniqueStrings(values);
|
||||
}
|
||||
|
||||
function firstNonEmpty(values: Array<string | null | undefined>): string | null {
|
||||
for (const value of values) {
|
||||
const text = String(value ?? "").trim();
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
type AssistantMcpDiscoveryBusinessOverviewMissingProofFamily = NonNullable<
|
||||
AssistantMcpDiscoveryPilotExecutionContract["derived_business_overview"]
|
||||
>["missing_proof_families"][number];
|
||||
|
||||
function routeCandidateProofFamiliesFor(actionFamily: string | null, proofExpectation: string | null): string[] {
|
||||
const combined = `${actionFamily ?? ""} ${proofExpectation ?? ""}`.trim().toLowerCase();
|
||||
const result: string[] = [];
|
||||
const add = (family: string) => {
|
||||
if (!result.includes(family)) {
|
||||
result.push(family);
|
||||
}
|
||||
};
|
||||
if (!combined || combined === "broad_evaluation bounded_inference") {
|
||||
return result;
|
||||
}
|
||||
if (/(?:inventory|stock|warehouse|reserve|liquidation|write[-_ ]?off|obsolete|obsolescence)/iu.test(combined)) {
|
||||
add("inventory_reserve_liquidation_quality");
|
||||
}
|
||||
if (/(?:debt|due[-_ ]?date|overdue|aging|credit[-_ ]?risk)/iu.test(combined)) {
|
||||
add("debt_due_date_aging_quality");
|
||||
}
|
||||
if (/(?:vendor|supplier|procurement|sourcing)/iu.test(combined)) {
|
||||
add("vendor_risk_procurement_quality");
|
||||
}
|
||||
if (/(?:profit|margin|pnl|p&l|financial[-_ ]?result)/iu.test(combined)) {
|
||||
add("accounting_profit_margin");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function routeCandidateMissingProofFamily(
|
||||
planner: AssistantMcpDiscoveryPlannerContract,
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract
|
||||
): AssistantMcpDiscoveryBusinessOverviewMissingProofFamily | null {
|
||||
if (planner.data_need_graph?.business_fact_family !== "business_overview") {
|
||||
return null;
|
||||
}
|
||||
const wantedFamilies = routeCandidateProofFamiliesFor(
|
||||
planner.data_need_graph?.action_family ?? null,
|
||||
planner.data_need_graph?.proof_expectation ?? null
|
||||
);
|
||||
if (wantedFamilies.length <= 0) {
|
||||
return null;
|
||||
}
|
||||
const missingProofFamilies = pilot.derived_business_overview?.missing_proof_families ?? [];
|
||||
return missingProofFamilies.find((item) => wantedFamilies.includes(item.family)) ?? null;
|
||||
}
|
||||
|
||||
function routeCandidateEnablementReason(
|
||||
status: AssistantMcpRouteCandidateStatus,
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract,
|
||||
missingAxes: string[],
|
||||
missingProofFamily: AssistantMcpDiscoveryBusinessOverviewMissingProofFamily | null
|
||||
): string | null {
|
||||
if (status === "ready_for_reviewed_execution") {
|
||||
return null;
|
||||
}
|
||||
if (status === "needs_user_scope") {
|
||||
return missingAxes.length > 0
|
||||
? `Missing scope axes: ${missingAxes.join(", ")}`
|
||||
: "Selected chain needs user clarification before MCP execution";
|
||||
}
|
||||
if (missingProofFamily) {
|
||||
return [
|
||||
`Missing reviewed proof family: ${missingProofFamily.family}`,
|
||||
`next_required_evidence=${missingProofFamily.next_required_evidence}`,
|
||||
missingProofFamily.current_supported_evidence
|
||||
? `current_supported_evidence=${missingProofFamily.current_supported_evidence}`
|
||||
: null,
|
||||
`must_not_claim=${missingProofFamily.must_not_claim}`
|
||||
]
|
||||
.filter((item): item is string => Boolean(item))
|
||||
.join("; ");
|
||||
}
|
||||
return firstNonEmpty([
|
||||
...pilot.query_limitations,
|
||||
...pilot.evidence.unknown_facts,
|
||||
"Selected chain is not safely executable by the reviewed MCP runtime yet"
|
||||
]);
|
||||
}
|
||||
|
||||
function routeCandidateNextAction(status: AssistantMcpRouteCandidateStatus): string {
|
||||
if (status === "ready_for_reviewed_execution") {
|
||||
return "Execute through the reviewed runtime bridge and truth gate.";
|
||||
}
|
||||
if (status === "needs_user_scope") {
|
||||
return "Ask the user for the missing scope axes before MCP execution.";
|
||||
}
|
||||
if (status === "needs_route_enablement") {
|
||||
return "Create or wire a reviewed exact route for the selected chain before treating the fact as answerable.";
|
||||
}
|
||||
return "Do not execute until the blocking reason is resolved.";
|
||||
}
|
||||
|
||||
function buildRouteCandidate(
|
||||
planner: AssistantMcpDiscoveryPlannerContract,
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract,
|
||||
bridgeStatus: AssistantMcpDiscoveryRuntimeBridgeStatus
|
||||
): AssistantMcpRouteCandidateContract {
|
||||
const plannerClarificationGaps = planner.discovery_plan.clarification_gaps ?? [];
|
||||
const providedAxes = flattenAxes(pilot, "provided_axes");
|
||||
const missingAxes = plannerClarificationGaps.length > 0 ? plannerClarificationGaps : flattenAxes(pilot, "missing_axis_options");
|
||||
const missingProofFamily = routeCandidateMissingProofFamily(planner, pilot);
|
||||
const candidateStatus = routeCandidateStatusFor(bridgeStatus, pilot, missingProofFamily);
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_ROUTE_CANDIDATE_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
|
||||
candidate_status: candidateStatus,
|
||||
selected_chain_id: planner.selected_chain_id,
|
||||
selected_chain_summary: planner.selected_chain_summary,
|
||||
nearest_catalog_chain_template: planner.catalog_chain_template_alignment.top_chain_template_match,
|
||||
catalog_alignment_status: planner.catalog_chain_template_alignment.alignment_status,
|
||||
business_fact_family: planner.data_need_graph?.business_fact_family ?? null,
|
||||
action_family: planner.data_need_graph?.action_family ?? null,
|
||||
proof_expectation: planner.data_need_graph?.proof_expectation ?? null,
|
||||
required_axes: [...planner.required_axes],
|
||||
provided_axes: providedAxes,
|
||||
missing_axes: missingAxes,
|
||||
executable_now: candidateStatus === "ready_for_reviewed_execution",
|
||||
enablement_reason: routeCandidateEnablementReason(candidateStatus, pilot, missingAxes, missingProofFamily),
|
||||
recommended_next_action: routeCandidateNextAction(candidateStatus),
|
||||
forbidden_overclaim_flags: uniqueStrings([
|
||||
...(planner.data_need_graph?.forbidden_overclaim_flags ?? []),
|
||||
...(missingProofFamily ? [missingProofFamily.must_not_claim] : [])
|
||||
])
|
||||
};
|
||||
}
|
||||
|
||||
function buildLoopState(
|
||||
planner: AssistantMcpDiscoveryPlannerContract,
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract,
|
||||
|
|
@ -215,11 +401,14 @@ export async function runAssistantMcpDiscoveryRuntimeBridge(
|
|||
const answerDraft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
const bridgeStatus = bridgeStatusFor(pilot, answerDraft);
|
||||
const loopState = buildLoopState(planner, pilot, bridgeStatus);
|
||||
const routeCandidate = buildRouteCandidate(planner, pilot, bridgeStatus);
|
||||
const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]);
|
||||
|
||||
pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`);
|
||||
pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer");
|
||||
pushReason(reasonCodes, `runtime_bridge_loop_state_${loopState.loop_status}`);
|
||||
pushReason(reasonCodes, "runtime_bridge_route_candidate_built");
|
||||
pushReason(reasonCodes, `runtime_bridge_route_candidate_${routeCandidate.candidate_status}`);
|
||||
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION,
|
||||
|
|
@ -230,6 +419,7 @@ export async function runAssistantMcpDiscoveryRuntimeBridge(
|
|||
pilot,
|
||||
answer_draft: answerDraft,
|
||||
loop_state: loopState,
|
||||
route_candidate: routeCandidate,
|
||||
user_facing_response_allowed: bridgeStatus !== "blocked",
|
||||
business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft),
|
||||
requires_user_clarification: bridgeStatus === "needs_clarification",
|
||||
|
|
|
|||
|
|
@ -180,6 +180,11 @@ function isGarbageSemanticAnchorCandidate(value: string | null): boolean {
|
|||
"всему",
|
||||
"всей",
|
||||
"всем",
|
||||
"год",
|
||||
"года",
|
||||
"году",
|
||||
"годом",
|
||||
"годы",
|
||||
"выводу",
|
||||
"выводам",
|
||||
"аудиту",
|
||||
|
|
@ -824,6 +829,51 @@ function hasOrganizationLevelEarningsOverviewSignal(text: string): boolean {
|
|||
return hasYearRankingCue || hasCompanyEarningsCue || hasCompanyProfitMarginCue;
|
||||
}
|
||||
|
||||
function hasOrganizationLevelProfitMarginBoundaryOverviewSignal(text: string): boolean {
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
const hasProfitMarginCue =
|
||||
/(?:\u043f\u0440\u0438\u0431\u044b\u043b\w*|\u043c\u0430\u0440\u0436\w*|\u0440\u0435\u043d\u0442\u0430\u0431\w*|\u0444\u0438\u043d(?:\u0430\u043d\u0441\w*)?\s*[- ]?\s*\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442|p\s*&\s*l|profit(?:ability)?|margin|financial\s+result)/iu.test(
|
||||
text
|
||||
);
|
||||
const hasCompanyScopeCue =
|
||||
/(?:\u0443\s+\u043d\u0430\u0441|\u043d\u0430\u0448\w*|\u043c\u044b\b|\u043f\u043e\s+\u043a\u043e\u043c\u043f\u0430\u043d|\u043a\u043e\u043c\u043f\u0430\u043d|\u0431\u0438\u0437\u043d\u0435\u0441|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0432\s+\u0446\u0435\u043b\u043e\u043c|\b(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e|\u043e\u0430\u043e)\b|(?:19|20)\d{2}|company|business|organization|our|we|us)/iu.test(
|
||||
text
|
||||
);
|
||||
return hasProfitMarginCue && hasCompanyScopeCue;
|
||||
}
|
||||
|
||||
function hasProfitMarginBoundaryFollowupSignal(text: string): boolean {
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
const hasProfitOrResultCue =
|
||||
/(?:\u043f\u0440\u0438\u0431\u044b\u043b\w*|\u0443\u0431\u044b\u0442\w*|\u043c\u0430\u0440\u0436\w*|\u0440\u0435\u043d\u0442\u0430\u0431\w*|\u0444\u0438\u043d(?:\u0430\u043d\u0441\w*)?\s*[- ]?\s*\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442|p\s*&\s*l|profit|loss|margin|financial\s+result)/iu.test(
|
||||
text
|
||||
);
|
||||
const hasFollowupShape =
|
||||
/(?:\u044d\u0442\u043e|\u0438\u0442\u043e\u0433|\u0438\u0442\u043e\u0433\u043e|\u043f\u043e\u043b\u0443\u0447\w*|\u043a\u043e\u0440\u043e\u0442\w*|\u0432\s+\u0438\u0442\u043e\u0433\u0435|\u043c\u043e\u0436\u043d\u043e\s+(?:\u043b\u0438\s+)?\u0441\u043a\u0430\u0437\u0430\u0442\u044c|is\s+it|result|short|brief)/iu.test(
|
||||
text
|
||||
);
|
||||
return hasProfitOrResultCue && hasFollowupShape;
|
||||
}
|
||||
|
||||
function hasDebtDueDateBoundaryFollowupSignal(text: string): boolean {
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
const hasDueDateCue =
|
||||
/(?:\u043f\u0440\u043e\u0441\u0440\u043e\u0447\w*|\u0441\u0440\u043e\u043a\w*\s+\u043e\u043f\u043b\u0430\u0442|\u0434\u043e\u043a\u0430\u0437\w*|\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\w*|due[-\s]?date|overdue|debt\s+aging|aging)/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;
|
||||
}
|
||||
|
||||
function hasOrganizationLevelDebtDueDateOverviewSignal(text: string): boolean {
|
||||
if (!text) {
|
||||
return false;
|
||||
|
|
@ -999,6 +1049,13 @@ function hasExplicitVatQuestionSignal(text: string): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
function hasExplicitVatMovementEvidenceSignal(text: string): boolean {
|
||||
if (!/(?:\u043d\u0434\u0441|vat)/iu.test(text)) {
|
||||
return false;
|
||||
}
|
||||
return hasMovementEvidenceFollowupSignal(text);
|
||||
}
|
||||
|
||||
function hasBusinessOverviewSeparateCounterpartySignal(text: string): boolean {
|
||||
if (!text) {
|
||||
return false;
|
||||
|
|
@ -1534,6 +1591,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
);
|
||||
const rawPrimaryBusinessOverviewSignal = hasBusinessOverviewSignal(rawText);
|
||||
const explicitVatQuestionSignal = hasExplicitVatQuestionSignal(rawText);
|
||||
const explicitVatMovementEvidenceSignal = hasExplicitVatMovementEvidenceSignal(rawText);
|
||||
const explicitVatSuppressesBusinessOverviewContinuation = Boolean(
|
||||
explicitVatQuestionSignal && !rawPrimaryBusinessOverviewSignal
|
||||
);
|
||||
|
|
@ -1552,6 +1610,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
const rawMetadataSignal =
|
||||
!rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
!explicitVatMovementEvidenceSignal &&
|
||||
!rawReferentialDocumentExclusionSignal &&
|
||||
hasMetadataSignal(rawText);
|
||||
const rawEntityResolutionSignal =
|
||||
|
|
@ -1569,7 +1628,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(dateScopeSignalText);
|
||||
const relativeCurrentDateHintDetected = hasRelativeCurrentDateHint(rawText);
|
||||
const rawDateScope = collectDateScopeFromRawText(dateScopeSignalText);
|
||||
const rawMetadataScopeHint = rawMetadataSignal ? metadataScopeHintFromRawText(rawText) : null;
|
||||
const rawMetadataScopeHint =
|
||||
rawMetadataSignal || explicitVatMovementEvidenceSignal ? metadataScopeHintFromRawText(rawText) : null;
|
||||
const rawTopicSwitchSignal = hasExplicitTopicSwitchSignal(rawText);
|
||||
const rawEntityCandidate = rawEntityResolutionSignal ? rawEntityResolutionCandidate(rawEntitySourceText) : null;
|
||||
const rawScopedEntityCandidate =
|
||||
|
|
@ -1594,12 +1654,49 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis);
|
||||
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
|
||||
const broadBusinessEvaluationUnsupported = unsupported === "broad_business_evaluation";
|
||||
const businessOverviewSignal =
|
||||
rawBusinessOverviewSignal ||
|
||||
const seededBusinessOverviewSignal =
|
||||
broadBusinessEvaluationUnsupported ||
|
||||
rawDomain === "business_summary" ||
|
||||
rawDomain === "business_overview" ||
|
||||
rawAction === "broad_evaluation";
|
||||
const inventoryReserveBusinessOverviewSignal =
|
||||
(rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
||||
hasOrganizationLevelInventoryReserveLiquidationOverviewSignal(rawText);
|
||||
const debtDueDateFollowupBusinessOverviewSignal =
|
||||
businessOverviewContinuationSignal && hasDebtDueDateBoundaryFollowupSignal(rawText);
|
||||
const debtDueDateBusinessOverviewSignal =
|
||||
((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
||||
hasOrganizationLevelDebtDueDateOverviewSignal(rawText)) ||
|
||||
debtDueDateFollowupBusinessOverviewSignal;
|
||||
const supplierQualityBusinessOverviewSignal =
|
||||
(rawBusinessOverviewSignal || seededBusinessOverviewSignal) && hasOrganizationLevelSupplierQualityOverviewSignal(rawText);
|
||||
const profitMarginFollowupBusinessOverviewSignal =
|
||||
businessOverviewContinuationSignal && hasProfitMarginBoundaryFollowupSignal(rawText);
|
||||
const profitMarginBusinessOverviewSignal =
|
||||
((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
||||
hasOrganizationLevelProfitMarginBoundaryOverviewSignal(rawText)) ||
|
||||
profitMarginFollowupBusinessOverviewSignal;
|
||||
const businessOverviewActionFamily = inventoryReserveBusinessOverviewSignal
|
||||
? "inventory_reserve_boundary"
|
||||
: debtDueDateBusinessOverviewSignal
|
||||
? "debt_due_date_boundary"
|
||||
: supplierQualityBusinessOverviewSignal
|
||||
? "vendor_risk_procurement_boundary"
|
||||
: profitMarginBusinessOverviewSignal
|
||||
? "profit_margin_boundary"
|
||||
: "broad_evaluation";
|
||||
const businessOverviewUnsupportedFamily = inventoryReserveBusinessOverviewSignal
|
||||
? "inventory_reserve_liquidation_boundary"
|
||||
: debtDueDateBusinessOverviewSignal
|
||||
? "debt_due_date_boundary"
|
||||
: supplierQualityBusinessOverviewSignal
|
||||
? "vendor_risk_procurement_boundary"
|
||||
: profitMarginBusinessOverviewSignal
|
||||
? "profit_margin_boundary"
|
||||
: "broad_business_evaluation";
|
||||
const businessOverviewSignal =
|
||||
rawBusinessOverviewSignal ||
|
||||
seededBusinessOverviewSignal;
|
||||
const businessOverviewSeparateCounterpartySignal = Boolean(
|
||||
businessOverviewSignal && hasBusinessOverviewSeparateCounterpartySignal(rawText)
|
||||
);
|
||||
|
|
@ -1630,7 +1727,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
: rawAssistantTurnMeaningOrganizationScope;
|
||||
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
|
||||
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
|
||||
const currentTurnFreshOrganizationScope = rawOrganizationScope ?? predecomposeEntities.organization;
|
||||
const currentTurnFreshOrganizationScope = predecomposeEntities.organization ?? rawOrganizationScope;
|
||||
const currentTurnOrganizationScope =
|
||||
currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope;
|
||||
const followupCounterpartyIsMetadataOrganizationScope = Boolean(
|
||||
|
|
@ -2029,9 +2126,21 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
const semanticDataNeed = metadataAmbiguityLaneClarificationApplicable
|
||||
? "metadata lane clarification"
|
||||
: semanticNeedFor({
|
||||
domain: businessOverviewSignal ? "business_overview" : rawDomain ?? seededDomain,
|
||||
action: businessOverviewSignal ? "broad_evaluation" : rawAction ?? seededAction,
|
||||
unsupported: businessOverviewSignal ? "broad_business_evaluation" : unsupported ?? seededUnsupported,
|
||||
domain: explicitVatMovementEvidenceSignal
|
||||
? "movements"
|
||||
: businessOverviewSignal
|
||||
? "business_overview"
|
||||
: rawDomain ?? seededDomain,
|
||||
action: explicitVatMovementEvidenceSignal
|
||||
? "list_movements"
|
||||
: businessOverviewSignal
|
||||
? businessOverviewActionFamily
|
||||
: rawAction ?? seededAction,
|
||||
unsupported: explicitVatMovementEvidenceSignal
|
||||
? "movement_evidence"
|
||||
: businessOverviewSignal
|
||||
? businessOverviewUnsupportedFamily
|
||||
: unsupported ?? seededUnsupported,
|
||||
lifecycleSignal,
|
||||
valueFlowSignal,
|
||||
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
|
||||
|
|
@ -2065,7 +2174,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
followupSeed.metadataSelectedEntitySet ??
|
||||
null;
|
||||
const metadataScopedLaneWithoutSubject = Boolean(
|
||||
(metadataGroundedMovementLaneApplicable || metadataGroundedDocumentLaneApplicable) &&
|
||||
(metadataGroundedMovementLaneApplicable ||
|
||||
metadataGroundedDocumentLaneApplicable ||
|
||||
explicitVatMovementEvidenceSignal) &&
|
||||
!effectiveFollowupCounterparty &&
|
||||
metadataLaneCarryoverAvailable
|
||||
);
|
||||
|
|
@ -2283,6 +2394,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
? "counterparty_lifecycle"
|
||||
: valueFlowSignal
|
||||
? "counterparty_value"
|
||||
: explicitVatMovementEvidenceSignal
|
||||
? "movements"
|
||||
: metadataGroundedMovementLaneApplicable
|
||||
? "movements"
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
|
|
@ -2293,7 +2406,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
? "metadata"
|
||||
: rawDomain ?? seededDomain,
|
||||
asked_action_family: businessOverviewSignal
|
||||
? "broad_evaluation"
|
||||
? businessOverviewActionFamily
|
||||
: lifecycleSignal
|
||||
? "activity_duration"
|
||||
: valueFlowSignal
|
||||
|
|
@ -2302,6 +2415,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
: payoutSignal
|
||||
? "payout"
|
||||
: rawAction ?? seededAction ?? "turnover"
|
||||
: explicitVatMovementEvidenceSignal
|
||||
? "list_movements"
|
||||
: metadataGroundedMovementLaneApplicable
|
||||
? "list_movements"
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
|
|
@ -2334,7 +2449,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
subject_resolution_optional: metadataScopedLaneWithoutSubject || undefined,
|
||||
unsupported_but_understood_family:
|
||||
businessOverviewSignal
|
||||
? "broad_business_evaluation"
|
||||
? businessOverviewUnsupportedFamily
|
||||
: unsupported ??
|
||||
(lifecycleSignal
|
||||
? "counterparty_lifecycle"
|
||||
|
|
@ -2348,6 +2463,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
? "movement_evidence"
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
? "document_evidence"
|
||||
: explicitVatMovementEvidenceSignal
|
||||
? "movement_evidence"
|
||||
: metadataAmbiguityLaneClarificationApplicable
|
||||
? "metadata_lane_choice_clarification"
|
||||
: entityResolutionSignal
|
||||
|
|
@ -2363,6 +2480,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
unsupported ||
|
||||
lifecycleSignal ||
|
||||
valueFlowSignal ||
|
||||
explicitVatMovementEvidenceSignal ||
|
||||
metadataGroundedMovementLaneApplicable ||
|
||||
metadataGroundedDocumentLaneApplicable ||
|
||||
metadataAmbiguityLaneClarificationApplicable ||
|
||||
|
|
@ -2423,13 +2541,17 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
const currentTurnValueFlowExactOverrideApplicable = Boolean(
|
||||
valueFlowSignal &&
|
||||
explicitIntentCandidate &&
|
||||
rawValueFlowAggregateQuestionSignal &&
|
||||
(rawValueFlowAggregateQuestionSignal || hasValueRankingSignal(rawText)) &&
|
||||
semanticDataNeed &&
|
||||
(entityCandidates.length > 0 || explicitOrganizationScope || openScopeValueFlowWithoutResolvedCounterparty)
|
||||
);
|
||||
|
||||
const runDiscovery = shouldRunDiscovery({
|
||||
unsupported: businessOverviewSignal ? "broad_business_evaluation" : unsupported ?? seededUnsupported,
|
||||
unsupported: explicitVatMovementEvidenceSignal
|
||||
? "movement_evidence"
|
||||
: businessOverviewSignal
|
||||
? "broad_business_evaluation"
|
||||
: unsupported ?? seededUnsupported,
|
||||
lifecycleSignal,
|
||||
valueFlowSignal,
|
||||
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
|
||||
|
|
@ -2446,6 +2568,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
groundedValueFlowFollowupApplicable,
|
||||
forceDiscoveryOverExplicitIntent:
|
||||
businessOverviewSignal ||
|
||||
explicitVatMovementEvidenceSignal ||
|
||||
Boolean(entityResolutionClarificationCandidate) ||
|
||||
organizationClarificationFollowupApplicable ||
|
||||
periodClarificationFollowupApplicable ||
|
||||
|
|
@ -2469,6 +2592,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
? "followup_context"
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
? "followup_context"
|
||||
: explicitVatMovementEvidenceSignal
|
||||
? "raw_text"
|
||||
: predecomposeContract
|
||||
? "predecompose_contract"
|
||||
: lifecycleSignal
|
||||
|
|
@ -2490,6 +2615,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
if (rawMetadataSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_metadata_signal_detected");
|
||||
}
|
||||
if (explicitVatMovementEvidenceSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_vat_movement_evidence_signal_detected");
|
||||
}
|
||||
if (entityResolutionSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_entity_resolution_signal_detected");
|
||||
}
|
||||
|
|
@ -2613,6 +2741,12 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
if (businessOverviewContinuationSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_from_followup_context");
|
||||
}
|
||||
if (profitMarginFollowupBusinessOverviewSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_business_overview_profit_margin_followup_boundary");
|
||||
}
|
||||
if (debtDueDateFollowupBusinessOverviewSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_business_overview_debt_due_date_followup_boundary");
|
||||
}
|
||||
if (explicitVatSuppressesBusinessOverviewContinuation) {
|
||||
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_suppressed_by_explicit_vat_question");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -395,6 +395,13 @@ export function createAssistantRoutePolicy(deps) {
|
|||
const hasTemporalCue = /(?:на\s+эту\s+же\s+дат[ауеы]|на\s+тот\s+же\s+период|за\s+этот\s+же\s+период|за\s+этот\s+период|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр|\b(?:19|20)\d{2}\b)/iu.test(normalized);
|
||||
return hasRequestCue && hasTemporalCue;
|
||||
}
|
||||
function hasOrganizationClarificationTextCue(text) {
|
||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return /(?<!\p{L})(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e)(?!\p{L})|(?:\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u043a\u043e\u043c\u043f\u0430\u043d|llc|company|organization)/iu.test(normalized);
|
||||
}
|
||||
function resolveAssistantOrchestrationDecision(input) {
|
||||
const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? "");
|
||||
const effectiveAddressUserMessage = String(input?.effectiveAddressUserMessage ?? rawUserMessage);
|
||||
|
|
@ -559,6 +566,27 @@ export function createAssistantRoutePolicy(deps) {
|
|||
const followupPreviousFilters = followupContext?.previous_filters && typeof followupContext.previous_filters === "object"
|
||||
? followupContext.previous_filters
|
||||
: null;
|
||||
const followupLoopStatus = toNonEmptyString(followupContext?.previous_discovery_loop_status);
|
||||
const followupLoopSelectedChainId = toNonEmptyString(followupContext?.previous_discovery_loop_selected_chain_id);
|
||||
const followupLoopPendingAxes = Array.isArray(followupContext?.previous_discovery_loop_pending_axes)
|
||||
? followupContext.previous_discovery_loop_pending_axes.map((item) => toNonEmptyString(item)).filter(Boolean)
|
||||
: [];
|
||||
const currentTurnPredecomposeOrganization = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.entities?.organization) ??
|
||||
(toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.semantics?.anchor_kind) === "organization"
|
||||
? toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.semantics?.anchor_value)
|
||||
: null);
|
||||
const routeCandidateOrganizationClarificationDetected = Boolean(followupContext &&
|
||||
followupLoopStatus === "awaiting_clarification" &&
|
||||
followupLoopSelectedChainId &&
|
||||
followupLoopPendingAxes.includes("organization") &&
|
||||
(currentTurnPredecomposeOrganization ||
|
||||
explicitOrganizationClarificationSelection ||
|
||||
[
|
||||
rawUserMessage,
|
||||
repairedRawUserMessage,
|
||||
effectiveAddressUserMessage,
|
||||
repairedEffectiveAddressUserMessage
|
||||
].some((message) => hasOrganizationClarificationTextCue(message))));
|
||||
const protectedInventoryShortFollowup = Boolean(followupContext &&
|
||||
(isInventorySelectedObjectIntent(followupPreviousIntent) ||
|
||||
(followupPreviousIntent === "inventory_on_hand_as_of_date" &&
|
||||
|
|
@ -608,6 +636,8 @@ export function createAssistantRoutePolicy(deps) {
|
|||
"net_value_flow"
|
||||
].includes(String(toNonEmptyString(assistantTurnMeaning?.asked_action_family) ?? "")) ||
|
||||
/(?:нетто|сальдо|сколько\s+мы\s+(?:получили|заплатили)|incoming|outgoing)/iu.test(analyticsSample)));
|
||||
const effectiveGroundedValueFlowFollowupContextDetected =
|
||||
groundedValueFlowFollowupContextDetected || routeCandidateOrganizationClarificationDetected;
|
||||
const baseToolGatePreservesAddressLane = Boolean(baseToolGate?.runAddressLane &&
|
||||
[
|
||||
"address_intent_resolver_detected",
|
||||
|
|
@ -617,14 +647,15 @@ export function createAssistantRoutePolicy(deps) {
|
|||
].includes(String(baseToolGate?.reason ?? ""))) ||
|
||||
Boolean(baseToolGate?.runAddressLane &&
|
||||
String(baseToolGate?.reason ?? "") === "followup_context_detected" &&
|
||||
groundedValueFlowFollowupContextDetected);
|
||||
effectiveGroundedValueFlowFollowupContextDetected);
|
||||
const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate &&
|
||||
deterministicNonDomainGuard &&
|
||||
(llmFirstUnsupportedCandidate || llmContractMode === null) &&
|
||||
!baseToolGatePreservesAddressLane &&
|
||||
!groundedValueFlowFollowupContextDetected &&
|
||||
!effectiveGroundedValueFlowFollowupContextDetected &&
|
||||
!protectedInventoryShortFollowup &&
|
||||
!organizationClarificationContinuationDetected);
|
||||
!organizationClarificationContinuationDetected &&
|
||||
!routeCandidateOrganizationClarificationDetected);
|
||||
const lastAddressAssistantDebug = sessionItems
|
||||
? findLastAddressAssistantItem(sessionItems)?.debug ?? null
|
||||
: null;
|
||||
|
|
@ -668,7 +699,7 @@ export function createAssistantRoutePolicy(deps) {
|
|||
!turnMeaningIntentCandidate &&
|
||||
!dataScopeMetaQuery &&
|
||||
!dangerOrCoercionSignal &&
|
||||
!groundedValueFlowFollowupContextDetected &&
|
||||
!effectiveGroundedValueFlowFollowupContextDetected &&
|
||||
!organizationClarificationContinuationDetected);
|
||||
const hardMetaMode = resolveHardMetaMode({
|
||||
dataScopeMetaQuery,
|
||||
|
|
@ -834,7 +865,7 @@ export function createAssistantRoutePolicy(deps) {
|
|||
!dataScopeMetaQuery &&
|
||||
!capabilityMetaQuery &&
|
||||
!dangerOrCoercionSignal &&
|
||||
!groundedValueFlowFollowupContextDetected &&
|
||||
!effectiveGroundedValueFlowFollowupContextDetected &&
|
||||
!organizationClarificationContinuationDetected);
|
||||
if (unsupportedCurrentTurnMeaningBoundary) {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -2079,6 +2079,9 @@ function isAddressLaneDebugPayload(debug) {
|
|||
if (typeof debug.mcp_call_status === "string" && debug.mcp_call_status.trim().length > 0) {
|
||||
return true;
|
||||
}
|
||||
if (debug.mcp_discovery_response_applied === true && debug.assistant_mcp_discovery_entry_point_v1) {
|
||||
return true;
|
||||
}
|
||||
if (typeof debug.anchor_type === "string" && debug.anchor_type.trim().length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -208,6 +208,34 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
);
|
||||
}
|
||||
|
||||
function hasBusinessOverviewBoundaryFollowupCue(text) {
|
||||
const normalized = normalizeFollowupText(text);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const hasBoundaryCue =
|
||||
/(?:\u043f\u0440\u043e\u0441\u0440\u043e\u0447|\u0441\u0440\u043e\u043a\w*\s+\u043e\u043f\u043b\u0430\u0442|\u0434\u043e\u043a\u0430\u0437|\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434|\u043f\u0440\u0438\u0431\u044b\u043b|\u0443\u0431\u044b\u0442|\u043c\u0430\u0440\u0436|\u0440\u0435\u0437\u0435\u0440\u0432|\u043d\u0435\u043b\u0438\u043a\u0432\u0438\u0434|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0432\u0435\u043d\u0434\u043e\u0440|due[-\s]?date|overdue|aging|profit|loss|margin|vendor|risk)/iu.test(
|
||||
normalized
|
||||
);
|
||||
const hasFollowupShape =
|
||||
/(?:\u0442\u043e\s+\u0435\u0441\u0442\u044c|\u043f\u043e\u0447\u0435\u043c\u0443|\u043a\u043e\u0440\u043e\u0442\u043a|\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|\u0438\u0442\u043e\u0433|why|short|brief|so)/iu.test(
|
||||
normalized
|
||||
);
|
||||
return hasBoundaryCue && hasFollowupShape;
|
||||
}
|
||||
|
||||
function hasOrganizationClarificationTextCue(text) {
|
||||
const normalized = deps.compactWhitespace(
|
||||
deps.repairAddressMojibake(String(text ?? "")).toLowerCase()
|
||||
);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return /(?<!\p{L})(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e)(?!\p{L})|(?:\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u043a\u043e\u043c\u043f\u0430\u043d|llc|company|organization)/iu.test(
|
||||
normalized
|
||||
);
|
||||
}
|
||||
|
||||
function parseDmyDateToIso(value) {
|
||||
const match = String(value ?? "").trim().match(/^(\d{2})\.(\d{2})\.(\d{4})$/);
|
||||
if (!match) {
|
||||
|
|
@ -584,7 +612,12 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
(deps.toNonEmptyString(alternateMessage)
|
||||
? deps.hasDataRetrievalRequestSignal(String(alternateMessage ?? ""))
|
||||
: false);
|
||||
if (rawCapabilityMetaQuery && !rawDataRetrievalSignal) {
|
||||
const rawBusinessOverviewBoundaryFollowupCue =
|
||||
hasBusinessOverviewBoundaryFollowupCue(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
? hasBusinessOverviewBoundaryFollowupCue(String(alternateMessage ?? ""))
|
||||
: false);
|
||||
if (rawCapabilityMetaQuery && !rawDataRetrievalSignal && !rawBusinessOverviewBoundaryFollowupCue) {
|
||||
return null;
|
||||
}
|
||||
const assistantTurnMeaning =
|
||||
|
|
@ -660,11 +693,42 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const sourceDiscoveryLoopStatusHint = readAssistantMcpDiscoveryLoopStatus(
|
||||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const sourceDiscoveryLoopSelectedChainIdHint = readAssistantMcpDiscoveryLoopSelectedChainId(
|
||||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const sourceDiscoveryLoopPendingAxesHint = readAssistantMcpDiscoveryLoopPendingAxes(
|
||||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const sourceDiscoveryLoopProvidedAxesHint = readAssistantMcpDiscoveryLoopProvidedAxes(
|
||||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const currentTurnPredecomposeOrganization =
|
||||
deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.entities?.organization) ??
|
||||
(deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.semantics?.anchor_kind) === "organization"
|
||||
? deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.semantics?.anchor_value)
|
||||
: null);
|
||||
const mcpDiscoveryOrganizationClarificationContinuation = Boolean(
|
||||
sourceDiscoveryLoopStatusHint === "awaiting_clarification" &&
|
||||
sourceDiscoveryLoopSelectedChainIdHint &&
|
||||
sourceDiscoveryLoopPendingAxesHint.includes("organization") &&
|
||||
(currentTurnPredecomposeOrganization ||
|
||||
explicitOrganizationClarificationSelection ||
|
||||
[userMessage, alternateMessage].some((message) => hasOrganizationClarificationTextCue(message)))
|
||||
);
|
||||
const hasValueFlowCarryoverSourceHint =
|
||||
sourceIntentHint === "customer_revenue_and_payments" ||
|
||||
sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" ||
|
||||
sourceDiscoveryPilotScopeHint === "counterparty_supplier_payout_query_movements_v1" ||
|
||||
sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1";
|
||||
const hasBusinessOverviewCarryoverSourceHint =
|
||||
sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
|
||||
const navigationSessionState = resolveNavigationSessionContextState(
|
||||
addressNavigationState,
|
||||
deps.toNonEmptyString,
|
||||
|
|
@ -706,21 +770,31 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
|
||||
? hasShortValueFlowRetargetCue(String(alternateMessage ?? ""))
|
||||
: false;
|
||||
const businessOverviewBoundaryFollowupPrimary =
|
||||
hasBusinessOverviewCarryoverSourceHint && hasBusinessOverviewBoundaryFollowupCue(userMessage);
|
||||
const businessOverviewBoundaryFollowupAlternate =
|
||||
hasBusinessOverviewCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
|
||||
? hasBusinessOverviewBoundaryFollowupCue(String(alternateMessage ?? ""))
|
||||
: false;
|
||||
const explicitSummaryBundleReuseSignal = hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage);
|
||||
let hasPrimaryFollowupSignal =
|
||||
deps.hasAddressFollowupContextSignal(userMessage) ||
|
||||
Boolean(debtRoleSwapPrimary) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
explicitSummaryBundleReuseSignal;
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation;
|
||||
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||
Boolean(debtRoleSwapAlternate) ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
explicitSummaryBundleReuseSignal
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation
|
||||
: false;
|
||||
const hasPrimaryIndexReferenceSignal = deps.extractDisplayedEntityIndexMention(userMessage) !== null;
|
||||
const hasAlternateIndexReferenceSignal = deps.toNonEmptyString(alternateMessage)
|
||||
|
|
@ -760,6 +834,7 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
hasPrimaryIndexReferenceSignal ||
|
||||
hasAlternateIndexReferenceSignal ||
|
||||
hasOrganizationClarificationContinuation ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation ||
|
||||
hasImplicitContinuationSignal ||
|
||||
hasSuggestedIntentPivotSignal ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
|
|
@ -773,6 +848,8 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
Boolean(debtRoleSwapIntent) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
|
|
@ -783,6 +860,7 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
hasPrimaryIndexReferenceSignal ||
|
||||
hasAlternateIndexReferenceSignal ||
|
||||
hasOrganizationClarificationContinuation ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
hasInventoryRootTemporalFollowupPrimary ||
|
||||
|
|
@ -794,6 +872,8 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
Boolean(debtRoleSwapIntent) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
|
|
@ -826,6 +906,7 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
!hasImplicitContinuationSignal &&
|
||||
!hasSuggestedIntentPivotSignal &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
!mcpDiscoveryOrganizationClarificationContinuation &&
|
||||
!hasIndexReferenceSignal &&
|
||||
!explicitSummaryBundleReuseSignal
|
||||
) {
|
||||
|
|
@ -843,6 +924,7 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
!hasImplicitContinuationSignal &&
|
||||
!hasSuggestedIntentPivotSignal &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
!mcpDiscoveryOrganizationClarificationContinuation &&
|
||||
!hasIndexReferenceSignal &&
|
||||
!explicitSummaryBundleReuseSignal
|
||||
) {
|
||||
|
|
@ -884,19 +966,10 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const sourceDiscoveryLoopStatus = readAssistantMcpDiscoveryLoopStatus(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopSelectedChainId = readAssistantMcpDiscoveryLoopSelectedChainId(
|
||||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const sourceDiscoveryLoopPendingAxes = readAssistantMcpDiscoveryLoopPendingAxes(
|
||||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const sourceDiscoveryLoopProvidedAxes = readAssistantMcpDiscoveryLoopProvidedAxes(
|
||||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const sourceDiscoveryLoopStatus = sourceDiscoveryLoopStatusHint;
|
||||
const sourceDiscoveryLoopSelectedChainId = sourceDiscoveryLoopSelectedChainIdHint;
|
||||
const sourceDiscoveryLoopPendingAxes = sourceDiscoveryLoopPendingAxesHint;
|
||||
const sourceDiscoveryLoopProvidedAxes = sourceDiscoveryLoopProvidedAxesHint;
|
||||
const sourceDiscoveryLoopAskedDomainFamily = readAssistantMcpDiscoveryLoopAskedDomainFamily(
|
||||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
|
|
@ -959,6 +1032,7 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
explicitIntentFamily &&
|
||||
sourceIntentFamily !== explicitIntentFamily &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
!mcpDiscoveryOrganizationClarificationContinuation &&
|
||||
!hasImplicitContinuationSignal &&
|
||||
!hasIndexReferenceSignal &&
|
||||
!hasInventoryRootTemporalFollowupPrimary &&
|
||||
|
|
@ -967,6 +1041,8 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
!hasInventoryRootRestatementAlternate &&
|
||||
!inventoryShortFollowupPrimary &&
|
||||
!inventoryShortFollowupAlternate &&
|
||||
!businessOverviewBoundaryFollowupPrimary &&
|
||||
!businessOverviewBoundaryFollowupAlternate &&
|
||||
!foreignAccountingPivotOverInventory &&
|
||||
!deps.hasFollowupMarker(userMessage) &&
|
||||
!deps.hasReferentialPointer(userMessage) &&
|
||||
|
|
@ -1027,24 +1103,29 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
hasSuggestedIntentPivotSignal ||
|
||||
Boolean(debtRoleSwapPrimary) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
hasInventoryRootTemporalFollowupPrimary;
|
||||
hasInventoryRootTemporalFollowupPrimary ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation;
|
||||
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||
hasSuggestedIntentPivotSignal ||
|
||||
Boolean(debtRoleSwapAlternate) ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
hasInventoryRootTemporalFollowupAlternate
|
||||
hasInventoryRootTemporalFollowupAlternate ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation
|
||||
: false;
|
||||
hasStrongFollowupReference =
|
||||
hasPrimaryIndexReferenceSignal ||
|
||||
hasAlternateIndexReferenceSignal ||
|
||||
hasOrganizationClarificationContinuation ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation ||
|
||||
hasSuggestedIntentPivotSignal ||
|
||||
hasImplicitContinuationSignal ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
|
|
@ -1056,6 +1137,8 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
Boolean(debtRoleSwapIntent) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
export type CounterpartyRoleHint = "ordinary_counterparty" | "bank_or_financial_institution";
|
||||
|
||||
const FINANCIAL_INSTITUTION_PATTERNS: RegExp[] = [
|
||||
/(?:^|[\s"«(,-])банк(?:$|[\s"»),.-])/u,
|
||||
/сбербанк/u,
|
||||
/(?:^|[\s"«(,-])сбер(?:$|[\s"»),.-])/u,
|
||||
/(?:^|[\s"«(,-])втб(?:$|[\s"»),.-])/u,
|
||||
/альфа[\s-]*банк/u,
|
||||
/тинькофф/u,
|
||||
/(?:^|[\s"«(,-])т[\s-]*банк(?:$|[\s"»),.-])/u,
|
||||
/газпромбанк/u,
|
||||
/росбанк/u,
|
||||
/райффайзен/u,
|
||||
/совкомбанк/u,
|
||||
/промсвязьбанк/u,
|
||||
/(?:^|[\s"«(,-])псб(?:$|[\s"»),.-])/u,
|
||||
/(?:^|[\s"«(,-])мкб(?:$|[\s"»),.-])/u,
|
||||
/ак[\s-]*барс/u,
|
||||
/уралсиб/u,
|
||||
/юникредит/u,
|
||||
/почта[\s-]*банк/u,
|
||||
/(?:^|[\s"«(,-])открытие(?:$|[\s"»),.-])/u,
|
||||
/кредитн(?:ая|ый|ое|ые)\s+организац/u
|
||||
];
|
||||
|
||||
function normalizeCounterpartyRoleText(value: unknown): string {
|
||||
return String(value ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/ё/g, "е")
|
||||
.replace(/[._]+/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function isLikelyFinancialInstitutionCounterparty(value: unknown): boolean {
|
||||
const normalized = normalizeCounterpartyRoleText(value);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return FINANCIAL_INSTITUTION_PATTERNS.some((pattern) => pattern.test(normalized));
|
||||
}
|
||||
|
||||
export function counterpartyRoleHintForName(value: unknown): CounterpartyRoleHint {
|
||||
return isLikelyFinancialInstitutionCounterparty(value)
|
||||
? "bank_or_financial_institution"
|
||||
: "ordinary_counterparty";
|
||||
}
|
||||
|
|
@ -18,6 +18,8 @@ export type AddressIntent =
|
|||
| "vat_payable_forecast"
|
||||
| "vat_liability_confirmed_for_tax_period"
|
||||
| "vat_payable_confirmed_as_of_date"
|
||||
| "accounting_financial_result_for_organization"
|
||||
| "debt_due_date_aging_for_organization"
|
||||
| "open_contracts_confirmed_as_of_date"
|
||||
| "list_contracts_by_counterparty"
|
||||
| "list_open_contracts"
|
||||
|
|
@ -189,6 +191,8 @@ export interface AddressRecipeDefinition {
|
|||
| "vat_payable_forecast_profile"
|
||||
| "vat_liability_confirmed_tax_period_profile"
|
||||
| "vat_payable_confirmed_as_of_balance_profile"
|
||||
| "accounting_financial_result_profile"
|
||||
| "debt_due_date_aging_profile"
|
||||
| "open_contracts_confirmed_as_of_balance_profile"
|
||||
| "payables_confirmed_as_of_balance_profile"
|
||||
| "receivables_confirmed_as_of_balance_profile"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,24 @@ describe("addressIntentResolver regression bridges", () => {
|
|||
expect(result.intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||
});
|
||||
|
||||
it("detects VAT movement inspection wording with an explicit year", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"\u043f\u043e\u043a\u0430\u0436\u0438 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f \u043f\u043e \u041d\u0414\u0421 \u0437\u0430 2020 \u043f\u043e \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
|
||||
);
|
||||
|
||||
expect(result.intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||
expect(result.reasons).toContain("vat_period_inspection_bridge_signal_detected");
|
||||
});
|
||||
|
||||
it("detects canonical VAT charged-or-paid wording with an explicit year", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"\u041a\u0430\u043a\u043e\u0439 \u041d\u0414\u0421 \u0431\u044b\u043b \u043d\u0430\u0447\u0438\u0441\u043b\u0435\u043d \u0438\u043b\u0438 \u0443\u043f\u043b\u0430\u0447\u0435\u043d \u0432 2020 \u0433\u043e\u0434\u0443 \u043f\u043e \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438 \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441?"
|
||||
);
|
||||
|
||||
expect(result.intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||
expect(result.reasons).toContain("vat_liability_explicit_period_bridge_signal_detected");
|
||||
});
|
||||
|
||||
it("detects payables snapshot wording in plain human form", () => {
|
||||
const result = resolveAddressIntent("мы должны комуто денег на сегодня?");
|
||||
|
||||
|
|
|
|||
|
|
@ -1336,9 +1336,9 @@ describe("address compose stage utf8 headers", () => {
|
|||
{ userMessage: "Сколько у нас заказчиков, поставщиков и смешанных контрагентов?" }
|
||||
);
|
||||
|
||||
expect(reply.text).toContain("Роли контрагентов по активности:");
|
||||
expect(reply.text).toContain("Заказчики (только customer-роль): 122.");
|
||||
expect(reply.text).toContain("Поставщики (только supplier-роль): 71.");
|
||||
expect(reply.text).toContain("Распределение ролей по активности:");
|
||||
expect(reply.text).toContain("Заказчики с ролью покупателя: 122.");
|
||||
expect(reply.text).toContain("Поставщики с ролью поставщика: 71.");
|
||||
expect(reply.text).toContain("Смешанные (и покупатель, и поставщик): 23.");
|
||||
expect(reply.text).not.toContain("Всего уникальных контрагентов в базе");
|
||||
});
|
||||
|
|
@ -1391,7 +1391,7 @@ describe("address compose stage utf8 headers", () => {
|
|||
{ userMessage: "скока поставщиков в базе" }
|
||||
);
|
||||
|
||||
expect(reply.text).toContain("Поставщиков (только supplier-роль): 71.");
|
||||
expect(reply.text).toContain("Поставщиков с ролью поставщика: 71.");
|
||||
expect(reply.text).not.toContain("Роли контрагентов по активности:");
|
||||
expect(reply.text).not.toContain("Всего уникальных контрагентов в базе");
|
||||
});
|
||||
|
|
@ -1444,7 +1444,7 @@ describe("address compose stage utf8 headers", () => {
|
|||
{ userMessage: "скок клиентов" }
|
||||
);
|
||||
|
||||
expect(reply.text).toContain("Заказчиков (только customer-роль): 122.");
|
||||
expect(reply.text).toContain("Заказчиков с ролью покупателя: 122.");
|
||||
expect(reply.text).not.toContain("Роли контрагентов по активности:");
|
||||
expect(reply.text).not.toContain("Всего уникальных контрагентов в базе");
|
||||
});
|
||||
|
|
@ -1711,7 +1711,7 @@ describe("address compose stage utf8 headers", () => {
|
|||
|
||||
expect(reply.responseType).toBe("FACTUAL_LIST");
|
||||
expect(reply.text).toContain("по максимальной сумме одной входящей операции");
|
||||
expect(reply.text).toContain("1. Клиент Б | max single: 1200");
|
||||
expect(reply.text).toContain("1. Клиент Б | максимальная разовая сумма: 1.200,00 ₽");
|
||||
});
|
||||
|
||||
it("renders supplier payout list by operations count", () => {
|
||||
|
|
@ -2033,7 +2033,7 @@ describe("address compose stage utf8 headers", () => {
|
|||
);
|
||||
|
||||
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
||||
expect(reply.text).toContain("Покрытие VAT-источников через MCP");
|
||||
expect(reply.text).toContain("Покрытие VAT-источников в 1С");
|
||||
expect(reply.text).toContain("Найдено VAT-объектов: 5");
|
||||
expect(reply.text).toContain("РегистрНакопления.НДСПродажи");
|
||||
});
|
||||
|
|
@ -2063,12 +2063,14 @@ describe("address compose stage utf8 headers", () => {
|
|||
userMessage: "сколько платить ндс в налоговую за декабрь 2019",
|
||||
periodFrom: "2019-10-01",
|
||||
periodTo: "2019-12-31",
|
||||
organizationHint: "ООО Альтернатива Плюс",
|
||||
useRubCurrency: true
|
||||
}
|
||||
);
|
||||
|
||||
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
||||
expect(reply.text).toContain("Коротко: подтвержденный НДС к уплате за налоговый период");
|
||||
expect(reply.text).toContain("Коротко: подтвержденный НДС к уплате за налоговый период по организации ООО Альтернатива Плюс");
|
||||
expect(reply.text).toContain("- Организация: ООО Альтернатива Плюс.");
|
||||
expect(reply.text).toContain("50.000,00 ₽");
|
||||
expect(reply.semantics?.result_mode).toBe("confirmed_balance");
|
||||
expect(reply.semantics?.balance_confirmed).toBe(true);
|
||||
|
|
@ -2214,7 +2216,7 @@ describe("address compose stage utf8 headers", () => {
|
|||
);
|
||||
|
||||
expect(reply.responseType).toBe("FACTUAL_LIST");
|
||||
expect(reply.text).toContain("Блок 2.1. MCP-проверка VAT-источников");
|
||||
expect(reply.text).toContain("Блок 2.1. Проверка VAT-источников в 1С");
|
||||
expect(reply.text).toContain("VAT-объектов в метаданных 1С: 3");
|
||||
expect(reply.text).toContain("Источников с движениями до даты среза: 1");
|
||||
expect(reply.text).toContain("РегистрНакопления.НДСНачисленный");
|
||||
|
|
@ -2247,7 +2249,7 @@ describe("address compose stage utf8 headers", () => {
|
|||
);
|
||||
|
||||
expect(reply.responseType).toBe("FACTUAL_LIST");
|
||||
expect(reply.text).toContain("Probe VAT-источников завершился ошибкой");
|
||||
expect(reply.text).toContain("Дополнительная проверка VAT-источников завершилась ошибкой");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -5099,13 +5101,13 @@ describe("address recipe catalog counterparty filtering", () => {
|
|||
expect(aging.extracted_filters.limit).toBeUndefined();
|
||||
});
|
||||
|
||||
it("selects customer value recipe and keeps top-20 default", () => {
|
||||
it("selects customer value recipe and keeps expanded top-200 default", () => {
|
||||
const selected = selectAddressRecipe("customer_revenue_and_payments", {});
|
||||
expect(selected.selected_recipe).toBeTruthy();
|
||||
const plan = buildAddressRecipePlan(selected.selected_recipe!, {});
|
||||
|
||||
expect(plan.recipe.recipe_id).toBe("address_customer_revenue_and_payments_v1");
|
||||
expect(plan.limit).toBe(20);
|
||||
expect(plan.limit).toBe(200);
|
||||
expect(plan.query).toContain("ПоступлениеНаРасчетныйСчет");
|
||||
expect(plan.query).toContain("БанкПоступление.ДоговорКонтрагента");
|
||||
});
|
||||
|
|
@ -5138,13 +5140,13 @@ describe("address recipe catalog counterparty filtering", () => {
|
|||
expect(plan.limit).toBe(1000);
|
||||
});
|
||||
|
||||
it("selects supplier payouts recipe and keeps top-20 default", () => {
|
||||
it("selects supplier payouts recipe and keeps expanded top-200 default", () => {
|
||||
const selected = selectAddressRecipe("supplier_payouts_profile", {});
|
||||
expect(selected.selected_recipe).toBeTruthy();
|
||||
const plan = buildAddressRecipePlan(selected.selected_recipe!, {});
|
||||
|
||||
expect(plan.recipe.recipe_id).toBe("address_supplier_payouts_profile_v1");
|
||||
expect(plan.limit).toBe(20);
|
||||
expect(plan.limit).toBe(200);
|
||||
expect(plan.query).toContain("СписаниеСРасчетногоСчета");
|
||||
expect(plan.query).toContain("БанкСписание.ДоговорКонтрагента");
|
||||
});
|
||||
|
|
@ -5343,6 +5345,26 @@ describe("address recipe catalog counterparty filtering", () => {
|
|||
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Остатки.Счет.Код, \"\"), 1, 2) = \"60\"");
|
||||
});
|
||||
|
||||
it("builds debt due-date aging query without carrying noisy organization suffix", () => {
|
||||
const selected = selectAddressRecipe("debt_due_date_aging_for_organization", {
|
||||
as_of_date: "2020-12-31",
|
||||
organization: "ООО Альтернатива Плюс на конец 2020 можно точно понять"
|
||||
});
|
||||
expect(selected.selected_recipe?.recipe_id).toBe("address_debt_due_date_aging_for_organization_v1");
|
||||
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
|
||||
as_of_date: "2020-12-31",
|
||||
organization: "ООО Альтернатива Плюс на конец 2020 можно точно понять"
|
||||
});
|
||||
|
||||
expect(plan.query).toContain("УстановленСрокОплаты");
|
||||
expect(plan.query).toContain('Наименование ПОДОБНО "%Альтернатива%"');
|
||||
expect(plan.query).toContain('Наименование ПОДОБНО "%Плюс%"');
|
||||
expect(plan.query).not.toContain('Наименование ПОДОБНО "%конец%"');
|
||||
expect(plan.query).not.toContain('Наименование ПОДОБНО "%2020%"');
|
||||
expect(plan.query).not.toContain('Наименование ПОДОБНО "%можно%"');
|
||||
expect(plan.query).not.toContain('Наименование ПОДОБНО "%понять%"');
|
||||
});
|
||||
|
||||
it("injects account condition into movements query for account snapshot", () => {
|
||||
const filters = extractAddressFilters(
|
||||
"Какой остаток по счету 60 на дату 2020-07-31",
|
||||
|
|
|
|||
|
|
@ -180,6 +180,76 @@ describe("assistant MCP discovery answer adapter", () => {
|
|||
expect(draft.must_not_claim).toContain("Do not present the confirmed movement rows as a complete movement universe.");
|
||||
});
|
||||
|
||||
it("does not present bank-like ranked value-flow leaders as ordinary customers", () => {
|
||||
const draft = buildAssistantMcpDiscoveryAnswerDraft({
|
||||
pilot_status: "executed",
|
||||
pilot_scope: "counterparty_ranked_value_flow_query_movements_v1",
|
||||
dry_run: false,
|
||||
mcp_execution_performed: true,
|
||||
executed_primitives: ["query_movements"],
|
||||
skipped_primitives: [],
|
||||
probe_results: [],
|
||||
evidence: {
|
||||
evidence_status: "confirmed",
|
||||
answer_permission: "confirmed_answer",
|
||||
confirmed_facts: [],
|
||||
inferred_facts: [],
|
||||
unknown_facts: [],
|
||||
query_limitations: [],
|
||||
reason_codes: [],
|
||||
query_plan: {}
|
||||
},
|
||||
source_rows_summary: "2 MCP movement rows fetched, 2 matched ranking scope",
|
||||
derived_metadata_surface: null,
|
||||
derived_entity_resolution: null,
|
||||
derived_activity_period: null,
|
||||
derived_value_flow: null,
|
||||
derived_bidirectional_value_flow: null,
|
||||
derived_business_overview: null,
|
||||
derived_ranked_value_flow: {
|
||||
organization_scope: "ООО Альтернатива Плюс",
|
||||
period_scope: "2020",
|
||||
value_flow_direction: "incoming_customer_revenue",
|
||||
ranking_need: "top_desc",
|
||||
aggregation_axis: "counterparty",
|
||||
coverage_limited_by_probe_limit: false,
|
||||
ranked_values: [
|
||||
{
|
||||
axis_value: "СБЕРБАНК, ПАО",
|
||||
total_amount: 12792194.31,
|
||||
total_amount_human_ru: "12 792 194,31 руб.",
|
||||
rows_with_amount: 3,
|
||||
rows_matched: 3,
|
||||
first_movement_date: "2020-01-15",
|
||||
latest_movement_date: "2020-12-20",
|
||||
counterparty_role_hint: "bank_or_financial_institution"
|
||||
},
|
||||
{
|
||||
axis_value: "Группа СВК",
|
||||
total_amount: 12093465,
|
||||
total_amount_human_ru: "12 093 465 руб.",
|
||||
rows_with_amount: 2,
|
||||
rows_matched: 2,
|
||||
first_movement_date: "2020-02-15",
|
||||
latest_movement_date: "2020-11-10",
|
||||
counterparty_role_hint: "ordinary_counterparty"
|
||||
}
|
||||
],
|
||||
inference_basis: "confirmed_1c_movement_rows_grouped_by_counterparty"
|
||||
},
|
||||
query_limitations: [],
|
||||
reason_codes: ["pilot_derived_ranked_value_flow_from_confirmed_rows"]
|
||||
} as any);
|
||||
|
||||
const confirmed = draft.confirmed_lines.join("\n");
|
||||
expect(confirmed).toContain("Крупнейший входящий денежный источник СБЕРБАНК, ПАО");
|
||||
expect(confirmed).toContain("не называю это клиентской выручкой");
|
||||
expect(confirmed).not.toContain("Больше всего денег принёс контрагент СБЕРБАНК");
|
||||
expect(draft.must_not_claim).toContain(
|
||||
"Do not present bank-like counterparties as ordinary customers, suppliers, revenue, procurement dependency, or business quality evidence without payment-purpose/contract proof."
|
||||
);
|
||||
});
|
||||
|
||||
it("turns business overview multi-probe evidence into an analyst-safe draft", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
dataNeedGraph: {
|
||||
|
|
@ -355,6 +425,7 @@ describe("assistant MCP discovery answer adapter", () => {
|
|||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{
|
||||
rows: [
|
||||
{ Период: "2020-01-15T00:00:00", Регистратор: "Поступление 1" },
|
||||
|
|
@ -429,6 +500,7 @@ describe("assistant MCP discovery answer adapter", () => {
|
|||
{ rows: [{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" }] },
|
||||
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Поставщик А" }] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-12-31T00:00:00", Amount: 100000, Counterparty: "Клиент А" }
|
||||
|
|
@ -528,6 +600,7 @@ describe("assistant MCP discovery answer adapter", () => {
|
|||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-12-31T00:00:00", Amount: 250000, Quantity: 10, Item: "Товар А" },
|
||||
|
|
@ -576,6 +649,73 @@ describe("assistant MCP discovery answer adapter", () => {
|
|||
expect(draft.must_not_claim).toContain("Do not present business overview inventory staleness risk proxy as confirmed obsolete stock, reserve, write-off, or liquidation value.");
|
||||
});
|
||||
|
||||
it("answers inventory reserve boundary questions direct-first before broad overview detail", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
dataNeedGraph: {
|
||||
schema_version: "assistant_data_need_graph_v1",
|
||||
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||
subject_candidates: [],
|
||||
business_fact_family: "business_overview",
|
||||
action_family: "inventory_reserve_boundary",
|
||||
aggregation_need: null,
|
||||
time_scope_need: "explicit_period",
|
||||
comparison_need: null,
|
||||
ranking_need: null,
|
||||
proof_expectation: "bounded_inference",
|
||||
clarification_gaps: [],
|
||||
decomposition_candidates: ["collect_scoped_movements", "probe_coverage", "explain_evidence_basis"],
|
||||
forbidden_overclaim_flags: ["no_raw_model_claims", "no_profit_or_margin_claim_without_evidence"],
|
||||
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
|
||||
},
|
||||
turnMeaning: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "inventory_reserve_boundary",
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "inventory_reserve_liquidation_boundary"
|
||||
}
|
||||
});
|
||||
const pilot = await executeAssistantMcpDiscoveryPilot(
|
||||
planner,
|
||||
buildSequentialDeps([
|
||||
{ rows: [{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" }] },
|
||||
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Поставщик А" }] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-12-31T00:00:00", Amount: 250000, Quantity: 10, Item: "Товар А" }
|
||||
]
|
||||
},
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-01-10T00:00:00", Amount: 200000, Quantity: 8, Item: "Товар А" }
|
||||
]
|
||||
},
|
||||
{ rows: [{ Period: "2020-01-15T00:00:00", Registrator: "Поступление 1" }] },
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-03-01T00:00:00", Amount: 600000, Item: "Товар А", Counterparty: "Клиент А", AccountKt: "41.01" },
|
||||
{ Period: "2020-02-01T00:00:00", Amount: 240000, Item: "Товар А", Counterparty: "Поставщик А", AccountDt: "41.01" }
|
||||
]
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
|
||||
expect(draft.headline).toContain(
|
||||
"\u0442\u043e\u0447\u043d\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u0440\u0435\u0437\u0435\u0440\u0432"
|
||||
);
|
||||
expect(draft.headline).toContain("\u043d\u0435\u043b\u044c\u0437\u044f");
|
||||
expect(draft.headline).toContain("staleness-risk proxy");
|
||||
expect(draft.headline).not.toContain("бизнес-обзор");
|
||||
expect(draft.must_not_claim).toContain("Do not present business overview inventory staleness risk proxy as confirmed obsolete stock, reserve, write-off, or liquidation value.");
|
||||
});
|
||||
|
||||
it("renders metadata-scoped movement all-time follow-up as an all-time bounded answer", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
dataNeedGraph: {
|
||||
|
|
@ -796,7 +936,9 @@ describe("assistant MCP discovery answer adapter", () => {
|
|||
|
||||
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
|
||||
expect(draft.confirmed_lines).toHaveLength(1);
|
||||
expect(userText).toContain("\u0411\u043e\u043b\u044c\u0448\u0435 \u0432\u0441\u0435\u0433\u043e \u0434\u0435\u043d\u0435\u0433 \u043f\u0440\u0438\u043d\u0451\u0441 \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442");
|
||||
expect(userText).toContain("Крупнейший входящий денежный источник");
|
||||
expect(userText).toContain("не называю это клиентской выручкой");
|
||||
expect(userText).not.toContain("\u0411\u043e\u043b\u044c\u0448\u0435 \u0432\u0441\u0435\u0433\u043e \u0434\u0435\u043d\u0435\u0433 \u043f\u0440\u0438\u043d\u0451\u0441 \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442 \u0421\u0411\u0415\u0420\u0411\u0410\u041d\u041a");
|
||||
expect(userText).toContain("\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441");
|
||||
expect(userText).not.toContain("1C incoming value-flow");
|
||||
expect(userText).not.toContain("Full ranking outside");
|
||||
|
|
@ -1409,7 +1551,7 @@ describe("assistant MCP discovery answer adapter", () => {
|
|||
unsupported_but_understood_family: "counterparty_payouts_or_outflow"
|
||||
}
|
||||
});
|
||||
const broadRows = Array.from({ length: 100 }, (_, index) => ({
|
||||
const broadRows = Array.from({ length: 200 }, (_, index) => ({
|
||||
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
|
||||
Amount: 10,
|
||||
Counterparty: "SVK"
|
||||
|
|
|
|||
|
|
@ -26,6 +26,25 @@ function entryPointContract(overrides: Record<string, unknown> = {}) {
|
|||
selected_chain_matches_top: true
|
||||
}
|
||||
},
|
||||
route_candidate: {
|
||||
schema_version: "assistant_mcp_route_candidate_v1",
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
|
||||
candidate_status: "ready_for_reviewed_execution",
|
||||
selected_chain_id: "value_flow_ranking",
|
||||
selected_chain_summary: "Rank value flow",
|
||||
nearest_catalog_chain_template: "value_flow_ranking",
|
||||
catalog_alignment_status: "selected_matches_top",
|
||||
business_fact_family: "value_flow",
|
||||
action_family: "turnover",
|
||||
proof_expectation: "coverage_checked_fact",
|
||||
required_axes: ["organization", "period"],
|
||||
provided_axes: ["organization", "period"],
|
||||
missing_axes: [],
|
||||
executable_now: true,
|
||||
enablement_reason: null,
|
||||
recommended_next_action: "Execute through the reviewed runtime bridge and truth gate.",
|
||||
forbidden_overclaim_flags: ["no_unchecked_fact_totals"]
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference"
|
||||
}
|
||||
|
|
@ -54,6 +73,15 @@ describe("assistant MCP discovery debug attachment", () => {
|
|||
expect(debug.mcp_discovery_catalog_chain_alignment_status).toBe("selected_matches_top");
|
||||
expect(debug.mcp_discovery_catalog_chain_top_match).toBe("value_flow_ranking");
|
||||
expect(debug.mcp_discovery_catalog_chain_selected_matches_top).toBe(true);
|
||||
expect(debug.mcp_discovery_route_candidate_status).toBe("ready_for_reviewed_execution");
|
||||
expect(debug.mcp_discovery_route_candidate_fact_family).toBe("value_flow");
|
||||
expect(debug.mcp_discovery_route_candidate_action_family).toBe("turnover");
|
||||
expect(debug.mcp_discovery_route_candidate_missing_axes).toEqual([]);
|
||||
expect(debug.mcp_discovery_route_candidate_provided_axes).toEqual(["organization", "period"]);
|
||||
expect(debug.mcp_discovery_route_candidate_executable_now).toBe(true);
|
||||
expect(debug.mcp_discovery_route_candidate_next_action).toBe(
|
||||
"Execute through the reviewed runtime bridge and truth gate."
|
||||
);
|
||||
expect(debug.mcp_discovery_answer_mode).toBe("confirmed_with_bounded_inference");
|
||||
expect(debug.mcp_discovery_business_fact_answer_allowed).toBe(true);
|
||||
expect(debug.mcp_discovery_user_facing_response_allowed).toBe(true);
|
||||
|
|
@ -76,6 +104,11 @@ describe("assistant MCP discovery debug attachment", () => {
|
|||
expect(debug.mcp_discovery_catalog_chain_alignment_status).toBeNull();
|
||||
expect(debug.mcp_discovery_catalog_chain_top_match).toBeNull();
|
||||
expect(debug.mcp_discovery_catalog_chain_selected_matches_top).toBe(false);
|
||||
expect(debug.mcp_discovery_route_candidate_v1).toBeNull();
|
||||
expect(debug.mcp_discovery_route_candidate_status).toBeNull();
|
||||
expect(debug.mcp_discovery_route_candidate_missing_axes).toEqual([]);
|
||||
expect(debug.mcp_discovery_route_candidate_provided_axes).toEqual([]);
|
||||
expect(debug.mcp_discovery_route_candidate_executable_now).toBe(false);
|
||||
expect(debug.mcp_discovery_answer_mode).toBeNull();
|
||||
expect(debug.mcp_discovery_business_fact_answer_allowed).toBe(false);
|
||||
expect(debug.mcp_discovery_user_facing_response_allowed).toBe(false);
|
||||
|
|
|
|||
|
|
@ -293,6 +293,80 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(6);
|
||||
});
|
||||
|
||||
it("marks bank-like counterparties in business-overview rankings before evidence wording", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
dataNeedGraph: {
|
||||
schema_version: "assistant_data_need_graph_v1",
|
||||
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||
subject_candidates: [],
|
||||
business_fact_family: "business_overview",
|
||||
action_family: "broad_evaluation",
|
||||
aggregation_need: null,
|
||||
time_scope_need: "all_time_scope",
|
||||
comparison_need: null,
|
||||
ranking_need: null,
|
||||
proof_expectation: "bounded_inference",
|
||||
clarification_gaps: [],
|
||||
decomposition_candidates: [
|
||||
"collect_scoped_movements",
|
||||
"aggregate_checked_amounts",
|
||||
"aggregate_ranked_axis_values",
|
||||
"fetch_supporting_documents",
|
||||
"probe_coverage",
|
||||
"explain_evidence_basis"
|
||||
],
|
||||
forbidden_overclaim_flags: ["no_raw_model_claims", "no_profit_or_margin_claim_without_evidence"],
|
||||
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
|
||||
},
|
||||
turnMeaning: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс"
|
||||
}
|
||||
});
|
||||
const deps = buildSequentialDeps([
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-01-15T00:00:00", Amount: 1200000, Counterparty: "СБЕРБАНК, ПАО" },
|
||||
{ Period: "2020-02-15T00:00:00", Amount: 800000, Counterparty: "Группа СВК" }
|
||||
]
|
||||
},
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-01-20T00:00:00", Amount: 650000, Counterparty: "СБЕРБАНК, ПАО" },
|
||||
{ Period: "2020-03-20T00:00:00", Amount: 50000, Counterparty: "ООО Поставщик" }
|
||||
]
|
||||
},
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] }
|
||||
]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
||||
expect(result.derived_business_overview?.top_customers[0]).toMatchObject({
|
||||
axis_value: "СБЕРБАНК, ПАО",
|
||||
counterparty_role_hint: "bank_or_financial_institution"
|
||||
});
|
||||
expect(result.derived_business_overview?.top_customers[1]).toMatchObject({
|
||||
axis_value: "Группа СВК",
|
||||
counterparty_role_hint: "ordinary_counterparty"
|
||||
});
|
||||
expect(result.derived_business_overview?.top_suppliers[0]).toMatchObject({
|
||||
axis_value: "СБЕРБАНК, ПАО",
|
||||
counterparty_role_hint: "bank_or_financial_institution"
|
||||
});
|
||||
const confirmedFacts = result.evidence.confirmed_facts.join("\n");
|
||||
const inferredFacts = result.evidence.inferred_facts.join("\n");
|
||||
expect(confirmedFacts).toContain("Крупнейший входящий денежный источник");
|
||||
expect(confirmedFacts).toContain("Крупнейший небанковский входящий контрагент");
|
||||
expect(confirmedFacts).toContain("Крупнейший получатель исходящих денег");
|
||||
expect(confirmedFacts).not.toContain("Самый крупный подтвержденный клиент в проверенном срезе: СБЕРБАНК");
|
||||
expect(confirmedFacts).not.toContain("Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: СБЕРБАНК");
|
||||
expect(inferredFacts).toContain("outgoing cash concentration proxy");
|
||||
});
|
||||
|
||||
it("adds a checked VAT/tax family to business overview only when an explicit period is available", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
dataNeedGraph: {
|
||||
|
|
@ -347,6 +421,7 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{
|
||||
rows: [
|
||||
{ Период: "2020-01-15T00:00:00", Регистратор: "Поступление 1" },
|
||||
|
|
@ -402,9 +477,9 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
expect(result.reason_codes).toContain("pilot_derived_business_overview_tax_position_from_confirmed_rows");
|
||||
expect(result.reason_codes).toContain("pilot_business_overview_trading_margin_query_mcp_executed");
|
||||
expect(result.reason_codes).toContain("pilot_derived_business_overview_trading_margin_proxy_from_confirmed_rows");
|
||||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(13);
|
||||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(14);
|
||||
const taxCall = deps.executeAddressMcpQuery.mock.calls[2]?.[0];
|
||||
const tradingMarginCall = deps.executeAddressMcpQuery.mock.calls[9]?.[0];
|
||||
const tradingMarginCall = deps.executeAddressMcpQuery.mock.calls[10]?.[0];
|
||||
expect(String(taxCall?.query ?? "")).toContain("НДСЗаписиКнигиПродаж");
|
||||
expect(String(taxCall?.query ?? "")).toContain("НДСЗаписиКнигиПокупок");
|
||||
expect(String(tradingMarginCall?.query ?? "")).toContain("Документ.РеализацияТоваровУслуг.Товары");
|
||||
|
|
@ -453,6 +528,7 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-03-01T00:00:00", Amount: 100000, Item: "Товар А", Counterparty: "Клиент А", AccountKt: "41.01" },
|
||||
|
|
@ -510,6 +586,7 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
{ rows: [{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" }] },
|
||||
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Поставщик А" }] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-12-31T00:00:00", Amount: 70000, Counterparty: "Клиент А" },
|
||||
|
|
@ -602,15 +679,107 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
expect(result.reason_codes).toContain("pilot_derived_business_overview_open_settlement_quality_from_confirmed_rows");
|
||||
expect(result.reason_codes).toContain("pilot_derived_business_overview_debt_age_signal_from_contract_dates");
|
||||
expect(result.reason_codes).toContain("pilot_derived_business_overview_debt_staleness_risk_proxy_from_confirmed_rows");
|
||||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(13);
|
||||
const receivablesCall = deps.executeAddressMcpQuery.mock.calls[3]?.[0];
|
||||
const payablesCall = deps.executeAddressMcpQuery.mock.calls[4]?.[0];
|
||||
const openContractsCall = deps.executeAddressMcpQuery.mock.calls[5]?.[0];
|
||||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(14);
|
||||
const receivablesCall = deps.executeAddressMcpQuery.mock.calls[4]?.[0];
|
||||
const payablesCall = deps.executeAddressMcpQuery.mock.calls[5]?.[0];
|
||||
const openContractsCall = deps.executeAddressMcpQuery.mock.calls[6]?.[0];
|
||||
expect(String(receivablesCall?.query ?? "")).toContain("62");
|
||||
expect(String(payablesCall?.query ?? "")).toContain("60");
|
||||
expect(String(openContractsCall?.query ?? "")).toContain("СуммаРазвернутыйОстатокКт");
|
||||
});
|
||||
|
||||
it("checks debt due-date aging with contract payment terms before claiming overdue debt", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
dataNeedGraph: {
|
||||
schema_version: "assistant_data_need_graph_v1",
|
||||
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||
subject_candidates: [],
|
||||
business_fact_family: "business_overview",
|
||||
action_family: "debt_due_date_boundary",
|
||||
aggregation_need: null,
|
||||
time_scope_need: "explicit_period",
|
||||
comparison_need: null,
|
||||
ranking_need: null,
|
||||
proof_expectation: "due_date_aging",
|
||||
clarification_gaps: [],
|
||||
decomposition_candidates: [
|
||||
"collect_scoped_movements",
|
||||
"aggregate_checked_amounts",
|
||||
"aggregate_ranked_axis_values",
|
||||
"fetch_supporting_documents",
|
||||
"probe_coverage",
|
||||
"explain_evidence_basis"
|
||||
],
|
||||
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_overdue_claim"],
|
||||
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
|
||||
},
|
||||
turnMeaning: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "debt_due_date_boundary",
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
});
|
||||
const deps = buildSequentialDeps([
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-12-31T00:00:00", Amount: 100000, Counterparty: "Клиент А", Contract: "Договор А от 10.02.2019" }
|
||||
]
|
||||
},
|
||||
{
|
||||
rows: [
|
||||
{
|
||||
Period: "2020-12-31T00:00:00",
|
||||
Amount: 100000,
|
||||
Counterparty: "Клиент А",
|
||||
Contract: "Договор А от 10.02.2019",
|
||||
ДокументРасчетов: "Реализация товаров от 10.03.2020",
|
||||
УстановленСрокОплаты: false,
|
||||
СрокОплаты: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] }
|
||||
]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
||||
expect(result.derived_business_overview?.debt_due_date_aging).toMatchObject({
|
||||
as_of_date: "2020-12-31",
|
||||
rows_with_amount: 1,
|
||||
gross_open_amount: 100000,
|
||||
rows_with_payment_terms: 0,
|
||||
rows_without_payment_terms: 1,
|
||||
overdue_rows: 0,
|
||||
evidence_status: "no_payment_terms_configured"
|
||||
});
|
||||
expect(result.derived_business_overview?.missing_signal_families).not.toContain("debt_due_date_aging_quality");
|
||||
expect(result.derived_business_overview?.missing_proof_families.map((item) => item.family)).not.toContain(
|
||||
"debt_due_date_aging_quality"
|
||||
);
|
||||
expect(result.evidence.confirmed_facts.join("\n")).toContain("срок оплаты не установлен");
|
||||
expect(result.evidence.confirmed_facts.join("\n")).toContain("Подтвержденной просрочки");
|
||||
expect(result.evidence.unknown_facts.join("\n")).not.toContain("due-date aging этим бизнес-обзором не подтверждены");
|
||||
expect(result.reason_codes).toContain("pilot_business_overview_debt_due_date_aging_query_mcp_executed");
|
||||
expect(result.reason_codes).toContain("pilot_derived_business_overview_debt_due_date_aging_from_confirmed_rows");
|
||||
expect(result.reason_codes).toContain("pilot_derived_business_overview_debt_due_date_aging_no_payment_terms_configured");
|
||||
const dueDateCall = deps.executeAddressMcpQuery.mock.calls[7]?.[0];
|
||||
expect(String(dueDateCall?.query ?? "")).toContain("УстановленСрокОплаты");
|
||||
expect(String(dueDateCall?.query ?? "")).toContain("СрокОплаты");
|
||||
});
|
||||
|
||||
it("adds a checked inventory-position family to business overview only as an as-of-date snapshot", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
dataNeedGraph: {
|
||||
|
|
@ -650,6 +819,7 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-12-31T00:00:00", Amount: 250000, Quantity: 10, Item: "Товар А" },
|
||||
|
|
@ -729,8 +899,8 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
expect(result.reason_codes).toContain("pilot_derived_business_overview_inventory_position_from_confirmed_rows");
|
||||
expect(result.reason_codes).toContain("pilot_derived_business_overview_inventory_turnover_proxy_from_confirmed_rows");
|
||||
expect(result.reason_codes).toContain("pilot_derived_business_overview_inventory_staleness_risk_proxy_from_confirmed_rows");
|
||||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(13);
|
||||
const inventoryCall = deps.executeAddressMcpQuery.mock.calls[6]?.[0];
|
||||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(14);
|
||||
const inventoryCall = deps.executeAddressMcpQuery.mock.calls[7]?.[0];
|
||||
expect(inventoryCall?.account_scope).toContain("41.01");
|
||||
});
|
||||
|
||||
|
|
@ -1335,7 +1505,7 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
unsupported_but_understood_family: "counterparty_payouts_or_outflow"
|
||||
}
|
||||
});
|
||||
const rows = Array.from({ length: 100 }, (_, index) => ({
|
||||
const rows = Array.from({ length: 200 }, (_, index) => ({
|
||||
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
|
||||
Amount: 10,
|
||||
Counterparty: "SVK"
|
||||
|
|
@ -1359,7 +1529,7 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
unsupported_but_understood_family: "counterparty_payouts_or_outflow"
|
||||
}
|
||||
});
|
||||
const broadRows = Array.from({ length: 100 }, (_, index) => ({
|
||||
const broadRows = Array.from({ length: 200 }, (_, index) => ({
|
||||
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
|
||||
Amount: 10,
|
||||
Counterparty: "SVK"
|
||||
|
|
@ -1560,7 +1730,7 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
|
||||
}
|
||||
});
|
||||
const outgoingBroadRows = Array.from({ length: 100 }, (_, index) => ({
|
||||
const outgoingBroadRows = Array.from({ length: 200 }, (_, index) => ({
|
||||
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
|
||||
Amount: 10,
|
||||
Counterparty: "SVK"
|
||||
|
|
|
|||
|
|
@ -44,6 +44,177 @@ describe("assistant MCP discovery response candidate", () => {
|
|||
expect(candidate.reply_text).not.toContain("primitive");
|
||||
});
|
||||
|
||||
it("keeps inventory reserve boundary answers direct instead of compacting into a money overview", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "inventory_reserve_boundary",
|
||||
unsupported_but_understood_family: "inventory_reserve_liquidation_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: {
|
||||
period_scope: null,
|
||||
incoming_customer_revenue: {
|
||||
total_amount_human_ru: "157 192 981,43 руб.",
|
||||
coverage_limited_by_probe_limit: true
|
||||
},
|
||||
outgoing_supplier_payout: {
|
||||
total_amount_human_ru: "35 439 044,74 руб.",
|
||||
coverage_limited_by_probe_limit: true
|
||||
},
|
||||
net_amount_human_ru: "121 753 936,69 руб.",
|
||||
net_direction: "net_incoming"
|
||||
}
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference",
|
||||
headline:
|
||||
"\u041a\u043e\u0440\u043e\u0442\u043a\u043e: \u0442\u043e\u0447\u043d\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u0440\u0435\u0437\u0435\u0440\u0432 \u043f\u043e\u0434 \u043d\u0435\u043b\u0438\u043a\u0432\u0438\u0434\u044b \u043f\u043e \u0442\u0435\u043a\u0443\u0449\u0438\u043c \u0434\u0430\u043d\u043d\u044b\u043c \u043d\u0435\u043b\u044c\u0437\u044f.",
|
||||
confirmed_lines: ["Денежный обзор здесь не является ответом на складской резерв."],
|
||||
inference_lines: [],
|
||||
unknown_lines: [
|
||||
"\u0420\u0435\u0437\u0435\u0440\u0432\u044b, \u0441\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u0438 \u043b\u0438\u043a\u0432\u0438\u0434\u0430\u0446\u0438\u043e\u043d\u043d\u0430\u044f \u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c \u0441\u043a\u043b\u0430\u0434\u0430 \u043d\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u044b."
|
||||
],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
expect(candidate.reply_text).toContain(
|
||||
"\u0442\u043e\u0447\u043d\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u0440\u0435\u0437\u0435\u0440\u0432"
|
||||
);
|
||||
expect(candidate.reply_text).toContain("\u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043d\u0443\u0436\u043d\u043e");
|
||||
expect(candidate.reply_text).not.toContain("157 192 981");
|
||||
expect(candidate.reply_text).not.toContain("лимит");
|
||||
});
|
||||
|
||||
it("keeps profit/margin boundary answers direct instead of compacting into a money overview", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "profit_margin_boundary",
|
||||
unsupported_but_understood_family: "profit_margin_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: {
|
||||
incoming_customer_revenue: {
|
||||
total_amount_human_ru: "47 628 853,03 руб."
|
||||
},
|
||||
outgoing_supplier_payout: {
|
||||
total_amount_human_ru: "19 568 878,06 руб."
|
||||
},
|
||||
net_amount_human_ru: "28 059 974,97 руб.",
|
||||
net_direction: "net_incoming"
|
||||
}
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference",
|
||||
headline:
|
||||
"Коротко: нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только bounded operating-flow/trading-margin proxy, не P&L и не бухгалтерский финансовый результат.",
|
||||
confirmed_lines: ["Денежный обзор здесь не является ответом на чистую прибыль."],
|
||||
inference_lines: [],
|
||||
unknown_lines: ["Для точного P&L нужны себестоимость, расходы и закрытие периода."],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
expect(candidate.reply_text).toContain("нельзя точно подтвердить чистую прибыль");
|
||||
expect(candidate.reply_text).toContain("P&L");
|
||||
expect(candidate.reply_text).toContain("себестоимости");
|
||||
expect(candidate.reply_text).not.toContain("47 628 853");
|
||||
});
|
||||
|
||||
it("keeps vendor-risk boundary answers direct instead of compacting into a money overview", () => {
|
||||
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: {
|
||||
outgoing_supplier_payout: {
|
||||
total_amount_human_ru: "19 568 878,06 руб."
|
||||
},
|
||||
top_suppliers: [
|
||||
{
|
||||
axis_value: "СБЕРБАНК, ПАО",
|
||||
total_amount_human_ru: "6 653 022,52 руб."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference",
|
||||
headline: "Коротко: точный риск зависимости от одного поставщика по текущим данным не подтвержден.",
|
||||
confirmed_lines: [],
|
||||
inference_lines: [],
|
||||
unknown_lines: ["Vendor-risk route не подключен."],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
expect(candidate.reply_text).toContain("риск зависимости");
|
||||
expect(candidate.reply_text).toContain("outgoing cash concentration proxy");
|
||||
expect(candidate.reply_text).toContain("банк/финансовая организация");
|
||||
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", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
|
|
@ -114,13 +285,15 @@ describe("assistant MCP discovery response candidate", () => {
|
|||
})
|
||||
);
|
||||
|
||||
expect(candidate.reply_text).toContain("в доступном проверенном MCP-срезе");
|
||||
expect(candidate.reply_text).toContain("в доступном проверенном срезе 1С");
|
||||
expect(candidate.reply_text).toContain("лидирует 2015");
|
||||
expect(candidate.reply_text).toContain("2015");
|
||||
expect(candidate.reply_text).toContain("136 723 459,73 руб.");
|
||||
expect(candidate.reply_text).toContain("не полный бухгалтерский рейтинг доходности");
|
||||
expect(candidate.reply_text).toContain("не как чистую бухгалтерскую прибыль");
|
||||
expect(candidate.reply_text).toContain("лимит выборки MCP");
|
||||
expect(candidate.reply_text).toContain("проверка достигла лимита строк");
|
||||
expect(candidate.reply_text).not.toContain("лимит выборки MCP");
|
||||
expect(candidate.reply_text).not.toContain("MCP-срез");
|
||||
expect(candidate.reply_text).not.toContain("Что подтверждено:");
|
||||
expect(candidate.reply_text).not.toContain("Профиль операционной активности");
|
||||
});
|
||||
|
|
@ -197,6 +370,76 @@ describe("assistant MCP discovery response candidate", () => {
|
|||
expect(candidate.reply_text).not.toContain("Складской срез");
|
||||
});
|
||||
|
||||
it("does not present bank-like incoming leaders as ordinary client revenue", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
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: "2020",
|
||||
incoming_customer_revenue: {
|
||||
total_amount_human_ru: "24 885 659,31 руб.",
|
||||
coverage_limited_by_probe_limit: false
|
||||
},
|
||||
outgoing_supplier_payout: {
|
||||
total_amount_human_ru: "19 568 878,06 руб.",
|
||||
coverage_limited_by_probe_limit: false
|
||||
},
|
||||
net_amount_human_ru: "5 316 781,25 руб.",
|
||||
net_direction: "net_incoming",
|
||||
top_customers: [
|
||||
{
|
||||
axis_value: "СБЕРБАНК, ПАО",
|
||||
total_amount_human_ru: "12 792 194,31 руб.",
|
||||
counterparty_role_hint: "bank_or_financial_institution"
|
||||
},
|
||||
{
|
||||
axis_value: "Группа СВК",
|
||||
total_amount_human_ru: "12 093 465 руб.",
|
||||
counterparty_role_hint: "ordinary_counterparty"
|
||||
}
|
||||
],
|
||||
top_suppliers: [],
|
||||
yearly_breakdown: []
|
||||
}
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference",
|
||||
headline: "Ограниченный бизнес-обзор.",
|
||||
confirmed_lines: [],
|
||||
inference_lines: [],
|
||||
unknown_lines: [],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const firstLine = candidate.reply_text?.split("\n")[0] ?? "";
|
||||
expect(firstLine).toContain("крупнейший входящий денежный источник: СБЕРБАНК, ПАО");
|
||||
expect(firstLine).toContain("не называю это клиентской выручкой");
|
||||
expect(firstLine).toContain("крупнейший небанковский входящий контрагент: Группа СВК");
|
||||
expect(candidate.reply_text).not.toContain("крупнейший источник входящих денег: СБЕРБАНК");
|
||||
expect(candidate.reply_text).not.toContain("Самый крупный подтвержденный клиент");
|
||||
});
|
||||
|
||||
it("mentions separate counterparty scope in company plus counterparty business summaries", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
|
|
@ -607,7 +850,7 @@ describe("assistant MCP discovery response candidate", () => {
|
|||
requires_user_clarification: false,
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference",
|
||||
headline: "По данным 1С найдены строки исходящих платежей/списаний; сумму можно называть только в рамках проверенного периода и найденных строк.",
|
||||
headline: "По данным 1С найдены строки исходящих платежей/списаний; сумму можно называть только в рамках проверенного периода и найденных строк.",
|
||||
confirmed_lines: ["1C supplier-payout rows were found for counterparty SVK"],
|
||||
inference_lines: [
|
||||
"Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit"
|
||||
|
|
@ -661,7 +904,7 @@ describe("assistant MCP discovery response candidate", () => {
|
|||
requires_user_clarification: false,
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference",
|
||||
headline: "По данным 1С найдены строки движений; ответ ограничен проверенным периодом и найденными строками.",
|
||||
headline: "По данным 1С найдены строки движений; ответ ограничен проверенным периодом и найденными строками.",
|
||||
confirmed_lines: ["1C movement rows were found for counterparty SVK"],
|
||||
inference_lines: ["Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope"],
|
||||
unknown_lines: ["Full movement history outside the checked period is not proven by this MCP discovery pilot"],
|
||||
|
|
@ -672,9 +915,9 @@ describe("assistant MCP discovery response candidate", () => {
|
|||
})
|
||||
);
|
||||
|
||||
expect(candidate.reply_text).toContain("В 1С найдены строки движений по контрагенту SVK.");
|
||||
expect(candidate.reply_text).toContain("Срез движений ограничен только подтвержденными строками движений");
|
||||
expect(candidate.reply_text).toContain("Полный исторический срез движений вне проверенного периода этим поиском не подтвержден.");
|
||||
expect(candidate.reply_text).toContain("В 1С найдены строки движений по контрагенту SVK.");
|
||||
expect(candidate.reply_text).toContain("Срез движений ограничен только подтвержденными строками движений");
|
||||
expect(candidate.reply_text).toContain("Полный исторический срез движений вне проверенного периода этим поиском не подтвержден.");
|
||||
expect(candidate.reply_text).not.toContain("1C movement rows were found");
|
||||
});
|
||||
|
||||
|
|
@ -841,9 +1084,8 @@ describe("assistant MCP discovery response candidate", () => {
|
|||
expect(candidate.reply_text).toContain(
|
||||
"\u0412 1\u0421 \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u043d\u044b \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0438 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0435 \u0441\u0442\u0440\u043e\u043a\u0438 \u0432 \u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u043e\u043c \u0441\u0440\u0435\u0437\u0435"
|
||||
);
|
||||
expect(candidate.reply_text).toContain(
|
||||
"\u0417\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u044b\u0439 \u043f\u0435\u0440\u0438\u043e\u0434 \u0443\u043f\u0435\u0440\u0441\u044f \u0432 \u043b\u0438\u043c\u0438\u0442 \u0441\u0442\u0440\u043e\u043a MCP"
|
||||
);
|
||||
expect(candidate.reply_text).toContain("Запрошенный период достиг лимита строк");
|
||||
expect(candidate.reply_text).not.toContain("уперся в лимит строк MCP");
|
||||
expect(candidate.reply_text).not.toContain(
|
||||
"\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0441\u043a\u043e\u043c\u0443 \u043a\u043e\u043d\u0442\u0443\u0440\u0443"
|
||||
);
|
||||
|
|
|
|||
|
|
@ -621,7 +621,7 @@ describe("assistant MCP discovery response policy", () => {
|
|||
answer_mode: "confirmed_with_bounded_inference",
|
||||
headline: "Рейтинг по контрагентам построен по подтвержденным строкам 1С.",
|
||||
confirmed_lines: [
|
||||
"Больше всего денег принёс контрагент СБЕРБАНК, ПАО по организации ООО Альтернатива Плюс за период 2020: 12 792 194,31 руб. по 9 строкам с суммой."
|
||||
"Крупнейший входящий денежный источник — СБЕРБАНК, ПАО по организации ООО Альтернатива Плюс за период 2020: 12 792 194,31 руб. по 9 строкам с суммой. Это банк/финансовая организация; без назначения платежа или договора не считаю это обычным клиентом или выручкой."
|
||||
],
|
||||
inference_lines: [
|
||||
"Рейтинг по контрагентам по организации ООО Альтернатива Плюс за период 2020 рассчитан только по подтвержденным строкам 1С."
|
||||
|
|
@ -639,6 +639,7 @@ describe("assistant MCP discovery response policy", () => {
|
|||
expect(result.decision).toBe("apply_candidate");
|
||||
expect(result.reply_text).toContain("ООО Альтернатива Плюс");
|
||||
expect(result.reply_text).toContain("2020");
|
||||
expect(result.reply_text).toContain("не считаю это обычным клиентом");
|
||||
expect(result.reason_codes).toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_aligned_factual_address_reply");
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_factual_address_continuation_target");
|
||||
|
|
|
|||
|
|
@ -37,6 +37,22 @@ function buildBidirectionalDeps(
|
|||
};
|
||||
}
|
||||
|
||||
function buildSequentialDeps(results: Array<{ rows: Array<Record<string, unknown>>; error?: string | null }>) {
|
||||
const executeAddressMcpQuery = vi.fn(async () => {
|
||||
const next = results.shift() ?? { rows: [] };
|
||||
const rows = next.rows;
|
||||
const error = next.error ?? null;
|
||||
return {
|
||||
fetched_rows: rows.length,
|
||||
matched_rows: error ? 0 : rows.length,
|
||||
raw_rows: rows,
|
||||
rows: error ? [] : rows,
|
||||
error
|
||||
};
|
||||
});
|
||||
return { executeAddressMcpQuery };
|
||||
}
|
||||
|
||||
describe("assistant MCP discovery runtime bridge", () => {
|
||||
it("composes planner, pilot executor, and answer draft without wiring the hot runtime", async () => {
|
||||
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||
|
|
@ -150,8 +166,24 @@ describe("assistant MCP discovery runtime bridge", () => {
|
|||
expect(result.loop_state.catalog_chain_template_matches[0]).toBe("value_flow_ranking");
|
||||
expect(result.loop_state.catalog_chain_template_alignment.alignment_status).toBe("selected_matches_top");
|
||||
expect(result.loop_state.catalog_chain_template_alignment.selected_chain_matches_top).toBe(true);
|
||||
expect(result.route_candidate).toMatchObject({
|
||||
schema_version: "assistant_mcp_route_candidate_v1",
|
||||
candidate_status: "needs_user_scope",
|
||||
selected_chain_id: "value_flow_ranking",
|
||||
nearest_catalog_chain_template: "value_flow_ranking",
|
||||
catalog_alignment_status: "selected_matches_top",
|
||||
business_fact_family: "value_flow",
|
||||
action_family: "turnover",
|
||||
executable_now: false
|
||||
});
|
||||
expect(result.route_candidate.missing_axes).toContain("organization");
|
||||
expect(result.route_candidate.provided_axes).toContain("aggregate_axis");
|
||||
expect(result.route_candidate.recommended_next_action).toBe(
|
||||
"Ask the user for the missing scope axes before MCP execution."
|
||||
);
|
||||
expect(result.reason_codes).toContain("planner_selected_chain_matches_catalog_top");
|
||||
expect(result.reason_codes).toContain("runtime_bridge_loop_state_awaiting_clarification");
|
||||
expect(result.reason_codes).toContain("runtime_bridge_route_candidate_needs_user_scope");
|
||||
});
|
||||
|
||||
it("produces a bounded ranked value-flow answer when period and organization are known", async () => {
|
||||
|
|
@ -192,6 +224,12 @@ describe("assistant MCP discovery runtime bridge", () => {
|
|||
axis_value: "СВК-А",
|
||||
total_amount: 2100
|
||||
});
|
||||
expect(result.route_candidate).toMatchObject({
|
||||
candidate_status: "ready_for_reviewed_execution",
|
||||
selected_chain_id: "value_flow_ranking",
|
||||
executable_now: true,
|
||||
enablement_reason: null
|
||||
});
|
||||
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("СВК-А");
|
||||
});
|
||||
|
||||
|
|
@ -437,6 +475,288 @@ describe("assistant MCP discovery runtime bridge", () => {
|
|||
expect(userFacing).not.toContain("MCP discovery pilot");
|
||||
});
|
||||
|
||||
it("marks exact business-overview proof gaps as route enablement instead of reviewed execution", async () => {
|
||||
const deps = buildSequentialDeps([
|
||||
{ rows: [{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Client A" }] },
|
||||
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Supplier A" }] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-12-31T00:00:00", Amount: 250000, Quantity: 10, Item: "Widget A" },
|
||||
{ Period: "2020-12-31T00:00:00", Amount: 50000, Quantity: 5, Item: "Widget B" }
|
||||
]
|
||||
},
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-01-10T00:00:00", Amount: 200000, Quantity: 8, Item: "Widget A" },
|
||||
{ Period: "2020-11-01T00:00:00", Amount: 50000, Quantity: 2, Item: "Widget B" }
|
||||
]
|
||||
},
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-01-15T00:00:00", Registrator: "Purchase 1" },
|
||||
{ Period: "2020-12-15T00:00:00", Registrator: "Purchase 2" }
|
||||
]
|
||||
},
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-03-01T00:00:00", Amount: 600000, Item: "Widget A", Counterparty: "Client A", AccountKt: "41.01" },
|
||||
{ Period: "2020-02-01T00:00:00", Amount: 240000, Item: "Widget A", Counterparty: "Supplier A", AccountDt: "41.01" }
|
||||
]
|
||||
}
|
||||
]);
|
||||
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||
dataNeedGraph: {
|
||||
schema_version: "assistant_data_need_graph_v1",
|
||||
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||
subject_candidates: [],
|
||||
business_fact_family: "business_overview",
|
||||
action_family: "inventory_reserve_boundary",
|
||||
aggregation_need: null,
|
||||
time_scope_need: "explicit_period",
|
||||
comparison_need: null,
|
||||
ranking_need: null,
|
||||
proof_expectation: "bounded_inference",
|
||||
clarification_gaps: [],
|
||||
decomposition_candidates: [
|
||||
"collect_scoped_movements",
|
||||
"aggregate_checked_amounts",
|
||||
"fetch_supporting_documents",
|
||||
"probe_coverage",
|
||||
"explain_evidence_basis"
|
||||
],
|
||||
forbidden_overclaim_flags: [
|
||||
"no_raw_model_claims",
|
||||
"no_unchecked_fact_totals",
|
||||
"no_unchecked_business_health_claim"
|
||||
],
|
||||
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
|
||||
},
|
||||
turnMeaning: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "inventory_reserve_boundary",
|
||||
explicit_organization_scope: "OOO Alternative Plus",
|
||||
explicit_date_scope: "2020"
|
||||
},
|
||||
deps
|
||||
});
|
||||
|
||||
expect(result.bridge_status).toBe("answer_draft_ready");
|
||||
expect(result.business_fact_answer_allowed).toBe(true);
|
||||
expect(result.route_candidate).toMatchObject({
|
||||
candidate_status: "needs_route_enablement",
|
||||
selected_chain_id: "business_overview",
|
||||
business_fact_family: "business_overview",
|
||||
action_family: "inventory_reserve_boundary",
|
||||
executable_now: false
|
||||
});
|
||||
expect(result.route_candidate.enablement_reason).toContain("inventory_reserve_liquidation_quality");
|
||||
expect(result.route_candidate.enablement_reason).toContain(
|
||||
"reviewed_inventory_quality_route_with_reserves_writeoffs_obsolescence_and_liquidation_value"
|
||||
);
|
||||
expect(result.route_candidate.forbidden_overclaim_flags).toContain(
|
||||
"confirmed_obsolete_stock_reserve_writeoff_or_liquidation_value"
|
||||
);
|
||||
expect(result.reason_codes).toContain("runtime_bridge_route_candidate_needs_route_enablement");
|
||||
});
|
||||
|
||||
it("promotes profit-margin boundary when accounting 90/91/99 proof is available", async () => {
|
||||
const deps = buildSequentialDeps([
|
||||
{ rows: [{ Period: "2020-01-15T00:00:00", Amount: 12012833.72, Counterparty: "Client A" }] },
|
||||
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 6430415.34, Counterparty: "Supplier A" }] },
|
||||
{ rows: [] },
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-12-31T00:00:00", Registrator: "ACC90_REVENUE_KT", Amount: 12012833.72 },
|
||||
{ Period: "2020-12-31T00:00:00", Registrator: "ACC90_COST_DT", Amount: 6430415.34 },
|
||||
{ Period: "2020-12-31T00:00:00", Registrator: "ACC90_SELLING_DT", Amount: 1715450.75 },
|
||||
{ Period: "2020-12-31T00:00:00", Registrator: "ACC90_RESULT_TO_99_PROFIT", Amount: 3053486.79 },
|
||||
{ Period: "2020-12-31T00:00:00", Registrator: "ACC90_RESULT_FROM_99_LOSS", Amount: 1188658.09 },
|
||||
{ Period: "2020-12-31T00:00:00", Registrator: "ACC91_RESULT_FROM_99_LOSS", Amount: 9001644.55 },
|
||||
{ Period: "2020-12-31T00:00:00", Registrator: "ACC84_TO99_LOSS_TRANSFER", Amount: 7136815.85 }
|
||||
]
|
||||
},
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [{ Period: "2020-01-15T00:00:00", Registrator: "Purchase 1" }] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] }
|
||||
]);
|
||||
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||
dataNeedGraph: {
|
||||
schema_version: "assistant_data_need_graph_v1",
|
||||
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||
subject_candidates: [],
|
||||
business_fact_family: "business_overview",
|
||||
action_family: "profit_margin_boundary",
|
||||
aggregation_need: null,
|
||||
time_scope_need: "explicit_period",
|
||||
comparison_need: null,
|
||||
ranking_need: null,
|
||||
proof_expectation: "bounded_inference",
|
||||
clarification_gaps: [],
|
||||
decomposition_candidates: [
|
||||
"collect_scoped_movements",
|
||||
"aggregate_checked_amounts",
|
||||
"fetch_supporting_documents",
|
||||
"probe_coverage",
|
||||
"explain_evidence_basis"
|
||||
],
|
||||
forbidden_overclaim_flags: [
|
||||
"no_raw_model_claims",
|
||||
"no_unchecked_fact_totals",
|
||||
"no_unchecked_business_health_claim"
|
||||
],
|
||||
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
|
||||
},
|
||||
turnMeaning: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "profit_margin_boundary",
|
||||
explicit_organization_scope: "OOO Alternative Plus",
|
||||
explicit_date_scope: "2020"
|
||||
},
|
||||
deps
|
||||
});
|
||||
const overview = result.pilot.derived_business_overview;
|
||||
const missingFamilies = overview?.missing_proof_families.map((item) => item.family) ?? [];
|
||||
const userFacing = [
|
||||
result.answer_draft.headline,
|
||||
...result.answer_draft.confirmed_lines,
|
||||
...result.answer_draft.inference_lines,
|
||||
...result.answer_draft.unknown_lines,
|
||||
...result.answer_draft.limitation_lines,
|
||||
result.answer_draft.next_step_line ?? ""
|
||||
].join("\n");
|
||||
|
||||
expect(result.bridge_status).toBe("answer_draft_ready");
|
||||
expect(result.business_fact_answer_allowed).toBe(true);
|
||||
expect(result.route_candidate).toMatchObject({
|
||||
candidate_status: "ready_for_reviewed_execution",
|
||||
selected_chain_id: "business_overview",
|
||||
business_fact_family: "business_overview",
|
||||
action_family: "profit_margin_boundary",
|
||||
executable_now: true
|
||||
});
|
||||
expect(overview?.accounting_financial_result).toMatchObject({
|
||||
period_scope: "2020",
|
||||
final_result_amount: -7136815.85,
|
||||
final_result_direction: "loss",
|
||||
final_transfer_basis: "account_99_to_84_period_close",
|
||||
period_close_rows_with_amount: 4
|
||||
});
|
||||
expect(overview?.accounting_financial_result?.net_margin_to_revenue_pct).toBe(-59.41);
|
||||
expect(missingFamilies).not.toContain("accounting_profit_margin");
|
||||
expect(userFacing).toContain("90/91/99");
|
||||
expect(userFacing).toContain("90.01");
|
||||
expect(userFacing).toContain("7 136 815,85");
|
||||
expect(userFacing).not.toContain("operating-flow/trading-margin proxy");
|
||||
expect(result.reason_codes).toContain("pilot_derived_business_overview_accounting_financial_result_from_confirmed_rows");
|
||||
expect(result.reason_codes).toContain("answer_contains_business_overview_accounting_financial_result");
|
||||
expect(result.reason_codes).toContain("runtime_bridge_route_candidate_ready_for_reviewed_execution");
|
||||
});
|
||||
|
||||
it("promotes debt due-date boundary after reviewed payment-term route returns a checked negative", async () => {
|
||||
const deps = buildSequentialDeps([
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-12-31T00:00:00", Amount: 100000, Counterparty: "Клиент А", Contract: "Договор А" }
|
||||
]
|
||||
},
|
||||
{
|
||||
rows: [
|
||||
{
|
||||
Period: "2020-12-31T00:00:00",
|
||||
Amount: 100000,
|
||||
Counterparty: "Клиент А",
|
||||
Contract: "Договор А",
|
||||
ДокументРасчетов: "Реализация товаров от 10.03.2020",
|
||||
УстановленСрокОплаты: false,
|
||||
СрокОплаты: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] }
|
||||
]);
|
||||
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||
dataNeedGraph: {
|
||||
schema_version: "assistant_data_need_graph_v1",
|
||||
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||
subject_candidates: [],
|
||||
business_fact_family: "business_overview",
|
||||
action_family: "debt_due_date_boundary",
|
||||
aggregation_need: null,
|
||||
time_scope_need: "explicit_period",
|
||||
comparison_need: null,
|
||||
ranking_need: null,
|
||||
proof_expectation: "due_date_aging",
|
||||
clarification_gaps: [],
|
||||
decomposition_candidates: [
|
||||
"collect_scoped_movements",
|
||||
"aggregate_checked_amounts",
|
||||
"fetch_supporting_documents",
|
||||
"probe_coverage",
|
||||
"explain_evidence_basis"
|
||||
],
|
||||
forbidden_overclaim_flags: [
|
||||
"no_raw_model_claims",
|
||||
"no_unchecked_overdue_claim"
|
||||
],
|
||||
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
|
||||
},
|
||||
turnMeaning: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "debt_due_date_boundary",
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020"
|
||||
},
|
||||
deps
|
||||
});
|
||||
const userFacing = [
|
||||
result.answer_draft.headline,
|
||||
...result.answer_draft.confirmed_lines,
|
||||
...result.answer_draft.unknown_lines
|
||||
].join("\n");
|
||||
|
||||
expect(result.bridge_status).toBe("answer_draft_ready");
|
||||
expect(result.route_candidate).toMatchObject({
|
||||
candidate_status: "ready_for_reviewed_execution",
|
||||
selected_chain_id: "business_overview",
|
||||
business_fact_family: "business_overview",
|
||||
action_family: "debt_due_date_boundary",
|
||||
executable_now: true
|
||||
});
|
||||
expect(result.pilot.derived_business_overview?.debt_due_date_aging).toMatchObject({
|
||||
evidence_status: "no_payment_terms_configured",
|
||||
rows_without_payment_terms: 1,
|
||||
overdue_rows: 0
|
||||
});
|
||||
expect(userFacing).toContain("срок оплаты не установлен");
|
||||
expect(userFacing).toContain("Подтвержденной просрочки");
|
||||
expect(result.reason_codes).toContain("runtime_bridge_route_candidate_ready_for_reviewed_execution");
|
||||
expect(result.reason_codes).toContain("answer_contains_business_overview_debt_due_date_aging_no_payment_terms_configured");
|
||||
});
|
||||
|
||||
it("bridges selected-item inventory provenance templates through exact document evidence", async () => {
|
||||
const deps = buildDeps([
|
||||
{
|
||||
|
|
@ -711,5 +1031,15 @@ describe("assistant MCP discovery runtime bridge", () => {
|
|||
});
|
||||
expect(result.loop_state.pending_axes).toEqual(["organization", "period"]);
|
||||
expect(result.loop_state.explicit_entity_candidates).toEqual([]);
|
||||
expect(result.route_candidate).toMatchObject({
|
||||
candidate_status: "needs_user_scope",
|
||||
selected_chain_id: "movement_evidence",
|
||||
business_fact_family: "movement_evidence",
|
||||
action_family: "list_movements",
|
||||
proof_expectation: "clarification_required",
|
||||
executable_now: false
|
||||
});
|
||||
expect(result.route_candidate.missing_axes).toEqual(["organization", "period"]);
|
||||
expect(result.route_candidate.forbidden_overclaim_flags).toContain("no_unchecked_fact_totals");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1505,6 +1505,36 @@ describe("assistant MCP discovery turn input adapter", () => {
|
|||
expect(result.reason_codes).toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
|
||||
});
|
||||
|
||||
it("forces discovery over an exact ranking intent when organization scope is missing", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage:
|
||||
"\u043a\u0430\u043a\u043e\u0439 \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442 \u043f\u0440\u0438\u043d\u0435\u0441 \u0431\u043e\u043b\u044c\u0448\u0435 \u0432\u0441\u0435\u0433\u043e \u0434\u0435\u043d\u0435\u0433 \u0437\u0430 2020 \u0433\u043e\u0434?",
|
||||
assistantTurnMeaning: {
|
||||
asked_domain_family: "counterparty",
|
||||
asked_action_family: "counterparty_value_or_turnover",
|
||||
explicit_intent_candidate: "customer_revenue_and_payments",
|
||||
explicit_entity_candidates: [{ type: "counterparty", value: "\u0433\u043e\u0434\u0443", source: "current_turn_loose_entity_tail" }],
|
||||
explicit_date_scope: "2020",
|
||||
stale_replay_forbidden: false
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.adapter_status).toBe("ready");
|
||||
expect(result.should_run_discovery).toBe(true);
|
||||
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
|
||||
expect(result.data_need_graph?.business_fact_family).toBe("value_flow");
|
||||
expect(result.data_need_graph?.ranking_need).toBe("top_desc");
|
||||
expect(result.data_need_graph?.clarification_gaps).toEqual(["organization"]);
|
||||
expect(result.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "counterparty_value_or_turnover",
|
||||
explicit_date_scope: "2020",
|
||||
stale_replay_forbidden: true
|
||||
});
|
||||
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
|
||||
});
|
||||
|
||||
it("routes broad business evaluation into business overview discovery without metadata drift", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage:
|
||||
|
|
@ -1536,6 +1566,46 @@ describe("assistant MCP discovery turn input adapter", () => {
|
|||
expect(result.reason_codes).not.toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
|
||||
});
|
||||
|
||||
it("keeps inventory reserve questions as a bounded boundary check instead of generic overview", () => {
|
||||
const orgName =
|
||||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage:
|
||||
"\u041c\u043e\u0436\u043d\u043e \u043b\u0438 \u043f\u043e \u044d\u0442\u0438\u043c \u0434\u0430\u043d\u043d\u044b\u043c \u0442\u043e\u0447\u043d\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u0440\u0435\u0437\u0435\u0440\u0432 \u043f\u043e\u0434 \u043d\u0435\u043b\u0438\u043a\u0432\u0438\u0434\u044b \u043d\u0430 \u0441\u043a\u043b\u0430\u0434\u0435?",
|
||||
assistantTurnMeaning: {
|
||||
asked_domain_family: "business_summary",
|
||||
asked_action_family: "broad_evaluation",
|
||||
unsupported_but_understood_family: "broad_business_evaluation",
|
||||
stale_replay_forbidden: true
|
||||
},
|
||||
followupContext: {
|
||||
previous_discovery_pilot_scope: "business_overview_route_template_v1",
|
||||
previous_filters: {
|
||||
organization: orgName,
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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.data_need_graph?.subject_candidates).toEqual([]);
|
||||
expect(result.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "inventory_reserve_boundary",
|
||||
explicit_organization_scope: orgName,
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "inventory_reserve_liquidation_boundary",
|
||||
stale_replay_forbidden: true
|
||||
});
|
||||
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||
expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_route_candidate");
|
||||
expect(result.reason_codes).toContain("mcp_discovery_data_need_graph_built");
|
||||
});
|
||||
|
||||
it("lets raw business-overview wording override stale exact turnover meaning", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage:
|
||||
|
|
@ -2170,10 +2240,10 @@ describe("assistant MCP discovery turn input adapter", () => {
|
|||
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: "broad_evaluation",
|
||||
asked_action_family: "profit_margin_boundary",
|
||||
explicit_organization_scope: orgName,
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "broad_business_evaluation",
|
||||
unsupported_but_understood_family: "profit_margin_boundary",
|
||||
stale_replay_forbidden: true
|
||||
});
|
||||
expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_route_candidate");
|
||||
|
|
@ -2181,6 +2251,23 @@ describe("assistant MCP discovery turn input adapter", () => {
|
|||
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
|
||||
});
|
||||
|
||||
it("routes legal-entity profit and margin wording to the missing P&L proof-family boundary", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage: "по ООО Альтернатива Плюс за 2020 можно точно сказать чистую прибыль и маржу?"
|
||||
});
|
||||
|
||||
expect(result.adapter_status).toBe("ready");
|
||||
expect(result.should_run_discovery).toBe(true);
|
||||
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: "profit_margin_boundary",
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "profit_margin_boundary"
|
||||
});
|
||||
});
|
||||
|
||||
it("routes organization-level overdue debt wording to business overview instead of exact receivables recipes", () => {
|
||||
const orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
|
|
@ -2201,10 +2288,10 @@ describe("assistant MCP discovery turn input adapter", () => {
|
|||
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: "broad_evaluation",
|
||||
asked_action_family: "debt_due_date_boundary",
|
||||
explicit_organization_scope: orgName,
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "broad_business_evaluation",
|
||||
unsupported_but_understood_family: "debt_due_date_boundary",
|
||||
stale_replay_forbidden: true
|
||||
});
|
||||
expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_route_candidate");
|
||||
|
|
@ -2232,10 +2319,10 @@ describe("assistant MCP discovery turn input adapter", () => {
|
|||
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: "broad_evaluation",
|
||||
asked_action_family: "inventory_reserve_boundary",
|
||||
explicit_organization_scope: orgName,
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "broad_business_evaluation",
|
||||
unsupported_but_understood_family: "inventory_reserve_liquidation_boundary",
|
||||
stale_replay_forbidden: true
|
||||
});
|
||||
expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_route_candidate");
|
||||
|
|
@ -2263,10 +2350,10 @@ describe("assistant MCP discovery turn input adapter", () => {
|
|||
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: "broad_evaluation",
|
||||
asked_action_family: "vendor_risk_procurement_boundary",
|
||||
explicit_organization_scope: orgName,
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "broad_business_evaluation",
|
||||
unsupported_but_understood_family: "vendor_risk_procurement_boundary",
|
||||
stale_replay_forbidden: true
|
||||
});
|
||||
expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_route_candidate");
|
||||
|
|
@ -2708,6 +2795,42 @@ describe("assistant MCP discovery turn input adapter", () => {
|
|||
expect(result.data_need_graph?.clarification_gaps).toEqual(["period"]);
|
||||
});
|
||||
|
||||
it("routes explicit VAT movement wording to metadata-scoped movement evidence over exact VAT aggregate", () => {
|
||||
const orgName =
|
||||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage:
|
||||
"\u043f\u043e\u043a\u0430\u0436\u0438 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f \u043f\u043e \u041d\u0414\u0421 \u0437\u0430 2020 \u043f\u043e \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
|
||||
assistantTurnMeaning: {
|
||||
explicit_intent_candidate: "vat_liability_confirmed_for_tax_period"
|
||||
},
|
||||
predecomposeContract: {
|
||||
entities: { organization: orgName },
|
||||
period: {
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.adapter_status).toBe("ready");
|
||||
expect(result.should_run_discovery).toBe(true);
|
||||
expect(result.semantic_data_need).toBe("movement evidence");
|
||||
expect(result.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "movements",
|
||||
asked_action_family: "list_movements",
|
||||
metadata_scope_hint: "\u041d\u0414\u0421",
|
||||
subject_resolution_optional: true,
|
||||
explicit_organization_scope: orgName,
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "movement_evidence",
|
||||
stale_replay_forbidden: true
|
||||
});
|
||||
expect(result.data_need_graph?.business_fact_family).toBe("movement_evidence");
|
||||
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
|
||||
expect(result.reason_codes).toContain("mcp_discovery_vat_movement_evidence_signal_detected");
|
||||
});
|
||||
|
||||
it("pivots a metadata-scoped subjectless movement retrieval into documents without inventing a counterparty", () => {
|
||||
const orgName =
|
||||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||
|
|
@ -2867,6 +2990,62 @@ describe("assistant MCP discovery turn input adapter", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("prefers semantic organization over noisy raw organization tail for debt due-date overview", () => {
|
||||
const orgName = "ООО Альтернатива Плюс";
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage: "по ООО Альтернатива Плюс на конец 2020 можно точно понять, какая дебиторка просрочена?",
|
||||
predecomposeContract: {
|
||||
entities: { counterparty: orgName, organization: orgName },
|
||||
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
|
||||
}
|
||||
});
|
||||
|
||||
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.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "debt_due_date_boundary",
|
||||
explicit_organization_scope: orgName,
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "debt_due_date_boundary",
|
||||
stale_replay_forbidden: true
|
||||
});
|
||||
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||
});
|
||||
|
||||
it("routes debt due-date boundary follow-up through the business overview lane", () => {
|
||||
const orgName =
|
||||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage:
|
||||
"\u0442\u043e \u0435\u0441\u0442\u044c \u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a\u0443 \u0434\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0435\u043b\u044c\u0437\u044f, \u043a\u043e\u0440\u043e\u0442\u043a\u043e \u043f\u043e\u0447\u0435\u043c\u0443?",
|
||||
followupContext: {
|
||||
previous_discovery_pilot_scope: "business_overview_route_template_v1",
|
||||
previous_filters: {
|
||||
organization: orgName,
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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: "debt_due_date_boundary",
|
||||
explicit_organization_scope: orgName,
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "debt_due_date_boundary",
|
||||
stale_replay_forbidden: true
|
||||
});
|
||||
expect(result.reason_codes).toContain("mcp_discovery_business_overview_continuation_from_followup_context");
|
||||
expect(result.reason_codes).toContain("mcp_discovery_business_overview_debt_due_date_followup_boundary");
|
||||
});
|
||||
|
||||
it("lets raw metadata scope override stale document evidence subject on topic switch", () => {
|
||||
const orgName =
|
||||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||
|
|
@ -2946,6 +3125,44 @@ describe("assistant MCP discovery turn input adapter", () => {
|
|||
expect(result.reason_codes).toContain("mcp_discovery_business_overview_continuation_from_followup_context");
|
||||
});
|
||||
|
||||
it("keeps short profit/loss follow-up on the accounting profit-margin boundary", () => {
|
||||
const orgName =
|
||||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage:
|
||||
"\u0430 \u044d\u0442\u043e \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0438\u043b\u0438 \u0443\u0431\u044b\u0442\u043e\u043a, \u043a\u043e\u0440\u043e\u0442\u043a\u043e?",
|
||||
assistantTurnMeaning: {
|
||||
asked_domain_family: "unknown",
|
||||
asked_action_family: "unknown"
|
||||
},
|
||||
followupContext: {
|
||||
previous_discovery_pilot_scope: "business_overview_route_template_v1",
|
||||
previous_filters: {
|
||||
organization: orgName,
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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.data_need_graph?.subject_candidates).toEqual([]);
|
||||
expect(result.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "profit_margin_boundary",
|
||||
explicit_organization_scope: orgName,
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "profit_margin_boundary",
|
||||
stale_replay_forbidden: true
|
||||
});
|
||||
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||
expect(result.reason_codes).toContain("mcp_discovery_business_overview_continuation_from_followup_context");
|
||||
expect(result.reason_codes).toContain("mcp_discovery_business_overview_profit_margin_followup_boundary");
|
||||
});
|
||||
|
||||
it("keeps detailed money-breakdown follow-up in business overview without pseudo counterparty anchors", () => {
|
||||
const orgName =
|
||||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||
|
|
|
|||
|
|
@ -557,6 +557,45 @@ describe("assistantRoutePolicy", () => {
|
|||
expect(decision.orchestrationContract?.followup_context_detected).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps plain organization clarification in address lane for pending route-candidate scope", () => {
|
||||
const orgName =
|
||||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||
const policy = buildPolicy({
|
||||
resolveAddressToolGateDecision: undefined
|
||||
});
|
||||
|
||||
const decision = policy.resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: orgName,
|
||||
effectiveAddressUserMessage: orgName,
|
||||
followupContext: {
|
||||
previous_intent: "customer_revenue_and_payments",
|
||||
target_intent: "customer_revenue_and_payments",
|
||||
previous_discovery_pilot_scope: "counterparty_value_flow_query_movements_v1",
|
||||
previous_discovery_loop_status: "awaiting_clarification",
|
||||
previous_discovery_loop_selected_chain_id: "value_flow_ranking",
|
||||
previous_discovery_loop_pending_axes: ["organization"],
|
||||
previous_discovery_loop_asked_domain_family: "counterparty_value",
|
||||
previous_discovery_loop_asked_action_family: "turnover",
|
||||
previous_discovery_loop_unsupported_family: "counterparty_value_or_turnover"
|
||||
},
|
||||
llmPreDecomposeMeta: {
|
||||
predecomposeContract: {
|
||||
mode: "unsupported",
|
||||
intent: "unknown",
|
||||
entities: { organization: orgName },
|
||||
semantics: { anchor_kind: "organization", anchor_value: orgName }
|
||||
}
|
||||
},
|
||||
useMock: false
|
||||
});
|
||||
|
||||
expect(decision.runAddressLane).toBe(true);
|
||||
expect(decision.toolGateReason).toBe("followup_context_detected");
|
||||
expect(decision.livingMode).toBe("address_data");
|
||||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||||
expect(decision.orchestrationContract?.hard_meta_mode).toBeNull();
|
||||
});
|
||||
|
||||
it("does not turn short entity follow-up into organization switch just because scope already has an active company", () => {
|
||||
const policy = buildPolicy({
|
||||
resolveAddressToolGateDecision: undefined,
|
||||
|
|
|
|||
|
|
@ -1206,6 +1206,67 @@ describe("assistantTransitionPolicy", () => {
|
|||
period_to: "2020-12-31"
|
||||
});
|
||||
});
|
||||
|
||||
it("carries business overview boundary context through short capability-shaped follow-up", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => null,
|
||||
shouldHandleAsAssistantCapabilityMetaQuery: () => true,
|
||||
hasDataRetrievalRequestSignal: () => false,
|
||||
hasAddressFollowupContextSignal: () => false
|
||||
});
|
||||
const organization = "ООО Альтернатива Плюс";
|
||||
|
||||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||
"то есть просрочку доказать нельзя, коротко почему?",
|
||||
[
|
||||
{
|
||||
role: "assistant",
|
||||
text: "На 2020-12-31 подтвержденной просрочки нет: в договорах срок оплаты не установлен.",
|
||||
debug: {
|
||||
execution_lane: "living_chat",
|
||||
mcp_discovery_response_applied: true,
|
||||
assistant_active_organization: organization,
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
entry_status: "bridge_executed",
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "debt_due_date_boundary",
|
||||
unsupported_but_understood_family: "debt_due_date_boundary",
|
||||
explicit_organization_scope: organization,
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
business_fact_answer_allowed: true,
|
||||
pilot: {
|
||||
pilot_scope: "business_overview_route_template_v1"
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
|
||||
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe(
|
||||
"business_overview_route_template_v1"
|
||||
);
|
||||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||||
organization,
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31"
|
||||
});
|
||||
});
|
||||
it("carries resolved entity candidates from grounded entity-resolution discovery into followup context", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => null,
|
||||
|
|
@ -1506,6 +1567,67 @@ describe("assistantTransitionPolicy", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("treats a plain organization reply as continuation of a pending route-candidate organization scope", () => {
|
||||
const orgName =
|
||||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => ({
|
||||
role: "assistant",
|
||||
text: "\u041d\u0443\u0436\u043d\u043e \u0443\u0442\u043e\u0447\u043d\u0438\u0442\u044c \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044e.",
|
||||
debug: {
|
||||
execution_lane: "living_chat",
|
||||
detected_intent: "customer_revenue_and_payments",
|
||||
mcp_discovery_response_applied: true,
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
entry_status: "bridge_executed",
|
||||
bridge: {
|
||||
bridge_status: "needs_clarification",
|
||||
business_fact_answer_allowed: false,
|
||||
pilot: {
|
||||
pilot_scope: "counterparty_value_flow_query_movements_v1"
|
||||
},
|
||||
loop_state: {
|
||||
schema_version: "assistant_mcp_discovery_loop_state_v1",
|
||||
loop_status: "awaiting_clarification",
|
||||
selected_chain_id: "value_flow_ranking",
|
||||
pilot_scope: "counterparty_value_flow_query_movements_v1",
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
unsupported_but_understood_family: "counterparty_value_or_turnover",
|
||||
ranking_need: "top_desc",
|
||||
pending_axes: ["organization"],
|
||||
provided_axes: ["aggregate_axis", "amount", "coverage_target"],
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
hasAddressFollowupContextSignal: () => false
|
||||
});
|
||||
|
||||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||
orgName,
|
||||
[],
|
||||
null,
|
||||
{
|
||||
predecomposeContract: {
|
||||
mode: "unsupported",
|
||||
intent: "unknown",
|
||||
entities: { organization: orgName },
|
||||
semantics: { anchor_kind: "organization", anchor_value: orgName }
|
||||
}
|
||||
},
|
||||
null
|
||||
);
|
||||
|
||||
expect(carryover?.followupContext?.previous_discovery_loop_status).toBe("awaiting_clarification");
|
||||
expect(carryover?.followupContext?.previous_discovery_loop_selected_chain_id).toBe("value_flow_ranking");
|
||||
expect(carryover?.followupContext?.previous_discovery_loop_pending_axes).toEqual(["organization"]);
|
||||
expect(carryover?.followupContext?.target_intent).toBe("customer_revenue_and_payments");
|
||||
});
|
||||
|
||||
it("carries grounded metadata downstream route hints into followup context", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => null,
|
||||
|
|
|
|||
|
|
@ -114,6 +114,40 @@ describe("counterparty analytics reply builders", () => {
|
|||
expect(reply.text).not.toContain("Топ-");
|
||||
});
|
||||
|
||||
it("does not mistake a year period in top-counterparty wording for a top-year question", () => {
|
||||
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: ["Клиент Б", "Договор Б-1"]
|
||||
}
|
||||
],
|
||||
{
|
||||
userMessage: "какой контрагент принес больше всего денег за 2020 год?",
|
||||
periodFrom: "2020-01-01",
|
||||
periodTo: "2020-12-31"
|
||||
}
|
||||
);
|
||||
|
||||
expect(reply.responseType).toBe("FACTUAL_LIST");
|
||||
expect(reply.text).toContain("Клиент Б");
|
||||
expect(reply.text).not.toContain("Клиент А");
|
||||
expect(reply.text).not.toContain("1. 2020 |");
|
||||
});
|
||||
|
||||
it("explains organization activity age as 1C activity rather than legal age", () => {
|
||||
const reply = composeFactualReply(
|
||||
"counterparty_activity_lifecycle",
|
||||
|
|
|
|||
Loading…
Reference in New Issue