Закрепить 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_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_CHANNEL = process.env.ASSISTANT_MCP_CHANNEL ?? "default";
|
||||||
exports.ASSISTANT_MCP_TIMEOUT_MS = toNumberFlag(process.env.ASSISTANT_MCP_TIMEOUT_MS, 6000);
|
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_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.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");
|
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") ||
|
const periodWasDerivedHeuristically = warnings.includes("period_derived_from_month_phrase") ||
|
||||||
warnings.includes("period_derived_from_year_range_phrase") ||
|
warnings.includes("period_derived_from_year_range_phrase") ||
|
||||||
warnings.includes("period_derived_from_year_phrase");
|
warnings.includes("period_derived_from_year_phrase");
|
||||||
const preserveDerivedPeriodWindow = usesAsOfPrimaryWindow(intent) ||
|
const preserveDerivedPeriodWindow = intent === "inventory_on_hand_as_of_date" ||
|
||||||
intent === "inventory_on_hand_as_of_date" ||
|
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date";
|
intent === "inventory_supplier_stock_overlap_as_of_date";
|
||||||
if (periodWasDerivedHeuristically && !warnings.includes("exact_historical_period_window_requested")) {
|
if (periodWasDerivedHeuristically && !warnings.includes("exact_historical_period_window_requested")) {
|
||||||
warnings.push("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 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 hasInspectionCue = /(?:что\s+с|позици|основан|не\s+хватает|налогов[а-яё]*\s+вывод|вывод|декларац|книга\s+(?:продаж|покупок)|расшифр|разбор)/iu.test(normalized);
|
||||||
const forecastOnlyCue = /(?:прогноз|план|примерн|ориентировочн)/iu.test(normalized) && !hasInspectionCue;
|
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) {
|
function resolveUnicodeAddressIntentBridge(text) {
|
||||||
const normalized = String(text ?? "").trim().toLowerCase();
|
const normalized = String(text ?? "").trim().toLowerCase();
|
||||||
|
|
@ -2044,6 +2045,16 @@ function resolveAddressIntent(userMessage) {
|
||||||
reasons
|
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) &&
|
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) &&
|
/(?:\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);
|
/(?:\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__
|
Сумма __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 = `
|
const VAT_PAYABLE_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
||||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||||
__AS_OF_EXPR__ КАК Период,
|
__AS_OF_EXPR__ КАК Период,
|
||||||
|
|
@ -723,7 +768,7 @@ const BASE_RECIPES = [
|
||||||
purpose: "Build customer value ranking and incoming deal profile from bank inflow docs",
|
purpose: "Build customer value ranking and incoming deal profile from bank inflow docs",
|
||||||
required_filters: [],
|
required_filters: [],
|
||||||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
||||||
default_limit: 20,
|
default_limit: 200,
|
||||||
account_scope_mode: "preferred",
|
account_scope_mode: "preferred",
|
||||||
query_template: "customer_revenue_profile"
|
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",
|
purpose: "Build supplier payout ranking and outgoing deal profile from bank outflow docs",
|
||||||
required_filters: [],
|
required_filters: [],
|
||||||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
||||||
default_limit: 20,
|
default_limit: 200,
|
||||||
account_scope_mode: "preferred",
|
account_scope_mode: "preferred",
|
||||||
query_template: "supplier_payout_profile"
|
query_template: "supplier_payout_profile"
|
||||||
},
|
},
|
||||||
|
|
@ -778,6 +823,17 @@ const BASE_RECIPES = [
|
||||||
account_scope_mode: "preferred",
|
account_scope_mode: "preferred",
|
||||||
query_template: "vat_liability_confirmed_tax_period_profile"
|
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",
|
recipe_id: "address_inventory_on_hand_as_of_date_v1",
|
||||||
intent: "inventory_on_hand_as_of_date",
|
intent: "inventory_on_hand_as_of_date",
|
||||||
|
|
@ -888,6 +944,17 @@ const BASE_RECIPES = [
|
||||||
account_scope_mode: "strict",
|
account_scope_mode: "strict",
|
||||||
query_template: "open_contracts_confirmed_as_of_balance_profile"
|
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",
|
recipe_id: "address_contracts_by_counterparty_v1",
|
||||||
intent: "list_contracts_by_counterparty",
|
intent: "list_contracts_by_counterparty",
|
||||||
|
|
@ -1093,6 +1160,32 @@ function buildContractValueWhereClause(filters, fieldPath, contractFieldPath) {
|
||||||
`${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) {
|
function normalizeAccountTokenForQuery(value) {
|
||||||
const source = String(value ?? "").trim().replace(",", ".");
|
const source = String(value ?? "").trim().replace(",", ".");
|
||||||
const match = source.match(/^(\d{2})(?:\.(\d{1,2}))?/);
|
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}"`);
|
const clauses = normalizedPrefixes.map((prefix) => `ПОДСТРОКА(ЕСТЬNULL(${fieldPath}.Код, ""), 1, ${prefix.length}) = "${prefix}"`);
|
||||||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
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) {
|
function buildInventoryMovementQuery(filters, resolvedLimit, side) {
|
||||||
const debitPredicate = buildAccountPrefixPredicate("Движения.СчетДт", ["41.01"]);
|
const debitPredicate = buildAccountPrefixPredicate("Движения.СчетДт", ["41.01"]);
|
||||||
const creditPredicate = buildAccountPrefixPredicate("Движения.СчетКт", ["41.01"]);
|
const creditPredicate = buildAccountPrefixPredicate("Движения.СчетКт", ["41.01"]);
|
||||||
|
|
@ -1246,6 +1368,148 @@ function buildCounterpartyReferenceCondition(filters, fieldPaths) {
|
||||||
}
|
}
|
||||||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
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) {
|
function buildInventorySaleDocumentQuery(filters, resolvedLimit) {
|
||||||
const itemCondition = buildInventoryItemReferenceCondition(filters, ["Товары.Номенклатура"]);
|
const itemCondition = buildInventoryItemReferenceCondition(filters, ["Товары.Номенклатура"]);
|
||||||
return INVENTORY_SALE_DOCUMENTS_QUERY_TEMPLATE
|
return INVENTORY_SALE_DOCUMENTS_QUERY_TEMPLATE
|
||||||
|
|
@ -1324,6 +1588,8 @@ function maxLimitForIntent(intent) {
|
||||||
intent === "contract_usage_and_value" ||
|
intent === "contract_usage_and_value" ||
|
||||||
intent === "vat_payable_forecast" ||
|
intent === "vat_payable_forecast" ||
|
||||||
intent === "vat_liability_confirmed_for_tax_period" ||
|
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_on_hand_as_of_date" ||
|
||||||
intent === "inventory_purchase_provenance_for_item" ||
|
intent === "inventory_purchase_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_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 === "counterparty_roles_profile" ||
|
||||||
recipe.query_template === "contract_usage_profile" ||
|
recipe.query_template === "contract_usage_profile" ||
|
||||||
recipe.query_template === "vat_payable_forecast_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)
|
const baseLimit = typeof filters.limit === "number" && Number.isFinite(filters.limit)
|
||||||
? Math.max(1, Math.min(maxLimit, Math.trunc(filters.limit)))
|
? Math.max(1, Math.min(maxLimit, Math.trunc(filters.limit)))
|
||||||
: recipe.default_limit;
|
: recipe.default_limit;
|
||||||
|
|
@ -1467,97 +1734,65 @@ function buildAddressRecipePlan(recipe, filters) {
|
||||||
.replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период"))
|
.replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период"))
|
||||||
.replaceAll("__PERIOD_TO_EXPR__", periodToExpr);
|
.replaceAll("__PERIOD_TO_EXPR__", periodToExpr);
|
||||||
})()
|
})()
|
||||||
: recipe.query_template === "vat_payable_confirmed_as_of_balance_profile"
|
: recipe.query_template === "accounting_financial_result_profile"
|
||||||
? (() => {
|
? buildAccountingFinancialResultQuery(filters)
|
||||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
: recipe.query_template === "debt_due_date_aging_profile"
|
||||||
? toDateTimeExpr(filters.as_of_date, true)
|
? buildDebtDueDateAgingQuery(filters, resolvedLimit)
|
||||||
: null) ??
|
: recipe.query_template === "vat_payable_confirmed_as_of_balance_profile"
|
||||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
? (() => {
|
||||||
? toDateTimeExpr(filters.period_to, true)
|
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||||
: null) ??
|
? toDateTimeExpr(filters.as_of_date, true)
|
||||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
|
||||||
? toDateTimeExpr(filters.period_from, true)
|
|
||||||
: null) ??
|
|
||||||
"ТЕКУЩАЯДАТА()";
|
|
||||||
return VAT_PAYABLE_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
|
||||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
|
||||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
|
||||||
.replaceAll("__VAT_PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", config_1.VAT_PAYABLE_68_PREFIXES))
|
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
|
||||||
})()
|
|
||||||
: recipe.query_template === "inventory_on_hand_as_of_balance_profile"
|
|
||||||
? (() => {
|
|
||||||
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) ??
|
: null) ??
|
||||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||||
? toDateTimeExpr(filters.period_from, true)
|
? toDateTimeExpr(filters.period_to, true)
|
||||||
: null) ??
|
: null) ??
|
||||||
"ТЕКУЩАЯДАТА()";
|
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||||
return INVENTORY_ON_HAND_AS_OF_QUERY_TEMPLATE
|
? toDateTimeExpr(filters.period_from, true)
|
||||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
: null) ??
|
||||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
"ТЕКУЩАЯДАТА()";
|
||||||
.replaceAll("__INVENTORY_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["41.01"]))
|
return VAT_PAYABLE_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||||
})()
|
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||||
: recipe.query_template === "inventory_purchase_provenance_profile"
|
.replaceAll("__VAT_PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", config_1.VAT_PAYABLE_68_PREFIXES))
|
||||||
? buildInventoryPurchaseDocumentQuery(filters, resolvedLimit)
|
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||||
: recipe.query_template === "inventory_purchase_documents_profile"
|
})()
|
||||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
: recipe.query_template === "inventory_on_hand_as_of_balance_profile"
|
||||||
: recipe.query_template === "inventory_supplier_stock_overlap_profile"
|
? (() => {
|
||||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||||
: recipe.query_template === "inventory_sale_trace_profile"
|
? toDateTimeExpr(filters.as_of_date, true)
|
||||||
? buildInventorySaleDocumentQuery(filters, resolvedLimit)
|
: null) ??
|
||||||
: recipe.query_template === "inventory_profitability_profile"
|
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
? toDateTimeExpr(filters.period_to, true)
|
||||||
: recipe.query_template === "inventory_trading_margin_proxy_profile"
|
: null) ??
|
||||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||||
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
? toDateTimeExpr(filters.period_from, true)
|
||||||
|
: null) ??
|
||||||
|
"ТЕКУЩАЯДАТА()";
|
||||||
|
return INVENTORY_ON_HAND_AS_OF_QUERY_TEMPLATE
|
||||||
|
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||||
|
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||||
|
.replaceAll("__INVENTORY_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["41.01"]))
|
||||||
|
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||||
|
})()
|
||||||
|
: recipe.query_template === "inventory_purchase_provenance_profile"
|
||||||
|
? buildInventoryPurchaseDocumentQuery(filters, resolvedLimit)
|
||||||
|
: recipe.query_template === "inventory_purchase_documents_profile"
|
||||||
|
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||||
|
: recipe.query_template === "inventory_supplier_stock_overlap_profile"
|
||||||
|
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||||
|
: recipe.query_template === "inventory_sale_trace_profile"
|
||||||
|
? buildInventorySaleDocumentQuery(filters, resolvedLimit)
|
||||||
|
: recipe.query_template === "inventory_profitability_profile"
|
||||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
: recipe.query_template === "inventory_trading_margin_proxy_profile"
|
||||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "contracts_by_counterparty_profile"
|
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
||||||
? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit))
|
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "open_contracts_confirmed_as_of_balance_profile"
|
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
||||||
? (() => {
|
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
: recipe.query_template === "contracts_by_counterparty_profile"
|
||||||
? toDateTimeExpr(filters.as_of_date, true)
|
? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||||
: null) ??
|
: recipe.query_template === "open_contracts_confirmed_as_of_balance_profile"
|
||||||
(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) ??
|
|
||||||
"ТЕКУЩАЯДАТА()";
|
|
||||||
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
|
||||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
|
||||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
|
||||||
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "62", "76"]))
|
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
|
||||||
})()
|
|
||||||
: recipe.query_template === "payables_confirmed_as_of_balance_profile"
|
|
||||||
? (() => {
|
|
||||||
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) ??
|
|
||||||
"ТЕКУЩАЯДАТА()";
|
|
||||||
return PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
|
||||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
|
||||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
|
||||||
.replaceAll("__PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
|
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
|
||||||
})()
|
|
||||||
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
|
|
||||||
? (() => {
|
? (() => {
|
||||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||||
? toDateTimeExpr(filters.as_of_date, true)
|
? toDateTimeExpr(filters.as_of_date, true)
|
||||||
|
|
@ -1569,23 +1804,59 @@ function buildAddressRecipePlan(recipe, filters) {
|
||||||
? toDateTimeExpr(filters.period_from, true)
|
? toDateTimeExpr(filters.period_from, true)
|
||||||
: null) ??
|
: null) ??
|
||||||
"ТЕКУЩАЯДАТА()";
|
"ТЕКУЩАЯДАТА()";
|
||||||
return RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||||
.replaceAll("__RECEIVABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
|
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "62", "76"]))
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||||
})()
|
})()
|
||||||
: MOVEMENTS_QUERY_TEMPLATE
|
: recipe.query_template === "payables_confirmed_as_of_balance_profile"
|
||||||
.replace("__LIMIT__", String(resolvedLimit))
|
? (() => {
|
||||||
.replace("__WHERE_CLAUSE__", (() => {
|
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||||
const extraConditions = [];
|
? toDateTimeExpr(filters.as_of_date, true)
|
||||||
const accountCondition = buildMovementAccountCondition(filters);
|
: null) ??
|
||||||
if (accountCondition) {
|
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||||
extraConditions.push(accountCondition);
|
? toDateTimeExpr(filters.period_to, true)
|
||||||
}
|
: null) ??
|
||||||
return buildWhereClause(filters, "Движения.Период", extraConditions);
|
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||||
})())
|
? toDateTimeExpr(filters.period_from, true)
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
: null) ??
|
||||||
|
"ТЕКУЩАЯДАТА()";
|
||||||
|
return PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||||
|
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||||
|
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||||
|
.replaceAll("__PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
|
||||||
|
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||||
|
})()
|
||||||
|
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
|
||||||
|
? (() => {
|
||||||
|
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) ??
|
||||||
|
"ТЕКУЩАЯДАТА()";
|
||||||
|
return RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||||
|
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||||
|
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||||
|
.replaceAll("__RECEIVABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
|
||||||
|
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||||
|
})()
|
||||||
|
: MOVEMENTS_QUERY_TEMPLATE
|
||||||
|
.replace("__LIMIT__", String(resolvedLimit))
|
||||||
|
.replace("__WHERE_CLAUSE__", (() => {
|
||||||
|
const extraConditions = [];
|
||||||
|
const accountCondition = buildMovementAccountCondition(filters);
|
||||||
|
if (accountCondition) {
|
||||||
|
extraConditions.push(accountCondition);
|
||||||
|
}
|
||||||
|
return buildWhereClause(filters, "Движения.Период", extraConditions);
|
||||||
|
})())
|
||||||
|
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||||
return {
|
return {
|
||||||
recipe,
|
recipe,
|
||||||
query,
|
query,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.contractCandidatesFromRows = contractCandidatesFromRows;
|
exports.contractCandidatesFromRows = contractCandidatesFromRows;
|
||||||
exports.composeFactualReply = composeFactualReply;
|
exports.composeFactualReply = composeFactualReply;
|
||||||
exports.inferReplyType = inferReplyType;
|
exports.inferReplyType = inferReplyType;
|
||||||
|
const assistantOrganizationMatcher_1 = require("../assistantOrganizationMatcher");
|
||||||
const replyPackaging_1 = require("./replyPackaging");
|
const replyPackaging_1 = require("./replyPackaging");
|
||||||
const counterpartyAnalyticsReplyBuilders_1 = require("./counterpartyAnalyticsReplyBuilders");
|
const counterpartyAnalyticsReplyBuilders_1 = require("./counterpartyAnalyticsReplyBuilders");
|
||||||
const inventoryReplyBuilders_1 = require("./inventoryReplyBuilders");
|
const inventoryReplyBuilders_1 = require("./inventoryReplyBuilders");
|
||||||
|
|
@ -515,10 +516,12 @@ function detectValueRankingFocus(userMessage) {
|
||||||
if (asksTotalMoneyEarned) {
|
if (asksTotalMoneyEarned) {
|
||||||
return "total_flow";
|
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) &&
|
const asksYearlyRevenueRanking = /(?:доходн|выручк|оборот|прибыл|деньг|денег|revenue|turnover|income)/iu.test(text) &&
|
||||||
/(?:год|года|годы|year|years|по\s+годам)/iu.test(text) &&
|
/(?:год|года|годы|year|years|по\s+годам)/iu.test(text) &&
|
||||||
/(?:сам(?:ый|ая|ое|ые)|топ|луч|best|max|наибольш|больше)/iu.test(text);
|
/(?:сам(?:ый|ая|ое|ые)|топ|луч|best|max|наибольш|больше)/iu.test(text);
|
||||||
if (asksYearlyRevenueRanking) {
|
if (asksYearlyRevenueRanking && (!hasCounterpartyRankingSubject || asksExplicitYearBreakdown)) {
|
||||||
return "top_years_by_total";
|
return "top_years_by_total";
|
||||||
}
|
}
|
||||||
if (/(?:сам(?:ый|ая|ое|ые)\s+высок[а-яё]*|highest|largest)\s+чек|(?:max\s+check|чек\s+макс)/iu.test(text)) {
|
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 nonErrorProbeRows = orderedProbeRows.filter((item) => item.status !== "error");
|
||||||
const visibleProbeRows = (nonErrorProbeRows.length > 0 ? nonErrorProbeRows : orderedProbeRows).slice(0, 6);
|
const visibleProbeRows = (nonErrorProbeRows.length > 0 ? nonErrorProbeRows : orderedProbeRows).slice(0, 6);
|
||||||
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").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 (visibleProbeRows.length > 0) {
|
if (visibleProbeRows.length > 0) {
|
||||||
lines.push(...visibleProbeRows.map((item, index) => {
|
lines.push(...visibleProbeRows.map((item, index) => {
|
||||||
const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName;
|
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-источники показаны для проверки покрытия.");
|
lines.push("- Сумма прогноза выше рассчитана строго по оборотам 68.02*/19*; прямые VAT-источники показаны для проверки покрытия.");
|
||||||
}
|
}
|
||||||
else if (vatProbe && vatProbe.status === "error") {
|
else if (vatProbe && vatProbe.status === "error") {
|
||||||
lines.push("", "Покрытие VAT-источников через MCP: probe завершился ошибкой, поэтому использован только базовый контур 68.02*/19*.");
|
lines.push("", "Покрытие VAT-источников в 1С: дополнительная проверка завершилась ошибкой, поэтому использован только базовый контур 68.02*/19*.");
|
||||||
}
|
}
|
||||||
if (!vatActivityDetected) {
|
if (!vatActivityDetected) {
|
||||||
lines.push(`В выбранном окне не найдено движений по НДС-субсчетам 68.02*/19*; поэтому оперативный прогноз к уплате равен ${formatForecastMoney(0)}.`);
|
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 periodWindowLabel = options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null;
|
||||||
const formatConfirmedMoney = (value) => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
const formatConfirmedMoney = (value) => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
||||||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
const vatProbe = options.vatDirectSourceProbe ?? null;
|
||||||
|
const organizationLabel = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.organizationHint);
|
||||||
|
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
|
||||||
const lines = [
|
const lines = [
|
||||||
`Коротко: подтвержденный НДС к уплате за налоговый период — ${formatConfirmedMoney(vatToPay)}.`,
|
`Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`,
|
||||||
`Если смотреть на возможный перенос или переплату, получается ${formatConfirmedMoney(carryoverOrOverpayment)}.`,
|
`Если смотреть на возможный перенос или переплату, получается ${formatConfirmedMoney(carryoverOrOverpayment)}.`,
|
||||||
"Это подтвержденный расчет по регистрам книг продаж и покупок, без surrogate-формулы 68/19.",
|
"Это подтвержденный расчет по регистрам книг продаж и покупок, без surrogate-формулы 68/19.",
|
||||||
"",
|
"",
|
||||||
"Что вошло в расчет:",
|
"Что вошло в расчет:",
|
||||||
|
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
|
||||||
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
|
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
|
||||||
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
|
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
|
||||||
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
|
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
|
||||||
|
|
@ -2602,14 +2608,14 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||||
if (vatProbe && vatProbe.status === "ok") {
|
if (vatProbe && vatProbe.status === "ok") {
|
||||||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
||||||
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").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) {
|
if (vatProbe.errors.length > 0) {
|
||||||
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
||||||
}
|
}
|
||||||
lines.push("- Сумма расчета выше получена по книгам продаж/покупок; probe использован для контроля полноты VAT-источников.");
|
lines.push("- Сумма расчета выше получена по книгам продаж/покупок; дополнительная проверка использована для контроля полноты VAT-источников.");
|
||||||
}
|
}
|
||||||
else if (vatProbe && vatProbe.status === "error") {
|
else if (vatProbe && vatProbe.status === "error") {
|
||||||
lines.push("", "Покрытие VAT-источников через MCP: дополнительный probe недоступен (например, timeout metadata).", "Итоговая сумма НДС выше рассчитана по основному маршруту книг продаж/покупок; probe влияет только на диагностику покрытия.");
|
lines.push("", "Покрытие VAT-источников в 1С: дополнительная проверка недоступна, поэтому использован основной бухгалтерский срез.", "Итоговая сумма НДС выше рассчитана по основному маршруту книг продаж/покупок; probe влияет только на диагностику покрытия.");
|
||||||
if (vatProbe.errors.length > 0) {
|
if (vatProbe.errors.length > 0) {
|
||||||
lines.push(`- Детали probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
lines.push(`- Детали probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
||||||
}
|
}
|
||||||
|
|
@ -2679,7 +2685,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
const vatProbe = options.vatDirectSourceProbe ?? null;
|
||||||
if (vatProbe && vatProbe.status === "ok") {
|
if (vatProbe && vatProbe.status === "ok") {
|
||||||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
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) {
|
if (vatProbe.probedSources.length > 0) {
|
||||||
lines.push(...vatProbe.probedSources.slice(0, 4).map((item, index) => {
|
lines.push(...vatProbe.probedSources.slice(0, 4).map((item, index) => {
|
||||||
const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName;
|
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") {
|
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. Подтвержденные позиции");
|
lines.push("", "Блок 3. Сводка", `- Строк в выборке: ${formatNumberWithDots(rows.length)}.`, `- Подтвержденных позиций по НДС: ${formatNumberWithDots(accountRows.length)}.`, "", "Блок 4. Подтвержденные позиции");
|
||||||
if (accountRows.length > 0) {
|
if (accountRows.length > 0) {
|
||||||
|
|
|
||||||
|
|
@ -48,9 +48,9 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||||
const includeTotal = focus === "full_profile" || focus === "total_only";
|
const includeTotal = focus === "full_profile" || focus === "total_only";
|
||||||
const includeRoles = focus === "full_profile" || focus === "roles_only";
|
const includeRoles = focus === "full_profile" || focus === "roles_only";
|
||||||
const directLead = focus === "suppliers_only"
|
const directLead = focus === "suppliers_only"
|
||||||
? `Поставщиков (только supplier-роль): ${supplierOnly}.`
|
? `Поставщиков с ролью поставщика: ${supplierOnly}.`
|
||||||
: focus === "customers_only"
|
: focus === "customers_only"
|
||||||
? `Заказчиков (только customer-роль): ${customerOnly}.`
|
? `Заказчиков с ролью покупателя: ${customerOnly}.`
|
||||||
: focus === "mixed_only"
|
: focus === "mixed_only"
|
||||||
? `Контрагентов со смешанной ролью: ${mixedActive}.`
|
? `Контрагентов со смешанной ролью: ${mixedActive}.`
|
||||||
: includeTotal && totalCounterparties > 0
|
: includeTotal && totalCounterparties > 0
|
||||||
|
|
@ -74,9 +74,9 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||||
}
|
}
|
||||||
if (includeRoles) {
|
if (includeRoles) {
|
||||||
if (resolvedActive > 0 || activeCounterparties > 0) {
|
if (resolvedActive > 0 || activeCounterparties > 0) {
|
||||||
lines.push("Роли контрагентов по активности:");
|
lines.push("Распределение ролей по активности:");
|
||||||
lines.push(`Заказчики (только customer-роль): ${customerOnly}.`);
|
lines.push(`Заказчики с ролью покупателя: ${customerOnly}.`);
|
||||||
lines.push(`Поставщики (только supplier-роль): ${supplierOnly}.`);
|
lines.push(`Поставщики с ролью поставщика: ${supplierOnly}.`);
|
||||||
lines.push(`Смешанные (и покупатель, и поставщик): ${mixedActive}.`);
|
lines.push(`Смешанные (и покупатель, и поставщик): ${mixedActive}.`);
|
||||||
lines.push(`4. Всего активных контрагентов: ${activeCounterparties}.`);
|
lines.push(`4. Всего активных контрагентов: ${activeCounterparties}.`);
|
||||||
if (otherCounterparties !== null) {
|
if (otherCounterparties !== null) {
|
||||||
|
|
@ -88,10 +88,10 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (focus === "suppliers_only") {
|
if (focus === "suppliers_only") {
|
||||||
lines.push(`Поставщиков (только supplier-роль): ${supplierOnly}.`);
|
lines.push(`Поставщиков с ролью поставщика: ${supplierOnly}.`);
|
||||||
}
|
}
|
||||||
if (focus === "customers_only") {
|
if (focus === "customers_only") {
|
||||||
lines.push(`Заказчиков (только customer-роль): ${customerOnly}.`);
|
lines.push(`Заказчиков с ролью покупателя: ${customerOnly}.`);
|
||||||
}
|
}
|
||||||
if (focus === "mixed_only") {
|
if (focus === "mixed_only") {
|
||||||
lines.push(`Контрагентов со смешанной ролью: ${mixedActive}.`);
|
lines.push(`Контрагентов со смешанной ролью: ${mixedActive}.`);
|
||||||
|
|
@ -387,6 +387,11 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||||
const limit = deps.detectRankingLimit(options.userMessage, 20);
|
const limit = deps.detectRankingLimit(options.userMessage, 20);
|
||||||
const minOpsForAvgCheck = deps.detectMinOpsForAvgCheck(options.userMessage);
|
const minOpsForAvgCheck = deps.detectMinOpsForAvgCheck(options.userMessage);
|
||||||
const normalizedQuestion = deps.normalizeQuestionText(options.userMessage);
|
const 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 byCounterparty = new Map();
|
||||||
const byYear = new Map();
|
const byYear = new Map();
|
||||||
const deals = [];
|
const deals = [];
|
||||||
|
|
@ -554,7 +559,7 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||||
? `Топ-${visible.length} поставщиков по максимальной разовой выплате:`
|
? `Топ-${visible.length} поставщиков по максимальной разовой выплате:`
|
||||||
: `Топ-${visible.length} заказчиков по максимальной сумме одной входящей операции:`;
|
: `Топ-${visible.length} заказчиков по максимальной сумме одной входящей операции:`;
|
||||||
lines.unshift(heading);
|
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);
|
return (0, replyContracts_1.buildFactualListReply)(lines);
|
||||||
}
|
}
|
||||||
if (focus === "top_by_avg_check_min_ops") {
|
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)}`));
|
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);
|
return (0, replyContracts_1.buildFactualListReply)(lines);
|
||||||
}
|
}
|
||||||
const visible = rankedByTotal.slice(0, limit);
|
const visible = rankedByTotal.slice(0, effectiveLimit);
|
||||||
const singleCandidateOnly = rankedByTotal.length === 1;
|
const singleCandidateOnly = rankedByTotal.length === 1;
|
||||||
|
const rankingPeriodLabel = options.periodFrom && options.periodTo
|
||||||
|
? `за период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)}`
|
||||||
|
: "за доступное время";
|
||||||
const heading = singleCandidateOnly
|
const heading = singleCandidateOnly
|
||||||
? isSupplier
|
? isSupplier
|
||||||
? "Найденный поставщик по сумме выплат:"
|
? "Найденный поставщик по сумме выплат:"
|
||||||
|
|
@ -603,14 +611,17 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||||
: `Топ-${visible.length} заказчиков по сумме поступлений:`;
|
: `Топ-${visible.length} заказчиков по сумме поступлений:`;
|
||||||
const leadingCounterparty = visible[0] ?? null;
|
const leadingCounterparty = visible[0] ?? null;
|
||||||
lines.unshift(heading);
|
lines.unshift(heading);
|
||||||
|
if (options.periodFrom && options.periodTo) {
|
||||||
|
lines.push(`Период рейтинга: ${rankingPeriodLabel}.`);
|
||||||
|
}
|
||||||
if (leadingCounterparty) {
|
if (leadingCounterparty) {
|
||||||
const directAnswerLine = singleCandidateOnly
|
const directAnswerLine = singleCandidateOnly
|
||||||
? isSupplier
|
? isSupplier
|
||||||
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
||||||
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг; сумма является денежным потоком, а не чистой прибылью.`
|
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг; сумма является денежным потоком, а не чистой прибылью.`
|
||||||
: isSupplier
|
: isSupplier
|
||||||
? `Крупнейший поставщик по подтвержденным выплатам за доступное время: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
? `Крупнейший поставщик по подтвержденным выплатам ${rankingPeriodLabel}: ${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} операциям). Это денежный поток, а не чистая прибыль.`;
|
||||||
lines.unshift(directAnswerLine);
|
lines.unshift(directAnswerLine);
|
||||||
}
|
}
|
||||||
lines.push(...visible.map((item, index) => {
|
lines.push(...visible.map((item, index) => {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION = void 0;
|
exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION = void 0;
|
||||||
exports.buildAssistantMcpDiscoveryAnswerDraft = buildAssistantMcpDiscoveryAnswerDraft;
|
exports.buildAssistantMcpDiscoveryAnswerDraft = buildAssistantMcpDiscoveryAnswerDraft;
|
||||||
|
const counterpartyRoleHeuristics_1 = require("./counterpartyRoleHeuristics");
|
||||||
exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION = "assistant_mcp_discovery_answer_draft_v1";
|
exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION = "assistant_mcp_discovery_answer_draft_v1";
|
||||||
function normalizeReasonCode(value) {
|
function normalizeReasonCode(value) {
|
||||||
const normalized = value
|
const normalized = value
|
||||||
|
|
@ -371,6 +372,26 @@ function metadataRouteFamilyLabelRu(routeFamily) {
|
||||||
}
|
}
|
||||||
return null;
|
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) {
|
function businessOverviewInventoryUnknownLabel(overview) {
|
||||||
if (overview.inventory_staleness_risk_proxy) {
|
if (overview.inventory_staleness_risk_proxy) {
|
||||||
return "резервы/списания/ликвидационная стоимость склада";
|
return "резервы/списания/ликвидационная стоимость склада";
|
||||||
|
|
@ -433,6 +454,67 @@ function inlineBusinessOverviewAmount(value) {
|
||||||
.replace(/\s*руб\.$/u, " рублей")
|
.replace(/\s*руб\.$/u, " рублей")
|
||||||
.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) {
|
function businessOverviewHeadlineMetricsLine(overview) {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (overview.incoming_customer_revenue.rows_with_amount > 0) {
|
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) {
|
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)}`);
|
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);
|
const strongestIncomingYear = businessOverviewStrongestIncomingYear(overview);
|
||||||
if (strongestIncomingYear) {
|
if (strongestIncomingYear) {
|
||||||
parts.push(`самый сильный год по подтвержденным входящим поступлениям ${strongestIncomingYear.year_bucket}: ${inlineBusinessOverviewAmount(strongestIncomingYear.incoming_total_amount_human_ru)}`);
|
parts.push(`самый сильный год по подтвержденным входящим поступлениям ${strongestIncomingYear.year_bucket}: ${inlineBusinessOverviewAmount(strongestIncomingYear.incoming_total_amount_human_ru)}`);
|
||||||
}
|
}
|
||||||
return parts.length > 0
|
return parts.length > 0
|
||||||
? `${parts.join("; ")}. Это operating-flow proxy по найденным строкам, не бухгалтерская прибыль и не финрезультат`
|
? overview.accounting_financial_result
|
||||||
|
? `${parts.join("; ")}. Финрезультат ограничен найденными строками 1С и не является внешним аудитом или юридически подтвержденной отчетностью`
|
||||||
|
: `${parts.join("; ")}. Это operating-flow proxy по найденным строкам, не бухгалтерская прибыль и не финрезультат`
|
||||||
: null;
|
: 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) {
|
function headlineFor(mode, pilot) {
|
||||||
const askedMonthlyBreakdown = pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
|
const askedMonthlyBreakdown = pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
|
||||||
pilot.derived_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") {
|
if (isBusinessOverviewPilot(pilot) && pilot.derived_business_overview && mode === "confirmed_with_bounded_inference") {
|
||||||
const overview = pilot.derived_business_overview;
|
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 = [];
|
const families = [];
|
||||||
if (overview.incoming_customer_revenue.rows_with_amount > 0 ||
|
if (overview.incoming_customer_revenue.rows_with_amount > 0 ||
|
||||||
overview.outgoing_supplier_payout.rows_with_amount > 0) {
|
overview.outgoing_supplier_payout.rows_with_amount > 0) {
|
||||||
|
|
@ -492,6 +661,9 @@ function headlineFor(mode, pilot) {
|
||||||
if (overview.tax_position) {
|
if (overview.tax_position) {
|
||||||
families.push("НДС-позиция");
|
families.push("НДС-позиция");
|
||||||
}
|
}
|
||||||
|
if (overview.accounting_financial_result) {
|
||||||
|
families.push("учетный финрезультат 90/91/99");
|
||||||
|
}
|
||||||
if (overview.trading_margin_proxy) {
|
if (overview.trading_margin_proxy) {
|
||||||
families.push("торговый margin proxy");
|
families.push("торговый margin proxy");
|
||||||
}
|
}
|
||||||
|
|
@ -507,6 +679,9 @@ function headlineFor(mode, pilot) {
|
||||||
if (overview.debt_staleness_risk_proxy) {
|
if (overview.debt_staleness_risk_proxy) {
|
||||||
families.push("staleness risk proxy открытых расчетов");
|
families.push("staleness risk proxy открытых расчетов");
|
||||||
}
|
}
|
||||||
|
if (overview.debt_due_date_aging) {
|
||||||
|
families.push("due-date aging открытых расчетов");
|
||||||
|
}
|
||||||
if (overview.inventory_position) {
|
if (overview.inventory_position) {
|
||||||
families.push("складской срез на дату");
|
families.push("складской срез на дату");
|
||||||
}
|
}
|
||||||
|
|
@ -516,18 +691,22 @@ function headlineFor(mode, pilot) {
|
||||||
if (overview.inventory_staleness_risk_proxy) {
|
if (overview.inventory_staleness_risk_proxy) {
|
||||||
families.push("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) {
|
if (!overview.tax_position) {
|
||||||
unknownFamilies.push("НДС");
|
unknownFamilies.push("НДС");
|
||||||
}
|
}
|
||||||
if (!overview.debt_position) {
|
if (!overview.debt_position) {
|
||||||
unknownFamilies.push("долговой срез");
|
unknownFamilies.push("долговой срез");
|
||||||
}
|
}
|
||||||
unknownFamilies.push(overview.debt_staleness_risk_proxy
|
if (!overview.debt_due_date_aging) {
|
||||||
? "договорные сроки оплаты/due-date просрочка"
|
unknownFamilies.push(overview.debt_staleness_risk_proxy
|
||||||
: overview.debt_open_settlement_quality
|
? "договорные сроки оплаты/due-date просрочка"
|
||||||
? "due-date просрочка"
|
: overview.debt_open_settlement_quality
|
||||||
: "качество открытых расчетов");
|
? "due-date просрочка"
|
||||||
|
: "качество открытых расчетов");
|
||||||
|
}
|
||||||
unknownFamilies.push(businessOverviewInventoryUnknownLabel(overview));
|
unknownFamilies.push(businessOverviewInventoryUnknownLabel(overview));
|
||||||
const metricLead = businessOverviewHeadlineMetricsLine(overview);
|
const metricLead = businessOverviewHeadlineMetricsLine(overview);
|
||||||
if (metricLead) {
|
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 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 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 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 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 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.");
|
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) {
|
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.");
|
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) {
|
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 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.");
|
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)) {
|
if (isDocumentPilot(pilot)) {
|
||||||
claims.push("Do not claim full document history outside the checked period.");
|
claims.push("Do not claim full document history outside the checked period.");
|
||||||
|
|
@ -885,24 +1072,38 @@ function derivedRankedValueFlowConfirmedLine(pilot) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const leader = ranking.ranked_values[0];
|
const leader = ranking.ranked_values[0];
|
||||||
|
const leaderLooksFinancial = isFinancialInstitutionBucket(leader);
|
||||||
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
|
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
|
||||||
const period = ranking.period_scope ? ` за период ${ranking.period_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) {
|
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
|
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.value_flow_direction === "outgoing_supplier_payout"
|
||||||
? "Меньше всего заплатили контрагенту"
|
? "Крупнейший получатель исходящих денег"
|
||||||
: "Меньше всего денег принёс контрагент"
|
: "Крупнейший входящий денежный источник"
|
||||||
: ranking.value_flow_direction === "outgoing_supplier_payout"
|
: ranking.ranking_need === "bottom_asc"
|
||||||
? "Больше всего заплатили контрагенту"
|
? ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||||
: "Больше всего денег принёс контрагент";
|
? "Меньше всего заплатили контрагенту"
|
||||||
|
: "Меньше всего денег принёс контрагент"
|
||||||
|
: ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||||
|
? "Больше всего заплатили контрагенту"
|
||||||
|
: "Больше всего денег принёс контрагент";
|
||||||
const tail = ranking.ranked_values
|
const tail = ranking.ranked_values
|
||||||
.slice(1, 3)
|
.slice(1, 3)
|
||||||
.map((bucket) => `${bucket.axis_value} — ${bucket.total_amount_human_ru}`)
|
.map((bucket) => `${bucket.axis_value} — ${bucket.total_amount_human_ru}`)
|
||||||
|
|
@ -911,7 +1112,7 @@ function derivedRankedValueFlowConfirmedLine(pilot) {
|
||||||
const limitCaveat = ranking.coverage_limited_by_probe_limit
|
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) {
|
function derivedValueFlowConfirmedLine(pilot) {
|
||||||
const flow = pilot.derived_value_flow;
|
const flow = pilot.derived_value_flow;
|
||||||
|
|
@ -1070,13 +1271,13 @@ function derivedBusinessOverviewConfirmedLines(pilot) {
|
||||||
if (strongestIncomingYear) {
|
if (strongestIncomingYear) {
|
||||||
lines.push(`Самый сильный год по подтвержденным входящим поступлениям: ${strongestIncomingYear.year_bucket} — ${strongestIncomingYear.incoming_total_amount_human_ru} по ${strongestIncomingYear.incoming_rows_with_amount} строкам с суммой. Это не бухгалтерская прибыль.`);
|
lines.push(`Самый сильный год по подтвержденным входящим поступлениям: ${strongestIncomingYear.year_bucket} — ${strongestIncomingYear.incoming_total_amount_human_ru} по ${strongestIncomingYear.incoming_rows_with_amount} строкам с суммой. Это не бухгалтерская прибыль.`);
|
||||||
}
|
}
|
||||||
const leader = overview.top_customers[0];
|
const incomingLeaderLine = businessOverviewIncomingLeaderLine(overview);
|
||||||
if (leader) {
|
if (incomingLeaderLine) {
|
||||||
lines.push(`Самый крупный подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`);
|
lines.push(incomingLeaderLine);
|
||||||
}
|
}
|
||||||
const supplierLeader = overview.top_suppliers?.[0];
|
const outgoingLeaderLine = businessOverviewOutgoingLeaderLine(overview);
|
||||||
if (supplierLeader) {
|
if (outgoingLeaderLine) {
|
||||||
lines.push(`Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value} — ${supplierLeader.total_amount_human_ru}.`);
|
lines.push(outgoingLeaderLine);
|
||||||
}
|
}
|
||||||
if (overview.yearly_breakdown?.length) {
|
if (overview.yearly_breakdown?.length) {
|
||||||
lines.push(`Годовая раскладка операционного денежного потока построена по подтвержденным строкам 1С за ${yearCountHumanRu(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}.`);
|
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) {
|
if (overview.trading_margin_proxy) {
|
||||||
const proxy = overview.trading_margin_proxy;
|
const proxy = overview.trading_margin_proxy;
|
||||||
const marginText = proxy.margin_to_revenue_pct === null ? "не рассчитана" : `${proxy.margin_to_revenue_pct}%`;
|
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}` : "";
|
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.`);
|
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) {
|
if (overview.inventory_position) {
|
||||||
const leader = overview.inventory_position.top_items[0];
|
const leader = overview.inventory_position.top_items[0];
|
||||||
const leaderText = leader
|
const leaderText = leader
|
||||||
|
|
@ -1203,6 +1414,13 @@ function businessOverviewCustomerConcentrationLine(overview) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const share = percentText(leader.total_amount, overview.incoming_customer_revenue.total_amount);
|
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
|
return share
|
||||||
? `Концентрация входящего потока: крупнейший подтвержденный клиент ${leader.axis_value} дает около ${share} проверенных входящих поступлений (${leader.total_amount_human_ru}). Это сигнал зависимости от клиента, а не полный customer-risk аудит.`
|
? `Концентрация входящего потока: крупнейший подтвержденный клиент ${leader.axis_value} дает около ${share} проверенных входящих поступлений (${leader.total_amount_human_ru}). Это сигнал зависимости от клиента, а не полный customer-risk аудит.`
|
||||||
: `Крупнейший подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`;
|
: `Крупнейший подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`;
|
||||||
|
|
@ -1213,6 +1431,13 @@ function businessOverviewSupplierConcentrationLine(overview) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const share = percentText(leader.total_amount, overview.outgoing_supplier_payout.total_amount);
|
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
|
return share
|
||||||
? `Концентрация исходящего потока: крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru}). Это сигнал procurement concentration по найденным строкам, а не полный vendor-risk аудит или структура всех расходов.`
|
? `Концентрация исходящего потока: крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru}). Это сигнал procurement concentration по найденным строкам, а не полный vendor-risk аудит или структура всех расходов.`
|
||||||
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`;
|
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`;
|
||||||
|
|
@ -1257,6 +1482,18 @@ function businessOverviewRiskSynthesisLine(overview) {
|
||||||
: `маржинальность proxy ${overview.trading_margin_proxy.margin_to_revenue_pct}%`;
|
: `маржинальность proxy ${overview.trading_margin_proxy.margin_to_revenue_pct}%`;
|
||||||
signals.push(`торговый спред proxy ${overview.trading_margin_proxy.gross_spread_proxy_human_ru}, ${marginText}`);
|
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) {
|
if (overview.debt_position) {
|
||||||
const debtDirection = overview.debt_position.net_debt_position_direction === "net_receivable"
|
const debtDirection = overview.debt_position.net_debt_position_direction === "net_receivable"
|
||||||
? `дебиторка больше кредиторки на ${overview.debt_position.net_debt_position_amount_human_ru}`
|
? `дебиторка больше кредиторки на ${overview.debt_position.net_debt_position_amount_human_ru}`
|
||||||
|
|
@ -1275,6 +1512,16 @@ function businessOverviewRiskSynthesisLine(overview) {
|
||||||
if (overview.debt_staleness_risk_proxy) {
|
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}%`);
|
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) {
|
if (overview.document_activity_profile) {
|
||||||
const topDocument = overview.document_activity_profile.top_document_types[0];
|
const topDocument = overview.document_activity_profile.top_document_types[0];
|
||||||
const topSection = overview.document_activity_profile.top_account_sections[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) {
|
if (pilot.derived_business_overview?.tax_position) {
|
||||||
pushReason(reasonCodes, "answer_contains_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) {
|
if (pilot.derived_business_overview?.trading_margin_proxy) {
|
||||||
pushReason(reasonCodes, "answer_contains_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) {
|
if (pilot.derived_business_overview?.debt_staleness_risk_proxy) {
|
||||||
pushReason(reasonCodes, "answer_contains_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) {
|
if (pilot.derived_business_overview?.inventory_position) {
|
||||||
pushReason(reasonCodes, "answer_contains_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" &&
|
return (record?.schema_version === "assistant_mcp_discovery_runtime_entry_point_v1" &&
|
||||||
record?.policy_owner === "assistantMcpDiscoveryRuntimeEntryPoint");
|
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) {
|
function resolveEntryPoint(input) {
|
||||||
if (isMcpDiscoveryEntryPointContract(input.entryPoint)) {
|
if (isMcpDiscoveryEntryPointContract(input.entryPoint)) {
|
||||||
return input.entryPoint;
|
return input.entryPoint;
|
||||||
|
|
@ -47,6 +52,7 @@ function buildAssistantMcpDiscoveryDebugAttachmentFields(input) {
|
||||||
const bridge = toRecordObject(entryPoint?.bridge);
|
const bridge = toRecordObject(entryPoint?.bridge);
|
||||||
const planner = toRecordObject(bridge?.planner);
|
const planner = toRecordObject(bridge?.planner);
|
||||||
const chainAlignment = toRecordObject(planner?.catalog_chain_template_alignment);
|
const chainAlignment = toRecordObject(planner?.catalog_chain_template_alignment);
|
||||||
|
const routeCandidate = isRouteCandidateContract(bridge?.route_candidate) ? bridge.route_candidate : null;
|
||||||
const answerDraft = toRecordObject(bridge?.answer_draft);
|
const answerDraft = toRecordObject(bridge?.answer_draft);
|
||||||
return {
|
return {
|
||||||
assistant_mcp_discovery_entry_point_v1: entryPoint,
|
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_alignment_status: toNonEmptyString(chainAlignment?.alignment_status),
|
||||||
mcp_discovery_catalog_chain_top_match: toNonEmptyString(chainAlignment?.top_chain_template_match),
|
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_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_answer_mode: toNonEmptyString(answerDraft?.answer_mode),
|
||||||
mcp_discovery_business_fact_answer_allowed: bridge?.business_fact_answer_allowed === true,
|
mcp_discovery_business_fact_answer_allowed: bridge?.business_fact_answer_allowed === true,
|
||||||
mcp_discovery_user_facing_response_allowed: bridge?.user_facing_response_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 assistantMcpDiscoveryRuntimeAdapter_1 = require("./assistantMcpDiscoveryRuntimeAdapter");
|
||||||
const assistantMcpDiscoveryPolicy_1 = require("./assistantMcpDiscoveryPolicy");
|
const assistantMcpDiscoveryPolicy_1 = require("./assistantMcpDiscoveryPolicy");
|
||||||
const addressRecipeCatalog_1 = require("./addressRecipeCatalog");
|
const addressRecipeCatalog_1 = require("./addressRecipeCatalog");
|
||||||
|
const counterpartyRoleHeuristics_1 = require("./counterpartyRoleHeuristics");
|
||||||
exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION = "assistant_mcp_discovery_pilot_executor_v1";
|
exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION = "assistant_mcp_discovery_pilot_executor_v1";
|
||||||
const DEFAULT_DEPS = {
|
const DEFAULT_DEPS = {
|
||||||
executeAddressMcpQuery: addressMcpClient_1.executeAddressMcpQuery,
|
executeAddressMcpQuery: addressMcpClient_1.executeAddressMcpQuery,
|
||||||
|
|
@ -200,6 +201,16 @@ function buildBusinessOverviewDebtFilters(planner) {
|
||||||
sort: "period_asc"
|
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) {
|
function buildBusinessOverviewInventoryFilters(planner) {
|
||||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||||
const organization = toNonEmptyString(meaning?.explicit_organization_scope);
|
const organization = toNonEmptyString(meaning?.explicit_organization_scope);
|
||||||
|
|
@ -231,6 +242,17 @@ function buildBusinessOverviewTradingMarginFilters(planner) {
|
||||||
sort: "period_asc"
|
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) {
|
function buildInventoryExactFilters(planner) {
|
||||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||||
const subject = firstEntityCandidate(planner);
|
const subject = firstEntityCandidate(planner);
|
||||||
|
|
@ -715,7 +737,8 @@ async function executeCoverageAwareValueFlowQuery(input) {
|
||||||
});
|
});
|
||||||
executedProbeCount += 1;
|
executedProbeCount += 1;
|
||||||
probeResults.push(queryResultToProbeResult(input.primitiveId, broadResult));
|
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) {
|
if (broadResult.error) {
|
||||||
pushUnique(queryLimitations, broadResult.error);
|
pushUnique(queryLimitations, broadResult.error);
|
||||||
return {
|
return {
|
||||||
|
|
@ -772,7 +795,8 @@ async function executeCoverageAwareValueFlowQuery(input) {
|
||||||
pushUnique(queryLimitations, chunkResult.error);
|
pushUnique(queryLimitations, chunkResult.error);
|
||||||
continue;
|
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;
|
anyChunkLimited = true;
|
||||||
}
|
}
|
||||||
chunkResults.push(chunkResult);
|
chunkResults.push(chunkResult);
|
||||||
|
|
@ -1604,6 +1628,13 @@ function extractContractDateFromText(value) {
|
||||||
if (!text || !/(?:договор|contract|дог\.)/iu.test(text)) {
|
if (!text || !/(?:договор|contract|дог\.)/iu.test(text)) {
|
||||||
return null;
|
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})/);
|
const isoLikeMatch = text.match(/(\d{4})[-./](\d{1,2})[-./](\d{1,2})/);
|
||||||
if (isoLikeMatch) {
|
if (isoLikeMatch) {
|
||||||
return normalizeDateParts(isoLikeMatch[1], isoLikeMatch[2], isoLikeMatch[3]);
|
return normalizeDateParts(isoLikeMatch[1], isoLikeMatch[2], isoLikeMatch[3]);
|
||||||
|
|
@ -1614,6 +1645,59 @@ function extractContractDateFromText(value) {
|
||||||
}
|
}
|
||||||
return null;
|
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) {
|
function earlierIsoDate(left, right) {
|
||||||
if (!left) {
|
if (!left) {
|
||||||
return right;
|
return right;
|
||||||
|
|
@ -2038,7 +2122,8 @@ function deriveRankedValueFlow(result, input) {
|
||||||
axis_value: axisValue,
|
axis_value: axisValue,
|
||||||
rows_with_amount: bucket.rows_with_amount,
|
rows_with_amount: bucket.rows_with_amount,
|
||||||
total_amount: bucket.total_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) => {
|
.sort((left, right) => {
|
||||||
const amountDelta = right.total_amount - left.total_amount;
|
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"
|
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) {
|
function deriveBusinessOverviewTradingMarginProxy(result, periodScope) {
|
||||||
if (!result || result.error || result.matched_rows <= 0 || !periodScope) {
|
if (!result || result.error || result.matched_rows <= 0 || !periodScope) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -2554,6 +2732,121 @@ function deriveBusinessOverviewDebtStalenessRiskProxy(quality) {
|
||||||
inference_basis: "contract_date_age_and_open_balance_concentration_confirmed_1c_rows"
|
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) {
|
function debtStalenessRiskBandRu(riskBand) {
|
||||||
if (riskBand === "high") {
|
if (riskBand === "high") {
|
||||||
return "высокая зона внимания";
|
return "высокая зона внимания";
|
||||||
|
|
@ -2748,7 +3041,7 @@ function buildBusinessOverviewMissingProofFamilies(input) {
|
||||||
items.push(item);
|
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({
|
pushUnique({
|
||||||
family: "accounting_profit_margin",
|
family: "accounting_profit_margin",
|
||||||
current_status: input.tradingMarginProxy ? "proxy_only_currently" : "reviewed_route_not_wired",
|
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"
|
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({
|
pushUnique({
|
||||||
family: "debt_due_date_aging_quality",
|
family: "debt_due_date_aging_quality",
|
||||||
current_status: input.debtStalenessRiskProxy
|
current_status: input.debtStalenessRiskProxy
|
||||||
|
|
@ -2830,6 +3123,7 @@ function deriveBusinessOverview(input) {
|
||||||
});
|
});
|
||||||
const activityPeriod = deriveActivityPeriod(input.lifecycleResult);
|
const activityPeriod = deriveActivityPeriod(input.lifecycleResult);
|
||||||
const taxPosition = deriveBusinessOverviewTaxPosition(input.taxResult, input.periodScope);
|
const taxPosition = deriveBusinessOverviewTaxPosition(input.taxResult, input.periodScope);
|
||||||
|
const accountingFinancialResult = deriveBusinessOverviewAccountingFinancialResult(input.accountingFinancialResultResult, input.periodScope);
|
||||||
const tradingMarginProxy = deriveBusinessOverviewTradingMarginProxy(input.tradingMarginResult, input.periodScope);
|
const tradingMarginProxy = deriveBusinessOverviewTradingMarginProxy(input.tradingMarginResult, input.periodScope);
|
||||||
const debtPosition = deriveBusinessOverviewDebtPosition({
|
const debtPosition = deriveBusinessOverviewDebtPosition({
|
||||||
receivablesResult: input.receivablesResult,
|
receivablesResult: input.receivablesResult,
|
||||||
|
|
@ -2844,6 +3138,10 @@ function deriveBusinessOverview(input) {
|
||||||
const counterpartyProfile = deriveBusinessOverviewCounterpartyProfile(input.counterpartyProfileResult, input.periodScope);
|
const counterpartyProfile = deriveBusinessOverviewCounterpartyProfile(input.counterpartyProfileResult, input.periodScope);
|
||||||
const contractUsageProfile = deriveBusinessOverviewContractUsageProfile(input.contractUsageProfileResult, input.periodScope);
|
const contractUsageProfile = deriveBusinessOverviewContractUsageProfile(input.contractUsageProfileResult, input.periodScope);
|
||||||
const debtStalenessRiskProxy = deriveBusinessOverviewDebtStalenessRiskProxy(debtOpenSettlementQuality);
|
const debtStalenessRiskProxy = deriveBusinessOverviewDebtStalenessRiskProxy(debtOpenSettlementQuality);
|
||||||
|
const debtDueDateAging = deriveBusinessOverviewDebtDueDateAging({
|
||||||
|
dueDateResult: input.dueDateAgingResult,
|
||||||
|
debtAsOfDate: input.debtAsOfDate
|
||||||
|
});
|
||||||
const inventoryPosition = deriveBusinessOverviewInventoryPosition({
|
const inventoryPosition = deriveBusinessOverviewInventoryPosition({
|
||||||
inventoryOnHandResult: input.inventoryOnHandResult,
|
inventoryOnHandResult: input.inventoryOnHandResult,
|
||||||
inventoryAgingResult: input.inventoryAgingResult,
|
inventoryAgingResult: input.inventoryAgingResult,
|
||||||
|
|
@ -2862,10 +3160,12 @@ function deriveBusinessOverview(input) {
|
||||||
outgoing.rows_with_amount > 0,
|
outgoing.rows_with_amount > 0,
|
||||||
Boolean(activityPeriod),
|
Boolean(activityPeriod),
|
||||||
Boolean(taxPosition),
|
Boolean(taxPosition),
|
||||||
|
Boolean(accountingFinancialResult),
|
||||||
Boolean(tradingMarginProxy),
|
Boolean(tradingMarginProxy),
|
||||||
Boolean(debtPosition),
|
Boolean(debtPosition),
|
||||||
Boolean(debtOpenSettlementQuality),
|
Boolean(debtOpenSettlementQuality),
|
||||||
Boolean(debtStalenessRiskProxy),
|
Boolean(debtStalenessRiskProxy),
|
||||||
|
Boolean(debtDueDateAging),
|
||||||
Boolean(documentActivityProfile),
|
Boolean(documentActivityProfile),
|
||||||
Boolean(counterpartyProfile),
|
Boolean(counterpartyProfile),
|
||||||
Boolean(contractUsageProfile),
|
Boolean(contractUsageProfile),
|
||||||
|
|
@ -2879,9 +3179,9 @@ function deriveBusinessOverview(input) {
|
||||||
const netAmount = incoming.total_amount - outgoing.total_amount;
|
const netAmount = incoming.total_amount - outgoing.total_amount;
|
||||||
const hasBusinessOverviewProfileSignal = Boolean(documentActivityProfile || counterpartyProfile || contractUsageProfile);
|
const hasBusinessOverviewProfileSignal = Boolean(documentActivityProfile || counterpartyProfile || contractUsageProfile);
|
||||||
const missingSignalFamilies = [
|
const missingSignalFamilies = [
|
||||||
tradingMarginProxy ? "accounting_profit_margin" : "profit_margin",
|
accountingFinancialResult ? null : tradingMarginProxy ? "accounting_profit_margin" : "profit_margin",
|
||||||
debtPosition ? null : "debt_position",
|
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",
|
taxPosition ? null : "tax_position",
|
||||||
inventoryPosition
|
inventoryPosition
|
||||||
? inventoryStalenessRiskProxy
|
? inventoryStalenessRiskProxy
|
||||||
|
|
@ -2894,9 +3194,11 @@ function deriveBusinessOverview(input) {
|
||||||
].filter((item) => Boolean(item));
|
].filter((item) => Boolean(item));
|
||||||
const missingProofFamilies = buildBusinessOverviewMissingProofFamilies({
|
const missingProofFamilies = buildBusinessOverviewMissingProofFamilies({
|
||||||
missingSignalFamilies,
|
missingSignalFamilies,
|
||||||
|
accountingFinancialResult,
|
||||||
tradingMarginProxy,
|
tradingMarginProxy,
|
||||||
debtOpenSettlementQuality,
|
debtOpenSettlementQuality,
|
||||||
debtStalenessRiskProxy,
|
debtStalenessRiskProxy,
|
||||||
|
debtDueDateAging,
|
||||||
inventoryPosition,
|
inventoryPosition,
|
||||||
inventoryTurnoverProxy,
|
inventoryTurnoverProxy,
|
||||||
inventoryStalenessRiskProxy,
|
inventoryStalenessRiskProxy,
|
||||||
|
|
@ -2915,10 +3217,12 @@ function deriveBusinessOverview(input) {
|
||||||
yearly_breakdown: yearlyBreakdown,
|
yearly_breakdown: yearlyBreakdown,
|
||||||
activity_period: activityPeriod,
|
activity_period: activityPeriod,
|
||||||
tax_position: taxPosition,
|
tax_position: taxPosition,
|
||||||
|
accounting_financial_result: accountingFinancialResult,
|
||||||
trading_margin_proxy: tradingMarginProxy,
|
trading_margin_proxy: tradingMarginProxy,
|
||||||
debt_position: debtPosition,
|
debt_position: debtPosition,
|
||||||
debt_open_settlement_quality: debtOpenSettlementQuality,
|
debt_open_settlement_quality: debtOpenSettlementQuality,
|
||||||
debt_staleness_risk_proxy: debtStalenessRiskProxy,
|
debt_staleness_risk_proxy: debtStalenessRiskProxy,
|
||||||
|
debt_due_date_aging: debtDueDateAging,
|
||||||
inventory_position: inventoryPosition,
|
inventory_position: inventoryPosition,
|
||||||
inventory_turnover_proxy: inventoryTurnoverProxy,
|
inventory_turnover_proxy: inventoryTurnoverProxy,
|
||||||
inventory_staleness_risk_proxy: inventoryStalenessRiskProxy,
|
inventory_staleness_risk_proxy: inventoryStalenessRiskProxy,
|
||||||
|
|
@ -2929,9 +3233,9 @@ function deriveBusinessOverview(input) {
|
||||||
checked_signal_count: checkedSignalCount,
|
checked_signal_count: checkedSignalCount,
|
||||||
missing_signal_families: missingSignalFamilies,
|
missing_signal_families: missingSignalFamilies,
|
||||||
missing_proof_families: missingProofFamilies,
|
missing_proof_families: missingProofFamilies,
|
||||||
inference_basis: hasBusinessOverviewProfileSignal || inventoryPosition
|
inference_basis: hasBusinessOverviewProfileSignal || inventoryPosition || accountingFinancialResult
|
||||||
? "business_overview_from_confirmed_1c_multi_family_rows"
|
? "business_overview_from_confirmed_1c_multi_family_rows"
|
||||||
: debtOpenSettlementQuality
|
: debtOpenSettlementQuality || debtDueDateAging
|
||||||
? "business_overview_from_confirmed_1c_multi_family_rows"
|
? "business_overview_from_confirmed_1c_multi_family_rows"
|
||||||
: taxPosition && debtPosition
|
: taxPosition && debtPosition
|
||||||
? "business_overview_from_confirmed_1c_money_activity_tax_and_debt_rows"
|
? "business_overview_from_confirmed_1c_money_activity_tax_and_debt_rows"
|
||||||
|
|
@ -2956,6 +3260,9 @@ function summarizeBusinessOverviewRows(input) {
|
||||||
if (input.taxResult && !input.taxResult.error) {
|
if (input.taxResult && !input.taxResult.error) {
|
||||||
parts.push(`${input.taxResult.fetched_rows} VAT/tax rows fetched, ${input.taxResult.matched_rows} matched`);
|
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) {
|
if (input.tradingMarginResult && !input.tradingMarginResult.error) {
|
||||||
parts.push(`${input.tradingMarginResult.fetched_rows} trading-margin document rows fetched, ${input.tradingMarginResult.matched_rows} matched`);
|
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) {
|
if (input.openContractsResult && !input.openContractsResult.error) {
|
||||||
parts.push(`${input.openContractsResult.fetched_rows} open-contract rows fetched, ${input.openContractsResult.matched_rows} matched`);
|
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) {
|
if (input.documentActivityProfileResult && !input.documentActivityProfileResult.error) {
|
||||||
parts.push(`${input.documentActivityProfileResult.fetched_rows} document/account-section profile rows fetched, ${input.documentActivityProfileResult.matched_rows} matched`);
|
parts.push(`${input.documentActivityProfileResult.fetched_rows} document/account-section profile rows fetched, ${input.documentActivityProfileResult.matched_rows} matched`);
|
||||||
}
|
}
|
||||||
|
|
@ -3000,11 +3310,29 @@ function buildBusinessOverviewConfirmedFacts(derived) {
|
||||||
}
|
}
|
||||||
if (derived.top_customers.length > 0) {
|
if (derived.top_customers.length > 0) {
|
||||||
const leader = derived.top_customers[0];
|
const leader = derived.top_customers[0];
|
||||||
facts.push(`Самый крупный подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`);
|
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) {
|
if (derived.top_suppliers.length > 0) {
|
||||||
const leader = derived.top_suppliers[0];
|
const leader = derived.top_suppliers[0];
|
||||||
facts.push(`Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`);
|
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) {
|
if (derived.yearly_breakdown.length > 0) {
|
||||||
facts.push(`Годовая раскладка операционного денежного потока построена по подтвержденным строкам 1С за ${yearCountHumanRu(derived.yearly_breakdown.length)}.`);
|
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}` : "";
|
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.`);
|
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) {
|
if (derived.inventory_position) {
|
||||||
const leader = derived.inventory_position.top_items[0];
|
const leader = derived.inventory_position.top_items[0];
|
||||||
const leaderText = leader
|
const leaderText = leader
|
||||||
|
|
@ -3133,6 +3480,9 @@ function buildBusinessOverviewInferredFacts(derived) {
|
||||||
const supplierSharePct = supplierLeader
|
const supplierSharePct = supplierLeader
|
||||||
? percentageOfTotal(supplierLeader.total_amount, derived.outgoing_supplier_payout.total_amount)
|
? percentageOfTotal(supplierLeader.total_amount, derived.outgoing_supplier_payout.total_amount)
|
||||||
: null;
|
: null;
|
||||||
|
const supplierLeaderIsFinancial = supplierLeader
|
||||||
|
? (0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(supplierLeader.axis_value)
|
||||||
|
: false;
|
||||||
const strongestIncomingYear = [...derived.yearly_breakdown]
|
const strongestIncomingYear = [...derived.yearly_breakdown]
|
||||||
.filter((bucket) => bucket.incoming_total_amount > 0)
|
.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];
|
.sort((left, right) => right.incoming_total_amount - left.incoming_total_amount || left.year_bucket.localeCompare(right.year_bucket))[0];
|
||||||
|
|
@ -3142,9 +3492,13 @@ function buildBusinessOverviewInferredFacts(derived) {
|
||||||
return [
|
return [
|
||||||
`Расчетное нетто по найденным строкам: ${derived.net_amount_human_ru}; ${direction}.`,
|
`Расчетное нетто по найденным строкам: ${derived.net_amount_human_ru}; ${direction}.`,
|
||||||
supplierLeader
|
supplierLeader
|
||||||
? supplierSharePct !== null
|
? supplierLeaderIsFinancial
|
||||||
? `Крупнейший подтвержденный поставщик/получатель исходящих платежей ${supplierLeader.axis_value} держит около ${supplierSharePct}% проверенного исходящего потока (${supplierLeader.total_amount_human_ru}). Это procurement concentration proxy по найденным строкам, а не полный vendor-risk аудит.`
|
? supplierSharePct !== null
|
||||||
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value} — ${supplierLeader.total_amount_human_ru}.`
|
? `Крупнейший получатель исходящих денег ${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,
|
: null,
|
||||||
strongestIncomingYear
|
strongestIncomingYear
|
||||||
? `Самый сильный год по подтвержденным входящим поступлениям: ${strongestIncomingYear.year_bucket} (${strongestIncomingYear.incoming_total_amount_human_ru}).`
|
? `Самый сильный год по подтвержденным входящим поступлениям: ${strongestIncomingYear.year_bucket} (${strongestIncomingYear.incoming_total_amount_human_ru}).`
|
||||||
|
|
@ -3803,10 +4157,12 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||||
let outgoingResult = null;
|
let outgoingResult = null;
|
||||||
let lifecycleResult = null;
|
let lifecycleResult = null;
|
||||||
let taxResult = null;
|
let taxResult = null;
|
||||||
|
let accountingFinancialResultResult = null;
|
||||||
let tradingMarginResult = null;
|
let tradingMarginResult = null;
|
||||||
let receivablesResult = null;
|
let receivablesResult = null;
|
||||||
let payablesResult = null;
|
let payablesResult = null;
|
||||||
let openContractsResult = null;
|
let openContractsResult = null;
|
||||||
|
let dueDateAgingResult = null;
|
||||||
let documentActivityProfileResult = null;
|
let documentActivityProfileResult = null;
|
||||||
let counterpartyProfileResult = null;
|
let counterpartyProfileResult = null;
|
||||||
let contractUsageProfileResult = null;
|
let contractUsageProfileResult = null;
|
||||||
|
|
@ -3816,8 +4172,10 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||||
const lifecycleFilters = buildLifecycleFilters(planner);
|
const lifecycleFilters = buildLifecycleFilters(planner);
|
||||||
const profileFilters = buildBusinessOverviewProfileFilters(planner);
|
const profileFilters = buildBusinessOverviewProfileFilters(planner);
|
||||||
const taxFilters = buildBusinessOverviewTaxFilters(planner);
|
const taxFilters = buildBusinessOverviewTaxFilters(planner);
|
||||||
|
const accountingFinancialResultFilters = buildBusinessOverviewAccountingFinancialResultFilters(planner);
|
||||||
const tradingMarginFilters = buildBusinessOverviewTradingMarginFilters(planner);
|
const tradingMarginFilters = buildBusinessOverviewTradingMarginFilters(planner);
|
||||||
const debtFilters = buildBusinessOverviewDebtFilters(planner);
|
const debtFilters = buildBusinessOverviewDebtFilters(planner);
|
||||||
|
const debtDueDateAgingProbeEnabled = shouldRunDebtDueDateAgingProbe(planner);
|
||||||
const inventoryFilters = buildBusinessOverviewInventoryFilters(planner);
|
const inventoryFilters = buildBusinessOverviewInventoryFilters(planner);
|
||||||
const debtAsOfDate = toNonEmptyString(debtFilters?.as_of_date);
|
const debtAsOfDate = toNonEmptyString(debtFilters?.as_of_date);
|
||||||
const inventoryAsOfDate = toNonEmptyString(inventoryFilters?.as_of_date);
|
const inventoryAsOfDate = toNonEmptyString(inventoryFilters?.as_of_date);
|
||||||
|
|
@ -3830,6 +4188,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||||
const taxSelection = taxFilters
|
const taxSelection = taxFilters
|
||||||
? (0, addressRecipeCatalog_1.selectAddressRecipe)("vat_liability_confirmed_for_tax_period", taxFilters)
|
? (0, addressRecipeCatalog_1.selectAddressRecipe)("vat_liability_confirmed_for_tax_period", taxFilters)
|
||||||
: null;
|
: null;
|
||||||
|
const accountingFinancialResultSelection = accountingFinancialResultFilters
|
||||||
|
? (0, addressRecipeCatalog_1.selectAddressRecipe)("accounting_financial_result_for_organization", accountingFinancialResultFilters)
|
||||||
|
: null;
|
||||||
const tradingMarginSelection = tradingMarginFilters
|
const tradingMarginSelection = tradingMarginFilters
|
||||||
? (0, addressRecipeCatalog_1.selectAddressRecipe)("inventory_trading_margin_proxy_for_organization", tradingMarginFilters)
|
? (0, addressRecipeCatalog_1.selectAddressRecipe)("inventory_trading_margin_proxy_for_organization", tradingMarginFilters)
|
||||||
: null;
|
: null;
|
||||||
|
|
@ -3842,6 +4203,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||||
const openContractsSelection = debtFilters
|
const openContractsSelection = debtFilters
|
||||||
? (0, addressRecipeCatalog_1.selectAddressRecipe)("open_contracts_confirmed_as_of_date", debtFilters)
|
? (0, addressRecipeCatalog_1.selectAddressRecipe)("open_contracts_confirmed_as_of_date", debtFilters)
|
||||||
: null;
|
: null;
|
||||||
|
const dueDateAgingSelection = debtFilters && debtDueDateAgingProbeEnabled
|
||||||
|
? (0, addressRecipeCatalog_1.selectAddressRecipe)("debt_due_date_aging_for_organization", debtFilters)
|
||||||
|
: null;
|
||||||
const inventoryOnHandSelection = inventoryFilters
|
const inventoryOnHandSelection = inventoryFilters
|
||||||
? (0, addressRecipeCatalog_1.selectAddressRecipe)("inventory_on_hand_as_of_date", inventoryFilters)
|
? (0, addressRecipeCatalog_1.selectAddressRecipe)("inventory_on_hand_as_of_date", inventoryFilters)
|
||||||
: null;
|
: null;
|
||||||
|
|
@ -3909,6 +4273,16 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||||
pushReason(reasonCodes, "pilot_business_overview_tax_recipe_not_available");
|
pushReason(reasonCodes, "pilot_business_overview_tax_recipe_not_available");
|
||||||
pushUnique(queryLimitations, "Business overview VAT/tax probe requires an executable tax-period recipe");
|
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) {
|
if (tradingMarginSelection?.selected_recipe) {
|
||||||
pushReason(reasonCodes, "pilot_business_overview_trading_margin_recipe_selected");
|
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");
|
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");
|
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) {
|
if (inventoryOnHandSelection?.selected_recipe) {
|
||||||
pushReason(reasonCodes, "pilot_business_overview_inventory_on_hand_recipe_selected");
|
pushReason(reasonCodes, "pilot_business_overview_inventory_on_hand_recipe_selected");
|
||||||
if (inventoryAgingSelection?.selected_recipe) {
|
if (inventoryAgingSelection?.selected_recipe) {
|
||||||
|
|
@ -3982,6 +4369,14 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||||
account_scope: taxPlan.account_scope
|
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) {
|
if (receivablesSelection?.selected_recipe && payablesSelection?.selected_recipe) {
|
||||||
const receivablesPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(receivablesSelection.selected_recipe, debtFilters);
|
const receivablesPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(receivablesSelection.selected_recipe, debtFilters);
|
||||||
receivablesResult = await runtimeDeps.executeAddressMcpQuery({
|
receivablesResult = await runtimeDeps.executeAddressMcpQuery({
|
||||||
|
|
@ -4004,6 +4399,14 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||||
account_scope: openContractsPlan.account_scope
|
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) {
|
if (inventoryOnHandSelection?.selected_recipe) {
|
||||||
const inventoryOnHandPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(inventoryOnHandSelection.selected_recipe, inventoryFilters);
|
const inventoryOnHandPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(inventoryOnHandSelection.selected_recipe, inventoryFilters);
|
||||||
inventoryOnHandResult = await runtimeDeps.executeAddressMcpQuery({
|
inventoryOnHandResult = await runtimeDeps.executeAddressMcpQuery({
|
||||||
|
|
@ -4025,6 +4428,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||||
if (taxResult) {
|
if (taxResult) {
|
||||||
probeResults.push(queryResultToProbeResult(step.primitive_id, taxResult));
|
probeResults.push(queryResultToProbeResult(step.primitive_id, taxResult));
|
||||||
}
|
}
|
||||||
|
if (accountingFinancialResultResult) {
|
||||||
|
probeResults.push(queryResultToProbeResult(step.primitive_id, accountingFinancialResultResult));
|
||||||
|
}
|
||||||
if (receivablesResult) {
|
if (receivablesResult) {
|
||||||
probeResults.push(queryResultToProbeResult(step.primitive_id, receivablesResult));
|
probeResults.push(queryResultToProbeResult(step.primitive_id, receivablesResult));
|
||||||
}
|
}
|
||||||
|
|
@ -4034,6 +4440,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||||
if (openContractsResult) {
|
if (openContractsResult) {
|
||||||
probeResults.push(queryResultToProbeResult(step.primitive_id, openContractsResult));
|
probeResults.push(queryResultToProbeResult(step.primitive_id, openContractsResult));
|
||||||
}
|
}
|
||||||
|
if (dueDateAgingResult) {
|
||||||
|
probeResults.push(queryResultToProbeResult(step.primitive_id, dueDateAgingResult));
|
||||||
|
}
|
||||||
if (inventoryOnHandResult) {
|
if (inventoryOnHandResult) {
|
||||||
probeResults.push(queryResultToProbeResult(step.primitive_id, inventoryOnHandResult));
|
probeResults.push(queryResultToProbeResult(step.primitive_id, inventoryOnHandResult));
|
||||||
}
|
}
|
||||||
|
|
@ -4059,6 +4468,13 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||||
else if (taxResult) {
|
else if (taxResult) {
|
||||||
pushReason(reasonCodes, "pilot_business_overview_tax_query_mcp_executed");
|
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) {
|
if (receivablesResult?.error) {
|
||||||
pushUnique(queryLimitations, receivablesResult.error);
|
pushUnique(queryLimitations, receivablesResult.error);
|
||||||
pushReason(reasonCodes, "pilot_business_overview_receivables_query_mcp_error");
|
pushReason(reasonCodes, "pilot_business_overview_receivables_query_mcp_error");
|
||||||
|
|
@ -4084,6 +4500,13 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||||
else if (openContractsResult) {
|
else if (openContractsResult) {
|
||||||
pushReason(reasonCodes, "pilot_business_overview_open_contracts_query_mcp_executed");
|
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) {
|
if (inventoryOnHandResult?.error) {
|
||||||
pushUnique(queryLimitations, inventoryOnHandResult.error);
|
pushUnique(queryLimitations, inventoryOnHandResult.error);
|
||||||
pushReason(reasonCodes, "pilot_business_overview_inventory_on_hand_query_mcp_error");
|
pushReason(reasonCodes, "pilot_business_overview_inventory_on_hand_query_mcp_error");
|
||||||
|
|
@ -4194,10 +4617,12 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||||
outgoingResult,
|
outgoingResult,
|
||||||
lifecycleResult,
|
lifecycleResult,
|
||||||
taxResult,
|
taxResult,
|
||||||
|
accountingFinancialResultResult,
|
||||||
tradingMarginResult,
|
tradingMarginResult,
|
||||||
receivablesResult,
|
receivablesResult,
|
||||||
payablesResult,
|
payablesResult,
|
||||||
openContractsResult,
|
openContractsResult,
|
||||||
|
dueDateAgingResult,
|
||||||
documentActivityProfileResult,
|
documentActivityProfileResult,
|
||||||
counterpartyProfileResult,
|
counterpartyProfileResult,
|
||||||
contractUsageProfileResult,
|
contractUsageProfileResult,
|
||||||
|
|
@ -4234,6 +4659,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||||
if (derivedBusinessOverview.tax_position) {
|
if (derivedBusinessOverview.tax_position) {
|
||||||
pushReason(reasonCodes, "pilot_derived_business_overview_tax_position_from_confirmed_rows");
|
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) {
|
if (derivedBusinessOverview.trading_margin_proxy) {
|
||||||
pushReason(reasonCodes, "pilot_derived_business_overview_trading_margin_proxy_from_confirmed_rows");
|
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) {
|
if (derivedBusinessOverview.debt_staleness_risk_proxy) {
|
||||||
pushReason(reasonCodes, "pilot_derived_business_overview_debt_staleness_risk_proxy_from_confirmed_rows");
|
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) {
|
if (derivedBusinessOverview.inventory_position) {
|
||||||
pushReason(reasonCodes, "pilot_derived_business_overview_inventory_position_from_confirmed_rows");
|
pushReason(reasonCodes, "pilot_derived_business_overview_inventory_position_from_confirmed_rows");
|
||||||
}
|
}
|
||||||
|
|
@ -4267,10 +4699,12 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||||
outgoingResult,
|
outgoingResult,
|
||||||
lifecycleResult,
|
lifecycleResult,
|
||||||
taxResult,
|
taxResult,
|
||||||
|
accountingFinancialResultResult,
|
||||||
tradingMarginResult,
|
tradingMarginResult,
|
||||||
receivablesResult,
|
receivablesResult,
|
||||||
payablesResult,
|
payablesResult,
|
||||||
openContractsResult,
|
openContractsResult,
|
||||||
|
dueDateAgingResult,
|
||||||
documentActivityProfileResult,
|
documentActivityProfileResult,
|
||||||
counterpartyProfileResult,
|
counterpartyProfileResult,
|
||||||
contractUsageProfileResult,
|
contractUsageProfileResult,
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ exports.ASSISTANT_MCP_DISCOVERY_PRIMITIVES = [
|
||||||
];
|
];
|
||||||
const DEFAULT_DISCOVERY_BUDGET = {
|
const DEFAULT_DISCOVERY_BUDGET = {
|
||||||
max_probe_count: 3,
|
max_probe_count: 3,
|
||||||
max_rows_per_probe: 100
|
max_rows_per_probe: 200
|
||||||
};
|
};
|
||||||
const MAX_PROBE_COUNT = 36;
|
const MAX_PROBE_COUNT = 36;
|
||||||
const MAX_ROWS_PER_PROBE = 500;
|
const MAX_ROWS_PER_PROBE = 500;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION = void 0;
|
exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION = void 0;
|
||||||
exports.buildAssistantMcpDiscoveryResponseCandidate = buildAssistantMcpDiscoveryResponseCandidate;
|
exports.buildAssistantMcpDiscoveryResponseCandidate = buildAssistantMcpDiscoveryResponseCandidate;
|
||||||
|
const counterpartyRoleHeuristics_1 = require("./counterpartyRoleHeuristics");
|
||||||
exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION = "assistant_mcp_discovery_response_candidate_v1";
|
exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION = "assistant_mcp_discovery_response_candidate_v1";
|
||||||
function toRecordObject(value) {
|
function toRecordObject(value) {
|
||||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||||
|
|
@ -67,7 +68,26 @@ function hasInternalMechanics(value) {
|
||||||
function userFacingLines(values) {
|
function userFacingLines(values) {
|
||||||
return uniqueStrings(values).filter((line) => !hasInternalMechanics(line));
|
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) {
|
function localizeLine(value) {
|
||||||
|
const sanitizedValue = sanitizeUserFacingMechanics(value);
|
||||||
if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) {
|
if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) {
|
||||||
return "В 1С найдены строки активности в запрошенном срезе.";
|
return "В 1С найдены строки активности в запрошенном срезе.";
|
||||||
}
|
}
|
||||||
|
|
@ -88,7 +108,7 @@ function localizeLine(value) {
|
||||||
return `В 1С проверены входящие и исходящие денежные строки в запрошенном срезе: ${incoming}, ${outgoing}.`;
|
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)) {
|
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);
|
const counterpartyMatch = value.match(/^1C activity rows were found for counterparty\s+(.+)$/i);
|
||||||
if (counterpartyMatch) {
|
if (counterpartyMatch) {
|
||||||
|
|
@ -113,10 +133,10 @@ function localizeLine(value) {
|
||||||
}
|
}
|
||||||
const movementRowsMatch = value.match(/^1C movement rows were found for counterparty\s+(.+)$/i);
|
const movementRowsMatch = value.match(/^1C movement rows were found for counterparty\s+(.+)$/i);
|
||||||
if (movementRowsMatch) {
|
if (movementRowsMatch) {
|
||||||
return `В 1С найдены строки движений по контрагенту ${movementRowsMatch[1]}.`;
|
return `В 1С найдены строки движений по контрагенту ${movementRowsMatch[1]}.`;
|
||||||
}
|
}
|
||||||
if (/^1C movement rows were found for the requested scope$/i.test(value)) {
|
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);
|
const supplierPayoutMatch = value.match(/^1C supplier-payout rows were found for counterparty\s+(.+)$/i);
|
||||||
if (supplierPayoutMatch) {
|
if (supplierPayoutMatch) {
|
||||||
|
|
@ -144,7 +164,7 @@ function localizeLine(value) {
|
||||||
return "Срез документов ограничен только подтвержденными строками документов в проверенном окне.";
|
return "Срез документов ограничен только подтвержденными строками документов в проверенном окне.";
|
||||||
}
|
}
|
||||||
if (/^Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope$/i.test(value)) {
|
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)) {
|
if (/^Counterparty value-flow total was calculated from confirmed 1C movement rows$/i.test(value)) {
|
||||||
return "Сумма входящих поступлений рассчитана только по подтвержденным строкам поступлений в 1С.";
|
return "Сумма входящих поступлений рассчитана только по подтвержденным строкам поступлений в 1С.";
|
||||||
|
|
@ -227,10 +247,10 @@ function localizeLine(value) {
|
||||||
return "Полный срез документов без явно проверенного периода не подтвержден.";
|
return "Полный срез документов без явно проверенного периода не подтвержден.";
|
||||||
}
|
}
|
||||||
if (/^Full movement history outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
|
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)) {
|
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)) {
|
if (/^Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||||
return "Полный объем исходящих платежей вне проверенного периода этим поиском не подтвержден.";
|
return "Полный объем исходящих платежей вне проверенного периода этим поиском не подтвержден.";
|
||||||
|
|
@ -245,10 +265,10 @@ function localizeLine(value) {
|
||||||
return "Полный двусторонний денежный поток за все время без явно проверенного периода не подтвержден.";
|
return "Полный двусторонний денежный поток за все время без явно проверенного периода не подтвержден.";
|
||||||
}
|
}
|
||||||
if (/^Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit$/i.test(value)) {
|
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)) {
|
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)) {
|
if (/^Requested period coverage was recovered through monthly 1C value-flow probes$/i.test(value)) {
|
||||||
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С.";
|
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)) {
|
if (/^Complete requested-period coverage for bidirectional value-flow is not proven by the available checked rows$/i.test(value)) {
|
||||||
return "Полное покрытие запрошенного периода по двустороннему денежному потоку не подтверждено доступными проверенными строками.";
|
return "Полное покрытие запрошенного периода по двустороннему денежному потоку не подтверждено доступными проверенными строками.";
|
||||||
}
|
}
|
||||||
return value;
|
return sanitizedValue;
|
||||||
}
|
}
|
||||||
function section(title, lines) {
|
function section(title, lines) {
|
||||||
const clean = userFacingLines(lines.map(localizeLine));
|
const clean = userFacingLines(lines.map(localizeLine));
|
||||||
|
|
@ -345,7 +365,7 @@ function businessOverviewCoverageLimitLine(overview) {
|
||||||
limited.push("исходящие");
|
limited.push("исходящие");
|
||||||
}
|
}
|
||||||
return limited.length > 0
|
return limited.length > 0
|
||||||
? `Важно: ${limited.join(" и ")} уперлись в лимит выборки MCP, поэтому это проверенный срез найденных строк, а не гарантированно полный бухгалтерский оборот.`
|
? `Важно: по направлению ${limited.join(" и ")} проверка достигла лимита строк; это расширенный проверенный срез найденных строк, но не гарантия полного бухгалтерского оборота без отдельной полной выгрузки.`
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
function businessOverviewYearRowsLine(overview) {
|
function businessOverviewYearRowsLine(overview) {
|
||||||
|
|
@ -371,6 +391,30 @@ function firstOverviewAxisLabel(rows, amountKey = "total_amount_human_ru") {
|
||||||
const amount = moneyText(first?.[amountKey]);
|
const amount = moneyText(first?.[amountKey]);
|
||||||
return label && amount ? `${label} — ${sentenceAmount(amount) ?? amount}` : null;
|
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) {
|
function businessOverviewTaxLine(overview) {
|
||||||
const tax = toRecordObject(overview.tax_position);
|
const tax = toRecordObject(overview.tax_position);
|
||||||
if (!tax) {
|
if (!tax) {
|
||||||
|
|
@ -487,7 +531,7 @@ function buildCompactBidirectionalValueFlowReply(entryPoint, draft) {
|
||||||
lines.push(`Основа: ${basis.join("; ")}.`);
|
lines.push(`Основа: ${basis.join("; ")}.`);
|
||||||
}
|
}
|
||||||
if (flow.coverage_limited_by_probe_limit === true) {
|
if (flow.coverage_limited_by_probe_limit === true) {
|
||||||
lines.push("Важно: часть проверки уперлась в лимит строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");
|
lines.push("Важно: часть проверки достигла лимита строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");
|
||||||
}
|
}
|
||||||
lines.push("Метод: нетто рассчитано как подтвержденные входящие строки 1С минус подтвержденные исходящие строки; это не полное бухгалтерское сальдо вне проверенного окна.");
|
lines.push("Метод: нетто рассчитано как подтвержденные входящие строки 1С минус подтвержденные исходящие строки; это не полное бухгалтерское сальдо вне проверенного окна.");
|
||||||
const fallbackNextStep = toNonEmptyString(draft.next_step_line);
|
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 topCustomer = toRecordObject(Array.isArray(overview.top_customers) ? overview.top_customers[0] : null);
|
||||||
const customerName = toNonEmptyString(topCustomer?.axis_value);
|
const customerName = toNonEmptyString(topCustomer?.axis_value);
|
||||||
const customerAmount = moneyText(topCustomer?.total_amount_human_ru);
|
const customerAmount = moneyText(topCustomer?.total_amount_human_ru);
|
||||||
|
const topCustomerLooksFinancial = overviewAxisLooksFinancial(topCustomer);
|
||||||
|
const nonFinancialCustomer = firstNonFinancialOverviewAxisLabel(topCustomerLooksFinancial ? overview.top_customers : []);
|
||||||
const topCustomerLead = customerName && customerAmount
|
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 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 roleBoundaryLead = topCustomer || topSupplier ? "; клиент/поставщик как бизнес-роли этим денежным срезом не подтверждены" : "";
|
||||||
const graphReasonCodes = toStringList(graph?.reason_codes);
|
const graphReasonCodes = toStringList(graph?.reason_codes);
|
||||||
const directMoneyAnswer = graphReasonCodes.includes("data_need_graph_business_overview_direct_money_answer");
|
const directMoneyAnswer = graphReasonCodes.includes("data_need_graph_business_overview_direct_money_answer");
|
||||||
const crossScopeExecutiveSummary = Boolean(separateSubject && previousCounterpartySummary);
|
const crossScopeExecutiveSummary = Boolean(separateSubject && previousCounterpartySummary);
|
||||||
const lines = [];
|
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)) {
|
if (crossScopeExecutiveSummary && separateSubject && previousCounterpartySummary && (incomingAmount || outgoingAmount || netAmount)) {
|
||||||
lines.push(`Коротко: по компании ${organizationScope ?? "в выбранном контуре"} ${period} подтвержден денежный срез: получили ${incomingAmount ?? "0 руб."}, исходящие платежи/списания ${outgoingAmount ?? "0 руб."}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}${previousCounterpartySummary.lead}; можно утверждать только эти подтвержденные срезы, нельзя называть это чистой прибылью, полным оборотом или доказанной ролью главного клиента/поставщика.`);
|
lines.push(`Коротко: по компании ${organizationScope ?? "в выбранном контуре"} ${period} подтвержден денежный срез: получили ${incomingAmount ?? "0 руб."}, исходящие платежи/списания ${outgoingAmount ?? "0 руб."}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}${previousCounterpartySummary.lead}; можно утверждать только эти подтвержденные срезы, нельзя называть это чистой прибылью, полным оборотом или доказанной ролью главного клиента/поставщика.`);
|
||||||
lines.push(previousCounterpartySummary.line);
|
lines.push(previousCounterpartySummary.line);
|
||||||
|
|
@ -640,7 +826,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||||
if (!leaderYear || !leaderAmount) {
|
if (!leaderYear || !leaderAmount) {
|
||||||
return null;
|
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 netYear = toNonEmptyString(netLeader?.year_bucket);
|
||||||
const netYearAmount = moneyText(netLeader?.net_amount_human_ru);
|
const netYearAmount = moneyText(netLeader?.net_amount_human_ru);
|
||||||
if (netYear && netYearAmount) {
|
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(`Коротко: ${organizationPrefix}${period} по подтвержденным строкам 1С получили ${incomingAmount ?? "0 руб."}; исходящие платежи/списания ${outgoingAmount ?? "0 руб."}; ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб"}${topCustomerLead}${topSupplierLead}${roleBoundaryLead}${separateSubjectLead}.`);
|
||||||
lines.push('Метод: "заработали" здесь считаю как денежный operating-flow proxy по 1С; это не чистая прибыль и не финрезультат.');
|
lines.push('Метод: "заработали" здесь считаю как денежный operating-flow proxy по 1С; это не чистая прибыль и не финрезультат.');
|
||||||
if (!directMoneyAnswer && customerName && customerAmount) {
|
if (!directMoneyAnswer && customerName && customerAmount) {
|
||||||
lines.push(`Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}.`);
|
lines.push(topCustomerLooksFinancial
|
||||||
|
? `Крупнейший входящий денежный источник в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}. По названию это банк/финансовая организация, поэтому без назначения платежа не называю это клиентской выручкой.${nonFinancialCustomer ? ` Крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}.` : ""}`
|
||||||
|
: `Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
@ -671,10 +859,14 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||||
`Отдельно по контрагенту ${separateSubject}: этот итог не переносит суммы компании на контрагента. Можно утверждать только разделение контура; нельзя делать вывод о выручке, долге или прибыльности ${separateSubject} без отдельного контрагентского среза документов и движений.`);
|
`Отдельно по контрагенту ${separateSubject}: этот итог не переносит суммы компании на контрагента. Можно утверждать только разделение контура; нельзя делать вывод о выручке, долге или прибыльности ${separateSubject} без отдельного контрагентского среза документов и движений.`);
|
||||||
}
|
}
|
||||||
if (!directMoneyAnswer && topSupplier) {
|
if (!directMoneyAnswer && topSupplier) {
|
||||||
lines.push(`Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`);
|
lines.push(topSupplierLooksFinancial
|
||||||
|
? `Крупнейший получатель исходящих денег: ${topSupplier}. По названию это банк/финансовая организация, поэтому без назначения платежа/договора не считаю это обычным поставщиком.${nonFinancialSupplier ? ` Крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}.` : ""}`
|
||||||
|
: `Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`);
|
||||||
}
|
}
|
||||||
if (!directMoneyAnswer && (topCustomer || topSupplier)) {
|
if (!directMoneyAnswer && (topCustomer || topSupplier)) {
|
||||||
lines.push("Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль.");
|
lines.push(topCustomerLooksFinancial || topSupplierLooksFinancial
|
||||||
|
? "Важно по ролям: текущий денежный срез подтверждает источники и получателей денег, но банковские контрагенты требуют проверки назначения платежа/счетов и не доказывают роль клиента или поставщика."
|
||||||
|
: "Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль.");
|
||||||
}
|
}
|
||||||
if (!directMoneyAnswer) {
|
if (!directMoneyAnswer) {
|
||||||
lines.push(`Что подтверждено: денежный срез по компании${organizationScope ? ` ${organizationScope}` : ""}${period ? ` ${period}` : ""}${topCustomer ? ", крупнейший источник входящих денег" : ""}${topSupplier ? ", крупнейший получатель исходящих денег" : ""}.`);
|
lines.push(`Что подтверждено: денежный срез по компании${organizationScope ? ` ${organizationScope}` : ""}${period ? ` ${period}` : ""}${topCustomer ? ", крупнейший источник входящих денег" : ""}${topSupplier ? ", крупнейший получатель исходящих денег" : ""}.`);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
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;
|
exports.runAssistantMcpDiscoveryRuntimeBridge = runAssistantMcpDiscoveryRuntimeBridge;
|
||||||
const assistantMcpDiscoveryAnswerAdapter_1 = require("./assistantMcpDiscoveryAnswerAdapter");
|
const assistantMcpDiscoveryAnswerAdapter_1 = require("./assistantMcpDiscoveryAnswerAdapter");
|
||||||
const assistantMcpDiscoveryPilotExecutor_1 = require("./assistantMcpDiscoveryPilotExecutor");
|
const assistantMcpDiscoveryPilotExecutor_1 = require("./assistantMcpDiscoveryPilotExecutor");
|
||||||
const assistantMcpDiscoveryPlanner_1 = require("./assistantMcpDiscoveryPlanner");
|
const assistantMcpDiscoveryPlanner_1 = require("./assistantMcpDiscoveryPlanner");
|
||||||
exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION = "assistant_mcp_discovery_runtime_bridge_v1";
|
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_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) {
|
function normalizeReasonCode(value) {
|
||||||
const normalized = value
|
const normalized = value
|
||||||
.trim()
|
.trim()
|
||||||
|
|
@ -58,6 +59,21 @@ function loopStatusFor(bridgeStatus) {
|
||||||
}
|
}
|
||||||
return "ready_for_next_hop";
|
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) {
|
function flattenAxes(pilot, source) {
|
||||||
const result = [];
|
const result = [];
|
||||||
for (const step of pilot.dry_run.execution_steps) {
|
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 ?? [];
|
const values = planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? [];
|
||||||
return uniqueStrings(values);
|
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) {
|
function buildLoopState(planner, pilot, bridgeStatus) {
|
||||||
const plannerClarificationGaps = planner.discovery_plan.clarification_gaps ?? [];
|
const plannerClarificationGaps = planner.discovery_plan.clarification_gaps ?? [];
|
||||||
return {
|
return {
|
||||||
|
|
@ -120,10 +249,13 @@ async function runAssistantMcpDiscoveryRuntimeBridge(input) {
|
||||||
const answerDraft = (0, assistantMcpDiscoveryAnswerAdapter_1.buildAssistantMcpDiscoveryAnswerDraft)(pilot);
|
const answerDraft = (0, assistantMcpDiscoveryAnswerAdapter_1.buildAssistantMcpDiscoveryAnswerDraft)(pilot);
|
||||||
const bridgeStatus = bridgeStatusFor(pilot, answerDraft);
|
const bridgeStatus = bridgeStatusFor(pilot, answerDraft);
|
||||||
const loopState = buildLoopState(planner, pilot, bridgeStatus);
|
const loopState = buildLoopState(planner, pilot, bridgeStatus);
|
||||||
|
const routeCandidate = buildRouteCandidate(planner, pilot, bridgeStatus);
|
||||||
const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]);
|
const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]);
|
||||||
pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`);
|
pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`);
|
||||||
pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer");
|
pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer");
|
||||||
pushReason(reasonCodes, `runtime_bridge_loop_state_${loopState.loop_status}`);
|
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 {
|
return {
|
||||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION,
|
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION,
|
||||||
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
|
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
|
||||||
|
|
@ -133,6 +265,7 @@ async function runAssistantMcpDiscoveryRuntimeBridge(input) {
|
||||||
pilot,
|
pilot,
|
||||||
answer_draft: answerDraft,
|
answer_draft: answerDraft,
|
||||||
loop_state: loopState,
|
loop_state: loopState,
|
||||||
|
route_candidate: routeCandidate,
|
||||||
user_facing_response_allowed: bridgeStatus !== "blocked",
|
user_facing_response_allowed: bridgeStatus !== "blocked",
|
||||||
business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft),
|
business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft),
|
||||||
requires_user_clarification: bridgeStatus === "needs_clarification",
|
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);
|
/(?:\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;
|
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) {
|
function hasOrganizationLevelDebtDueDateOverviewSignal(text) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -711,6 +740,12 @@ function hasExplicitVatQuestionSignal(text) {
|
||||||
return (/(?:\u043d\u0434\u0441|vat)/iu.test(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));
|
/(?:\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) {
|
function hasBusinessOverviewSeparateCounterpartySignal(text) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -1102,6 +1137,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
const rawReferentialDocumentExclusionSignal = hasReferentialDocumentExclusionFollowupSignal(repairedUserText ?? rawUserText ?? "");
|
const rawReferentialDocumentExclusionSignal = hasReferentialDocumentExclusionFollowupSignal(repairedUserText ?? rawUserText ?? "");
|
||||||
const rawPrimaryBusinessOverviewSignal = hasBusinessOverviewSignal(rawText);
|
const rawPrimaryBusinessOverviewSignal = hasBusinessOverviewSignal(rawText);
|
||||||
const explicitVatQuestionSignal = hasExplicitVatQuestionSignal(rawText);
|
const explicitVatQuestionSignal = hasExplicitVatQuestionSignal(rawText);
|
||||||
|
const explicitVatMovementEvidenceSignal = hasExplicitVatMovementEvidenceSignal(rawText);
|
||||||
const explicitVatSuppressesBusinessOverviewContinuation = Boolean(explicitVatQuestionSignal && !rawPrimaryBusinessOverviewSignal);
|
const explicitVatSuppressesBusinessOverviewContinuation = Boolean(explicitVatQuestionSignal && !rawPrimaryBusinessOverviewSignal);
|
||||||
const businessOverviewContinuationSignal = hasBusinessOverviewFollowupSeed(followupSeed) &&
|
const businessOverviewContinuationSignal = hasBusinessOverviewFollowupSeed(followupSeed) &&
|
||||||
hasBusinessOverviewContinuationSignal(rawText) &&
|
hasBusinessOverviewContinuationSignal(rawText) &&
|
||||||
|
|
@ -1114,6 +1150,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
(hasValueFlowSignal(rawText) || hasValueRankingSignal(rawText) || rawBidirectionalValueFlowSignal);
|
(hasValueFlowSignal(rawText) || hasValueRankingSignal(rawText) || rawBidirectionalValueFlowSignal);
|
||||||
const rawMetadataSignal = !rawLifecycleSignal &&
|
const rawMetadataSignal = !rawLifecycleSignal &&
|
||||||
!rawValueFlowSignal &&
|
!rawValueFlowSignal &&
|
||||||
|
!explicitVatMovementEvidenceSignal &&
|
||||||
!rawReferentialDocumentExclusionSignal &&
|
!rawReferentialDocumentExclusionSignal &&
|
||||||
hasMetadataSignal(rawText);
|
hasMetadataSignal(rawText);
|
||||||
const rawEntityResolutionSignal = !rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
|
const rawEntityResolutionSignal = !rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
|
||||||
|
|
@ -1128,7 +1165,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(dateScopeSignalText);
|
const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(dateScopeSignalText);
|
||||||
const relativeCurrentDateHintDetected = hasRelativeCurrentDateHint(rawText);
|
const relativeCurrentDateHintDetected = hasRelativeCurrentDateHint(rawText);
|
||||||
const rawDateScope = collectDateScopeFromRawText(dateScopeSignalText);
|
const rawDateScope = collectDateScopeFromRawText(dateScopeSignalText);
|
||||||
const rawMetadataScopeHint = rawMetadataSignal ? metadataScopeHintFromRawText(rawText) : null;
|
const rawMetadataScopeHint = rawMetadataSignal || explicitVatMovementEvidenceSignal ? metadataScopeHintFromRawText(rawText) : null;
|
||||||
const rawTopicSwitchSignal = hasExplicitTopicSwitchSignal(rawText);
|
const rawTopicSwitchSignal = hasExplicitTopicSwitchSignal(rawText);
|
||||||
const rawEntityCandidate = rawEntityResolutionSignal ? rawEntityResolutionCandidate(rawEntitySourceText) : null;
|
const rawEntityCandidate = rawEntityResolutionSignal ? rawEntityResolutionCandidate(rawEntitySourceText) : null;
|
||||||
const rawScopedEntityCandidate = !predecomposeEntities.counterparty &&
|
const rawScopedEntityCandidate = !predecomposeEntities.counterparty &&
|
||||||
|
|
@ -1147,11 +1184,41 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis);
|
const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis);
|
||||||
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
|
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
|
||||||
const broadBusinessEvaluationUnsupported = unsupported === "broad_business_evaluation";
|
const broadBusinessEvaluationUnsupported = unsupported === "broad_business_evaluation";
|
||||||
const businessOverviewSignal = rawBusinessOverviewSignal ||
|
const seededBusinessOverviewSignal = broadBusinessEvaluationUnsupported ||
|
||||||
broadBusinessEvaluationUnsupported ||
|
|
||||||
rawDomain === "business_summary" ||
|
rawDomain === "business_summary" ||
|
||||||
rawDomain === "business_overview" ||
|
rawDomain === "business_overview" ||
|
||||||
rawAction === "broad_evaluation";
|
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 businessOverviewSeparateCounterpartySignal = Boolean(businessOverviewSignal && hasBusinessOverviewSeparateCounterpartySignal(rawText));
|
||||||
const businessOverviewSeparateCounterpartyCandidate = businessOverviewSeparateCounterpartySignal
|
const businessOverviewSeparateCounterpartyCandidate = businessOverviewSeparateCounterpartySignal
|
||||||
? businessOverviewSeparateCounterpartyCandidateFromText(rawText)
|
? businessOverviewSeparateCounterpartyCandidateFromText(rawText)
|
||||||
|
|
@ -1176,7 +1243,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
: rawAssistantTurnMeaningOrganizationScope;
|
: rawAssistantTurnMeaningOrganizationScope;
|
||||||
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
|
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
|
||||||
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
|
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
|
||||||
const currentTurnFreshOrganizationScope = rawOrganizationScope ?? predecomposeEntities.organization;
|
const currentTurnFreshOrganizationScope = predecomposeEntities.organization ?? rawOrganizationScope;
|
||||||
const currentTurnOrganizationScope = currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope;
|
const currentTurnOrganizationScope = currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope;
|
||||||
const followupCounterpartyIsMetadataOrganizationScope = Boolean(followupSeed.subjectResolutionOptional &&
|
const followupCounterpartyIsMetadataOrganizationScope = Boolean(followupSeed.subjectResolutionOptional &&
|
||||||
followupSeed.counterparty &&
|
followupSeed.counterparty &&
|
||||||
|
|
@ -1500,9 +1567,21 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
const semanticDataNeed = metadataAmbiguityLaneClarificationApplicable
|
const semanticDataNeed = metadataAmbiguityLaneClarificationApplicable
|
||||||
? "metadata lane clarification"
|
? "metadata lane clarification"
|
||||||
: semanticNeedFor({
|
: semanticNeedFor({
|
||||||
domain: businessOverviewSignal ? "business_overview" : rawDomain ?? seededDomain,
|
domain: explicitVatMovementEvidenceSignal
|
||||||
action: businessOverviewSignal ? "broad_evaluation" : rawAction ?? seededAction,
|
? "movements"
|
||||||
unsupported: businessOverviewSignal ? "broad_business_evaluation" : unsupported ?? seededUnsupported,
|
: businessOverviewSignal
|
||||||
|
? "business_overview"
|
||||||
|
: rawDomain ?? seededDomain,
|
||||||
|
action: explicitVatMovementEvidenceSignal
|
||||||
|
? "list_movements"
|
||||||
|
: businessOverviewSignal
|
||||||
|
? businessOverviewActionFamily
|
||||||
|
: rawAction ?? seededAction,
|
||||||
|
unsupported: explicitVatMovementEvidenceSignal
|
||||||
|
? "movement_evidence"
|
||||||
|
: businessOverviewSignal
|
||||||
|
? businessOverviewUnsupportedFamily
|
||||||
|
: unsupported ?? seededUnsupported,
|
||||||
lifecycleSignal,
|
lifecycleSignal,
|
||||||
valueFlowSignal,
|
valueFlowSignal,
|
||||||
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
|
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
|
||||||
|
|
@ -1530,7 +1609,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
followupSeed.discoveryEntity ??
|
followupSeed.discoveryEntity ??
|
||||||
followupSeed.metadataSelectedEntitySet ??
|
followupSeed.metadataSelectedEntitySet ??
|
||||||
null;
|
null;
|
||||||
const metadataScopedLaneWithoutSubject = Boolean((metadataGroundedMovementLaneApplicable || metadataGroundedDocumentLaneApplicable) &&
|
const metadataScopedLaneWithoutSubject = Boolean((metadataGroundedMovementLaneApplicable ||
|
||||||
|
metadataGroundedDocumentLaneApplicable ||
|
||||||
|
explicitVatMovementEvidenceSignal) &&
|
||||||
!effectiveFollowupCounterparty &&
|
!effectiveFollowupCounterparty &&
|
||||||
metadataLaneCarryoverAvailable);
|
metadataLaneCarryoverAvailable);
|
||||||
const groundedFollowupEntity = metadataScopedLaneWithoutSubject
|
const groundedFollowupEntity = metadataScopedLaneWithoutSubject
|
||||||
|
|
@ -1711,17 +1792,19 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
? "counterparty_lifecycle"
|
? "counterparty_lifecycle"
|
||||||
: valueFlowSignal
|
: valueFlowSignal
|
||||||
? "counterparty_value"
|
? "counterparty_value"
|
||||||
: metadataGroundedMovementLaneApplicable
|
: explicitVatMovementEvidenceSignal
|
||||||
? "movements"
|
? "movements"
|
||||||
: metadataGroundedDocumentLaneApplicable
|
: metadataGroundedMovementLaneApplicable
|
||||||
? "documents"
|
? "movements"
|
||||||
: entityResolutionSignal
|
: metadataGroundedDocumentLaneApplicable
|
||||||
? "entity_resolution"
|
? "documents"
|
||||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
: entityResolutionSignal
|
||||||
? "metadata"
|
? "entity_resolution"
|
||||||
: rawDomain ?? seededDomain,
|
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||||
|
? "metadata"
|
||||||
|
: rawDomain ?? seededDomain,
|
||||||
asked_action_family: businessOverviewSignal
|
asked_action_family: businessOverviewSignal
|
||||||
? "broad_evaluation"
|
? businessOverviewActionFamily
|
||||||
: lifecycleSignal
|
: lifecycleSignal
|
||||||
? "activity_duration"
|
? "activity_duration"
|
||||||
: valueFlowSignal
|
: valueFlowSignal
|
||||||
|
|
@ -1730,15 +1813,17 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
: payoutSignal
|
: payoutSignal
|
||||||
? "payout"
|
? "payout"
|
||||||
: rawAction ?? seededAction ?? "turnover"
|
: rawAction ?? seededAction ?? "turnover"
|
||||||
: metadataGroundedMovementLaneApplicable
|
: explicitVatMovementEvidenceSignal
|
||||||
? "list_movements"
|
? "list_movements"
|
||||||
: metadataGroundedDocumentLaneApplicable
|
: metadataGroundedMovementLaneApplicable
|
||||||
? "list_documents"
|
? "list_movements"
|
||||||
: entityResolutionSignal
|
: metadataGroundedDocumentLaneApplicable
|
||||||
? "search_business_entity"
|
? "list_documents"
|
||||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
: entityResolutionSignal
|
||||||
? metadataActionFromRawText(rawText) ?? seededAction
|
? "search_business_entity"
|
||||||
: rawAction ?? seededAction,
|
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||||
|
? metadataActionFromRawText(rawText) ?? seededAction
|
||||||
|
: rawAction ?? seededAction,
|
||||||
asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis,
|
asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis,
|
||||||
seeded_ranking_need: valueFlowSignal && followupSeed.rankingNeed && !rawEntitySearchOverridesStaleScope
|
seeded_ranking_need: valueFlowSignal && followupSeed.rankingNeed && !rawEntitySearchOverridesStaleScope
|
||||||
? followupSeed.rankingNeed
|
? followupSeed.rankingNeed
|
||||||
|
|
@ -1757,7 +1842,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
explicit_date_scope: explicitDateScope,
|
explicit_date_scope: explicitDateScope,
|
||||||
subject_resolution_optional: metadataScopedLaneWithoutSubject || undefined,
|
subject_resolution_optional: metadataScopedLaneWithoutSubject || undefined,
|
||||||
unsupported_but_understood_family: businessOverviewSignal
|
unsupported_but_understood_family: businessOverviewSignal
|
||||||
? "broad_business_evaluation"
|
? businessOverviewUnsupportedFamily
|
||||||
: unsupported ??
|
: unsupported ??
|
||||||
(lifecycleSignal
|
(lifecycleSignal
|
||||||
? "counterparty_lifecycle"
|
? "counterparty_lifecycle"
|
||||||
|
|
@ -1771,20 +1856,23 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
? "movement_evidence"
|
? "movement_evidence"
|
||||||
: metadataGroundedDocumentLaneApplicable
|
: metadataGroundedDocumentLaneApplicable
|
||||||
? "document_evidence"
|
? "document_evidence"
|
||||||
: metadataAmbiguityLaneClarificationApplicable
|
: explicitVatMovementEvidenceSignal
|
||||||
? "metadata_lane_choice_clarification"
|
? "movement_evidence"
|
||||||
: entityResolutionSignal
|
: metadataAmbiguityLaneClarificationApplicable
|
||||||
? "entity_resolution"
|
? "metadata_lane_choice_clarification"
|
||||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
: entityResolutionSignal
|
||||||
? "1c_metadata_surface"
|
? "entity_resolution"
|
||||||
: followupDiscoverySeedApplicable
|
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||||
? seededUnsupported
|
? "1c_metadata_surface"
|
||||||
: null),
|
: followupDiscoverySeedApplicable
|
||||||
|
? seededUnsupported
|
||||||
|
: null),
|
||||||
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden ||
|
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden ||
|
||||||
businessOverviewSignal ||
|
businessOverviewSignal ||
|
||||||
unsupported ||
|
unsupported ||
|
||||||
lifecycleSignal ||
|
lifecycleSignal ||
|
||||||
valueFlowSignal ||
|
valueFlowSignal ||
|
||||||
|
explicitVatMovementEvidenceSignal ||
|
||||||
metadataGroundedMovementLaneApplicable ||
|
metadataGroundedMovementLaneApplicable ||
|
||||||
metadataGroundedDocumentLaneApplicable ||
|
metadataGroundedDocumentLaneApplicable ||
|
||||||
metadataAmbiguityLaneClarificationApplicable ||
|
metadataAmbiguityLaneClarificationApplicable ||
|
||||||
|
|
@ -1841,11 +1929,15 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
}
|
}
|
||||||
const currentTurnValueFlowExactOverrideApplicable = Boolean(valueFlowSignal &&
|
const currentTurnValueFlowExactOverrideApplicable = Boolean(valueFlowSignal &&
|
||||||
explicitIntentCandidate &&
|
explicitIntentCandidate &&
|
||||||
rawValueFlowAggregateQuestionSignal &&
|
(rawValueFlowAggregateQuestionSignal || hasValueRankingSignal(rawText)) &&
|
||||||
semanticDataNeed &&
|
semanticDataNeed &&
|
||||||
(entityCandidates.length > 0 || explicitOrganizationScope || openScopeValueFlowWithoutResolvedCounterparty));
|
(entityCandidates.length > 0 || explicitOrganizationScope || openScopeValueFlowWithoutResolvedCounterparty));
|
||||||
const runDiscovery = shouldRunDiscovery({
|
const runDiscovery = shouldRunDiscovery({
|
||||||
unsupported: businessOverviewSignal ? "broad_business_evaluation" : unsupported ?? seededUnsupported,
|
unsupported: explicitVatMovementEvidenceSignal
|
||||||
|
? "movement_evidence"
|
||||||
|
: businessOverviewSignal
|
||||||
|
? "broad_business_evaluation"
|
||||||
|
: unsupported ?? seededUnsupported,
|
||||||
lifecycleSignal,
|
lifecycleSignal,
|
||||||
valueFlowSignal,
|
valueFlowSignal,
|
||||||
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
|
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
|
||||||
|
|
@ -1860,6 +1952,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
metadataGroundedDocumentLaneApplicable ||
|
metadataGroundedDocumentLaneApplicable ||
|
||||||
groundedValueFlowFollowupApplicable,
|
groundedValueFlowFollowupApplicable,
|
||||||
forceDiscoveryOverExplicitIntent: businessOverviewSignal ||
|
forceDiscoveryOverExplicitIntent: businessOverviewSignal ||
|
||||||
|
explicitVatMovementEvidenceSignal ||
|
||||||
Boolean(entityResolutionClarificationCandidate) ||
|
Boolean(entityResolutionClarificationCandidate) ||
|
||||||
organizationClarificationFollowupApplicable ||
|
organizationClarificationFollowupApplicable ||
|
||||||
periodClarificationFollowupApplicable ||
|
periodClarificationFollowupApplicable ||
|
||||||
|
|
@ -1883,17 +1976,19 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
? "followup_context"
|
? "followup_context"
|
||||||
: metadataGroundedDocumentLaneApplicable
|
: metadataGroundedDocumentLaneApplicable
|
||||||
? "followup_context"
|
? "followup_context"
|
||||||
: predecomposeContract
|
: explicitVatMovementEvidenceSignal
|
||||||
? "predecompose_contract"
|
? "raw_text"
|
||||||
: lifecycleSignal
|
: predecomposeContract
|
||||||
? "raw_text"
|
? "predecompose_contract"
|
||||||
: valueFlowSignal
|
: lifecycleSignal
|
||||||
? "raw_text"
|
? "raw_text"
|
||||||
: entityResolutionSignal
|
: valueFlowSignal
|
||||||
? "raw_text"
|
? "raw_text"
|
||||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
: entityResolutionSignal
|
||||||
? "raw_text"
|
? "raw_text"
|
||||||
: "none";
|
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||||
|
? "raw_text"
|
||||||
|
: "none";
|
||||||
if (lifecycleSignal) {
|
if (lifecycleSignal) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_lifecycle_signal_detected");
|
pushReason(reasonCodes, "mcp_discovery_lifecycle_signal_detected");
|
||||||
}
|
}
|
||||||
|
|
@ -1903,6 +1998,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
if (rawMetadataSignal) {
|
if (rawMetadataSignal) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_metadata_signal_detected");
|
pushReason(reasonCodes, "mcp_discovery_metadata_signal_detected");
|
||||||
}
|
}
|
||||||
|
if (explicitVatMovementEvidenceSignal) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_vat_movement_evidence_signal_detected");
|
||||||
|
}
|
||||||
if (entityResolutionSignal) {
|
if (entityResolutionSignal) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_entity_resolution_signal_detected");
|
pushReason(reasonCodes, "mcp_discovery_entity_resolution_signal_detected");
|
||||||
}
|
}
|
||||||
|
|
@ -2026,6 +2124,12 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
if (businessOverviewContinuationSignal) {
|
if (businessOverviewContinuationSignal) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_from_followup_context");
|
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) {
|
if (explicitVatSuppressesBusinessOverviewContinuation) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_suppressed_by_explicit_vat_question");
|
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_suppressed_by_explicit_vat_question");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
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;
|
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) {
|
function resolveAssistantOrchestrationDecision(input) {
|
||||||
const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? "");
|
const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? "");
|
||||||
const effectiveAddressUserMessage = String(input?.effectiveAddressUserMessage ?? rawUserMessage);
|
const effectiveAddressUserMessage = String(input?.effectiveAddressUserMessage ?? rawUserMessage);
|
||||||
|
|
@ -475,6 +482,27 @@ function createAssistantRoutePolicy(deps) {
|
||||||
const followupPreviousFilters = followupContext?.previous_filters && typeof followupContext.previous_filters === "object"
|
const followupPreviousFilters = followupContext?.previous_filters && typeof followupContext.previous_filters === "object"
|
||||||
? followupContext.previous_filters
|
? followupContext.previous_filters
|
||||||
: null;
|
: 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 &&
|
const protectedInventoryShortFollowup = Boolean(followupContext &&
|
||||||
(isInventorySelectedObjectIntent(followupPreviousIntent) ||
|
(isInventorySelectedObjectIntent(followupPreviousIntent) ||
|
||||||
(followupPreviousIntent === "inventory_on_hand_as_of_date" &&
|
(followupPreviousIntent === "inventory_on_hand_as_of_date" &&
|
||||||
|
|
@ -524,6 +552,7 @@ function createAssistantRoutePolicy(deps) {
|
||||||
"net_value_flow"
|
"net_value_flow"
|
||||||
].includes(String(toNonEmptyString(assistantTurnMeaning?.asked_action_family) ?? "")) ||
|
].includes(String(toNonEmptyString(assistantTurnMeaning?.asked_action_family) ?? "")) ||
|
||||||
/(?:нетто|сальдо|сколько\s+мы\s+(?:получили|заплатили)|incoming|outgoing)/iu.test(analyticsSample)));
|
/(?:нетто|сальдо|сколько\s+мы\s+(?:получили|заплатили)|incoming|outgoing)/iu.test(analyticsSample)));
|
||||||
|
const effectiveGroundedValueFlowFollowupContextDetected = groundedValueFlowFollowupContextDetected || routeCandidateOrganizationClarificationDetected;
|
||||||
const baseToolGatePreservesAddressLane = Boolean(baseToolGate?.runAddressLane &&
|
const baseToolGatePreservesAddressLane = Boolean(baseToolGate?.runAddressLane &&
|
||||||
[
|
[
|
||||||
"address_intent_resolver_detected",
|
"address_intent_resolver_detected",
|
||||||
|
|
@ -533,14 +562,15 @@ function createAssistantRoutePolicy(deps) {
|
||||||
].includes(String(baseToolGate?.reason ?? ""))) ||
|
].includes(String(baseToolGate?.reason ?? ""))) ||
|
||||||
Boolean(baseToolGate?.runAddressLane &&
|
Boolean(baseToolGate?.runAddressLane &&
|
||||||
String(baseToolGate?.reason ?? "") === "followup_context_detected" &&
|
String(baseToolGate?.reason ?? "") === "followup_context_detected" &&
|
||||||
groundedValueFlowFollowupContextDetected);
|
effectiveGroundedValueFlowFollowupContextDetected);
|
||||||
const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate &&
|
const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate &&
|
||||||
deterministicNonDomainGuard &&
|
deterministicNonDomainGuard &&
|
||||||
(llmFirstUnsupportedCandidate || llmContractMode === null) &&
|
(llmFirstUnsupportedCandidate || llmContractMode === null) &&
|
||||||
!baseToolGatePreservesAddressLane &&
|
!baseToolGatePreservesAddressLane &&
|
||||||
!groundedValueFlowFollowupContextDetected &&
|
!effectiveGroundedValueFlowFollowupContextDetected &&
|
||||||
!protectedInventoryShortFollowup &&
|
!protectedInventoryShortFollowup &&
|
||||||
!organizationClarificationContinuationDetected);
|
!organizationClarificationContinuationDetected &&
|
||||||
|
!routeCandidateOrganizationClarificationDetected);
|
||||||
const lastAddressAssistantDebug = sessionItems
|
const lastAddressAssistantDebug = sessionItems
|
||||||
? findLastAddressAssistantItem(sessionItems)?.debug ?? null
|
? findLastAddressAssistantItem(sessionItems)?.debug ?? null
|
||||||
: null;
|
: null;
|
||||||
|
|
@ -583,7 +613,7 @@ function createAssistantRoutePolicy(deps) {
|
||||||
!turnMeaningIntentCandidate &&
|
!turnMeaningIntentCandidate &&
|
||||||
!dataScopeMetaQuery &&
|
!dataScopeMetaQuery &&
|
||||||
!dangerOrCoercionSignal &&
|
!dangerOrCoercionSignal &&
|
||||||
!groundedValueFlowFollowupContextDetected &&
|
!effectiveGroundedValueFlowFollowupContextDetected &&
|
||||||
!organizationClarificationContinuationDetected);
|
!organizationClarificationContinuationDetected);
|
||||||
const hardMetaMode = resolveHardMetaMode({
|
const hardMetaMode = resolveHardMetaMode({
|
||||||
dataScopeMetaQuery,
|
dataScopeMetaQuery,
|
||||||
|
|
@ -748,7 +778,7 @@ function createAssistantRoutePolicy(deps) {
|
||||||
!dataScopeMetaQuery &&
|
!dataScopeMetaQuery &&
|
||||||
!capabilityMetaQuery &&
|
!capabilityMetaQuery &&
|
||||||
!dangerOrCoercionSignal &&
|
!dangerOrCoercionSignal &&
|
||||||
!groundedValueFlowFollowupContextDetected &&
|
!effectiveGroundedValueFlowFollowupContextDetected &&
|
||||||
!organizationClarificationContinuationDetected);
|
!organizationClarificationContinuationDetected);
|
||||||
if (unsupportedCurrentTurnMeaningBoundary) {
|
if (unsupportedCurrentTurnMeaningBoundary) {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -2123,6 +2123,9 @@ function isAddressLaneDebugPayload(debug) {
|
||||||
if (typeof debug.mcp_call_status === "string" && debug.mcp_call_status.trim().length > 0) {
|
if (typeof debug.mcp_call_status === "string" && debug.mcp_call_status.trim().length > 0) {
|
||||||
return true;
|
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) {
|
if (typeof debug.anchor_type === "string" && debug.anchor_type.trim().length > 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,22 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
const samples = [userMessage, alternateMessage].map((item) => normalizeFollowupText(item)).filter(Boolean);
|
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));
|
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) {
|
function parseDmyDateToIso(value) {
|
||||||
const match = String(value ?? "").trim().match(/^(\d{2})\.(\d{2})\.(\d{4})$/);
|
const match = String(value ?? "").trim().match(/^(\d{2})\.(\d{2})\.(\d{4})$/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
|
|
@ -433,7 +449,11 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
(deps.toNonEmptyString(alternateMessage)
|
(deps.toNonEmptyString(alternateMessage)
|
||||||
? deps.hasDataRetrievalRequestSignal(String(alternateMessage ?? ""))
|
? deps.hasDataRetrievalRequestSignal(String(alternateMessage ?? ""))
|
||||||
: false);
|
: false);
|
||||||
if (rawCapabilityMetaQuery && !rawDataRetrievalSignal) {
|
const rawBusinessOverviewBoundaryFollowupCue = hasBusinessOverviewBoundaryFollowupCue(userMessage) ||
|
||||||
|
(deps.toNonEmptyString(alternateMessage)
|
||||||
|
? hasBusinessOverviewBoundaryFollowupCue(String(alternateMessage ?? ""))
|
||||||
|
: false);
|
||||||
|
if (rawCapabilityMetaQuery && !rawDataRetrievalSignal && !rawBusinessOverviewBoundaryFollowupCue) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const assistantTurnMeaning = typeof deps.resolveAssistantTurnMeaning === "function"
|
const assistantTurnMeaning = typeof deps.resolveAssistantTurnMeaning === "function"
|
||||||
|
|
@ -492,10 +512,25 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
: false));
|
: false));
|
||||||
const sourceIntentHint = (0, assistantContinuityPolicy_1.readAddressDebugIntent)(carryoverSourceDebug, deps.toNonEmptyString);
|
const sourceIntentHint = (0, assistantContinuityPolicy_1.readAddressDebugIntent)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||||
const sourceDiscoveryPilotScopeHint = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryPilotScope)(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" ||
|
const hasValueFlowCarryoverSourceHint = sourceIntentHint === "customer_revenue_and_payments" ||
|
||||||
sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" ||
|
sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" ||
|
||||||
sourceDiscoveryPilotScopeHint === "counterparty_supplier_payout_query_movements_v1" ||
|
sourceDiscoveryPilotScopeHint === "counterparty_supplier_payout_query_movements_v1" ||
|
||||||
sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_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 navigationSessionState = (0, assistantContinuityPolicy_1.resolveNavigationSessionContextState)(addressNavigationState, deps.toNonEmptyString, deps.normalizeOrganizationScopeValue);
|
||||||
const navigationFocusObjectHint = navigationSessionState.focusObject;
|
const navigationFocusObjectHint = navigationSessionState.focusObject;
|
||||||
const hasNavigationInventoryItemFocusHint = Boolean(deps.toNonEmptyString(navigationFocusObjectHint?.label) &&
|
const hasNavigationInventoryItemFocusHint = Boolean(deps.toNonEmptyString(navigationFocusObjectHint?.label) &&
|
||||||
|
|
@ -521,20 +556,28 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
const shortValueFlowRetargetAlternate = hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
|
const shortValueFlowRetargetAlternate = hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
|
||||||
? hasShortValueFlowRetargetCue(String(alternateMessage ?? ""))
|
? hasShortValueFlowRetargetCue(String(alternateMessage ?? ""))
|
||||||
: false;
|
: false;
|
||||||
|
const businessOverviewBoundaryFollowupPrimary = hasBusinessOverviewCarryoverSourceHint && hasBusinessOverviewBoundaryFollowupCue(userMessage);
|
||||||
|
const businessOverviewBoundaryFollowupAlternate = hasBusinessOverviewCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
|
||||||
|
? hasBusinessOverviewBoundaryFollowupCue(String(alternateMessage ?? ""))
|
||||||
|
: false;
|
||||||
const explicitSummaryBundleReuseSignal = hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage);
|
const explicitSummaryBundleReuseSignal = hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage);
|
||||||
let hasPrimaryFollowupSignal = deps.hasAddressFollowupContextSignal(userMessage) ||
|
let hasPrimaryFollowupSignal = deps.hasAddressFollowupContextSignal(userMessage) ||
|
||||||
Boolean(debtRoleSwapPrimary) ||
|
Boolean(debtRoleSwapPrimary) ||
|
||||||
shortValueFlowRetargetPrimary ||
|
shortValueFlowRetargetPrimary ||
|
||||||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
inventoryShortFollowupPrimary ||
|
inventoryShortFollowupPrimary ||
|
||||||
inventoryPurchaseDateVatBridge ||
|
inventoryPurchaseDateVatBridge ||
|
||||||
explicitSummaryBundleReuseSignal;
|
explicitSummaryBundleReuseSignal ||
|
||||||
|
mcpDiscoveryOrganizationClarificationContinuation;
|
||||||
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||||
Boolean(debtRoleSwapAlternate) ||
|
Boolean(debtRoleSwapAlternate) ||
|
||||||
shortValueFlowRetargetAlternate ||
|
shortValueFlowRetargetAlternate ||
|
||||||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
inventoryShortFollowupAlternate ||
|
inventoryShortFollowupAlternate ||
|
||||||
inventoryPurchaseDateVatBridge ||
|
inventoryPurchaseDateVatBridge ||
|
||||||
explicitSummaryBundleReuseSignal
|
explicitSummaryBundleReuseSignal ||
|
||||||
|
mcpDiscoveryOrganizationClarificationContinuation
|
||||||
: false;
|
: false;
|
||||||
const hasPrimaryIndexReferenceSignal = deps.extractDisplayedEntityIndexMention(userMessage) !== null;
|
const hasPrimaryIndexReferenceSignal = deps.extractDisplayedEntityIndexMention(userMessage) !== null;
|
||||||
const hasAlternateIndexReferenceSignal = deps.toNonEmptyString(alternateMessage)
|
const hasAlternateIndexReferenceSignal = deps.toNonEmptyString(alternateMessage)
|
||||||
|
|
@ -557,6 +600,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
let hasStrongFollowupReference = hasPrimaryIndexReferenceSignal ||
|
let hasStrongFollowupReference = hasPrimaryIndexReferenceSignal ||
|
||||||
hasAlternateIndexReferenceSignal ||
|
hasAlternateIndexReferenceSignal ||
|
||||||
hasOrganizationClarificationContinuation ||
|
hasOrganizationClarificationContinuation ||
|
||||||
|
mcpDiscoveryOrganizationClarificationContinuation ||
|
||||||
hasImplicitContinuationSignal ||
|
hasImplicitContinuationSignal ||
|
||||||
hasSuggestedIntentPivotSignal ||
|
hasSuggestedIntentPivotSignal ||
|
||||||
inventoryShortFollowupPrimary ||
|
inventoryShortFollowupPrimary ||
|
||||||
|
|
@ -570,6 +614,8 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
Boolean(debtRoleSwapIntent) ||
|
Boolean(debtRoleSwapIntent) ||
|
||||||
shortValueFlowRetargetPrimary ||
|
shortValueFlowRetargetPrimary ||
|
||||||
shortValueFlowRetargetAlternate ||
|
shortValueFlowRetargetAlternate ||
|
||||||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
deps.hasFollowupMarker(userMessage) ||
|
deps.hasFollowupMarker(userMessage) ||
|
||||||
deps.hasReferentialPointer(userMessage) ||
|
deps.hasReferentialPointer(userMessage) ||
|
||||||
(deps.toNonEmptyString(alternateMessage)
|
(deps.toNonEmptyString(alternateMessage)
|
||||||
|
|
@ -579,6 +625,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
const hasConcreteFollowupReference = hasPrimaryIndexReferenceSignal ||
|
const hasConcreteFollowupReference = hasPrimaryIndexReferenceSignal ||
|
||||||
hasAlternateIndexReferenceSignal ||
|
hasAlternateIndexReferenceSignal ||
|
||||||
hasOrganizationClarificationContinuation ||
|
hasOrganizationClarificationContinuation ||
|
||||||
|
mcpDiscoveryOrganizationClarificationContinuation ||
|
||||||
inventoryShortFollowupPrimary ||
|
inventoryShortFollowupPrimary ||
|
||||||
inventoryShortFollowupAlternate ||
|
inventoryShortFollowupAlternate ||
|
||||||
hasInventoryRootTemporalFollowupPrimary ||
|
hasInventoryRootTemporalFollowupPrimary ||
|
||||||
|
|
@ -590,6 +637,8 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
Boolean(debtRoleSwapIntent) ||
|
Boolean(debtRoleSwapIntent) ||
|
||||||
shortValueFlowRetargetPrimary ||
|
shortValueFlowRetargetPrimary ||
|
||||||
shortValueFlowRetargetAlternate ||
|
shortValueFlowRetargetAlternate ||
|
||||||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
deps.hasFollowupMarker(userMessage) ||
|
deps.hasFollowupMarker(userMessage) ||
|
||||||
deps.hasReferentialPointer(userMessage) ||
|
deps.hasReferentialPointer(userMessage) ||
|
||||||
(deps.toNonEmptyString(alternateMessage)
|
(deps.toNonEmptyString(alternateMessage)
|
||||||
|
|
@ -617,6 +666,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
!hasImplicitContinuationSignal &&
|
!hasImplicitContinuationSignal &&
|
||||||
!hasSuggestedIntentPivotSignal &&
|
!hasSuggestedIntentPivotSignal &&
|
||||||
!hasOrganizationClarificationContinuation &&
|
!hasOrganizationClarificationContinuation &&
|
||||||
|
!mcpDiscoveryOrganizationClarificationContinuation &&
|
||||||
!hasIndexReferenceSignal &&
|
!hasIndexReferenceSignal &&
|
||||||
!explicitSummaryBundleReuseSignal) {
|
!explicitSummaryBundleReuseSignal) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -632,6 +682,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
!hasImplicitContinuationSignal &&
|
!hasImplicitContinuationSignal &&
|
||||||
!hasSuggestedIntentPivotSignal &&
|
!hasSuggestedIntentPivotSignal &&
|
||||||
!hasOrganizationClarificationContinuation &&
|
!hasOrganizationClarificationContinuation &&
|
||||||
|
!mcpDiscoveryOrganizationClarificationContinuation &&
|
||||||
!hasIndexReferenceSignal &&
|
!hasIndexReferenceSignal &&
|
||||||
!explicitSummaryBundleReuseSignal) {
|
!explicitSummaryBundleReuseSignal) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -650,10 +701,10 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
const sourceDiscoveryMetadataAmbiguityEntitySets = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataAmbiguityEntitySets)(carryoverSourceDebug, deps.toNonEmptyString);
|
const sourceDiscoveryMetadataAmbiguityEntitySets = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataAmbiguityEntitySets)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||||
const sourceDiscoveryEntityResolutionStatus = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityResolutionStatus)(carryoverSourceDebug, deps.toNonEmptyString);
|
const sourceDiscoveryEntityResolutionStatus = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityResolutionStatus)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||||
const sourceDiscoveryEntityCandidates = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityCandidates)(carryoverSourceDebug, deps.toNonEmptyString);
|
const sourceDiscoveryEntityCandidates = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityCandidates)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||||
const sourceDiscoveryLoopStatus = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopStatus)(carryoverSourceDebug, deps.toNonEmptyString);
|
const sourceDiscoveryLoopStatus = sourceDiscoveryLoopStatusHint;
|
||||||
const sourceDiscoveryLoopSelectedChainId = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopSelectedChainId)(carryoverSourceDebug, deps.toNonEmptyString);
|
const sourceDiscoveryLoopSelectedChainId = sourceDiscoveryLoopSelectedChainIdHint;
|
||||||
const sourceDiscoveryLoopPendingAxes = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopPendingAxes)(carryoverSourceDebug, deps.toNonEmptyString);
|
const sourceDiscoveryLoopPendingAxes = sourceDiscoveryLoopPendingAxesHint;
|
||||||
const sourceDiscoveryLoopProvidedAxes = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopProvidedAxes)(carryoverSourceDebug, deps.toNonEmptyString);
|
const sourceDiscoveryLoopProvidedAxes = sourceDiscoveryLoopProvidedAxesHint;
|
||||||
const sourceDiscoveryLoopAskedDomainFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopAskedDomainFamily)(carryoverSourceDebug, deps.toNonEmptyString);
|
const sourceDiscoveryLoopAskedDomainFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopAskedDomainFamily)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||||
const sourceDiscoveryLoopAskedActionFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopAskedActionFamily)(carryoverSourceDebug, deps.toNonEmptyString);
|
const sourceDiscoveryLoopAskedActionFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopAskedActionFamily)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||||
const sourceDiscoveryLoopUnsupportedFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopUnsupportedFamily)(carryoverSourceDebug, deps.toNonEmptyString);
|
const sourceDiscoveryLoopUnsupportedFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopUnsupportedFamily)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||||
|
|
@ -689,6 +740,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
explicitIntentFamily &&
|
explicitIntentFamily &&
|
||||||
sourceIntentFamily !== explicitIntentFamily &&
|
sourceIntentFamily !== explicitIntentFamily &&
|
||||||
!hasOrganizationClarificationContinuation &&
|
!hasOrganizationClarificationContinuation &&
|
||||||
|
!mcpDiscoveryOrganizationClarificationContinuation &&
|
||||||
!hasImplicitContinuationSignal &&
|
!hasImplicitContinuationSignal &&
|
||||||
!hasIndexReferenceSignal &&
|
!hasIndexReferenceSignal &&
|
||||||
!hasInventoryRootTemporalFollowupPrimary &&
|
!hasInventoryRootTemporalFollowupPrimary &&
|
||||||
|
|
@ -697,6 +749,8 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
!hasInventoryRootRestatementAlternate &&
|
!hasInventoryRootRestatementAlternate &&
|
||||||
!inventoryShortFollowupPrimary &&
|
!inventoryShortFollowupPrimary &&
|
||||||
!inventoryShortFollowupAlternate &&
|
!inventoryShortFollowupAlternate &&
|
||||||
|
!businessOverviewBoundaryFollowupPrimary &&
|
||||||
|
!businessOverviewBoundaryFollowupAlternate &&
|
||||||
!foreignAccountingPivotOverInventory &&
|
!foreignAccountingPivotOverInventory &&
|
||||||
!deps.hasFollowupMarker(userMessage) &&
|
!deps.hasFollowupMarker(userMessage) &&
|
||||||
!deps.hasReferentialPointer(userMessage) &&
|
!deps.hasReferentialPointer(userMessage) &&
|
||||||
|
|
@ -753,24 +807,29 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
hasSuggestedIntentPivotSignal ||
|
hasSuggestedIntentPivotSignal ||
|
||||||
Boolean(debtRoleSwapPrimary) ||
|
Boolean(debtRoleSwapPrimary) ||
|
||||||
shortValueFlowRetargetPrimary ||
|
shortValueFlowRetargetPrimary ||
|
||||||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
inventoryShortFollowupPrimary ||
|
inventoryShortFollowupPrimary ||
|
||||||
inventoryPurchaseDateVatBridge ||
|
inventoryPurchaseDateVatBridge ||
|
||||||
explicitSummaryBundleReuseSignal ||
|
explicitSummaryBundleReuseSignal ||
|
||||||
hasInventoryRootTemporalFollowupPrimary;
|
hasInventoryRootTemporalFollowupPrimary ||
|
||||||
|
mcpDiscoveryOrganizationClarificationContinuation;
|
||||||
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||||
hasSuggestedIntentPivotSignal ||
|
hasSuggestedIntentPivotSignal ||
|
||||||
Boolean(debtRoleSwapAlternate) ||
|
Boolean(debtRoleSwapAlternate) ||
|
||||||
shortValueFlowRetargetAlternate ||
|
shortValueFlowRetargetAlternate ||
|
||||||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
inventoryShortFollowupAlternate ||
|
inventoryShortFollowupAlternate ||
|
||||||
inventoryPurchaseDateVatBridge ||
|
inventoryPurchaseDateVatBridge ||
|
||||||
explicitSummaryBundleReuseSignal ||
|
explicitSummaryBundleReuseSignal ||
|
||||||
hasInventoryRootTemporalFollowupAlternate
|
hasInventoryRootTemporalFollowupAlternate ||
|
||||||
|
mcpDiscoveryOrganizationClarificationContinuation
|
||||||
: false;
|
: false;
|
||||||
hasStrongFollowupReference =
|
hasStrongFollowupReference =
|
||||||
hasPrimaryIndexReferenceSignal ||
|
hasPrimaryIndexReferenceSignal ||
|
||||||
hasAlternateIndexReferenceSignal ||
|
hasAlternateIndexReferenceSignal ||
|
||||||
hasOrganizationClarificationContinuation ||
|
hasOrganizationClarificationContinuation ||
|
||||||
|
mcpDiscoveryOrganizationClarificationContinuation ||
|
||||||
hasSuggestedIntentPivotSignal ||
|
hasSuggestedIntentPivotSignal ||
|
||||||
hasImplicitContinuationSignal ||
|
hasImplicitContinuationSignal ||
|
||||||
inventoryShortFollowupPrimary ||
|
inventoryShortFollowupPrimary ||
|
||||||
|
|
@ -782,6 +841,8 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
Boolean(debtRoleSwapIntent) ||
|
Boolean(debtRoleSwapIntent) ||
|
||||||
shortValueFlowRetargetPrimary ||
|
shortValueFlowRetargetPrimary ||
|
||||||
shortValueFlowRetargetAlternate ||
|
shortValueFlowRetargetAlternate ||
|
||||||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
deps.hasFollowupMarker(userMessage) ||
|
deps.hasFollowupMarker(userMessage) ||
|
||||||
deps.hasReferentialPointer(userMessage) ||
|
deps.hasReferentialPointer(userMessage) ||
|
||||||
(deps.toNonEmptyString(alternateMessage)
|
(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_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_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_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"]);
|
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_range_phrase") ||
|
||||||
warnings.includes("period_derived_from_year_phrase");
|
warnings.includes("period_derived_from_year_phrase");
|
||||||
const preserveDerivedPeriodWindow =
|
const preserveDerivedPeriodWindow =
|
||||||
usesAsOfPrimaryWindow(intent) ||
|
|
||||||
intent === "inventory_on_hand_as_of_date" ||
|
intent === "inventory_on_hand_as_of_date" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date";
|
intent === "inventory_supplier_stock_overlap_as_of_date";
|
||||||
if (periodWasDerivedHeuristically && !warnings.includes("exact_historical_period_window_requested")) {
|
if (periodWasDerivedHeuristically && !warnings.includes("exact_historical_period_window_requested")) {
|
||||||
|
|
|
||||||
|
|
@ -2166,7 +2166,11 @@ function hasVatPeriodInspectionBridgeSignal(text: string): boolean {
|
||||||
normalized
|
normalized
|
||||||
);
|
);
|
||||||
const forecastOnlyCue = /(?:прогноз|план|примерн|ориентировочн)/iu.test(normalized) && !hasInspectionCue;
|
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 {
|
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 =
|
const hasLooseVatPayableBridge =
|
||||||
/(?:\u043d\u0434\u0441|vat)/iu.test(text) &&
|
/(?:\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(
|
/(?:\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__
|
Сумма __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 = `
|
const VAT_PAYABLE_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
||||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||||
__AS_OF_EXPR__ КАК Период,
|
__AS_OF_EXPR__ КАК Период,
|
||||||
|
|
@ -747,7 +793,7 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
|
||||||
purpose: "Build customer value ranking and incoming deal profile from bank inflow docs",
|
purpose: "Build customer value ranking and incoming deal profile from bank inflow docs",
|
||||||
required_filters: [],
|
required_filters: [],
|
||||||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
||||||
default_limit: 20,
|
default_limit: 200,
|
||||||
account_scope_mode: "preferred",
|
account_scope_mode: "preferred",
|
||||||
query_template: "customer_revenue_profile"
|
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",
|
purpose: "Build supplier payout ranking and outgoing deal profile from bank outflow docs",
|
||||||
required_filters: [],
|
required_filters: [],
|
||||||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
||||||
default_limit: 20,
|
default_limit: 200,
|
||||||
account_scope_mode: "preferred",
|
account_scope_mode: "preferred",
|
||||||
query_template: "supplier_payout_profile"
|
query_template: "supplier_payout_profile"
|
||||||
},
|
},
|
||||||
|
|
@ -802,6 +848,17 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
|
||||||
account_scope_mode: "preferred",
|
account_scope_mode: "preferred",
|
||||||
query_template: "vat_liability_confirmed_tax_period_profile"
|
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",
|
recipe_id: "address_inventory_on_hand_as_of_date_v1",
|
||||||
intent: "inventory_on_hand_as_of_date",
|
intent: "inventory_on_hand_as_of_date",
|
||||||
|
|
@ -912,6 +969,17 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
|
||||||
account_scope_mode: "strict",
|
account_scope_mode: "strict",
|
||||||
query_template: "open_contracts_confirmed_as_of_balance_profile"
|
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",
|
recipe_id: "address_contracts_by_counterparty_v1",
|
||||||
intent: "list_contracts_by_counterparty",
|
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 {
|
function normalizeAccountTokenForQuery(value: string): string {
|
||||||
const source = String(value ?? "").trim().replace(",", ".");
|
const source = String(value ?? "").trim().replace(",", ".");
|
||||||
const match = source.match(/^(\d{2})(?:\.(\d{1,2}))?/);
|
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(" ИЛИ ")})`;
|
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(
|
function buildInventoryMovementQuery(
|
||||||
filters: AddressFilterSet,
|
filters: AddressFilterSet,
|
||||||
resolvedLimit: number,
|
resolvedLimit: number,
|
||||||
|
|
@ -1340,6 +1481,179 @@ function buildCounterpartyReferenceCondition(filters: AddressFilterSet, fieldPat
|
||||||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
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 {
|
function buildInventorySaleDocumentQuery(filters: AddressFilterSet, resolvedLimit: number): string {
|
||||||
const itemCondition = buildInventoryItemReferenceCondition(filters, ["Товары.Номенклатура"]);
|
const itemCondition = buildInventoryItemReferenceCondition(filters, ["Товары.Номенклатура"]);
|
||||||
return INVENTORY_SALE_DOCUMENTS_QUERY_TEMPLATE
|
return INVENTORY_SALE_DOCUMENTS_QUERY_TEMPLATE
|
||||||
|
|
@ -1458,6 +1772,8 @@ function maxLimitForIntent(intent: AddressIntent): number {
|
||||||
intent === "contract_usage_and_value" ||
|
intent === "contract_usage_and_value" ||
|
||||||
intent === "vat_payable_forecast" ||
|
intent === "vat_payable_forecast" ||
|
||||||
intent === "vat_liability_confirmed_for_tax_period" ||
|
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_on_hand_as_of_date" ||
|
||||||
intent === "inventory_purchase_provenance_for_item" ||
|
intent === "inventory_purchase_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
|
|
@ -1518,7 +1834,8 @@ export function buildAddressRecipePlan(
|
||||||
recipe.query_template === "counterparty_roles_profile" ||
|
recipe.query_template === "counterparty_roles_profile" ||
|
||||||
recipe.query_template === "contract_usage_profile" ||
|
recipe.query_template === "contract_usage_profile" ||
|
||||||
recipe.query_template === "vat_payable_forecast_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 =
|
const baseLimit =
|
||||||
typeof filters.limit === "number" && Number.isFinite(filters.limit)
|
typeof filters.limit === "number" && Number.isFinite(filters.limit)
|
||||||
? Math.max(1, Math.min(maxLimit, Math.trunc(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("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период"))
|
||||||
.replaceAll("__PERIOD_TO_EXPR__", periodToExpr);
|
.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"
|
: recipe.query_template === "vat_payable_confirmed_as_of_balance_profile"
|
||||||
? (() => {
|
? (() => {
|
||||||
const asOfExpr =
|
const asOfExpr =
|
||||||
|
|
|
||||||
|
|
@ -705,11 +705,19 @@ function detectValueRankingFocus(userMessage: string | null | undefined): ValueR
|
||||||
if (asksTotalMoneyEarned) {
|
if (asksTotalMoneyEarned) {
|
||||||
return "total_flow";
|
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 =
|
const asksYearlyRevenueRanking =
|
||||||
/(?:доходн|выручк|оборот|прибыл|деньг|денег|revenue|turnover|income)/iu.test(text) &&
|
/(?:доходн|выручк|оборот|прибыл|деньг|денег|revenue|turnover|income)/iu.test(text) &&
|
||||||
/(?:год|года|годы|year|years|по\s+годам)/iu.test(text) &&
|
/(?:год|года|годы|year|years|по\s+годам)/iu.test(text) &&
|
||||||
/(?:сам(?:ый|ая|ое|ые)|топ|луч|best|max|наибольш|больше)/iu.test(text);
|
/(?:сам(?:ый|ая|ое|ые)|топ|луч|best|max|наибольш|больше)/iu.test(text);
|
||||||
if (asksYearlyRevenueRanking) {
|
if (asksYearlyRevenueRanking && (!hasCounterpartyRankingSubject || asksExplicitYearBreakdown)) {
|
||||||
return "top_years_by_total";
|
return "top_years_by_total";
|
||||||
}
|
}
|
||||||
if (/(?:сам(?:ый|ая|ое|ые)\s+высок[а-яё]*|highest|largest)\s+чек|(?:max\s+check|чек\s+макс)/iu.test(text)) {
|
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;
|
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
|
||||||
lines.push(
|
lines.push(
|
||||||
"",
|
"",
|
||||||
"Покрытие VAT-источников через MCP:",
|
"Покрытие VAT-источников в 1С:",
|
||||||
`- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`,
|
`- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`,
|
||||||
`- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`,
|
`- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`,
|
||||||
`- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`,
|
`- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`,
|
||||||
|
|
@ -3241,7 +3249,7 @@ function composeFactualReplyBody(
|
||||||
}
|
}
|
||||||
lines.push("- Сумма прогноза выше рассчитана строго по оборотам 68.02*/19*; прямые VAT-источники показаны для проверки покрытия.");
|
lines.push("- Сумма прогноза выше рассчитана строго по оборотам 68.02*/19*; прямые VAT-источники показаны для проверки покрытия.");
|
||||||
} else if (vatProbe && vatProbe.status === "error") {
|
} else if (vatProbe && vatProbe.status === "error") {
|
||||||
lines.push("", "Покрытие VAT-источников через MCP: probe завершился ошибкой, поэтому использован только базовый контур 68.02*/19*.");
|
lines.push("", "Покрытие VAT-источников в 1С: дополнительная проверка завершилась ошибкой, поэтому использован только базовый контур 68.02*/19*.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!vatActivityDetected) {
|
if (!vatActivityDetected) {
|
||||||
|
|
@ -3328,13 +3336,16 @@ function composeFactualReplyBody(
|
||||||
options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null;
|
options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null;
|
||||||
const formatConfirmedMoney = (value: number): string => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
const formatConfirmedMoney = (value: number): string => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
||||||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
const vatProbe = options.vatDirectSourceProbe ?? null;
|
||||||
|
const organizationLabel = normalizeOrganizationScopeValue(options.organizationHint);
|
||||||
|
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
|
||||||
|
|
||||||
const lines = [
|
const lines = [
|
||||||
`Коротко: подтвержденный НДС к уплате за налоговый период — ${formatConfirmedMoney(vatToPay)}.`,
|
`Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`,
|
||||||
`Если смотреть на возможный перенос или переплату, получается ${formatConfirmedMoney(carryoverOrOverpayment)}.`,
|
`Если смотреть на возможный перенос или переплату, получается ${formatConfirmedMoney(carryoverOrOverpayment)}.`,
|
||||||
"Это подтвержденный расчет по регистрам книг продаж и покупок, без surrogate-формулы 68/19.",
|
"Это подтвержденный расчет по регистрам книг продаж и покупок, без surrogate-формулы 68/19.",
|
||||||
"",
|
"",
|
||||||
"Что вошло в расчет:",
|
"Что вошло в расчет:",
|
||||||
|
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
|
||||||
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
|
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
|
||||||
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
|
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
|
||||||
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
|
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
|
||||||
|
|
@ -3346,7 +3357,7 @@ function composeFactualReplyBody(
|
||||||
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
|
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
|
||||||
lines.push(
|
lines.push(
|
||||||
"",
|
"",
|
||||||
"Покрытие VAT-источников через MCP:",
|
"Покрытие VAT-источников в 1С:",
|
||||||
`- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`,
|
`- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`,
|
||||||
`- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`,
|
`- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`,
|
||||||
`- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`,
|
`- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`,
|
||||||
|
|
@ -3355,11 +3366,11 @@ function composeFactualReplyBody(
|
||||||
if (vatProbe.errors.length > 0) {
|
if (vatProbe.errors.length > 0) {
|
||||||
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
||||||
}
|
}
|
||||||
lines.push("- Сумма расчета выше получена по книгам продаж/покупок; probe использован для контроля полноты VAT-источников.");
|
lines.push("- Сумма расчета выше получена по книгам продаж/покупок; дополнительная проверка использована для контроля полноты VAT-источников.");
|
||||||
} else if (vatProbe && vatProbe.status === "error") {
|
} else if (vatProbe && vatProbe.status === "error") {
|
||||||
lines.push(
|
lines.push(
|
||||||
"",
|
"",
|
||||||
"Покрытие VAT-источников через MCP: дополнительный probe недоступен (например, timeout metadata).",
|
"Покрытие VAT-источников в 1С: дополнительная проверка недоступна, поэтому использован основной бухгалтерский срез.",
|
||||||
"Итоговая сумма НДС выше рассчитана по основному маршруту книг продаж/покупок; probe влияет только на диагностику покрытия."
|
"Итоговая сумма НДС выше рассчитана по основному маршруту книг продаж/покупок; probe влияет только на диагностику покрытия."
|
||||||
);
|
);
|
||||||
if (vatProbe.errors.length > 0) {
|
if (vatProbe.errors.length > 0) {
|
||||||
|
|
@ -3453,7 +3464,7 @@ function composeFactualReplyBody(
|
||||||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
||||||
lines.push(
|
lines.push(
|
||||||
"",
|
"",
|
||||||
"Блок 2.1. MCP-проверка VAT-источников",
|
"Блок 2.1. Проверка VAT-источников в 1С",
|
||||||
`- VAT-объектов в метаданных 1С: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`,
|
`- VAT-объектов в метаданных 1С: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`,
|
||||||
`- Пробных прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`,
|
`- Пробных прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`,
|
||||||
`- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`
|
`- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`
|
||||||
|
|
@ -3478,8 +3489,8 @@ function composeFactualReplyBody(
|
||||||
} else if (vatProbe && vatProbe.status === "error") {
|
} else if (vatProbe && vatProbe.status === "error") {
|
||||||
lines.push(
|
lines.push(
|
||||||
"",
|
"",
|
||||||
"Блок 2.1. MCP-проверка VAT-источников",
|
"Блок 2.1. Проверка VAT-источников в 1С",
|
||||||
"- Probe VAT-источников завершился ошибкой, поэтому срез подтвержден по доступному бухгалтерскому источнику (68*)."
|
"- Дополнительная проверка VAT-источников завершилась ошибкой, поэтому срез подтвержден по доступному бухгалтерскому источнику (68*)."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -148,9 +148,9 @@ export function composeCounterpartyAnalyticsReply(
|
||||||
const includeRoles = focus === "full_profile" || focus === "roles_only";
|
const includeRoles = focus === "full_profile" || focus === "roles_only";
|
||||||
const directLead =
|
const directLead =
|
||||||
focus === "suppliers_only"
|
focus === "suppliers_only"
|
||||||
? `Поставщиков (только supplier-роль): ${supplierOnly}.`
|
? `Поставщиков с ролью поставщика: ${supplierOnly}.`
|
||||||
: focus === "customers_only"
|
: focus === "customers_only"
|
||||||
? `Заказчиков (только customer-роль): ${customerOnly}.`
|
? `Заказчиков с ролью покупателя: ${customerOnly}.`
|
||||||
: focus === "mixed_only"
|
: focus === "mixed_only"
|
||||||
? `Контрагентов со смешанной ролью: ${mixedActive}.`
|
? `Контрагентов со смешанной ролью: ${mixedActive}.`
|
||||||
: includeTotal && totalCounterparties > 0
|
: includeTotal && totalCounterparties > 0
|
||||||
|
|
@ -175,9 +175,9 @@ export function composeCounterpartyAnalyticsReply(
|
||||||
|
|
||||||
if (includeRoles) {
|
if (includeRoles) {
|
||||||
if (resolvedActive > 0 || activeCounterparties > 0) {
|
if (resolvedActive > 0 || activeCounterparties > 0) {
|
||||||
lines.push("Роли контрагентов по активности:");
|
lines.push("Распределение ролей по активности:");
|
||||||
lines.push(`Заказчики (только customer-роль): ${customerOnly}.`);
|
lines.push(`Заказчики с ролью покупателя: ${customerOnly}.`);
|
||||||
lines.push(`Поставщики (только supplier-роль): ${supplierOnly}.`);
|
lines.push(`Поставщики с ролью поставщика: ${supplierOnly}.`);
|
||||||
lines.push(`Смешанные (и покупатель, и поставщик): ${mixedActive}.`);
|
lines.push(`Смешанные (и покупатель, и поставщик): ${mixedActive}.`);
|
||||||
lines.push(`4. Всего активных контрагентов: ${activeCounterparties}.`);
|
lines.push(`4. Всего активных контрагентов: ${activeCounterparties}.`);
|
||||||
if (otherCounterparties !== null) {
|
if (otherCounterparties !== null) {
|
||||||
|
|
@ -189,10 +189,10 @@ export function composeCounterpartyAnalyticsReply(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (focus === "suppliers_only") {
|
if (focus === "suppliers_only") {
|
||||||
lines.push(`Поставщиков (только supplier-роль): ${supplierOnly}.`);
|
lines.push(`Поставщиков с ролью поставщика: ${supplierOnly}.`);
|
||||||
}
|
}
|
||||||
if (focus === "customers_only") {
|
if (focus === "customers_only") {
|
||||||
lines.push(`Заказчиков (только customer-роль): ${customerOnly}.`);
|
lines.push(`Заказчиков с ролью покупателя: ${customerOnly}.`);
|
||||||
}
|
}
|
||||||
if (focus === "mixed_only") {
|
if (focus === "mixed_only") {
|
||||||
lines.push(`Контрагентов со смешанной ролью: ${mixedActive}.`);
|
lines.push(`Контрагентов со смешанной ролью: ${mixedActive}.`);
|
||||||
|
|
@ -525,6 +525,16 @@ export function composeCounterpartyAnalyticsReply(
|
||||||
const limit = deps.detectRankingLimit(options.userMessage, 20);
|
const limit = deps.detectRankingLimit(options.userMessage, 20);
|
||||||
const minOpsForAvgCheck = deps.detectMinOpsForAvgCheck(options.userMessage);
|
const minOpsForAvgCheck = deps.detectMinOpsForAvgCheck(options.userMessage);
|
||||||
const normalizedQuestion = deps.normalizeQuestionText(options.userMessage);
|
const 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 byCounterparty = new Map<string, CounterpartyValuePoint>();
|
||||||
const byYear = new Map<number, CounterpartyYearPoint>();
|
const byYear = new Map<number, CounterpartyYearPoint>();
|
||||||
|
|
@ -728,7 +738,7 @@ export function composeCounterpartyAnalyticsReply(
|
||||||
lines.push(
|
lines.push(
|
||||||
...visible.map(
|
...visible.map(
|
||||||
(item, index) =>
|
(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);
|
return buildFactualListReply(lines);
|
||||||
|
|
@ -786,8 +796,12 @@ export function composeCounterpartyAnalyticsReply(
|
||||||
return buildFactualListReply(lines);
|
return buildFactualListReply(lines);
|
||||||
}
|
}
|
||||||
|
|
||||||
const visible = rankedByTotal.slice(0, limit);
|
const visible = rankedByTotal.slice(0, effectiveLimit);
|
||||||
const singleCandidateOnly = rankedByTotal.length === 1;
|
const singleCandidateOnly = rankedByTotal.length === 1;
|
||||||
|
const rankingPeriodLabel =
|
||||||
|
options.periodFrom && options.periodTo
|
||||||
|
? `за период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)}`
|
||||||
|
: "за доступное время";
|
||||||
const heading = singleCandidateOnly
|
const heading = singleCandidateOnly
|
||||||
? isSupplier
|
? isSupplier
|
||||||
? "Найденный поставщик по сумме выплат:"
|
? "Найденный поставщик по сумме выплат:"
|
||||||
|
|
@ -797,14 +811,17 @@ export function composeCounterpartyAnalyticsReply(
|
||||||
: `Топ-${visible.length} заказчиков по сумме поступлений:`;
|
: `Топ-${visible.length} заказчиков по сумме поступлений:`;
|
||||||
const leadingCounterparty = visible[0] ?? null;
|
const leadingCounterparty = visible[0] ?? null;
|
||||||
lines.unshift(heading);
|
lines.unshift(heading);
|
||||||
|
if (options.periodFrom && options.periodTo) {
|
||||||
|
lines.push(`Период рейтинга: ${rankingPeriodLabel}.`);
|
||||||
|
}
|
||||||
if (leadingCounterparty) {
|
if (leadingCounterparty) {
|
||||||
const directAnswerLine = singleCandidateOnly
|
const directAnswerLine = singleCandidateOnly
|
||||||
? isSupplier
|
? isSupplier
|
||||||
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
||||||
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг; сумма является денежным потоком, а не чистой прибылью.`
|
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг; сумма является денежным потоком, а не чистой прибылью.`
|
||||||
: isSupplier
|
: isSupplier
|
||||||
? `Крупнейший поставщик по подтвержденным выплатам за доступное время: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
? `Крупнейший поставщик по подтвержденным выплатам ${rankingPeriodLabel}: ${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} операциям). Это денежный поток, а не чистая прибыль.`;
|
||||||
lines.unshift(directAnswerLine);
|
lines.unshift(directAnswerLine);
|
||||||
}
|
}
|
||||||
lines.push(
|
lines.push(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { AssistantMcpDiscoveryPilotExecutionContract } from "./assistantMcpDiscoveryPilotExecutor";
|
import type { AssistantMcpDiscoveryPilotExecutionContract } from "./assistantMcpDiscoveryPilotExecutor";
|
||||||
|
import { isLikelyFinancialInstitutionCounterparty } from "./counterpartyRoleHeuristics";
|
||||||
|
|
||||||
export const ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION =
|
export const ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION =
|
||||||
"assistant_mcp_discovery_answer_draft_v1" as const;
|
"assistant_mcp_discovery_answer_draft_v1" as const;
|
||||||
|
|
@ -26,6 +27,7 @@ export interface AssistantMcpDiscoveryAnswerDraftContract {
|
||||||
}
|
}
|
||||||
|
|
||||||
type BusinessOverview = NonNullable<AssistantMcpDiscoveryPilotExecutionContract["derived_business_overview"]>;
|
type BusinessOverview = NonNullable<AssistantMcpDiscoveryPilotExecutionContract["derived_business_overview"]>;
|
||||||
|
type BusinessOverviewRankedBucket = BusinessOverview["top_customers"][number];
|
||||||
|
|
||||||
function normalizeReasonCode(value: string): string | null {
|
function normalizeReasonCode(value: string): string | null {
|
||||||
const normalized = value
|
const normalized = value
|
||||||
|
|
@ -483,6 +485,30 @@ function metadataRouteFamilyLabelRu(
|
||||||
return null;
|
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 {
|
function businessOverviewInventoryUnknownLabel(overview: BusinessOverview): string {
|
||||||
if (overview.inventory_staleness_risk_proxy) {
|
if (overview.inventory_staleness_risk_proxy) {
|
||||||
return "резервы/списания/ликвидационная стоимость склада";
|
return "резервы/списания/ликвидационная стоимость склада";
|
||||||
|
|
@ -543,6 +569,81 @@ function inlineBusinessOverviewAmount(value: string): string {
|
||||||
.replace(/[\s.]+$/u, "");
|
.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 {
|
function businessOverviewHeadlineMetricsLine(overview: BusinessOverview): string | null {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
if (overview.incoming_customer_revenue.rows_with_amount > 0) {
|
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) {
|
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)}`);
|
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);
|
const strongestIncomingYear = businessOverviewStrongestIncomingYear(overview);
|
||||||
if (strongestIncomingYear) {
|
if (strongestIncomingYear) {
|
||||||
parts.push(
|
parts.push(
|
||||||
|
|
@ -561,10 +680,59 @@ function businessOverviewHeadlineMetricsLine(overview: BusinessOverview): string
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return parts.length > 0
|
return parts.length > 0
|
||||||
? `${parts.join("; ")}. Это operating-flow proxy по найденным строкам, не бухгалтерская прибыль и не финрезультат`
|
? overview.accounting_financial_result
|
||||||
|
? `${parts.join("; ")}. Финрезультат ограничен найденными строками 1С и не является внешним аудитом или юридически подтвержденной отчетностью`
|
||||||
|
: `${parts.join("; ")}. Это operating-flow proxy по найденным строкам, не бухгалтерская прибыль и не финрезультат`
|
||||||
: null;
|
: 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 {
|
function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
|
||||||
const askedMonthlyBreakdown =
|
const askedMonthlyBreakdown =
|
||||||
pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
|
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") {
|
if (isBusinessOverviewPilot(pilot) && pilot.derived_business_overview && mode === "confirmed_with_bounded_inference") {
|
||||||
const overview = pilot.derived_business_overview;
|
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[] = [];
|
const families: string[] = [];
|
||||||
if (
|
if (
|
||||||
overview.incoming_customer_revenue.rows_with_amount > 0 ||
|
overview.incoming_customer_revenue.rows_with_amount > 0 ||
|
||||||
|
|
@ -608,6 +805,9 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
||||||
if (overview.tax_position) {
|
if (overview.tax_position) {
|
||||||
families.push("НДС-позиция");
|
families.push("НДС-позиция");
|
||||||
}
|
}
|
||||||
|
if (overview.accounting_financial_result) {
|
||||||
|
families.push("учетный финрезультат 90/91/99");
|
||||||
|
}
|
||||||
if (overview.trading_margin_proxy) {
|
if (overview.trading_margin_proxy) {
|
||||||
families.push("торговый margin proxy");
|
families.push("торговый margin proxy");
|
||||||
}
|
}
|
||||||
|
|
@ -623,6 +823,9 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
||||||
if (overview.debt_staleness_risk_proxy) {
|
if (overview.debt_staleness_risk_proxy) {
|
||||||
families.push("staleness risk proxy открытых расчетов");
|
families.push("staleness risk proxy открытых расчетов");
|
||||||
}
|
}
|
||||||
|
if (overview.debt_due_date_aging) {
|
||||||
|
families.push("due-date aging открытых расчетов");
|
||||||
|
}
|
||||||
if (overview.inventory_position) {
|
if (overview.inventory_position) {
|
||||||
families.push("складской срез на дату");
|
families.push("складской срез на дату");
|
||||||
}
|
}
|
||||||
|
|
@ -632,20 +835,24 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
||||||
if (overview.inventory_staleness_risk_proxy) {
|
if (overview.inventory_staleness_risk_proxy) {
|
||||||
families.push("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) {
|
if (!overview.tax_position) {
|
||||||
unknownFamilies.push("НДС");
|
unknownFamilies.push("НДС");
|
||||||
}
|
}
|
||||||
if (!overview.debt_position) {
|
if (!overview.debt_position) {
|
||||||
unknownFamilies.push("долговой срез");
|
unknownFamilies.push("долговой срез");
|
||||||
}
|
}
|
||||||
unknownFamilies.push(
|
if (!overview.debt_due_date_aging) {
|
||||||
overview.debt_staleness_risk_proxy
|
unknownFamilies.push(
|
||||||
|
overview.debt_staleness_risk_proxy
|
||||||
? "договорные сроки оплаты/due-date просрочка"
|
? "договорные сроки оплаты/due-date просрочка"
|
||||||
: overview.debt_open_settlement_quality
|
: overview.debt_open_settlement_quality
|
||||||
? "due-date просрочка"
|
? "due-date просрочка"
|
||||||
: "качество открытых расчетов"
|
: "качество открытых расчетов"
|
||||||
);
|
);
|
||||||
|
}
|
||||||
unknownFamilies.push(businessOverviewInventoryUnknownLabel(overview));
|
unknownFamilies.push(businessOverviewInventoryUnknownLabel(overview));
|
||||||
const metricLead = businessOverviewHeadlineMetricsLine(overview);
|
const metricLead = businessOverviewHeadlineMetricsLine(overview);
|
||||||
if (metricLead) {
|
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 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 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 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 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 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.");
|
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) {
|
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.");
|
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) {
|
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 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.");
|
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)) {
|
if (isDocumentPilot(pilot)) {
|
||||||
claims.push("Do not claim full document history outside the checked period.");
|
claims.push("Do not claim full document history outside the checked period.");
|
||||||
|
|
@ -1028,26 +1245,40 @@ function derivedRankedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotEx
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const leader = ranking.ranked_values[0];
|
const leader = ranking.ranked_values[0];
|
||||||
|
const leaderLooksFinancial = isFinancialInstitutionBucket(leader);
|
||||||
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
|
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
|
||||||
const period = ranking.period_scope ? ` за период ${ranking.period_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) {
|
if (ranking.ranked_values.length === 1) {
|
||||||
const singleLead =
|
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
|
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 =
|
const directionLead =
|
||||||
ranking.ranking_need === "bottom_asc"
|
leaderLooksFinancial
|
||||||
? ranking.value_flow_direction === "outgoing_supplier_payout"
|
? ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||||
? "Меньше всего заплатили контрагенту"
|
? "Крупнейший получатель исходящих денег"
|
||||||
: "Меньше всего денег принёс контрагент"
|
: "Крупнейший входящий денежный источник"
|
||||||
: ranking.value_flow_direction === "outgoing_supplier_payout"
|
: ranking.ranking_need === "bottom_asc"
|
||||||
? "Больше всего заплатили контрагенту"
|
? ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||||
: "Больше всего денег принёс контрагент";
|
? "Меньше всего заплатили контрагенту"
|
||||||
|
: "Меньше всего денег принёс контрагент"
|
||||||
|
: ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||||
|
? "Больше всего заплатили контрагенту"
|
||||||
|
: "Больше всего денег принёс контрагент";
|
||||||
const tail = ranking.ranked_values
|
const tail = ranking.ranked_values
|
||||||
.slice(1, 3)
|
.slice(1, 3)
|
||||||
.map((bucket) => `${bucket.axis_value} — ${bucket.total_amount_human_ru}`)
|
.map((bucket) => `${bucket.axis_value} — ${bucket.total_amount_human_ru}`)
|
||||||
|
|
@ -1056,7 +1287,7 @@ function derivedRankedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotEx
|
||||||
const limitCaveat = ranking.coverage_limited_by_probe_limit
|
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 {
|
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} строкам с суммой. Это не бухгалтерская прибыль.`
|
`Самый сильный год по подтвержденным входящим поступлениям: ${strongestIncomingYear.year_bucket} — ${strongestIncomingYear.incoming_total_amount_human_ru} по ${strongestIncomingYear.incoming_rows_with_amount} строкам с суммой. Это не бухгалтерская прибыль.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const leader = overview.top_customers[0];
|
const incomingLeaderLine = businessOverviewIncomingLeaderLine(overview);
|
||||||
if (leader) {
|
if (incomingLeaderLine) {
|
||||||
lines.push(`Самый крупный подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`);
|
lines.push(incomingLeaderLine);
|
||||||
}
|
}
|
||||||
const supplierLeader = overview.top_suppliers?.[0];
|
const outgoingLeaderLine = businessOverviewOutgoingLeaderLine(overview);
|
||||||
if (supplierLeader) {
|
if (outgoingLeaderLine) {
|
||||||
lines.push(
|
lines.push(outgoingLeaderLine);
|
||||||
`Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value} — ${supplierLeader.total_amount_human_ru}.`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (overview.yearly_breakdown?.length) {
|
if (overview.yearly_breakdown?.length) {
|
||||||
lines.push(
|
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}.`
|
`НДС-позиция за ${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) {
|
if (overview.trading_margin_proxy) {
|
||||||
const proxy = overview.trading_margin_proxy;
|
const proxy = overview.trading_margin_proxy;
|
||||||
const marginText = proxy.margin_to_revenue_pct === null ? "не рассчитана" : `${proxy.margin_to_revenue_pct}%`;
|
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.`
|
`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) {
|
if (overview.inventory_position) {
|
||||||
const leader = overview.inventory_position.top_items[0];
|
const leader = overview.inventory_position.top_items[0];
|
||||||
const leaderText = leader
|
const leaderText = leader
|
||||||
|
|
@ -1416,6 +1655,13 @@ function businessOverviewCustomerConcentrationLine(overview: BusinessOverview):
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const share = percentText(leader.total_amount, overview.incoming_customer_revenue.total_amount);
|
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
|
return share
|
||||||
? `Концентрация входящего потока: крупнейший подтвержденный клиент ${leader.axis_value} дает около ${share} проверенных входящих поступлений (${leader.total_amount_human_ru}). Это сигнал зависимости от клиента, а не полный customer-risk аудит.`
|
? `Концентрация входящего потока: крупнейший подтвержденный клиент ${leader.axis_value} дает около ${share} проверенных входящих поступлений (${leader.total_amount_human_ru}). Это сигнал зависимости от клиента, а не полный customer-risk аудит.`
|
||||||
: `Крупнейший подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`;
|
: `Крупнейший подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`;
|
||||||
|
|
@ -1427,6 +1673,13 @@ function businessOverviewSupplierConcentrationLine(overview: BusinessOverview):
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const share = percentText(leader.total_amount, overview.outgoing_supplier_payout.total_amount);
|
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
|
return share
|
||||||
? `Концентрация исходящего потока: крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru}). Это сигнал procurement concentration по найденным строкам, а не полный vendor-risk аудит или структура всех расходов.`
|
? `Концентрация исходящего потока: крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru}). Это сигнал procurement concentration по найденным строкам, а не полный vendor-risk аудит или структура всех расходов.`
|
||||||
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`;
|
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${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}%`;
|
: `маржинальность proxy ${overview.trading_margin_proxy.margin_to_revenue_pct}%`;
|
||||||
signals.push(`торговый спред proxy ${overview.trading_margin_proxy.gross_spread_proxy_human_ru}, ${marginText}`);
|
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) {
|
if (overview.debt_position) {
|
||||||
const debtDirection =
|
const debtDirection =
|
||||||
overview.debt_position.net_debt_position_direction === "net_receivable"
|
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}%`
|
`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) {
|
if (overview.document_activity_profile) {
|
||||||
const topDocument = overview.document_activity_profile.top_document_types[0];
|
const topDocument = overview.document_activity_profile.top_document_types[0];
|
||||||
const topSection = overview.document_activity_profile.top_account_sections[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) {
|
if (pilot.derived_business_overview?.tax_position) {
|
||||||
pushReason(reasonCodes, "answer_contains_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) {
|
if (pilot.derived_business_overview?.trading_margin_proxy) {
|
||||||
pushReason(reasonCodes, "answer_contains_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) {
|
if (pilot.derived_business_overview?.debt_staleness_risk_proxy) {
|
||||||
pushReason(reasonCodes, "answer_contains_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) {
|
if (pilot.derived_business_overview?.inventory_position) {
|
||||||
pushReason(reasonCodes, "answer_contains_business_overview_inventory_position");
|
pushReason(reasonCodes, "answer_contains_business_overview_inventory_position");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { AssistantMcpDiscoveryRuntimeEntryPointContract } from "./assistantMcpDiscoveryRuntimeEntryPoint";
|
import type { AssistantMcpDiscoveryRuntimeEntryPointContract } from "./assistantMcpDiscoveryRuntimeEntryPoint";
|
||||||
|
import type { AssistantMcpRouteCandidateContract } from "./assistantMcpDiscoveryRuntimeBridge";
|
||||||
|
|
||||||
export interface AssistantMcpDiscoveryDebugAttachmentFields {
|
export interface AssistantMcpDiscoveryDebugAttachmentFields {
|
||||||
assistant_mcp_discovery_entry_point_v1: AssistantMcpDiscoveryRuntimeEntryPointContract | null;
|
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_alignment_status: string | null;
|
||||||
mcp_discovery_catalog_chain_top_match: string | null;
|
mcp_discovery_catalog_chain_top_match: string | null;
|
||||||
mcp_discovery_catalog_chain_selected_matches_top: boolean;
|
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_answer_mode: string | null;
|
||||||
mcp_discovery_business_fact_answer_allowed: boolean;
|
mcp_discovery_business_fact_answer_allowed: boolean;
|
||||||
mcp_discovery_user_facing_response_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 {
|
function resolveEntryPoint(input: AttachAssistantMcpDiscoveryDebugInput): AssistantMcpDiscoveryRuntimeEntryPointContract | null {
|
||||||
if (isMcpDiscoveryEntryPointContract(input.entryPoint)) {
|
if (isMcpDiscoveryEntryPointContract(input.entryPoint)) {
|
||||||
return input.entryPoint;
|
return input.entryPoint;
|
||||||
|
|
@ -77,6 +96,7 @@ export function buildAssistantMcpDiscoveryDebugAttachmentFields(
|
||||||
const bridge = toRecordObject(entryPoint?.bridge);
|
const bridge = toRecordObject(entryPoint?.bridge);
|
||||||
const planner = toRecordObject(bridge?.planner);
|
const planner = toRecordObject(bridge?.planner);
|
||||||
const chainAlignment = toRecordObject(planner?.catalog_chain_template_alignment);
|
const chainAlignment = toRecordObject(planner?.catalog_chain_template_alignment);
|
||||||
|
const routeCandidate = isRouteCandidateContract(bridge?.route_candidate) ? bridge.route_candidate : null;
|
||||||
const answerDraft = toRecordObject(bridge?.answer_draft);
|
const answerDraft = toRecordObject(bridge?.answer_draft);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -90,6 +110,16 @@ export function buildAssistantMcpDiscoveryDebugAttachmentFields(
|
||||||
mcp_discovery_catalog_chain_alignment_status: toNonEmptyString(chainAlignment?.alignment_status),
|
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_top_match: toNonEmptyString(chainAlignment?.top_chain_template_match),
|
||||||
mcp_discovery_catalog_chain_selected_matches_top: chainAlignment?.selected_chain_matches_top === true,
|
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_answer_mode: toNonEmptyString(answerDraft?.answer_mode),
|
||||||
mcp_discovery_business_fact_answer_allowed: bridge?.business_fact_answer_allowed === true,
|
mcp_discovery_business_fact_answer_allowed: bridge?.business_fact_answer_allowed === true,
|
||||||
mcp_discovery_user_facing_response_allowed: bridge?.user_facing_response_allowed === true,
|
mcp_discovery_user_facing_response_allowed: bridge?.user_facing_response_allowed === true,
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,11 @@ import {
|
||||||
type AssistantMcpDiscoveryProbeResult
|
type AssistantMcpDiscoveryProbeResult
|
||||||
} from "./assistantMcpDiscoveryPolicy";
|
} from "./assistantMcpDiscoveryPolicy";
|
||||||
import { buildAddressRecipePlan, selectAddressRecipe } from "./addressRecipeCatalog";
|
import { buildAddressRecipePlan, selectAddressRecipe } from "./addressRecipeCatalog";
|
||||||
|
import {
|
||||||
|
counterpartyRoleHintForName,
|
||||||
|
isLikelyFinancialInstitutionCounterparty,
|
||||||
|
type CounterpartyRoleHint
|
||||||
|
} from "./counterpartyRoleHeuristics";
|
||||||
import type { AddressFilterSet, AddressIntent } from "../types/addressQuery";
|
import type { AddressFilterSet, AddressIntent } from "../types/addressQuery";
|
||||||
|
|
||||||
export const ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION =
|
export const ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION =
|
||||||
|
|
@ -83,6 +88,7 @@ export interface AssistantMcpDiscoveryRankedValueFlowBucket {
|
||||||
rows_with_amount: number;
|
rows_with_amount: number;
|
||||||
total_amount: number;
|
total_amount: number;
|
||||||
total_amount_human_ru: string;
|
total_amount_human_ru: string;
|
||||||
|
counterparty_role_hint?: CounterpartyRoleHint;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AssistantMcpDiscoveryDerivedRankedValueFlow {
|
export interface AssistantMcpDiscoveryDerivedRankedValueFlow {
|
||||||
|
|
@ -228,10 +234,12 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverview {
|
||||||
yearly_breakdown: AssistantMcpDiscoveryBusinessOverviewYearBucket[];
|
yearly_breakdown: AssistantMcpDiscoveryBusinessOverviewYearBucket[];
|
||||||
activity_period: AssistantMcpDiscoveryDerivedActivityPeriod | null;
|
activity_period: AssistantMcpDiscoveryDerivedActivityPeriod | null;
|
||||||
tax_position: AssistantMcpDiscoveryDerivedBusinessOverviewTaxPosition | null;
|
tax_position: AssistantMcpDiscoveryDerivedBusinessOverviewTaxPosition | null;
|
||||||
|
accounting_financial_result: AssistantMcpDiscoveryDerivedBusinessOverviewAccountingFinancialResult | null;
|
||||||
trading_margin_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewTradingMarginProxy | null;
|
trading_margin_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewTradingMarginProxy | null;
|
||||||
debt_position: AssistantMcpDiscoveryDerivedBusinessOverviewDebtPosition | null;
|
debt_position: AssistantMcpDiscoveryDerivedBusinessOverviewDebtPosition | null;
|
||||||
debt_open_settlement_quality: AssistantMcpDiscoveryDerivedBusinessOverviewDebtOpenSettlementQuality | null;
|
debt_open_settlement_quality: AssistantMcpDiscoveryDerivedBusinessOverviewDebtOpenSettlementQuality | null;
|
||||||
debt_staleness_risk_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewDebtStalenessRiskProxy | null;
|
debt_staleness_risk_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewDebtStalenessRiskProxy | null;
|
||||||
|
debt_due_date_aging: AssistantMcpDiscoveryDerivedBusinessOverviewDebtDueDateAging | null;
|
||||||
inventory_position: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryPosition | null;
|
inventory_position: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryPosition | null;
|
||||||
inventory_turnover_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryTurnoverProxy | null;
|
inventory_turnover_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryTurnoverProxy | null;
|
||||||
inventory_staleness_risk_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryStalenessRiskProxy | 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";
|
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 {
|
export interface AssistantMcpDiscoveryBusinessOverviewTradingItemBucket {
|
||||||
item: string;
|
item: string;
|
||||||
sales_revenue: number;
|
sales_revenue: number;
|
||||||
|
|
@ -373,6 +406,45 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverviewDebtStalenessRiskPr
|
||||||
inference_basis: "contract_date_age_and_open_balance_concentration_confirmed_1c_rows";
|
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 {
|
export interface AssistantMcpDiscoveryBusinessOverviewInventoryItemBucket {
|
||||||
item: string;
|
item: string;
|
||||||
rows_with_amount: number;
|
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 {
|
function buildBusinessOverviewInventoryFilters(planner: AssistantMcpDiscoveryPlannerContract): AddressFilterSet | null {
|
||||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||||
const organization = toNonEmptyString(meaning?.explicit_organization_scope);
|
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 {
|
function buildInventoryExactFilters(planner: AssistantMcpDiscoveryPlannerContract): AddressFilterSet {
|
||||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||||
const subject = firstEntityCandidate(planner);
|
const subject = firstEntityCandidate(planner);
|
||||||
|
|
@ -1359,7 +1456,8 @@ async function executeCoverageAwareValueFlowQuery(input: {
|
||||||
});
|
});
|
||||||
executedProbeCount += 1;
|
executedProbeCount += 1;
|
||||||
probeResults.push(queryResultToProbeResult(input.primitiveId, broadResult));
|
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) {
|
if (broadResult.error) {
|
||||||
pushUnique(queryLimitations, broadResult.error);
|
pushUnique(queryLimitations, broadResult.error);
|
||||||
|
|
@ -1424,7 +1522,8 @@ async function executeCoverageAwareValueFlowQuery(input: {
|
||||||
pushUnique(queryLimitations, chunkResult.error);
|
pushUnique(queryLimitations, chunkResult.error);
|
||||||
continue;
|
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;
|
anyChunkLimited = true;
|
||||||
}
|
}
|
||||||
chunkResults.push(chunkResult);
|
chunkResults.push(chunkResult);
|
||||||
|
|
@ -2402,6 +2501,14 @@ function extractContractDateFromText(value: string | null): string | null {
|
||||||
if (!text || !/(?:договор|contract|дог\.)/iu.test(text)) {
|
if (!text || !/(?:договор|contract|дог\.)/iu.test(text)) {
|
||||||
return null;
|
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})/);
|
const isoLikeMatch = text.match(/(\d{4})[-./](\d{1,2})[-./](\d{1,2})/);
|
||||||
if (isoLikeMatch) {
|
if (isoLikeMatch) {
|
||||||
return normalizeDateParts(isoLikeMatch[1], isoLikeMatch[2], isoLikeMatch[3]);
|
return normalizeDateParts(isoLikeMatch[1], isoLikeMatch[2], isoLikeMatch[3]);
|
||||||
|
|
@ -2413,6 +2520,64 @@ function extractContractDateFromText(value: string | null): string | null {
|
||||||
return 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 {
|
function earlierIsoDate(left: string | null, right: string | null): string | null {
|
||||||
if (!left) {
|
if (!left) {
|
||||||
return right;
|
return right;
|
||||||
|
|
@ -2908,7 +3073,8 @@ function deriveRankedValueFlow(
|
||||||
axis_value: axisValue,
|
axis_value: axisValue,
|
||||||
rows_with_amount: bucket.rows_with_amount,
|
rows_with_amount: bucket.rows_with_amount,
|
||||||
total_amount: bucket.total_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) => {
|
.sort((left, right) => {
|
||||||
const amountDelta = right.total_amount - left.total_amount;
|
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(
|
function deriveBusinessOverviewTradingMarginProxy(
|
||||||
result: AddressMcpQueryExecutorResult | null,
|
result: AddressMcpQueryExecutorResult | null,
|
||||||
periodScope: string | 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(
|
function debtStalenessRiskBandRu(
|
||||||
riskBand: AssistantMcpDiscoveryDerivedBusinessOverviewDebtStalenessRiskProxy["risk_band"]
|
riskBand: AssistantMcpDiscoveryDerivedBusinessOverviewDebtStalenessRiskProxy["risk_band"]
|
||||||
): string {
|
): string {
|
||||||
|
|
@ -3737,9 +4142,11 @@ function inventoryStalenessRiskBandRu(
|
||||||
|
|
||||||
function buildBusinessOverviewMissingProofFamilies(input: {
|
function buildBusinessOverviewMissingProofFamilies(input: {
|
||||||
missingSignalFamilies: string[];
|
missingSignalFamilies: string[];
|
||||||
|
accountingFinancialResult: AssistantMcpDiscoveryDerivedBusinessOverviewAccountingFinancialResult | null;
|
||||||
tradingMarginProxy: AssistantMcpDiscoveryDerivedBusinessOverviewTradingMarginProxy | null;
|
tradingMarginProxy: AssistantMcpDiscoveryDerivedBusinessOverviewTradingMarginProxy | null;
|
||||||
debtOpenSettlementQuality: AssistantMcpDiscoveryDerivedBusinessOverviewDebtOpenSettlementQuality | null;
|
debtOpenSettlementQuality: AssistantMcpDiscoveryDerivedBusinessOverviewDebtOpenSettlementQuality | null;
|
||||||
debtStalenessRiskProxy: AssistantMcpDiscoveryDerivedBusinessOverviewDebtStalenessRiskProxy | null;
|
debtStalenessRiskProxy: AssistantMcpDiscoveryDerivedBusinessOverviewDebtStalenessRiskProxy | null;
|
||||||
|
debtDueDateAging: AssistantMcpDiscoveryDerivedBusinessOverviewDebtDueDateAging | null;
|
||||||
inventoryPosition: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryPosition | null;
|
inventoryPosition: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryPosition | null;
|
||||||
inventoryTurnoverProxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryTurnoverProxy | null;
|
inventoryTurnoverProxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryTurnoverProxy | null;
|
||||||
inventoryStalenessRiskProxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryStalenessRiskProxy | 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({
|
pushUnique({
|
||||||
family: "accounting_profit_margin",
|
family: "accounting_profit_margin",
|
||||||
current_status: input.tradingMarginProxy ? "proxy_only_currently" : "reviewed_route_not_wired",
|
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({
|
pushUnique({
|
||||||
family: "debt_due_date_aging_quality",
|
family: "debt_due_date_aging_quality",
|
||||||
current_status: input.debtStalenessRiskProxy
|
current_status: input.debtStalenessRiskProxy
|
||||||
|
|
@ -3826,10 +4233,12 @@ function deriveBusinessOverview(input: {
|
||||||
outgoingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null;
|
outgoingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null;
|
||||||
lifecycleResult: AddressMcpQueryExecutorResult | null;
|
lifecycleResult: AddressMcpQueryExecutorResult | null;
|
||||||
taxResult: AddressMcpQueryExecutorResult | null;
|
taxResult: AddressMcpQueryExecutorResult | null;
|
||||||
|
accountingFinancialResultResult: AddressMcpQueryExecutorResult | null;
|
||||||
tradingMarginResult: AddressMcpQueryExecutorResult | null;
|
tradingMarginResult: AddressMcpQueryExecutorResult | null;
|
||||||
receivablesResult: AddressMcpQueryExecutorResult | null;
|
receivablesResult: AddressMcpQueryExecutorResult | null;
|
||||||
payablesResult: AddressMcpQueryExecutorResult | null;
|
payablesResult: AddressMcpQueryExecutorResult | null;
|
||||||
openContractsResult: AddressMcpQueryExecutorResult | null;
|
openContractsResult: AddressMcpQueryExecutorResult | null;
|
||||||
|
dueDateAgingResult: AddressMcpQueryExecutorResult | null;
|
||||||
documentActivityProfileResult: AddressMcpQueryExecutorResult | null;
|
documentActivityProfileResult: AddressMcpQueryExecutorResult | null;
|
||||||
counterpartyProfileResult: AddressMcpQueryExecutorResult | null;
|
counterpartyProfileResult: AddressMcpQueryExecutorResult | null;
|
||||||
contractUsageProfileResult: AddressMcpQueryExecutorResult | null;
|
contractUsageProfileResult: AddressMcpQueryExecutorResult | null;
|
||||||
|
|
@ -3860,6 +4269,10 @@ function deriveBusinessOverview(input: {
|
||||||
});
|
});
|
||||||
const activityPeriod = deriveActivityPeriod(input.lifecycleResult);
|
const activityPeriod = deriveActivityPeriod(input.lifecycleResult);
|
||||||
const taxPosition = deriveBusinessOverviewTaxPosition(input.taxResult, input.periodScope);
|
const taxPosition = deriveBusinessOverviewTaxPosition(input.taxResult, input.periodScope);
|
||||||
|
const accountingFinancialResult = deriveBusinessOverviewAccountingFinancialResult(
|
||||||
|
input.accountingFinancialResultResult,
|
||||||
|
input.periodScope
|
||||||
|
);
|
||||||
const tradingMarginProxy = deriveBusinessOverviewTradingMarginProxy(input.tradingMarginResult, input.periodScope);
|
const tradingMarginProxy = deriveBusinessOverviewTradingMarginProxy(input.tradingMarginResult, input.periodScope);
|
||||||
const debtPosition = deriveBusinessOverviewDebtPosition({
|
const debtPosition = deriveBusinessOverviewDebtPosition({
|
||||||
receivablesResult: input.receivablesResult,
|
receivablesResult: input.receivablesResult,
|
||||||
|
|
@ -3883,6 +4296,10 @@ function deriveBusinessOverview(input: {
|
||||||
input.periodScope
|
input.periodScope
|
||||||
);
|
);
|
||||||
const debtStalenessRiskProxy = deriveBusinessOverviewDebtStalenessRiskProxy(debtOpenSettlementQuality);
|
const debtStalenessRiskProxy = deriveBusinessOverviewDebtStalenessRiskProxy(debtOpenSettlementQuality);
|
||||||
|
const debtDueDateAging = deriveBusinessOverviewDebtDueDateAging({
|
||||||
|
dueDateResult: input.dueDateAgingResult,
|
||||||
|
debtAsOfDate: input.debtAsOfDate
|
||||||
|
});
|
||||||
const inventoryPosition = deriveBusinessOverviewInventoryPosition({
|
const inventoryPosition = deriveBusinessOverviewInventoryPosition({
|
||||||
inventoryOnHandResult: input.inventoryOnHandResult,
|
inventoryOnHandResult: input.inventoryOnHandResult,
|
||||||
inventoryAgingResult: input.inventoryAgingResult,
|
inventoryAgingResult: input.inventoryAgingResult,
|
||||||
|
|
@ -3901,10 +4318,12 @@ function deriveBusinessOverview(input: {
|
||||||
outgoing.rows_with_amount > 0,
|
outgoing.rows_with_amount > 0,
|
||||||
Boolean(activityPeriod),
|
Boolean(activityPeriod),
|
||||||
Boolean(taxPosition),
|
Boolean(taxPosition),
|
||||||
|
Boolean(accountingFinancialResult),
|
||||||
Boolean(tradingMarginProxy),
|
Boolean(tradingMarginProxy),
|
||||||
Boolean(debtPosition),
|
Boolean(debtPosition),
|
||||||
Boolean(debtOpenSettlementQuality),
|
Boolean(debtOpenSettlementQuality),
|
||||||
Boolean(debtStalenessRiskProxy),
|
Boolean(debtStalenessRiskProxy),
|
||||||
|
Boolean(debtDueDateAging),
|
||||||
Boolean(documentActivityProfile),
|
Boolean(documentActivityProfile),
|
||||||
Boolean(counterpartyProfile),
|
Boolean(counterpartyProfile),
|
||||||
Boolean(contractUsageProfile),
|
Boolean(contractUsageProfile),
|
||||||
|
|
@ -3921,9 +4340,9 @@ function deriveBusinessOverview(input: {
|
||||||
documentActivityProfile || counterpartyProfile || contractUsageProfile
|
documentActivityProfile || counterpartyProfile || contractUsageProfile
|
||||||
);
|
);
|
||||||
const missingSignalFamilies = [
|
const missingSignalFamilies = [
|
||||||
tradingMarginProxy ? "accounting_profit_margin" : "profit_margin",
|
accountingFinancialResult ? null : tradingMarginProxy ? "accounting_profit_margin" : "profit_margin",
|
||||||
debtPosition ? null : "debt_position",
|
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",
|
taxPosition ? null : "tax_position",
|
||||||
inventoryPosition
|
inventoryPosition
|
||||||
? inventoryStalenessRiskProxy
|
? inventoryStalenessRiskProxy
|
||||||
|
|
@ -3936,9 +4355,11 @@ function deriveBusinessOverview(input: {
|
||||||
].filter((item): item is string => Boolean(item));
|
].filter((item): item is string => Boolean(item));
|
||||||
const missingProofFamilies = buildBusinessOverviewMissingProofFamilies({
|
const missingProofFamilies = buildBusinessOverviewMissingProofFamilies({
|
||||||
missingSignalFamilies,
|
missingSignalFamilies,
|
||||||
|
accountingFinancialResult,
|
||||||
tradingMarginProxy,
|
tradingMarginProxy,
|
||||||
debtOpenSettlementQuality,
|
debtOpenSettlementQuality,
|
||||||
debtStalenessRiskProxy,
|
debtStalenessRiskProxy,
|
||||||
|
debtDueDateAging,
|
||||||
inventoryPosition,
|
inventoryPosition,
|
||||||
inventoryTurnoverProxy,
|
inventoryTurnoverProxy,
|
||||||
inventoryStalenessRiskProxy,
|
inventoryStalenessRiskProxy,
|
||||||
|
|
@ -3957,10 +4378,12 @@ function deriveBusinessOverview(input: {
|
||||||
yearly_breakdown: yearlyBreakdown,
|
yearly_breakdown: yearlyBreakdown,
|
||||||
activity_period: activityPeriod,
|
activity_period: activityPeriod,
|
||||||
tax_position: taxPosition,
|
tax_position: taxPosition,
|
||||||
|
accounting_financial_result: accountingFinancialResult,
|
||||||
trading_margin_proxy: tradingMarginProxy,
|
trading_margin_proxy: tradingMarginProxy,
|
||||||
debt_position: debtPosition,
|
debt_position: debtPosition,
|
||||||
debt_open_settlement_quality: debtOpenSettlementQuality,
|
debt_open_settlement_quality: debtOpenSettlementQuality,
|
||||||
debt_staleness_risk_proxy: debtStalenessRiskProxy,
|
debt_staleness_risk_proxy: debtStalenessRiskProxy,
|
||||||
|
debt_due_date_aging: debtDueDateAging,
|
||||||
inventory_position: inventoryPosition,
|
inventory_position: inventoryPosition,
|
||||||
inventory_turnover_proxy: inventoryTurnoverProxy,
|
inventory_turnover_proxy: inventoryTurnoverProxy,
|
||||||
inventory_staleness_risk_proxy: inventoryStalenessRiskProxy,
|
inventory_staleness_risk_proxy: inventoryStalenessRiskProxy,
|
||||||
|
|
@ -3973,9 +4396,9 @@ function deriveBusinessOverview(input: {
|
||||||
missing_signal_families: missingSignalFamilies,
|
missing_signal_families: missingSignalFamilies,
|
||||||
missing_proof_families: missingProofFamilies,
|
missing_proof_families: missingProofFamilies,
|
||||||
inference_basis:
|
inference_basis:
|
||||||
hasBusinessOverviewProfileSignal || inventoryPosition
|
hasBusinessOverviewProfileSignal || inventoryPosition || accountingFinancialResult
|
||||||
? "business_overview_from_confirmed_1c_multi_family_rows"
|
? "business_overview_from_confirmed_1c_multi_family_rows"
|
||||||
: debtOpenSettlementQuality
|
: debtOpenSettlementQuality || debtDueDateAging
|
||||||
? "business_overview_from_confirmed_1c_multi_family_rows"
|
? "business_overview_from_confirmed_1c_multi_family_rows"
|
||||||
: taxPosition && debtPosition
|
: taxPosition && debtPosition
|
||||||
? "business_overview_from_confirmed_1c_money_activity_tax_and_debt_rows"
|
? "business_overview_from_confirmed_1c_money_activity_tax_and_debt_rows"
|
||||||
|
|
@ -3992,10 +4415,12 @@ function summarizeBusinessOverviewRows(input: {
|
||||||
outgoingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null;
|
outgoingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null;
|
||||||
lifecycleResult: AddressMcpQueryExecutorResult | null;
|
lifecycleResult: AddressMcpQueryExecutorResult | null;
|
||||||
taxResult: AddressMcpQueryExecutorResult | null;
|
taxResult: AddressMcpQueryExecutorResult | null;
|
||||||
|
accountingFinancialResultResult: AddressMcpQueryExecutorResult | null;
|
||||||
tradingMarginResult: AddressMcpQueryExecutorResult | null;
|
tradingMarginResult: AddressMcpQueryExecutorResult | null;
|
||||||
receivablesResult: AddressMcpQueryExecutorResult | null;
|
receivablesResult: AddressMcpQueryExecutorResult | null;
|
||||||
payablesResult: AddressMcpQueryExecutorResult | null;
|
payablesResult: AddressMcpQueryExecutorResult | null;
|
||||||
openContractsResult: AddressMcpQueryExecutorResult | null;
|
openContractsResult: AddressMcpQueryExecutorResult | null;
|
||||||
|
dueDateAgingResult: AddressMcpQueryExecutorResult | null;
|
||||||
documentActivityProfileResult: AddressMcpQueryExecutorResult | null;
|
documentActivityProfileResult: AddressMcpQueryExecutorResult | null;
|
||||||
counterpartyProfileResult: AddressMcpQueryExecutorResult | null;
|
counterpartyProfileResult: AddressMcpQueryExecutorResult | null;
|
||||||
contractUsageProfileResult: AddressMcpQueryExecutorResult | null;
|
contractUsageProfileResult: AddressMcpQueryExecutorResult | null;
|
||||||
|
|
@ -4015,6 +4440,9 @@ function summarizeBusinessOverviewRows(input: {
|
||||||
if (input.taxResult && !input.taxResult.error) {
|
if (input.taxResult && !input.taxResult.error) {
|
||||||
parts.push(`${input.taxResult.fetched_rows} VAT/tax rows fetched, ${input.taxResult.matched_rows} matched`);
|
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) {
|
if (input.tradingMarginResult && !input.tradingMarginResult.error) {
|
||||||
parts.push(`${input.tradingMarginResult.fetched_rows} trading-margin document rows fetched, ${input.tradingMarginResult.matched_rows} matched`);
|
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) {
|
if (input.openContractsResult && !input.openContractsResult.error) {
|
||||||
parts.push(`${input.openContractsResult.fetched_rows} open-contract rows fetched, ${input.openContractsResult.matched_rows} matched`);
|
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) {
|
if (input.documentActivityProfileResult && !input.documentActivityProfileResult.error) {
|
||||||
parts.push(`${input.documentActivityProfileResult.fetched_rows} document/account-section profile rows fetched, ${input.documentActivityProfileResult.matched_rows} matched`);
|
parts.push(`${input.documentActivityProfileResult.fetched_rows} document/account-section profile rows fetched, ${input.documentActivityProfileResult.matched_rows} matched`);
|
||||||
}
|
}
|
||||||
|
|
@ -4064,15 +4495,39 @@ function buildBusinessOverviewConfirmedFacts(derived: AssistantMcpDiscoveryDeriv
|
||||||
}
|
}
|
||||||
if (derived.top_customers.length > 0) {
|
if (derived.top_customers.length > 0) {
|
||||||
const leader = derived.top_customers[0];
|
const leader = derived.top_customers[0];
|
||||||
facts.push(
|
if (isLikelyFinancialInstitutionCounterparty(leader.axis_value)) {
|
||||||
`Самый крупный подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`
|
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) {
|
if (derived.top_suppliers.length > 0) {
|
||||||
const leader = derived.top_suppliers[0];
|
const leader = derived.top_suppliers[0];
|
||||||
facts.push(
|
if (isLikelyFinancialInstitutionCounterparty(leader.axis_value)) {
|
||||||
`Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`
|
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) {
|
if (derived.yearly_breakdown.length > 0) {
|
||||||
facts.push(
|
facts.push(
|
||||||
|
|
@ -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.`
|
`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) {
|
if (derived.inventory_position) {
|
||||||
const leader = derived.inventory_position.top_items[0];
|
const leader = derived.inventory_position.top_items[0];
|
||||||
const leaderText = leader
|
const leaderText = leader
|
||||||
|
|
@ -4237,6 +4716,9 @@ function buildBusinessOverviewInferredFacts(derived: AssistantMcpDiscoveryDerive
|
||||||
const supplierSharePct = supplierLeader
|
const supplierSharePct = supplierLeader
|
||||||
? percentageOfTotal(supplierLeader.total_amount, derived.outgoing_supplier_payout.total_amount)
|
? percentageOfTotal(supplierLeader.total_amount, derived.outgoing_supplier_payout.total_amount)
|
||||||
: null;
|
: null;
|
||||||
|
const supplierLeaderIsFinancial = supplierLeader
|
||||||
|
? isLikelyFinancialInstitutionCounterparty(supplierLeader.axis_value)
|
||||||
|
: false;
|
||||||
const strongestIncomingYear = [...derived.yearly_breakdown]
|
const strongestIncomingYear = [...derived.yearly_breakdown]
|
||||||
.filter((bucket) => bucket.incoming_total_amount > 0)
|
.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];
|
.sort((left, right) => right.incoming_total_amount - left.incoming_total_amount || left.year_bucket.localeCompare(right.year_bucket))[0];
|
||||||
|
|
@ -4246,9 +4728,13 @@ function buildBusinessOverviewInferredFacts(derived: AssistantMcpDiscoveryDerive
|
||||||
return [
|
return [
|
||||||
`Расчетное нетто по найденным строкам: ${derived.net_amount_human_ru}; ${direction}.`,
|
`Расчетное нетто по найденным строкам: ${derived.net_amount_human_ru}; ${direction}.`,
|
||||||
supplierLeader
|
supplierLeader
|
||||||
? supplierSharePct !== null
|
? supplierLeaderIsFinancial
|
||||||
? `Крупнейший подтвержденный поставщик/получатель исходящих платежей ${supplierLeader.axis_value} держит около ${supplierSharePct}% проверенного исходящего потока (${supplierLeader.total_amount_human_ru}). Это procurement concentration proxy по найденным строкам, а не полный vendor-risk аудит.`
|
? supplierSharePct !== null
|
||||||
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value} — ${supplierLeader.total_amount_human_ru}.`
|
? `Крупнейший получатель исходящих денег ${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,
|
: null,
|
||||||
strongestIncomingYear
|
strongestIncomingYear
|
||||||
? `Самый сильный год по подтвержденным входящим поступлениям: ${strongestIncomingYear.year_bucket} (${strongestIncomingYear.incoming_total_amount_human_ru}).`
|
? `Самый сильный год по подтвержденным входящим поступлениям: ${strongestIncomingYear.year_bucket} (${strongestIncomingYear.incoming_total_amount_human_ru}).`
|
||||||
|
|
@ -5032,10 +5518,12 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||||
let outgoingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null = null;
|
let outgoingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null = null;
|
||||||
let lifecycleResult: AddressMcpQueryExecutorResult | null = null;
|
let lifecycleResult: AddressMcpQueryExecutorResult | null = null;
|
||||||
let taxResult: AddressMcpQueryExecutorResult | null = null;
|
let taxResult: AddressMcpQueryExecutorResult | null = null;
|
||||||
|
let accountingFinancialResultResult: AddressMcpQueryExecutorResult | null = null;
|
||||||
let tradingMarginResult: AddressMcpQueryExecutorResult | null = null;
|
let tradingMarginResult: AddressMcpQueryExecutorResult | null = null;
|
||||||
let receivablesResult: AddressMcpQueryExecutorResult | null = null;
|
let receivablesResult: AddressMcpQueryExecutorResult | null = null;
|
||||||
let payablesResult: AddressMcpQueryExecutorResult | null = null;
|
let payablesResult: AddressMcpQueryExecutorResult | null = null;
|
||||||
let openContractsResult: AddressMcpQueryExecutorResult | null = null;
|
let openContractsResult: AddressMcpQueryExecutorResult | null = null;
|
||||||
|
let dueDateAgingResult: AddressMcpQueryExecutorResult | null = null;
|
||||||
let documentActivityProfileResult: AddressMcpQueryExecutorResult | null = null;
|
let documentActivityProfileResult: AddressMcpQueryExecutorResult | null = null;
|
||||||
let counterpartyProfileResult: AddressMcpQueryExecutorResult | null = null;
|
let counterpartyProfileResult: AddressMcpQueryExecutorResult | null = null;
|
||||||
let contractUsageProfileResult: AddressMcpQueryExecutorResult | null = null;
|
let contractUsageProfileResult: AddressMcpQueryExecutorResult | null = null;
|
||||||
|
|
@ -5045,8 +5533,10 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||||
const lifecycleFilters = buildLifecycleFilters(planner);
|
const lifecycleFilters = buildLifecycleFilters(planner);
|
||||||
const profileFilters = buildBusinessOverviewProfileFilters(planner);
|
const profileFilters = buildBusinessOverviewProfileFilters(planner);
|
||||||
const taxFilters = buildBusinessOverviewTaxFilters(planner);
|
const taxFilters = buildBusinessOverviewTaxFilters(planner);
|
||||||
|
const accountingFinancialResultFilters = buildBusinessOverviewAccountingFinancialResultFilters(planner);
|
||||||
const tradingMarginFilters = buildBusinessOverviewTradingMarginFilters(planner);
|
const tradingMarginFilters = buildBusinessOverviewTradingMarginFilters(planner);
|
||||||
const debtFilters = buildBusinessOverviewDebtFilters(planner);
|
const debtFilters = buildBusinessOverviewDebtFilters(planner);
|
||||||
|
const debtDueDateAgingProbeEnabled = shouldRunDebtDueDateAgingProbe(planner);
|
||||||
const inventoryFilters = buildBusinessOverviewInventoryFilters(planner);
|
const inventoryFilters = buildBusinessOverviewInventoryFilters(planner);
|
||||||
const debtAsOfDate = toNonEmptyString(debtFilters?.as_of_date);
|
const debtAsOfDate = toNonEmptyString(debtFilters?.as_of_date);
|
||||||
const inventoryAsOfDate = toNonEmptyString(inventoryFilters?.as_of_date);
|
const inventoryAsOfDate = toNonEmptyString(inventoryFilters?.as_of_date);
|
||||||
|
|
@ -5059,6 +5549,9 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||||
const taxSelection = taxFilters
|
const taxSelection = taxFilters
|
||||||
? selectAddressRecipe("vat_liability_confirmed_for_tax_period", taxFilters)
|
? selectAddressRecipe("vat_liability_confirmed_for_tax_period", taxFilters)
|
||||||
: null;
|
: null;
|
||||||
|
const accountingFinancialResultSelection = accountingFinancialResultFilters
|
||||||
|
? selectAddressRecipe("accounting_financial_result_for_organization", accountingFinancialResultFilters)
|
||||||
|
: null;
|
||||||
const tradingMarginSelection = tradingMarginFilters
|
const tradingMarginSelection = tradingMarginFilters
|
||||||
? selectAddressRecipe("inventory_trading_margin_proxy_for_organization", tradingMarginFilters)
|
? selectAddressRecipe("inventory_trading_margin_proxy_for_organization", tradingMarginFilters)
|
||||||
: null;
|
: null;
|
||||||
|
|
@ -5071,6 +5564,9 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||||
const openContractsSelection = debtFilters
|
const openContractsSelection = debtFilters
|
||||||
? selectAddressRecipe("open_contracts_confirmed_as_of_date", debtFilters)
|
? selectAddressRecipe("open_contracts_confirmed_as_of_date", debtFilters)
|
||||||
: null;
|
: null;
|
||||||
|
const dueDateAgingSelection = debtFilters && debtDueDateAgingProbeEnabled
|
||||||
|
? selectAddressRecipe("debt_due_date_aging_for_organization", debtFilters)
|
||||||
|
: null;
|
||||||
const inventoryOnHandSelection = inventoryFilters
|
const inventoryOnHandSelection = inventoryFilters
|
||||||
? selectAddressRecipe("inventory_on_hand_as_of_date", inventoryFilters)
|
? selectAddressRecipe("inventory_on_hand_as_of_date", inventoryFilters)
|
||||||
: null;
|
: null;
|
||||||
|
|
@ -5135,6 +5631,14 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||||
pushReason(reasonCodes, "pilot_business_overview_tax_recipe_not_available");
|
pushReason(reasonCodes, "pilot_business_overview_tax_recipe_not_available");
|
||||||
pushUnique(queryLimitations, "Business overview VAT/tax probe requires an executable tax-period recipe");
|
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) {
|
if (tradingMarginSelection?.selected_recipe) {
|
||||||
pushReason(reasonCodes, "pilot_business_overview_trading_margin_recipe_selected");
|
pushReason(reasonCodes, "pilot_business_overview_trading_margin_recipe_selected");
|
||||||
} else if (!tradingMarginFilters) {
|
} else if (!tradingMarginFilters) {
|
||||||
|
|
@ -5159,6 +5663,16 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||||
pushReason(reasonCodes, "pilot_business_overview_open_contracts_recipe_not_available");
|
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");
|
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) {
|
if (inventoryOnHandSelection?.selected_recipe) {
|
||||||
pushReason(reasonCodes, "pilot_business_overview_inventory_on_hand_recipe_selected");
|
pushReason(reasonCodes, "pilot_business_overview_inventory_on_hand_recipe_selected");
|
||||||
if (inventoryAgingSelection?.selected_recipe) {
|
if (inventoryAgingSelection?.selected_recipe) {
|
||||||
|
|
@ -5200,6 +5714,17 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||||
account_scope: taxPlan.account_scope
|
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) {
|
if (receivablesSelection?.selected_recipe && payablesSelection?.selected_recipe) {
|
||||||
const receivablesPlan = buildAddressRecipePlan(receivablesSelection.selected_recipe, debtFilters!);
|
const receivablesPlan = buildAddressRecipePlan(receivablesSelection.selected_recipe, debtFilters!);
|
||||||
receivablesResult = await runtimeDeps.executeAddressMcpQuery({
|
receivablesResult = await runtimeDeps.executeAddressMcpQuery({
|
||||||
|
|
@ -5222,6 +5747,14 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||||
account_scope: openContractsPlan.account_scope
|
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) {
|
if (inventoryOnHandSelection?.selected_recipe) {
|
||||||
const inventoryOnHandPlan = buildAddressRecipePlan(inventoryOnHandSelection.selected_recipe, inventoryFilters!);
|
const inventoryOnHandPlan = buildAddressRecipePlan(inventoryOnHandSelection.selected_recipe, inventoryFilters!);
|
||||||
inventoryOnHandResult = await runtimeDeps.executeAddressMcpQuery({
|
inventoryOnHandResult = await runtimeDeps.executeAddressMcpQuery({
|
||||||
|
|
@ -5243,6 +5776,9 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||||
if (taxResult) {
|
if (taxResult) {
|
||||||
probeResults.push(queryResultToProbeResult(step.primitive_id, taxResult));
|
probeResults.push(queryResultToProbeResult(step.primitive_id, taxResult));
|
||||||
}
|
}
|
||||||
|
if (accountingFinancialResultResult) {
|
||||||
|
probeResults.push(queryResultToProbeResult(step.primitive_id, accountingFinancialResultResult));
|
||||||
|
}
|
||||||
if (receivablesResult) {
|
if (receivablesResult) {
|
||||||
probeResults.push(queryResultToProbeResult(step.primitive_id, receivablesResult));
|
probeResults.push(queryResultToProbeResult(step.primitive_id, receivablesResult));
|
||||||
}
|
}
|
||||||
|
|
@ -5252,6 +5788,9 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||||
if (openContractsResult) {
|
if (openContractsResult) {
|
||||||
probeResults.push(queryResultToProbeResult(step.primitive_id, openContractsResult));
|
probeResults.push(queryResultToProbeResult(step.primitive_id, openContractsResult));
|
||||||
}
|
}
|
||||||
|
if (dueDateAgingResult) {
|
||||||
|
probeResults.push(queryResultToProbeResult(step.primitive_id, dueDateAgingResult));
|
||||||
|
}
|
||||||
if (inventoryOnHandResult) {
|
if (inventoryOnHandResult) {
|
||||||
probeResults.push(queryResultToProbeResult(step.primitive_id, inventoryOnHandResult));
|
probeResults.push(queryResultToProbeResult(step.primitive_id, inventoryOnHandResult));
|
||||||
}
|
}
|
||||||
|
|
@ -5276,6 +5815,12 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||||
} else if (taxResult) {
|
} else if (taxResult) {
|
||||||
pushReason(reasonCodes, "pilot_business_overview_tax_query_mcp_executed");
|
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) {
|
if (receivablesResult?.error) {
|
||||||
pushUnique(queryLimitations, receivablesResult.error);
|
pushUnique(queryLimitations, receivablesResult.error);
|
||||||
pushReason(reasonCodes, "pilot_business_overview_receivables_query_mcp_error");
|
pushReason(reasonCodes, "pilot_business_overview_receivables_query_mcp_error");
|
||||||
|
|
@ -5300,6 +5845,12 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||||
} else if (openContractsResult) {
|
} else if (openContractsResult) {
|
||||||
pushReason(reasonCodes, "pilot_business_overview_open_contracts_query_mcp_executed");
|
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) {
|
if (inventoryOnHandResult?.error) {
|
||||||
pushUnique(queryLimitations, inventoryOnHandResult.error);
|
pushUnique(queryLimitations, inventoryOnHandResult.error);
|
||||||
pushReason(reasonCodes, "pilot_business_overview_inventory_on_hand_query_mcp_error");
|
pushReason(reasonCodes, "pilot_business_overview_inventory_on_hand_query_mcp_error");
|
||||||
|
|
@ -5411,10 +5962,12 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||||
outgoingResult,
|
outgoingResult,
|
||||||
lifecycleResult,
|
lifecycleResult,
|
||||||
taxResult,
|
taxResult,
|
||||||
|
accountingFinancialResultResult,
|
||||||
tradingMarginResult,
|
tradingMarginResult,
|
||||||
receivablesResult,
|
receivablesResult,
|
||||||
payablesResult,
|
payablesResult,
|
||||||
openContractsResult,
|
openContractsResult,
|
||||||
|
dueDateAgingResult,
|
||||||
documentActivityProfileResult,
|
documentActivityProfileResult,
|
||||||
counterpartyProfileResult,
|
counterpartyProfileResult,
|
||||||
contractUsageProfileResult,
|
contractUsageProfileResult,
|
||||||
|
|
@ -5451,6 +6004,9 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||||
if (derivedBusinessOverview.tax_position) {
|
if (derivedBusinessOverview.tax_position) {
|
||||||
pushReason(reasonCodes, "pilot_derived_business_overview_tax_position_from_confirmed_rows");
|
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) {
|
if (derivedBusinessOverview.trading_margin_proxy) {
|
||||||
pushReason(reasonCodes, "pilot_derived_business_overview_trading_margin_proxy_from_confirmed_rows");
|
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) {
|
if (derivedBusinessOverview.debt_staleness_risk_proxy) {
|
||||||
pushReason(reasonCodes, "pilot_derived_business_overview_debt_staleness_risk_proxy_from_confirmed_rows");
|
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) {
|
if (derivedBusinessOverview.inventory_position) {
|
||||||
pushReason(reasonCodes, "pilot_derived_business_overview_inventory_position_from_confirmed_rows");
|
pushReason(reasonCodes, "pilot_derived_business_overview_inventory_position_from_confirmed_rows");
|
||||||
}
|
}
|
||||||
|
|
@ -5484,10 +6044,12 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||||
outgoingResult,
|
outgoingResult,
|
||||||
lifecycleResult,
|
lifecycleResult,
|
||||||
taxResult,
|
taxResult,
|
||||||
|
accountingFinancialResultResult,
|
||||||
tradingMarginResult,
|
tradingMarginResult,
|
||||||
receivablesResult,
|
receivablesResult,
|
||||||
payablesResult,
|
payablesResult,
|
||||||
openContractsResult,
|
openContractsResult,
|
||||||
|
dueDateAgingResult,
|
||||||
documentActivityProfileResult,
|
documentActivityProfileResult,
|
||||||
counterpartyProfileResult,
|
counterpartyProfileResult,
|
||||||
contractUsageProfileResult,
|
contractUsageProfileResult,
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ export interface AssistantMcpDiscoveryEvidenceContract {
|
||||||
|
|
||||||
const DEFAULT_DISCOVERY_BUDGET: AssistantMcpDiscoveryExecutionBudget = {
|
const DEFAULT_DISCOVERY_BUDGET: AssistantMcpDiscoveryExecutionBudget = {
|
||||||
max_probe_count: 3,
|
max_probe_count: 3,
|
||||||
max_rows_per_probe: 100
|
max_rows_per_probe: 200
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_PROBE_COUNT = 36;
|
const MAX_PROBE_COUNT = 36;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { AssistantMcpDiscoveryRuntimeEntryPointContract } from "./assistantMcpDiscoveryRuntimeEntryPoint";
|
import type { AssistantMcpDiscoveryRuntimeEntryPointContract } from "./assistantMcpDiscoveryRuntimeEntryPoint";
|
||||||
|
import { isLikelyFinancialInstitutionCounterparty } from "./counterpartyRoleHeuristics";
|
||||||
|
|
||||||
export const ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION =
|
export const ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION =
|
||||||
"assistant_mcp_discovery_response_candidate_v1" as const;
|
"assistant_mcp_discovery_response_candidate_v1" as const;
|
||||||
|
|
@ -97,7 +98,27 @@ function userFacingLines(values: string[]): string[] {
|
||||||
return uniqueStrings(values).filter((line) => !hasInternalMechanics(line));
|
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 {
|
function localizeLine(value: string): string {
|
||||||
|
const sanitizedValue = sanitizeUserFacingMechanics(value);
|
||||||
if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) {
|
if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) {
|
||||||
return "В 1С найдены строки активности в запрошенном срезе.";
|
return "В 1С найдены строки активности в запрошенном срезе.";
|
||||||
}
|
}
|
||||||
|
|
@ -126,7 +147,7 @@ function localizeLine(value: string): string {
|
||||||
value
|
value
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return "Запрошенный период уперся в лимит строк MCP; доступного бюджета помесячных дозапросов не хватило, чтобы покрыть все подпериоды.";
|
return "Запрошенный период достиг лимита строк; доступного бюджета помесячных дозапросов не хватило, чтобы покрыть все подпериоды.";
|
||||||
}
|
}
|
||||||
const counterpartyMatch = value.match(/^1C activity rows were found for counterparty\s+(.+)$/i);
|
const counterpartyMatch = value.match(/^1C activity rows were found for counterparty\s+(.+)$/i);
|
||||||
if (counterpartyMatch) {
|
if (counterpartyMatch) {
|
||||||
|
|
@ -151,10 +172,10 @@ function localizeLine(value: string): string {
|
||||||
}
|
}
|
||||||
const movementRowsMatch = value.match(/^1C movement rows were found for counterparty\s+(.+)$/i);
|
const movementRowsMatch = value.match(/^1C movement rows were found for counterparty\s+(.+)$/i);
|
||||||
if (movementRowsMatch) {
|
if (movementRowsMatch) {
|
||||||
return `В 1С найдены строки движений по контрагенту ${movementRowsMatch[1]}.`;
|
return `В 1С найдены строки движений по контрагенту ${movementRowsMatch[1]}.`;
|
||||||
}
|
}
|
||||||
if (/^1C movement rows were found for the requested scope$/i.test(value)) {
|
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);
|
const supplierPayoutMatch = value.match(/^1C supplier-payout rows were found for counterparty\s+(.+)$/i);
|
||||||
if (supplierPayoutMatch) {
|
if (supplierPayoutMatch) {
|
||||||
|
|
@ -186,7 +207,7 @@ function localizeLine(value: string): string {
|
||||||
return "Срез документов ограничен только подтвержденными строками документов в проверенном окне.";
|
return "Срез документов ограничен только подтвержденными строками документов в проверенном окне.";
|
||||||
}
|
}
|
||||||
if (/^Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope$/i.test(value)) {
|
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)) {
|
if (/^Counterparty value-flow total was calculated from confirmed 1C movement rows$/i.test(value)) {
|
||||||
return "Сумма входящих поступлений рассчитана только по подтвержденным строкам поступлений в 1С.";
|
return "Сумма входящих поступлений рассчитана только по подтвержденным строкам поступлений в 1С.";
|
||||||
|
|
@ -274,10 +295,10 @@ function localizeLine(value: string): string {
|
||||||
return "Полный срез документов без явно проверенного периода не подтвержден.";
|
return "Полный срез документов без явно проверенного периода не подтвержден.";
|
||||||
}
|
}
|
||||||
if (/^Full movement history outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
|
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)) {
|
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)) {
|
if (/^Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||||
return "Полный объем исходящих платежей вне проверенного периода этим поиском не подтвержден.";
|
return "Полный объем исходящих платежей вне проверенного периода этим поиском не подтвержден.";
|
||||||
|
|
@ -296,14 +317,14 @@ function localizeLine(value: string): string {
|
||||||
value
|
value
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С после того, как общая выборка уперлась в лимит строк.";
|
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С после того, как общая выборка достигла лимита строк.";
|
||||||
}
|
}
|
||||||
if (
|
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(
|
/^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
|
value
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return "Покрытие запрошенного периода по двустороннему денежному потоку восстановлено помесячными проверками 1С после того, как общая выборка уперлась в лимит строк хотя бы по одной стороне.";
|
return "Покрытие запрошенного периода по двустороннему денежному потоку восстановлено помесячными проверками 1С после того, как общая выборка достигла лимита строк хотя бы по одной стороне.";
|
||||||
}
|
}
|
||||||
if (/^Requested period coverage was recovered through monthly 1C value-flow probes$/i.test(value)) {
|
if (/^Requested period coverage was recovered through monthly 1C value-flow probes$/i.test(value)) {
|
||||||
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С.";
|
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)) {
|
if (/^Complete requested-period coverage for bidirectional value-flow is not proven by the available checked rows$/i.test(value)) {
|
||||||
return "Полное покрытие запрошенного периода по двустороннему денежному потоку не подтверждено доступными проверенными строками.";
|
return "Полное покрытие запрошенного периода по двустороннему денежному потоку не подтверждено доступными проверенными строками.";
|
||||||
}
|
}
|
||||||
return value;
|
return sanitizedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
function section(title: string, lines: string[]): string | null {
|
function section(title: string, lines: string[]): string | null {
|
||||||
|
|
@ -408,7 +429,7 @@ function businessOverviewCoverageLimitLine(overview: Record<string, unknown>): s
|
||||||
limited.push("исходящие");
|
limited.push("исходящие");
|
||||||
}
|
}
|
||||||
return limited.length > 0
|
return limited.length > 0
|
||||||
? `Важно: ${limited.join(" и ")} уперлись в лимит выборки MCP, поэтому это проверенный срез найденных строк, а не гарантированно полный бухгалтерский оборот.`
|
? `Важно: по направлению ${limited.join(" и ")} проверка достигла лимита строк; это расширенный проверенный срез найденных строк, но не гарантия полного бухгалтерского оборота без отдельной полной выгрузки.`
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -437,6 +458,34 @@ function firstOverviewAxisLabel(rows: unknown, amountKey = "total_amount_human_r
|
||||||
return label && amount ? `${label} — ${sentenceAmount(amount) ?? amount}` : null;
|
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 {
|
function businessOverviewTaxLine(overview: Record<string, unknown>): string | null {
|
||||||
const tax = toRecordObject(overview.tax_position);
|
const tax = toRecordObject(overview.tax_position);
|
||||||
if (!tax) {
|
if (!tax) {
|
||||||
|
|
@ -568,7 +617,7 @@ function buildCompactBidirectionalValueFlowReply(
|
||||||
lines.push(`Основа: ${basis.join("; ")}.`);
|
lines.push(`Основа: ${basis.join("; ")}.`);
|
||||||
}
|
}
|
||||||
if (flow.coverage_limited_by_probe_limit === true) {
|
if (flow.coverage_limited_by_probe_limit === true) {
|
||||||
lines.push("Важно: часть проверки уперлась в лимит строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");
|
lines.push("Важно: часть проверки достигла лимита строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");
|
||||||
}
|
}
|
||||||
lines.push("Метод: нетто рассчитано как подтвержденные входящие строки 1С минус подтвержденные исходящие строки; это не полное бухгалтерское сальдо вне проверенного окна.");
|
lines.push("Метод: нетто рассчитано как подтвержденные входящие строки 1С минус подтвержденные исходящие строки; это не полное бухгалтерское сальдо вне проверенного окна.");
|
||||||
|
|
||||||
|
|
@ -721,17 +770,202 @@ function buildCompactBusinessOverviewReply(
|
||||||
const topCustomer = toRecordObject(Array.isArray(overview.top_customers) ? overview.top_customers[0] : null);
|
const topCustomer = toRecordObject(Array.isArray(overview.top_customers) ? overview.top_customers[0] : null);
|
||||||
const customerName = toNonEmptyString(topCustomer?.axis_value);
|
const customerName = toNonEmptyString(topCustomer?.axis_value);
|
||||||
const customerAmount = moneyText(topCustomer?.total_amount_human_ru);
|
const customerAmount = moneyText(topCustomer?.total_amount_human_ru);
|
||||||
|
const topCustomerLooksFinancial = overviewAxisLooksFinancial(topCustomer);
|
||||||
|
const nonFinancialCustomer = firstNonFinancialOverviewAxisLabel(
|
||||||
|
topCustomerLooksFinancial ? overview.top_customers : []
|
||||||
|
);
|
||||||
const topCustomerLead =
|
const topCustomerLead =
|
||||||
customerName && customerAmount
|
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 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 roleBoundaryLead = topCustomer || topSupplier ? "; клиент/поставщик как бизнес-роли этим денежным срезом не подтверждены" : "";
|
||||||
const graphReasonCodes = toStringList(graph?.reason_codes);
|
const graphReasonCodes = toStringList(graph?.reason_codes);
|
||||||
const directMoneyAnswer = graphReasonCodes.includes("data_need_graph_business_overview_direct_money_answer");
|
const directMoneyAnswer = graphReasonCodes.includes("data_need_graph_business_overview_direct_money_answer");
|
||||||
const crossScopeExecutiveSummary = Boolean(separateSubject && previousCounterpartySummary);
|
const crossScopeExecutiveSummary = Boolean(separateSubject && previousCounterpartySummary);
|
||||||
const lines: string[] = [];
|
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)) {
|
if (crossScopeExecutiveSummary && separateSubject && previousCounterpartySummary && (incomingAmount || outgoingAmount || netAmount)) {
|
||||||
lines.push(
|
lines.push(
|
||||||
|
|
@ -761,7 +995,7 @@ function buildCompactBusinessOverviewReply(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
lines.push(
|
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 netYear = toNonEmptyString(netLeader?.year_bucket);
|
||||||
const netYearAmount = moneyText(netLeader?.net_amount_human_ru);
|
const netYearAmount = moneyText(netLeader?.net_amount_human_ru);
|
||||||
|
|
@ -783,7 +1017,11 @@ function buildCompactBusinessOverviewReply(
|
||||||
);
|
);
|
||||||
lines.push('Метод: "заработали" здесь считаю как денежный operating-flow proxy по 1С; это не чистая прибыль и не финрезультат.');
|
lines.push('Метод: "заработали" здесь считаю как денежный operating-flow proxy по 1С; это не чистая прибыль и не финрезультат.');
|
||||||
if (!directMoneyAnswer && customerName && customerAmount) {
|
if (!directMoneyAnswer && customerName && customerAmount) {
|
||||||
lines.push(`Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}.`);
|
lines.push(
|
||||||
|
topCustomerLooksFinancial
|
||||||
|
? `Крупнейший входящий денежный источник в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}. По названию это банк/финансовая организация, поэтому без назначения платежа не называю это клиентской выручкой.${nonFinancialCustomer ? ` Крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}.` : ""}`
|
||||||
|
: `Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}.`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -797,10 +1035,18 @@ function buildCompactBusinessOverviewReply(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!directMoneyAnswer && topSupplier) {
|
if (!directMoneyAnswer && topSupplier) {
|
||||||
lines.push(`Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`);
|
lines.push(
|
||||||
|
topSupplierLooksFinancial
|
||||||
|
? `Крупнейший получатель исходящих денег: ${topSupplier}. По названию это банк/финансовая организация, поэтому без назначения платежа/договора не считаю это обычным поставщиком.${nonFinancialSupplier ? ` Крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}.` : ""}`
|
||||||
|
: `Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!directMoneyAnswer && (topCustomer || topSupplier)) {
|
if (!directMoneyAnswer && (topCustomer || topSupplier)) {
|
||||||
lines.push("Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль.");
|
lines.push(
|
||||||
|
topCustomerLooksFinancial || topSupplierLooksFinancial
|
||||||
|
? "Важно по ролям: текущий денежный срез подтверждает источники и получателей денег, но банковские контрагенты требуют проверки назначения платежа/счетов и не доказывают роль клиента или поставщика."
|
||||||
|
: "Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!directMoneyAnswer) {
|
if (!directMoneyAnswer) {
|
||||||
lines.push(
|
lines.push(
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ export const ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION =
|
||||||
"assistant_mcp_discovery_runtime_bridge_v1" as const;
|
"assistant_mcp_discovery_runtime_bridge_v1" as const;
|
||||||
export const ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION =
|
export const ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION =
|
||||||
"assistant_mcp_discovery_loop_state_v1" as const;
|
"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 =
|
export type AssistantMcpDiscoveryRuntimeBridgeStatus =
|
||||||
| "answer_draft_ready"
|
| "answer_draft_ready"
|
||||||
|
|
@ -31,6 +33,11 @@ export type AssistantMcpDiscoveryLoopStatus =
|
||||||
| "awaiting_clarification"
|
| "awaiting_clarification"
|
||||||
| "ready_for_next_hop"
|
| "ready_for_next_hop"
|
||||||
| "blocked";
|
| "blocked";
|
||||||
|
export type AssistantMcpRouteCandidateStatus =
|
||||||
|
| "ready_for_reviewed_execution"
|
||||||
|
| "needs_user_scope"
|
||||||
|
| "needs_route_enablement"
|
||||||
|
| "blocked";
|
||||||
|
|
||||||
export interface AssistantMcpDiscoveryRuntimeBridgeInput {
|
export interface AssistantMcpDiscoveryRuntimeBridgeInput {
|
||||||
semanticDataNeed?: string | null;
|
semanticDataNeed?: string | null;
|
||||||
|
|
@ -61,6 +68,26 @@ export interface AssistantMcpDiscoveryLoopStateContract {
|
||||||
explicit_date_scope: string | null;
|
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 {
|
export interface AssistantMcpDiscoveryRuntimeBridgeContract {
|
||||||
schema_version: typeof ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION;
|
schema_version: typeof ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION;
|
||||||
policy_owner: "assistantMcpDiscoveryRuntimeBridge";
|
policy_owner: "assistantMcpDiscoveryRuntimeBridge";
|
||||||
|
|
@ -70,6 +97,7 @@ export interface AssistantMcpDiscoveryRuntimeBridgeContract {
|
||||||
pilot: AssistantMcpDiscoveryPilotExecutionContract;
|
pilot: AssistantMcpDiscoveryPilotExecutionContract;
|
||||||
answer_draft: AssistantMcpDiscoveryAnswerDraftContract;
|
answer_draft: AssistantMcpDiscoveryAnswerDraftContract;
|
||||||
loop_state: AssistantMcpDiscoveryLoopStateContract;
|
loop_state: AssistantMcpDiscoveryLoopStateContract;
|
||||||
|
route_candidate: AssistantMcpRouteCandidateContract;
|
||||||
user_facing_response_allowed: boolean;
|
user_facing_response_allowed: boolean;
|
||||||
business_fact_answer_allowed: boolean;
|
business_fact_answer_allowed: boolean;
|
||||||
requires_user_clarification: boolean;
|
requires_user_clarification: boolean;
|
||||||
|
|
@ -138,6 +166,26 @@ function loopStatusFor(
|
||||||
return "ready_for_next_hop";
|
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(
|
function flattenAxes(
|
||||||
pilot: AssistantMcpDiscoveryPilotExecutionContract,
|
pilot: AssistantMcpDiscoveryPilotExecutionContract,
|
||||||
source: "provided_axes" | "missing_axis_options"
|
source: "provided_axes" | "missing_axis_options"
|
||||||
|
|
@ -168,6 +216,144 @@ function entityCandidatesFromPlanner(planner: AssistantMcpDiscoveryPlannerContra
|
||||||
return uniqueStrings(values);
|
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(
|
function buildLoopState(
|
||||||
planner: AssistantMcpDiscoveryPlannerContract,
|
planner: AssistantMcpDiscoveryPlannerContract,
|
||||||
pilot: AssistantMcpDiscoveryPilotExecutionContract,
|
pilot: AssistantMcpDiscoveryPilotExecutionContract,
|
||||||
|
|
@ -215,11 +401,14 @@ export async function runAssistantMcpDiscoveryRuntimeBridge(
|
||||||
const answerDraft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
const answerDraft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||||
const bridgeStatus = bridgeStatusFor(pilot, answerDraft);
|
const bridgeStatus = bridgeStatusFor(pilot, answerDraft);
|
||||||
const loopState = buildLoopState(planner, pilot, bridgeStatus);
|
const loopState = buildLoopState(planner, pilot, bridgeStatus);
|
||||||
|
const routeCandidate = buildRouteCandidate(planner, pilot, bridgeStatus);
|
||||||
const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]);
|
const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]);
|
||||||
|
|
||||||
pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`);
|
pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`);
|
||||||
pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer");
|
pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer");
|
||||||
pushReason(reasonCodes, `runtime_bridge_loop_state_${loopState.loop_status}`);
|
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 {
|
return {
|
||||||
schema_version: ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION,
|
schema_version: ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION,
|
||||||
|
|
@ -230,6 +419,7 @@ export async function runAssistantMcpDiscoveryRuntimeBridge(
|
||||||
pilot,
|
pilot,
|
||||||
answer_draft: answerDraft,
|
answer_draft: answerDraft,
|
||||||
loop_state: loopState,
|
loop_state: loopState,
|
||||||
|
route_candidate: routeCandidate,
|
||||||
user_facing_response_allowed: bridgeStatus !== "blocked",
|
user_facing_response_allowed: bridgeStatus !== "blocked",
|
||||||
business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft),
|
business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft),
|
||||||
requires_user_clarification: bridgeStatus === "needs_clarification",
|
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;
|
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 {
|
function hasOrganizationLevelDebtDueDateOverviewSignal(text: string): boolean {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return false;
|
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 {
|
function hasBusinessOverviewSeparateCounterpartySignal(text: string): boolean {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -1534,6 +1591,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
);
|
);
|
||||||
const rawPrimaryBusinessOverviewSignal = hasBusinessOverviewSignal(rawText);
|
const rawPrimaryBusinessOverviewSignal = hasBusinessOverviewSignal(rawText);
|
||||||
const explicitVatQuestionSignal = hasExplicitVatQuestionSignal(rawText);
|
const explicitVatQuestionSignal = hasExplicitVatQuestionSignal(rawText);
|
||||||
|
const explicitVatMovementEvidenceSignal = hasExplicitVatMovementEvidenceSignal(rawText);
|
||||||
const explicitVatSuppressesBusinessOverviewContinuation = Boolean(
|
const explicitVatSuppressesBusinessOverviewContinuation = Boolean(
|
||||||
explicitVatQuestionSignal && !rawPrimaryBusinessOverviewSignal
|
explicitVatQuestionSignal && !rawPrimaryBusinessOverviewSignal
|
||||||
);
|
);
|
||||||
|
|
@ -1552,6 +1610,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
const rawMetadataSignal =
|
const rawMetadataSignal =
|
||||||
!rawLifecycleSignal &&
|
!rawLifecycleSignal &&
|
||||||
!rawValueFlowSignal &&
|
!rawValueFlowSignal &&
|
||||||
|
!explicitVatMovementEvidenceSignal &&
|
||||||
!rawReferentialDocumentExclusionSignal &&
|
!rawReferentialDocumentExclusionSignal &&
|
||||||
hasMetadataSignal(rawText);
|
hasMetadataSignal(rawText);
|
||||||
const rawEntityResolutionSignal =
|
const rawEntityResolutionSignal =
|
||||||
|
|
@ -1569,7 +1628,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(dateScopeSignalText);
|
const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(dateScopeSignalText);
|
||||||
const relativeCurrentDateHintDetected = hasRelativeCurrentDateHint(rawText);
|
const relativeCurrentDateHintDetected = hasRelativeCurrentDateHint(rawText);
|
||||||
const rawDateScope = collectDateScopeFromRawText(dateScopeSignalText);
|
const rawDateScope = collectDateScopeFromRawText(dateScopeSignalText);
|
||||||
const rawMetadataScopeHint = rawMetadataSignal ? metadataScopeHintFromRawText(rawText) : null;
|
const rawMetadataScopeHint =
|
||||||
|
rawMetadataSignal || explicitVatMovementEvidenceSignal ? metadataScopeHintFromRawText(rawText) : null;
|
||||||
const rawTopicSwitchSignal = hasExplicitTopicSwitchSignal(rawText);
|
const rawTopicSwitchSignal = hasExplicitTopicSwitchSignal(rawText);
|
||||||
const rawEntityCandidate = rawEntityResolutionSignal ? rawEntityResolutionCandidate(rawEntitySourceText) : null;
|
const rawEntityCandidate = rawEntityResolutionSignal ? rawEntityResolutionCandidate(rawEntitySourceText) : null;
|
||||||
const rawScopedEntityCandidate =
|
const rawScopedEntityCandidate =
|
||||||
|
|
@ -1594,12 +1654,49 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis);
|
const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis);
|
||||||
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
|
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
|
||||||
const broadBusinessEvaluationUnsupported = unsupported === "broad_business_evaluation";
|
const broadBusinessEvaluationUnsupported = unsupported === "broad_business_evaluation";
|
||||||
const businessOverviewSignal =
|
const seededBusinessOverviewSignal =
|
||||||
rawBusinessOverviewSignal ||
|
|
||||||
broadBusinessEvaluationUnsupported ||
|
broadBusinessEvaluationUnsupported ||
|
||||||
rawDomain === "business_summary" ||
|
rawDomain === "business_summary" ||
|
||||||
rawDomain === "business_overview" ||
|
rawDomain === "business_overview" ||
|
||||||
rawAction === "broad_evaluation";
|
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(
|
const businessOverviewSeparateCounterpartySignal = Boolean(
|
||||||
businessOverviewSignal && hasBusinessOverviewSeparateCounterpartySignal(rawText)
|
businessOverviewSignal && hasBusinessOverviewSeparateCounterpartySignal(rawText)
|
||||||
);
|
);
|
||||||
|
|
@ -1630,7 +1727,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
: rawAssistantTurnMeaningOrganizationScope;
|
: rawAssistantTurnMeaningOrganizationScope;
|
||||||
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
|
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
|
||||||
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
|
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
|
||||||
const currentTurnFreshOrganizationScope = rawOrganizationScope ?? predecomposeEntities.organization;
|
const currentTurnFreshOrganizationScope = predecomposeEntities.organization ?? rawOrganizationScope;
|
||||||
const currentTurnOrganizationScope =
|
const currentTurnOrganizationScope =
|
||||||
currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope;
|
currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope;
|
||||||
const followupCounterpartyIsMetadataOrganizationScope = Boolean(
|
const followupCounterpartyIsMetadataOrganizationScope = Boolean(
|
||||||
|
|
@ -2029,9 +2126,21 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
const semanticDataNeed = metadataAmbiguityLaneClarificationApplicable
|
const semanticDataNeed = metadataAmbiguityLaneClarificationApplicable
|
||||||
? "metadata lane clarification"
|
? "metadata lane clarification"
|
||||||
: semanticNeedFor({
|
: semanticNeedFor({
|
||||||
domain: businessOverviewSignal ? "business_overview" : rawDomain ?? seededDomain,
|
domain: explicitVatMovementEvidenceSignal
|
||||||
action: businessOverviewSignal ? "broad_evaluation" : rawAction ?? seededAction,
|
? "movements"
|
||||||
unsupported: businessOverviewSignal ? "broad_business_evaluation" : unsupported ?? seededUnsupported,
|
: businessOverviewSignal
|
||||||
|
? "business_overview"
|
||||||
|
: rawDomain ?? seededDomain,
|
||||||
|
action: explicitVatMovementEvidenceSignal
|
||||||
|
? "list_movements"
|
||||||
|
: businessOverviewSignal
|
||||||
|
? businessOverviewActionFamily
|
||||||
|
: rawAction ?? seededAction,
|
||||||
|
unsupported: explicitVatMovementEvidenceSignal
|
||||||
|
? "movement_evidence"
|
||||||
|
: businessOverviewSignal
|
||||||
|
? businessOverviewUnsupportedFamily
|
||||||
|
: unsupported ?? seededUnsupported,
|
||||||
lifecycleSignal,
|
lifecycleSignal,
|
||||||
valueFlowSignal,
|
valueFlowSignal,
|
||||||
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
|
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
|
||||||
|
|
@ -2065,7 +2174,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
followupSeed.metadataSelectedEntitySet ??
|
followupSeed.metadataSelectedEntitySet ??
|
||||||
null;
|
null;
|
||||||
const metadataScopedLaneWithoutSubject = Boolean(
|
const metadataScopedLaneWithoutSubject = Boolean(
|
||||||
(metadataGroundedMovementLaneApplicable || metadataGroundedDocumentLaneApplicable) &&
|
(metadataGroundedMovementLaneApplicable ||
|
||||||
|
metadataGroundedDocumentLaneApplicable ||
|
||||||
|
explicitVatMovementEvidenceSignal) &&
|
||||||
!effectiveFollowupCounterparty &&
|
!effectiveFollowupCounterparty &&
|
||||||
metadataLaneCarryoverAvailable
|
metadataLaneCarryoverAvailable
|
||||||
);
|
);
|
||||||
|
|
@ -2278,11 +2389,13 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
const turnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {
|
const turnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {
|
||||||
asked_domain_family:
|
asked_domain_family:
|
||||||
businessOverviewSignal
|
businessOverviewSignal
|
||||||
? "business_overview"
|
? "business_overview"
|
||||||
: lifecycleSignal
|
: lifecycleSignal
|
||||||
? "counterparty_lifecycle"
|
? "counterparty_lifecycle"
|
||||||
: valueFlowSignal
|
: valueFlowSignal
|
||||||
? "counterparty_value"
|
? "counterparty_value"
|
||||||
|
: explicitVatMovementEvidenceSignal
|
||||||
|
? "movements"
|
||||||
: metadataGroundedMovementLaneApplicable
|
: metadataGroundedMovementLaneApplicable
|
||||||
? "movements"
|
? "movements"
|
||||||
: metadataGroundedDocumentLaneApplicable
|
: metadataGroundedDocumentLaneApplicable
|
||||||
|
|
@ -2293,7 +2406,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
? "metadata"
|
? "metadata"
|
||||||
: rawDomain ?? seededDomain,
|
: rawDomain ?? seededDomain,
|
||||||
asked_action_family: businessOverviewSignal
|
asked_action_family: businessOverviewSignal
|
||||||
? "broad_evaluation"
|
? businessOverviewActionFamily
|
||||||
: lifecycleSignal
|
: lifecycleSignal
|
||||||
? "activity_duration"
|
? "activity_duration"
|
||||||
: valueFlowSignal
|
: valueFlowSignal
|
||||||
|
|
@ -2302,6 +2415,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
: payoutSignal
|
: payoutSignal
|
||||||
? "payout"
|
? "payout"
|
||||||
: rawAction ?? seededAction ?? "turnover"
|
: rawAction ?? seededAction ?? "turnover"
|
||||||
|
: explicitVatMovementEvidenceSignal
|
||||||
|
? "list_movements"
|
||||||
: metadataGroundedMovementLaneApplicable
|
: metadataGroundedMovementLaneApplicable
|
||||||
? "list_movements"
|
? "list_movements"
|
||||||
: metadataGroundedDocumentLaneApplicable
|
: metadataGroundedDocumentLaneApplicable
|
||||||
|
|
@ -2334,7 +2449,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
subject_resolution_optional: metadataScopedLaneWithoutSubject || undefined,
|
subject_resolution_optional: metadataScopedLaneWithoutSubject || undefined,
|
||||||
unsupported_but_understood_family:
|
unsupported_but_understood_family:
|
||||||
businessOverviewSignal
|
businessOverviewSignal
|
||||||
? "broad_business_evaluation"
|
? businessOverviewUnsupportedFamily
|
||||||
: unsupported ??
|
: unsupported ??
|
||||||
(lifecycleSignal
|
(lifecycleSignal
|
||||||
? "counterparty_lifecycle"
|
? "counterparty_lifecycle"
|
||||||
|
|
@ -2346,8 +2461,10 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
: seededUnsupported ?? "counterparty_value_or_turnover"
|
: seededUnsupported ?? "counterparty_value_or_turnover"
|
||||||
: metadataGroundedMovementLaneApplicable
|
: metadataGroundedMovementLaneApplicable
|
||||||
? "movement_evidence"
|
? "movement_evidence"
|
||||||
: metadataGroundedDocumentLaneApplicable
|
: metadataGroundedDocumentLaneApplicable
|
||||||
? "document_evidence"
|
? "document_evidence"
|
||||||
|
: explicitVatMovementEvidenceSignal
|
||||||
|
? "movement_evidence"
|
||||||
: metadataAmbiguityLaneClarificationApplicable
|
: metadataAmbiguityLaneClarificationApplicable
|
||||||
? "metadata_lane_choice_clarification"
|
? "metadata_lane_choice_clarification"
|
||||||
: entityResolutionSignal
|
: entityResolutionSignal
|
||||||
|
|
@ -2363,6 +2480,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
unsupported ||
|
unsupported ||
|
||||||
lifecycleSignal ||
|
lifecycleSignal ||
|
||||||
valueFlowSignal ||
|
valueFlowSignal ||
|
||||||
|
explicitVatMovementEvidenceSignal ||
|
||||||
metadataGroundedMovementLaneApplicable ||
|
metadataGroundedMovementLaneApplicable ||
|
||||||
metadataGroundedDocumentLaneApplicable ||
|
metadataGroundedDocumentLaneApplicable ||
|
||||||
metadataAmbiguityLaneClarificationApplicable ||
|
metadataAmbiguityLaneClarificationApplicable ||
|
||||||
|
|
@ -2423,13 +2541,17 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
const currentTurnValueFlowExactOverrideApplicable = Boolean(
|
const currentTurnValueFlowExactOverrideApplicable = Boolean(
|
||||||
valueFlowSignal &&
|
valueFlowSignal &&
|
||||||
explicitIntentCandidate &&
|
explicitIntentCandidate &&
|
||||||
rawValueFlowAggregateQuestionSignal &&
|
(rawValueFlowAggregateQuestionSignal || hasValueRankingSignal(rawText)) &&
|
||||||
semanticDataNeed &&
|
semanticDataNeed &&
|
||||||
(entityCandidates.length > 0 || explicitOrganizationScope || openScopeValueFlowWithoutResolvedCounterparty)
|
(entityCandidates.length > 0 || explicitOrganizationScope || openScopeValueFlowWithoutResolvedCounterparty)
|
||||||
);
|
);
|
||||||
|
|
||||||
const runDiscovery = shouldRunDiscovery({
|
const runDiscovery = shouldRunDiscovery({
|
||||||
unsupported: businessOverviewSignal ? "broad_business_evaluation" : unsupported ?? seededUnsupported,
|
unsupported: explicitVatMovementEvidenceSignal
|
||||||
|
? "movement_evidence"
|
||||||
|
: businessOverviewSignal
|
||||||
|
? "broad_business_evaluation"
|
||||||
|
: unsupported ?? seededUnsupported,
|
||||||
lifecycleSignal,
|
lifecycleSignal,
|
||||||
valueFlowSignal,
|
valueFlowSignal,
|
||||||
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
|
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
|
||||||
|
|
@ -2446,6 +2568,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
groundedValueFlowFollowupApplicable,
|
groundedValueFlowFollowupApplicable,
|
||||||
forceDiscoveryOverExplicitIntent:
|
forceDiscoveryOverExplicitIntent:
|
||||||
businessOverviewSignal ||
|
businessOverviewSignal ||
|
||||||
|
explicitVatMovementEvidenceSignal ||
|
||||||
Boolean(entityResolutionClarificationCandidate) ||
|
Boolean(entityResolutionClarificationCandidate) ||
|
||||||
organizationClarificationFollowupApplicable ||
|
organizationClarificationFollowupApplicable ||
|
||||||
periodClarificationFollowupApplicable ||
|
periodClarificationFollowupApplicable ||
|
||||||
|
|
@ -2469,6 +2592,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
? "followup_context"
|
? "followup_context"
|
||||||
: metadataGroundedDocumentLaneApplicable
|
: metadataGroundedDocumentLaneApplicable
|
||||||
? "followup_context"
|
? "followup_context"
|
||||||
|
: explicitVatMovementEvidenceSignal
|
||||||
|
? "raw_text"
|
||||||
: predecomposeContract
|
: predecomposeContract
|
||||||
? "predecompose_contract"
|
? "predecompose_contract"
|
||||||
: lifecycleSignal
|
: lifecycleSignal
|
||||||
|
|
@ -2490,6 +2615,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
if (rawMetadataSignal) {
|
if (rawMetadataSignal) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_metadata_signal_detected");
|
pushReason(reasonCodes, "mcp_discovery_metadata_signal_detected");
|
||||||
}
|
}
|
||||||
|
if (explicitVatMovementEvidenceSignal) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_vat_movement_evidence_signal_detected");
|
||||||
|
}
|
||||||
if (entityResolutionSignal) {
|
if (entityResolutionSignal) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_entity_resolution_signal_detected");
|
pushReason(reasonCodes, "mcp_discovery_entity_resolution_signal_detected");
|
||||||
}
|
}
|
||||||
|
|
@ -2613,6 +2741,12 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
if (businessOverviewContinuationSignal) {
|
if (businessOverviewContinuationSignal) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_from_followup_context");
|
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) {
|
if (explicitVatSuppressesBusinessOverviewContinuation) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_suppressed_by_explicit_vat_question");
|
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_suppressed_by_explicit_vat_question");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
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;
|
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) {
|
function resolveAssistantOrchestrationDecision(input) {
|
||||||
const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? "");
|
const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? "");
|
||||||
const effectiveAddressUserMessage = String(input?.effectiveAddressUserMessage ?? rawUserMessage);
|
const effectiveAddressUserMessage = String(input?.effectiveAddressUserMessage ?? rawUserMessage);
|
||||||
|
|
@ -559,6 +566,27 @@ export function createAssistantRoutePolicy(deps) {
|
||||||
const followupPreviousFilters = followupContext?.previous_filters && typeof followupContext.previous_filters === "object"
|
const followupPreviousFilters = followupContext?.previous_filters && typeof followupContext.previous_filters === "object"
|
||||||
? followupContext.previous_filters
|
? followupContext.previous_filters
|
||||||
: null;
|
: 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 &&
|
const protectedInventoryShortFollowup = Boolean(followupContext &&
|
||||||
(isInventorySelectedObjectIntent(followupPreviousIntent) ||
|
(isInventorySelectedObjectIntent(followupPreviousIntent) ||
|
||||||
(followupPreviousIntent === "inventory_on_hand_as_of_date" &&
|
(followupPreviousIntent === "inventory_on_hand_as_of_date" &&
|
||||||
|
|
@ -608,6 +636,8 @@ export function createAssistantRoutePolicy(deps) {
|
||||||
"net_value_flow"
|
"net_value_flow"
|
||||||
].includes(String(toNonEmptyString(assistantTurnMeaning?.asked_action_family) ?? "")) ||
|
].includes(String(toNonEmptyString(assistantTurnMeaning?.asked_action_family) ?? "")) ||
|
||||||
/(?:нетто|сальдо|сколько\s+мы\s+(?:получили|заплатили)|incoming|outgoing)/iu.test(analyticsSample)));
|
/(?:нетто|сальдо|сколько\s+мы\s+(?:получили|заплатили)|incoming|outgoing)/iu.test(analyticsSample)));
|
||||||
|
const effectiveGroundedValueFlowFollowupContextDetected =
|
||||||
|
groundedValueFlowFollowupContextDetected || routeCandidateOrganizationClarificationDetected;
|
||||||
const baseToolGatePreservesAddressLane = Boolean(baseToolGate?.runAddressLane &&
|
const baseToolGatePreservesAddressLane = Boolean(baseToolGate?.runAddressLane &&
|
||||||
[
|
[
|
||||||
"address_intent_resolver_detected",
|
"address_intent_resolver_detected",
|
||||||
|
|
@ -617,14 +647,15 @@ export function createAssistantRoutePolicy(deps) {
|
||||||
].includes(String(baseToolGate?.reason ?? ""))) ||
|
].includes(String(baseToolGate?.reason ?? ""))) ||
|
||||||
Boolean(baseToolGate?.runAddressLane &&
|
Boolean(baseToolGate?.runAddressLane &&
|
||||||
String(baseToolGate?.reason ?? "") === "followup_context_detected" &&
|
String(baseToolGate?.reason ?? "") === "followup_context_detected" &&
|
||||||
groundedValueFlowFollowupContextDetected);
|
effectiveGroundedValueFlowFollowupContextDetected);
|
||||||
const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate &&
|
const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate &&
|
||||||
deterministicNonDomainGuard &&
|
deterministicNonDomainGuard &&
|
||||||
(llmFirstUnsupportedCandidate || llmContractMode === null) &&
|
(llmFirstUnsupportedCandidate || llmContractMode === null) &&
|
||||||
!baseToolGatePreservesAddressLane &&
|
!baseToolGatePreservesAddressLane &&
|
||||||
!groundedValueFlowFollowupContextDetected &&
|
!effectiveGroundedValueFlowFollowupContextDetected &&
|
||||||
!protectedInventoryShortFollowup &&
|
!protectedInventoryShortFollowup &&
|
||||||
!organizationClarificationContinuationDetected);
|
!organizationClarificationContinuationDetected &&
|
||||||
|
!routeCandidateOrganizationClarificationDetected);
|
||||||
const lastAddressAssistantDebug = sessionItems
|
const lastAddressAssistantDebug = sessionItems
|
||||||
? findLastAddressAssistantItem(sessionItems)?.debug ?? null
|
? findLastAddressAssistantItem(sessionItems)?.debug ?? null
|
||||||
: null;
|
: null;
|
||||||
|
|
@ -668,7 +699,7 @@ export function createAssistantRoutePolicy(deps) {
|
||||||
!turnMeaningIntentCandidate &&
|
!turnMeaningIntentCandidate &&
|
||||||
!dataScopeMetaQuery &&
|
!dataScopeMetaQuery &&
|
||||||
!dangerOrCoercionSignal &&
|
!dangerOrCoercionSignal &&
|
||||||
!groundedValueFlowFollowupContextDetected &&
|
!effectiveGroundedValueFlowFollowupContextDetected &&
|
||||||
!organizationClarificationContinuationDetected);
|
!organizationClarificationContinuationDetected);
|
||||||
const hardMetaMode = resolveHardMetaMode({
|
const hardMetaMode = resolveHardMetaMode({
|
||||||
dataScopeMetaQuery,
|
dataScopeMetaQuery,
|
||||||
|
|
@ -834,7 +865,7 @@ export function createAssistantRoutePolicy(deps) {
|
||||||
!dataScopeMetaQuery &&
|
!dataScopeMetaQuery &&
|
||||||
!capabilityMetaQuery &&
|
!capabilityMetaQuery &&
|
||||||
!dangerOrCoercionSignal &&
|
!dangerOrCoercionSignal &&
|
||||||
!groundedValueFlowFollowupContextDetected &&
|
!effectiveGroundedValueFlowFollowupContextDetected &&
|
||||||
!organizationClarificationContinuationDetected);
|
!organizationClarificationContinuationDetected);
|
||||||
if (unsupportedCurrentTurnMeaningBoundary) {
|
if (unsupportedCurrentTurnMeaningBoundary) {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -2079,6 +2079,9 @@ function isAddressLaneDebugPayload(debug) {
|
||||||
if (typeof debug.mcp_call_status === "string" && debug.mcp_call_status.trim().length > 0) {
|
if (typeof debug.mcp_call_status === "string" && debug.mcp_call_status.trim().length > 0) {
|
||||||
return true;
|
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) {
|
if (typeof debug.anchor_type === "string" && debug.anchor_type.trim().length > 0) {
|
||||||
return true;
|
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) {
|
function parseDmyDateToIso(value) {
|
||||||
const match = String(value ?? "").trim().match(/^(\d{2})\.(\d{2})\.(\d{4})$/);
|
const match = String(value ?? "").trim().match(/^(\d{2})\.(\d{2})\.(\d{4})$/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
|
|
@ -584,7 +612,12 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
(deps.toNonEmptyString(alternateMessage)
|
(deps.toNonEmptyString(alternateMessage)
|
||||||
? deps.hasDataRetrievalRequestSignal(String(alternateMessage ?? ""))
|
? deps.hasDataRetrievalRequestSignal(String(alternateMessage ?? ""))
|
||||||
: false);
|
: false);
|
||||||
if (rawCapabilityMetaQuery && !rawDataRetrievalSignal) {
|
const rawBusinessOverviewBoundaryFollowupCue =
|
||||||
|
hasBusinessOverviewBoundaryFollowupCue(userMessage) ||
|
||||||
|
(deps.toNonEmptyString(alternateMessage)
|
||||||
|
? hasBusinessOverviewBoundaryFollowupCue(String(alternateMessage ?? ""))
|
||||||
|
: false);
|
||||||
|
if (rawCapabilityMetaQuery && !rawDataRetrievalSignal && !rawBusinessOverviewBoundaryFollowupCue) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const assistantTurnMeaning =
|
const assistantTurnMeaning =
|
||||||
|
|
@ -660,11 +693,42 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
carryoverSourceDebug,
|
carryoverSourceDebug,
|
||||||
deps.toNonEmptyString
|
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 =
|
const hasValueFlowCarryoverSourceHint =
|
||||||
sourceIntentHint === "customer_revenue_and_payments" ||
|
sourceIntentHint === "customer_revenue_and_payments" ||
|
||||||
sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" ||
|
sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" ||
|
||||||
sourceDiscoveryPilotScopeHint === "counterparty_supplier_payout_query_movements_v1" ||
|
sourceDiscoveryPilotScopeHint === "counterparty_supplier_payout_query_movements_v1" ||
|
||||||
sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1";
|
sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1";
|
||||||
|
const hasBusinessOverviewCarryoverSourceHint =
|
||||||
|
sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
|
||||||
const navigationSessionState = resolveNavigationSessionContextState(
|
const navigationSessionState = resolveNavigationSessionContextState(
|
||||||
addressNavigationState,
|
addressNavigationState,
|
||||||
deps.toNonEmptyString,
|
deps.toNonEmptyString,
|
||||||
|
|
@ -706,21 +770,31 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
|
hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
|
||||||
? hasShortValueFlowRetargetCue(String(alternateMessage ?? ""))
|
? hasShortValueFlowRetargetCue(String(alternateMessage ?? ""))
|
||||||
: false;
|
: false;
|
||||||
|
const businessOverviewBoundaryFollowupPrimary =
|
||||||
|
hasBusinessOverviewCarryoverSourceHint && hasBusinessOverviewBoundaryFollowupCue(userMessage);
|
||||||
|
const businessOverviewBoundaryFollowupAlternate =
|
||||||
|
hasBusinessOverviewCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
|
||||||
|
? hasBusinessOverviewBoundaryFollowupCue(String(alternateMessage ?? ""))
|
||||||
|
: false;
|
||||||
const explicitSummaryBundleReuseSignal = hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage);
|
const explicitSummaryBundleReuseSignal = hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage);
|
||||||
let hasPrimaryFollowupSignal =
|
let hasPrimaryFollowupSignal =
|
||||||
deps.hasAddressFollowupContextSignal(userMessage) ||
|
deps.hasAddressFollowupContextSignal(userMessage) ||
|
||||||
Boolean(debtRoleSwapPrimary) ||
|
Boolean(debtRoleSwapPrimary) ||
|
||||||
shortValueFlowRetargetPrimary ||
|
shortValueFlowRetargetPrimary ||
|
||||||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
inventoryShortFollowupPrimary ||
|
inventoryShortFollowupPrimary ||
|
||||||
inventoryPurchaseDateVatBridge ||
|
inventoryPurchaseDateVatBridge ||
|
||||||
explicitSummaryBundleReuseSignal;
|
explicitSummaryBundleReuseSignal ||
|
||||||
|
mcpDiscoveryOrganizationClarificationContinuation;
|
||||||
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||||
Boolean(debtRoleSwapAlternate) ||
|
Boolean(debtRoleSwapAlternate) ||
|
||||||
shortValueFlowRetargetAlternate ||
|
shortValueFlowRetargetAlternate ||
|
||||||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
inventoryShortFollowupAlternate ||
|
inventoryShortFollowupAlternate ||
|
||||||
inventoryPurchaseDateVatBridge ||
|
inventoryPurchaseDateVatBridge ||
|
||||||
explicitSummaryBundleReuseSignal
|
explicitSummaryBundleReuseSignal ||
|
||||||
|
mcpDiscoveryOrganizationClarificationContinuation
|
||||||
: false;
|
: false;
|
||||||
const hasPrimaryIndexReferenceSignal = deps.extractDisplayedEntityIndexMention(userMessage) !== null;
|
const hasPrimaryIndexReferenceSignal = deps.extractDisplayedEntityIndexMention(userMessage) !== null;
|
||||||
const hasAlternateIndexReferenceSignal = deps.toNonEmptyString(alternateMessage)
|
const hasAlternateIndexReferenceSignal = deps.toNonEmptyString(alternateMessage)
|
||||||
|
|
@ -760,6 +834,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
hasPrimaryIndexReferenceSignal ||
|
hasPrimaryIndexReferenceSignal ||
|
||||||
hasAlternateIndexReferenceSignal ||
|
hasAlternateIndexReferenceSignal ||
|
||||||
hasOrganizationClarificationContinuation ||
|
hasOrganizationClarificationContinuation ||
|
||||||
|
mcpDiscoveryOrganizationClarificationContinuation ||
|
||||||
hasImplicitContinuationSignal ||
|
hasImplicitContinuationSignal ||
|
||||||
hasSuggestedIntentPivotSignal ||
|
hasSuggestedIntentPivotSignal ||
|
||||||
inventoryShortFollowupPrimary ||
|
inventoryShortFollowupPrimary ||
|
||||||
|
|
@ -773,6 +848,8 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
Boolean(debtRoleSwapIntent) ||
|
Boolean(debtRoleSwapIntent) ||
|
||||||
shortValueFlowRetargetPrimary ||
|
shortValueFlowRetargetPrimary ||
|
||||||
shortValueFlowRetargetAlternate ||
|
shortValueFlowRetargetAlternate ||
|
||||||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
deps.hasFollowupMarker(userMessage) ||
|
deps.hasFollowupMarker(userMessage) ||
|
||||||
deps.hasReferentialPointer(userMessage) ||
|
deps.hasReferentialPointer(userMessage) ||
|
||||||
(deps.toNonEmptyString(alternateMessage)
|
(deps.toNonEmptyString(alternateMessage)
|
||||||
|
|
@ -783,6 +860,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
hasPrimaryIndexReferenceSignal ||
|
hasPrimaryIndexReferenceSignal ||
|
||||||
hasAlternateIndexReferenceSignal ||
|
hasAlternateIndexReferenceSignal ||
|
||||||
hasOrganizationClarificationContinuation ||
|
hasOrganizationClarificationContinuation ||
|
||||||
|
mcpDiscoveryOrganizationClarificationContinuation ||
|
||||||
inventoryShortFollowupPrimary ||
|
inventoryShortFollowupPrimary ||
|
||||||
inventoryShortFollowupAlternate ||
|
inventoryShortFollowupAlternate ||
|
||||||
hasInventoryRootTemporalFollowupPrimary ||
|
hasInventoryRootTemporalFollowupPrimary ||
|
||||||
|
|
@ -794,6 +872,8 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
Boolean(debtRoleSwapIntent) ||
|
Boolean(debtRoleSwapIntent) ||
|
||||||
shortValueFlowRetargetPrimary ||
|
shortValueFlowRetargetPrimary ||
|
||||||
shortValueFlowRetargetAlternate ||
|
shortValueFlowRetargetAlternate ||
|
||||||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
deps.hasFollowupMarker(userMessage) ||
|
deps.hasFollowupMarker(userMessage) ||
|
||||||
deps.hasReferentialPointer(userMessage) ||
|
deps.hasReferentialPointer(userMessage) ||
|
||||||
(deps.toNonEmptyString(alternateMessage)
|
(deps.toNonEmptyString(alternateMessage)
|
||||||
|
|
@ -826,6 +906,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
!hasImplicitContinuationSignal &&
|
!hasImplicitContinuationSignal &&
|
||||||
!hasSuggestedIntentPivotSignal &&
|
!hasSuggestedIntentPivotSignal &&
|
||||||
!hasOrganizationClarificationContinuation &&
|
!hasOrganizationClarificationContinuation &&
|
||||||
|
!mcpDiscoveryOrganizationClarificationContinuation &&
|
||||||
!hasIndexReferenceSignal &&
|
!hasIndexReferenceSignal &&
|
||||||
!explicitSummaryBundleReuseSignal
|
!explicitSummaryBundleReuseSignal
|
||||||
) {
|
) {
|
||||||
|
|
@ -843,6 +924,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
!hasImplicitContinuationSignal &&
|
!hasImplicitContinuationSignal &&
|
||||||
!hasSuggestedIntentPivotSignal &&
|
!hasSuggestedIntentPivotSignal &&
|
||||||
!hasOrganizationClarificationContinuation &&
|
!hasOrganizationClarificationContinuation &&
|
||||||
|
!mcpDiscoveryOrganizationClarificationContinuation &&
|
||||||
!hasIndexReferenceSignal &&
|
!hasIndexReferenceSignal &&
|
||||||
!explicitSummaryBundleReuseSignal
|
!explicitSummaryBundleReuseSignal
|
||||||
) {
|
) {
|
||||||
|
|
@ -884,19 +966,10 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
carryoverSourceDebug,
|
carryoverSourceDebug,
|
||||||
deps.toNonEmptyString
|
deps.toNonEmptyString
|
||||||
);
|
);
|
||||||
const sourceDiscoveryLoopStatus = readAssistantMcpDiscoveryLoopStatus(carryoverSourceDebug, deps.toNonEmptyString);
|
const sourceDiscoveryLoopStatus = sourceDiscoveryLoopStatusHint;
|
||||||
const sourceDiscoveryLoopSelectedChainId = readAssistantMcpDiscoveryLoopSelectedChainId(
|
const sourceDiscoveryLoopSelectedChainId = sourceDiscoveryLoopSelectedChainIdHint;
|
||||||
carryoverSourceDebug,
|
const sourceDiscoveryLoopPendingAxes = sourceDiscoveryLoopPendingAxesHint;
|
||||||
deps.toNonEmptyString
|
const sourceDiscoveryLoopProvidedAxes = sourceDiscoveryLoopProvidedAxesHint;
|
||||||
);
|
|
||||||
const sourceDiscoveryLoopPendingAxes = readAssistantMcpDiscoveryLoopPendingAxes(
|
|
||||||
carryoverSourceDebug,
|
|
||||||
deps.toNonEmptyString
|
|
||||||
);
|
|
||||||
const sourceDiscoveryLoopProvidedAxes = readAssistantMcpDiscoveryLoopProvidedAxes(
|
|
||||||
carryoverSourceDebug,
|
|
||||||
deps.toNonEmptyString
|
|
||||||
);
|
|
||||||
const sourceDiscoveryLoopAskedDomainFamily = readAssistantMcpDiscoveryLoopAskedDomainFamily(
|
const sourceDiscoveryLoopAskedDomainFamily = readAssistantMcpDiscoveryLoopAskedDomainFamily(
|
||||||
carryoverSourceDebug,
|
carryoverSourceDebug,
|
||||||
deps.toNonEmptyString
|
deps.toNonEmptyString
|
||||||
|
|
@ -959,6 +1032,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
explicitIntentFamily &&
|
explicitIntentFamily &&
|
||||||
sourceIntentFamily !== explicitIntentFamily &&
|
sourceIntentFamily !== explicitIntentFamily &&
|
||||||
!hasOrganizationClarificationContinuation &&
|
!hasOrganizationClarificationContinuation &&
|
||||||
|
!mcpDiscoveryOrganizationClarificationContinuation &&
|
||||||
!hasImplicitContinuationSignal &&
|
!hasImplicitContinuationSignal &&
|
||||||
!hasIndexReferenceSignal &&
|
!hasIndexReferenceSignal &&
|
||||||
!hasInventoryRootTemporalFollowupPrimary &&
|
!hasInventoryRootTemporalFollowupPrimary &&
|
||||||
|
|
@ -967,6 +1041,8 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
!hasInventoryRootRestatementAlternate &&
|
!hasInventoryRootRestatementAlternate &&
|
||||||
!inventoryShortFollowupPrimary &&
|
!inventoryShortFollowupPrimary &&
|
||||||
!inventoryShortFollowupAlternate &&
|
!inventoryShortFollowupAlternate &&
|
||||||
|
!businessOverviewBoundaryFollowupPrimary &&
|
||||||
|
!businessOverviewBoundaryFollowupAlternate &&
|
||||||
!foreignAccountingPivotOverInventory &&
|
!foreignAccountingPivotOverInventory &&
|
||||||
!deps.hasFollowupMarker(userMessage) &&
|
!deps.hasFollowupMarker(userMessage) &&
|
||||||
!deps.hasReferentialPointer(userMessage) &&
|
!deps.hasReferentialPointer(userMessage) &&
|
||||||
|
|
@ -1027,24 +1103,29 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
hasSuggestedIntentPivotSignal ||
|
hasSuggestedIntentPivotSignal ||
|
||||||
Boolean(debtRoleSwapPrimary) ||
|
Boolean(debtRoleSwapPrimary) ||
|
||||||
shortValueFlowRetargetPrimary ||
|
shortValueFlowRetargetPrimary ||
|
||||||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
inventoryShortFollowupPrimary ||
|
inventoryShortFollowupPrimary ||
|
||||||
inventoryPurchaseDateVatBridge ||
|
inventoryPurchaseDateVatBridge ||
|
||||||
explicitSummaryBundleReuseSignal ||
|
explicitSummaryBundleReuseSignal ||
|
||||||
hasInventoryRootTemporalFollowupPrimary;
|
hasInventoryRootTemporalFollowupPrimary ||
|
||||||
|
mcpDiscoveryOrganizationClarificationContinuation;
|
||||||
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||||
hasSuggestedIntentPivotSignal ||
|
hasSuggestedIntentPivotSignal ||
|
||||||
Boolean(debtRoleSwapAlternate) ||
|
Boolean(debtRoleSwapAlternate) ||
|
||||||
shortValueFlowRetargetAlternate ||
|
shortValueFlowRetargetAlternate ||
|
||||||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
inventoryShortFollowupAlternate ||
|
inventoryShortFollowupAlternate ||
|
||||||
inventoryPurchaseDateVatBridge ||
|
inventoryPurchaseDateVatBridge ||
|
||||||
explicitSummaryBundleReuseSignal ||
|
explicitSummaryBundleReuseSignal ||
|
||||||
hasInventoryRootTemporalFollowupAlternate
|
hasInventoryRootTemporalFollowupAlternate ||
|
||||||
|
mcpDiscoveryOrganizationClarificationContinuation
|
||||||
: false;
|
: false;
|
||||||
hasStrongFollowupReference =
|
hasStrongFollowupReference =
|
||||||
hasPrimaryIndexReferenceSignal ||
|
hasPrimaryIndexReferenceSignal ||
|
||||||
hasAlternateIndexReferenceSignal ||
|
hasAlternateIndexReferenceSignal ||
|
||||||
hasOrganizationClarificationContinuation ||
|
hasOrganizationClarificationContinuation ||
|
||||||
|
mcpDiscoveryOrganizationClarificationContinuation ||
|
||||||
hasSuggestedIntentPivotSignal ||
|
hasSuggestedIntentPivotSignal ||
|
||||||
hasImplicitContinuationSignal ||
|
hasImplicitContinuationSignal ||
|
||||||
inventoryShortFollowupPrimary ||
|
inventoryShortFollowupPrimary ||
|
||||||
|
|
@ -1056,6 +1137,8 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
Boolean(debtRoleSwapIntent) ||
|
Boolean(debtRoleSwapIntent) ||
|
||||||
shortValueFlowRetargetPrimary ||
|
shortValueFlowRetargetPrimary ||
|
||||||
shortValueFlowRetargetAlternate ||
|
shortValueFlowRetargetAlternate ||
|
||||||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
deps.hasFollowupMarker(userMessage) ||
|
deps.hasFollowupMarker(userMessage) ||
|
||||||
deps.hasReferentialPointer(userMessage) ||
|
deps.hasReferentialPointer(userMessage) ||
|
||||||
(deps.toNonEmptyString(alternateMessage)
|
(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_payable_forecast"
|
||||||
| "vat_liability_confirmed_for_tax_period"
|
| "vat_liability_confirmed_for_tax_period"
|
||||||
| "vat_payable_confirmed_as_of_date"
|
| "vat_payable_confirmed_as_of_date"
|
||||||
|
| "accounting_financial_result_for_organization"
|
||||||
|
| "debt_due_date_aging_for_organization"
|
||||||
| "open_contracts_confirmed_as_of_date"
|
| "open_contracts_confirmed_as_of_date"
|
||||||
| "list_contracts_by_counterparty"
|
| "list_contracts_by_counterparty"
|
||||||
| "list_open_contracts"
|
| "list_open_contracts"
|
||||||
|
|
@ -189,6 +191,8 @@ export interface AddressRecipeDefinition {
|
||||||
| "vat_payable_forecast_profile"
|
| "vat_payable_forecast_profile"
|
||||||
| "vat_liability_confirmed_tax_period_profile"
|
| "vat_liability_confirmed_tax_period_profile"
|
||||||
| "vat_payable_confirmed_as_of_balance_profile"
|
| "vat_payable_confirmed_as_of_balance_profile"
|
||||||
|
| "accounting_financial_result_profile"
|
||||||
|
| "debt_due_date_aging_profile"
|
||||||
| "open_contracts_confirmed_as_of_balance_profile"
|
| "open_contracts_confirmed_as_of_balance_profile"
|
||||||
| "payables_confirmed_as_of_balance_profile"
|
| "payables_confirmed_as_of_balance_profile"
|
||||||
| "receivables_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");
|
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", () => {
|
it("detects payables snapshot wording in plain human form", () => {
|
||||||
const result = resolveAddressIntent("мы должны комуто денег на сегодня?");
|
const result = resolveAddressIntent("мы должны комуто денег на сегодня?");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1336,9 +1336,9 @@ describe("address compose stage utf8 headers", () => {
|
||||||
{ userMessage: "Сколько у нас заказчиков, поставщиков и смешанных контрагентов?" }
|
{ userMessage: "Сколько у нас заказчиков, поставщиков и смешанных контрагентов?" }
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(reply.text).toContain("Роли контрагентов по активности:");
|
expect(reply.text).toContain("Распределение ролей по активности:");
|
||||||
expect(reply.text).toContain("Заказчики (только customer-роль): 122.");
|
expect(reply.text).toContain("Заказчики с ролью покупателя: 122.");
|
||||||
expect(reply.text).toContain("Поставщики (только supplier-роль): 71.");
|
expect(reply.text).toContain("Поставщики с ролью поставщика: 71.");
|
||||||
expect(reply.text).toContain("Смешанные (и покупатель, и поставщик): 23.");
|
expect(reply.text).toContain("Смешанные (и покупатель, и поставщик): 23.");
|
||||||
expect(reply.text).not.toContain("Всего уникальных контрагентов в базе");
|
expect(reply.text).not.toContain("Всего уникальных контрагентов в базе");
|
||||||
});
|
});
|
||||||
|
|
@ -1391,7 +1391,7 @@ describe("address compose stage utf8 headers", () => {
|
||||||
{ userMessage: "скока поставщиков в базе" }
|
{ userMessage: "скока поставщиков в базе" }
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(reply.text).toContain("Поставщиков (только supplier-роль): 71.");
|
expect(reply.text).toContain("Поставщиков с ролью поставщика: 71.");
|
||||||
expect(reply.text).not.toContain("Роли контрагентов по активности:");
|
expect(reply.text).not.toContain("Роли контрагентов по активности:");
|
||||||
expect(reply.text).not.toContain("Всего уникальных контрагентов в базе");
|
expect(reply.text).not.toContain("Всего уникальных контрагентов в базе");
|
||||||
});
|
});
|
||||||
|
|
@ -1444,7 +1444,7 @@ describe("address compose stage utf8 headers", () => {
|
||||||
{ userMessage: "скок клиентов" }
|
{ userMessage: "скок клиентов" }
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(reply.text).toContain("Заказчиков (только customer-роль): 122.");
|
expect(reply.text).toContain("Заказчиков с ролью покупателя: 122.");
|
||||||
expect(reply.text).not.toContain("Роли контрагентов по активности:");
|
expect(reply.text).not.toContain("Роли контрагентов по активности:");
|
||||||
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.responseType).toBe("FACTUAL_LIST");
|
||||||
expect(reply.text).toContain("по максимальной сумме одной входящей операции");
|
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", () => {
|
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.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("Найдено VAT-объектов: 5");
|
||||||
expect(reply.text).toContain("РегистрНакопления.НДСПродажи");
|
expect(reply.text).toContain("РегистрНакопления.НДСПродажи");
|
||||||
});
|
});
|
||||||
|
|
@ -2063,12 +2063,14 @@ describe("address compose stage utf8 headers", () => {
|
||||||
userMessage: "сколько платить ндс в налоговую за декабрь 2019",
|
userMessage: "сколько платить ндс в налоговую за декабрь 2019",
|
||||||
periodFrom: "2019-10-01",
|
periodFrom: "2019-10-01",
|
||||||
periodTo: "2019-12-31",
|
periodTo: "2019-12-31",
|
||||||
|
organizationHint: "ООО Альтернатива Плюс",
|
||||||
useRubCurrency: true
|
useRubCurrency: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
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.text).toContain("50.000,00 ₽");
|
||||||
expect(reply.semantics?.result_mode).toBe("confirmed_balance");
|
expect(reply.semantics?.result_mode).toBe("confirmed_balance");
|
||||||
expect(reply.semantics?.balance_confirmed).toBe(true);
|
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.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("VAT-объектов в метаданных 1С: 3");
|
||||||
expect(reply.text).toContain("Источников с движениями до даты среза: 1");
|
expect(reply.text).toContain("Источников с движениями до даты среза: 1");
|
||||||
expect(reply.text).toContain("РегистрНакопления.НДСНачисленный");
|
expect(reply.text).toContain("РегистрНакопления.НДСНачисленный");
|
||||||
|
|
@ -2247,7 +2249,7 @@ describe("address compose stage utf8 headers", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(reply.responseType).toBe("FACTUAL_LIST");
|
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();
|
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", {});
|
const selected = selectAddressRecipe("customer_revenue_and_payments", {});
|
||||||
expect(selected.selected_recipe).toBeTruthy();
|
expect(selected.selected_recipe).toBeTruthy();
|
||||||
const plan = buildAddressRecipePlan(selected.selected_recipe!, {});
|
const plan = buildAddressRecipePlan(selected.selected_recipe!, {});
|
||||||
|
|
||||||
expect(plan.recipe.recipe_id).toBe("address_customer_revenue_and_payments_v1");
|
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("ПоступлениеНаРасчетныйСчет");
|
||||||
expect(plan.query).toContain("БанкПоступление.ДоговорКонтрагента");
|
expect(plan.query).toContain("БанкПоступление.ДоговорКонтрагента");
|
||||||
});
|
});
|
||||||
|
|
@ -5138,13 +5140,13 @@ describe("address recipe catalog counterparty filtering", () => {
|
||||||
expect(plan.limit).toBe(1000);
|
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", {});
|
const selected = selectAddressRecipe("supplier_payouts_profile", {});
|
||||||
expect(selected.selected_recipe).toBeTruthy();
|
expect(selected.selected_recipe).toBeTruthy();
|
||||||
const plan = buildAddressRecipePlan(selected.selected_recipe!, {});
|
const plan = buildAddressRecipePlan(selected.selected_recipe!, {});
|
||||||
|
|
||||||
expect(plan.recipe.recipe_id).toBe("address_supplier_payouts_profile_v1");
|
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("СписаниеСРасчетногоСчета");
|
||||||
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\"");
|
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", () => {
|
it("injects account condition into movements query for account snapshot", () => {
|
||||||
const filters = extractAddressFilters(
|
const filters = extractAddressFilters(
|
||||||
"Какой остаток по счету 60 на дату 2020-07-31",
|
"Какой остаток по счету 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.");
|
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 () => {
|
it("turns business overview multi-probe evidence into an analyst-safe draft", async () => {
|
||||||
const planner = planAssistantMcpDiscovery({
|
const planner = planAssistantMcpDiscovery({
|
||||||
dataNeedGraph: {
|
dataNeedGraph: {
|
||||||
|
|
@ -355,6 +425,7 @@ describe("assistant MCP discovery answer adapter", () => {
|
||||||
{ rows: [] },
|
{ rows: [] },
|
||||||
{ rows: [] },
|
{ rows: [] },
|
||||||
{ rows: [] },
|
{ rows: [] },
|
||||||
|
{ rows: [] },
|
||||||
{
|
{
|
||||||
rows: [
|
rows: [
|
||||||
{ Период: "2020-01-15T00:00:00", Регистратор: "Поступление 1" },
|
{ Период: "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-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" }] },
|
||||||
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Поставщик А" }] },
|
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Поставщик А" }] },
|
||||||
{ rows: [] },
|
{ rows: [] },
|
||||||
|
{ rows: [] },
|
||||||
{
|
{
|
||||||
rows: [
|
rows: [
|
||||||
{ Period: "2020-12-31T00:00:00", Amount: 100000, Counterparty: "Клиент А" }
|
{ Period: "2020-12-31T00:00:00", Amount: 100000, Counterparty: "Клиент А" }
|
||||||
|
|
@ -528,6 +600,7 @@ describe("assistant MCP discovery answer adapter", () => {
|
||||||
{ rows: [] },
|
{ rows: [] },
|
||||||
{ rows: [] },
|
{ rows: [] },
|
||||||
{ rows: [] },
|
{ rows: [] },
|
||||||
|
{ rows: [] },
|
||||||
{
|
{
|
||||||
rows: [
|
rows: [
|
||||||
{ Period: "2020-12-31T00:00:00", Amount: 250000, Quantity: 10, Item: "Товар А" },
|
{ 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.");
|
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 () => {
|
it("renders metadata-scoped movement all-time follow-up as an all-time bounded answer", async () => {
|
||||||
const planner = planAssistantMcpDiscovery({
|
const planner = planAssistantMcpDiscovery({
|
||||||
dataNeedGraph: {
|
dataNeedGraph: {
|
||||||
|
|
@ -796,7 +936,9 @@ describe("assistant MCP discovery answer adapter", () => {
|
||||||
|
|
||||||
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
|
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
|
||||||
expect(draft.confirmed_lines).toHaveLength(1);
|
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).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("1C incoming value-flow");
|
||||||
expect(userText).not.toContain("Full ranking outside");
|
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"
|
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`,
|
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
|
||||||
Amount: 10,
|
Amount: 10,
|
||||||
Counterparty: "SVK"
|
Counterparty: "SVK"
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,25 @@ function entryPointContract(overrides: Record<string, unknown> = {}) {
|
||||||
selected_chain_matches_top: true
|
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_draft: {
|
||||||
answer_mode: "confirmed_with_bounded_inference"
|
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_alignment_status).toBe("selected_matches_top");
|
||||||
expect(debug.mcp_discovery_catalog_chain_top_match).toBe("value_flow_ranking");
|
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_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_answer_mode).toBe("confirmed_with_bounded_inference");
|
||||||
expect(debug.mcp_discovery_business_fact_answer_allowed).toBe(true);
|
expect(debug.mcp_discovery_business_fact_answer_allowed).toBe(true);
|
||||||
expect(debug.mcp_discovery_user_facing_response_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_alignment_status).toBeNull();
|
||||||
expect(debug.mcp_discovery_catalog_chain_top_match).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_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_answer_mode).toBeNull();
|
||||||
expect(debug.mcp_discovery_business_fact_answer_allowed).toBe(false);
|
expect(debug.mcp_discovery_business_fact_answer_allowed).toBe(false);
|
||||||
expect(debug.mcp_discovery_user_facing_response_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);
|
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 () => {
|
it("adds a checked VAT/tax family to business overview only when an explicit period is available", async () => {
|
||||||
const planner = planAssistantMcpDiscovery({
|
const planner = planAssistantMcpDiscovery({
|
||||||
dataNeedGraph: {
|
dataNeedGraph: {
|
||||||
|
|
@ -347,6 +421,7 @@ describe("assistant MCP discovery pilot executor", () => {
|
||||||
{ rows: [] },
|
{ rows: [] },
|
||||||
{ rows: [] },
|
{ rows: [] },
|
||||||
{ rows: [] },
|
{ rows: [] },
|
||||||
|
{ rows: [] },
|
||||||
{
|
{
|
||||||
rows: [
|
rows: [
|
||||||
{ Период: "2020-01-15T00:00:00", Регистратор: "Поступление 1" },
|
{ Период: "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_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_business_overview_trading_margin_query_mcp_executed");
|
||||||
expect(result.reason_codes).toContain("pilot_derived_business_overview_trading_margin_proxy_from_confirmed_rows");
|
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 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(taxCall?.query ?? "")).toContain("НДСЗаписиКнигиПокупок");
|
expect(String(taxCall?.query ?? "")).toContain("НДСЗаписиКнигиПокупок");
|
||||||
expect(String(tradingMarginCall?.query ?? "")).toContain("Документ.РеализацияТоваровУслуг.Товары");
|
expect(String(tradingMarginCall?.query ?? "")).toContain("Документ.РеализацияТоваровУслуг.Товары");
|
||||||
|
|
@ -453,6 +528,7 @@ describe("assistant MCP discovery pilot executor", () => {
|
||||||
{ rows: [] },
|
{ rows: [] },
|
||||||
{ rows: [] },
|
{ rows: [] },
|
||||||
{ rows: [] },
|
{ rows: [] },
|
||||||
|
{ rows: [] },
|
||||||
{
|
{
|
||||||
rows: [
|
rows: [
|
||||||
{ Period: "2020-03-01T00:00:00", Amount: 100000, Item: "Товар А", Counterparty: "Клиент А", AccountKt: "41.01" },
|
{ 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-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" }] },
|
||||||
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Поставщик А" }] },
|
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Поставщик А" }] },
|
||||||
{ rows: [] },
|
{ rows: [] },
|
||||||
|
{ rows: [] },
|
||||||
{
|
{
|
||||||
rows: [
|
rows: [
|
||||||
{ Period: "2020-12-31T00:00:00", Amount: 70000, Counterparty: "Клиент А" },
|
{ 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_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_age_signal_from_contract_dates");
|
||||||
expect(result.reason_codes).toContain("pilot_derived_business_overview_debt_staleness_risk_proxy_from_confirmed_rows");
|
expect(result.reason_codes).toContain("pilot_derived_business_overview_debt_staleness_risk_proxy_from_confirmed_rows");
|
||||||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(13);
|
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(14);
|
||||||
const receivablesCall = deps.executeAddressMcpQuery.mock.calls[3]?.[0];
|
const receivablesCall = deps.executeAddressMcpQuery.mock.calls[4]?.[0];
|
||||||
const payablesCall = deps.executeAddressMcpQuery.mock.calls[4]?.[0];
|
const payablesCall = deps.executeAddressMcpQuery.mock.calls[5]?.[0];
|
||||||
const openContractsCall = deps.executeAddressMcpQuery.mock.calls[5]?.[0];
|
const openContractsCall = deps.executeAddressMcpQuery.mock.calls[6]?.[0];
|
||||||
expect(String(receivablesCall?.query ?? "")).toContain("62");
|
expect(String(receivablesCall?.query ?? "")).toContain("62");
|
||||||
expect(String(payablesCall?.query ?? "")).toContain("60");
|
expect(String(payablesCall?.query ?? "")).toContain("60");
|
||||||
expect(String(openContractsCall?.query ?? "")).toContain("СуммаРазвернутыйОстатокКт");
|
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 () => {
|
it("adds a checked inventory-position family to business overview only as an as-of-date snapshot", async () => {
|
||||||
const planner = planAssistantMcpDiscovery({
|
const planner = planAssistantMcpDiscovery({
|
||||||
dataNeedGraph: {
|
dataNeedGraph: {
|
||||||
|
|
@ -650,6 +819,7 @@ describe("assistant MCP discovery pilot executor", () => {
|
||||||
{ rows: [] },
|
{ rows: [] },
|
||||||
{ rows: [] },
|
{ rows: [] },
|
||||||
{ rows: [] },
|
{ rows: [] },
|
||||||
|
{ rows: [] },
|
||||||
{
|
{
|
||||||
rows: [
|
rows: [
|
||||||
{ Period: "2020-12-31T00:00:00", Amount: 250000, Quantity: 10, Item: "Товар А" },
|
{ 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_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_turnover_proxy_from_confirmed_rows");
|
||||||
expect(result.reason_codes).toContain("pilot_derived_business_overview_inventory_staleness_risk_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);
|
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(14);
|
||||||
const inventoryCall = deps.executeAddressMcpQuery.mock.calls[6]?.[0];
|
const inventoryCall = deps.executeAddressMcpQuery.mock.calls[7]?.[0];
|
||||||
expect(inventoryCall?.account_scope).toContain("41.01");
|
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"
|
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`,
|
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
|
||||||
Amount: 10,
|
Amount: 10,
|
||||||
Counterparty: "SVK"
|
Counterparty: "SVK"
|
||||||
|
|
@ -1359,7 +1529,7 @@ describe("assistant MCP discovery pilot executor", () => {
|
||||||
unsupported_but_understood_family: "counterparty_payouts_or_outflow"
|
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`,
|
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
|
||||||
Amount: 10,
|
Amount: 10,
|
||||||
Counterparty: "SVK"
|
Counterparty: "SVK"
|
||||||
|
|
@ -1560,7 +1730,7 @@ describe("assistant MCP discovery pilot executor", () => {
|
||||||
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
|
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`,
|
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
|
||||||
Amount: 10,
|
Amount: 10,
|
||||||
Counterparty: "SVK"
|
Counterparty: "SVK"
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,177 @@ describe("assistant MCP discovery response candidate", () => {
|
||||||
expect(candidate.reply_text).not.toContain("primitive");
|
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", () => {
|
it("uses a compact direct answer for business-overview top year questions", () => {
|
||||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
entryPoint({
|
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("2015");
|
expect(candidate.reply_text).toContain("2015");
|
||||||
expect(candidate.reply_text).toContain("136 723 459,73 руб.");
|
expect(candidate.reply_text).toContain("136 723 459,73 руб.");
|
||||||
expect(candidate.reply_text).toContain("не полный бухгалтерский рейтинг доходности");
|
expect(candidate.reply_text).toContain("не полный бухгалтерский рейтинг доходности");
|
||||||
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("Что подтверждено:");
|
||||||
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("Складской срез");
|
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", () => {
|
it("mentions separate counterparty scope in company plus counterparty business summaries", () => {
|
||||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
entryPoint({
|
entryPoint({
|
||||||
|
|
@ -607,7 +850,7 @@ describe("assistant MCP discovery response candidate", () => {
|
||||||
requires_user_clarification: false,
|
requires_user_clarification: false,
|
||||||
answer_draft: {
|
answer_draft: {
|
||||||
answer_mode: "confirmed_with_bounded_inference",
|
answer_mode: "confirmed_with_bounded_inference",
|
||||||
headline: "По данным 1С найдены строки исходящих платежей/списаний; сумму можно называть только в рамках проверенного периода и найденных строк.",
|
headline: "По данным 1С найдены строки исходящих платежей/списаний; сумму можно называть только в рамках проверенного периода и найденных строк.",
|
||||||
confirmed_lines: ["1C supplier-payout rows were found for counterparty SVK"],
|
confirmed_lines: ["1C supplier-payout rows were found for counterparty SVK"],
|
||||||
inference_lines: [
|
inference_lines: [
|
||||||
"Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit"
|
"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,
|
requires_user_clarification: false,
|
||||||
answer_draft: {
|
answer_draft: {
|
||||||
answer_mode: "confirmed_with_bounded_inference",
|
answer_mode: "confirmed_with_bounded_inference",
|
||||||
headline: "По данным 1С найдены строки движений; ответ ограничен проверенным периодом и найденными строками.",
|
headline: "По данным 1С найдены строки движений; ответ ограничен проверенным периодом и найденными строками.",
|
||||||
confirmed_lines: ["1C movement rows were found for counterparty SVK"],
|
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"],
|
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"],
|
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("В 1С найдены строки движений по контрагенту SVK.");
|
||||||
expect(candidate.reply_text).toContain("Срез движений ограничен только подтвержденными строками движений");
|
expect(candidate.reply_text).toContain("Срез движений ограничен только подтвержденными строками движений");
|
||||||
expect(candidate.reply_text).toContain("Полный исторический срез движений вне проверенного периода этим поиском не подтвержден.");
|
expect(candidate.reply_text).toContain("Полный исторический срез движений вне проверенного периода этим поиском не подтвержден.");
|
||||||
expect(candidate.reply_text).not.toContain("1C movement rows were found");
|
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(
|
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"
|
"\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(
|
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).not.toContain("уперся в лимит строк MCP");
|
||||||
);
|
|
||||||
expect(candidate.reply_text).not.toContain(
|
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"
|
"\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",
|
answer_mode: "confirmed_with_bounded_inference",
|
||||||
headline: "Рейтинг по контрагентам построен по подтвержденным строкам 1С.",
|
headline: "Рейтинг по контрагентам построен по подтвержденным строкам 1С.",
|
||||||
confirmed_lines: [
|
confirmed_lines: [
|
||||||
"Больше всего денег принёс контрагент СБЕРБАНК, ПАО по организации ООО Альтернатива Плюс за период 2020: 12 792 194,31 руб. по 9 строкам с суммой."
|
"Крупнейший входящий денежный источник — СБЕРБАНК, ПАО по организации ООО Альтернатива Плюс за период 2020: 12 792 194,31 руб. по 9 строкам с суммой. Это банк/финансовая организация; без назначения платежа или договора не считаю это обычным клиентом или выручкой."
|
||||||
],
|
],
|
||||||
inference_lines: [
|
inference_lines: [
|
||||||
"Рейтинг по контрагентам по организации ООО Альтернатива Плюс за период 2020 рассчитан только по подтвержденным строкам 1С."
|
"Рейтинг по контрагентам по организации ООО Альтернатива Плюс за период 2020 рассчитан только по подтвержденным строкам 1С."
|
||||||
|
|
@ -639,6 +639,7 @@ describe("assistant MCP discovery response policy", () => {
|
||||||
expect(result.decision).toBe("apply_candidate");
|
expect(result.decision).toBe("apply_candidate");
|
||||||
expect(result.reply_text).toContain("ООО Альтернатива Плюс");
|
expect(result.reply_text).toContain("ООО Альтернатива Плюс");
|
||||||
expect(result.reply_text).toContain("2020");
|
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).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_aligned_factual_address_reply");
|
||||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_factual_address_continuation_target");
|
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", () => {
|
describe("assistant MCP discovery runtime bridge", () => {
|
||||||
it("composes planner, pilot executor, and answer draft without wiring the hot runtime", async () => {
|
it("composes planner, pilot executor, and answer draft without wiring the hot runtime", async () => {
|
||||||
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
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_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.alignment_status).toBe("selected_matches_top");
|
||||||
expect(result.loop_state.catalog_chain_template_alignment.selected_chain_matches_top).toBe(true);
|
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("planner_selected_chain_matches_catalog_top");
|
||||||
expect(result.reason_codes).toContain("runtime_bridge_loop_state_awaiting_clarification");
|
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 () => {
|
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: "СВК-А",
|
axis_value: "СВК-А",
|
||||||
total_amount: 2100
|
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("СВК-А");
|
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");
|
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 () => {
|
it("bridges selected-item inventory provenance templates through exact document evidence", async () => {
|
||||||
const deps = buildDeps([
|
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.pending_axes).toEqual(["organization", "period"]);
|
||||||
expect(result.loop_state.explicit_entity_candidates).toEqual([]);
|
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");
|
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", () => {
|
it("routes broad business evaluation into business overview discovery without metadata drift", () => {
|
||||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
userMessage:
|
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");
|
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", () => {
|
it("lets raw business-overview wording override stale exact turnover meaning", () => {
|
||||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
userMessage:
|
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.data_need_graph?.business_fact_family).toBe("business_overview");
|
||||||
expect(result.turn_meaning_ref).toMatchObject({
|
expect(result.turn_meaning_ref).toMatchObject({
|
||||||
asked_domain_family: "business_overview",
|
asked_domain_family: "business_overview",
|
||||||
asked_action_family: "broad_evaluation",
|
asked_action_family: "profit_margin_boundary",
|
||||||
explicit_organization_scope: orgName,
|
explicit_organization_scope: orgName,
|
||||||
explicit_date_scope: "2020",
|
explicit_date_scope: "2020",
|
||||||
unsupported_but_understood_family: "broad_business_evaluation",
|
unsupported_but_understood_family: "profit_margin_boundary",
|
||||||
stale_replay_forbidden: true
|
stale_replay_forbidden: true
|
||||||
});
|
});
|
||||||
expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_route_candidate");
|
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([]);
|
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", () => {
|
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 orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
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.data_need_graph?.business_fact_family).toBe("business_overview");
|
||||||
expect(result.turn_meaning_ref).toMatchObject({
|
expect(result.turn_meaning_ref).toMatchObject({
|
||||||
asked_domain_family: "business_overview",
|
asked_domain_family: "business_overview",
|
||||||
asked_action_family: "broad_evaluation",
|
asked_action_family: "debt_due_date_boundary",
|
||||||
explicit_organization_scope: orgName,
|
explicit_organization_scope: orgName,
|
||||||
explicit_date_scope: "2020",
|
explicit_date_scope: "2020",
|
||||||
unsupported_but_understood_family: "broad_business_evaluation",
|
unsupported_but_understood_family: "debt_due_date_boundary",
|
||||||
stale_replay_forbidden: true
|
stale_replay_forbidden: true
|
||||||
});
|
});
|
||||||
expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_route_candidate");
|
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.data_need_graph?.business_fact_family).toBe("business_overview");
|
||||||
expect(result.turn_meaning_ref).toMatchObject({
|
expect(result.turn_meaning_ref).toMatchObject({
|
||||||
asked_domain_family: "business_overview",
|
asked_domain_family: "business_overview",
|
||||||
asked_action_family: "broad_evaluation",
|
asked_action_family: "inventory_reserve_boundary",
|
||||||
explicit_organization_scope: orgName,
|
explicit_organization_scope: orgName,
|
||||||
explicit_date_scope: "2020",
|
explicit_date_scope: "2020",
|
||||||
unsupported_but_understood_family: "broad_business_evaluation",
|
unsupported_but_understood_family: "inventory_reserve_liquidation_boundary",
|
||||||
stale_replay_forbidden: true
|
stale_replay_forbidden: true
|
||||||
});
|
});
|
||||||
expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_route_candidate");
|
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.data_need_graph?.business_fact_family).toBe("business_overview");
|
||||||
expect(result.turn_meaning_ref).toMatchObject({
|
expect(result.turn_meaning_ref).toMatchObject({
|
||||||
asked_domain_family: "business_overview",
|
asked_domain_family: "business_overview",
|
||||||
asked_action_family: "broad_evaluation",
|
asked_action_family: "vendor_risk_procurement_boundary",
|
||||||
explicit_organization_scope: orgName,
|
explicit_organization_scope: orgName,
|
||||||
explicit_date_scope: "2020",
|
explicit_date_scope: "2020",
|
||||||
unsupported_but_understood_family: "broad_business_evaluation",
|
unsupported_but_understood_family: "vendor_risk_procurement_boundary",
|
||||||
stale_replay_forbidden: true
|
stale_replay_forbidden: true
|
||||||
});
|
});
|
||||||
expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_route_candidate");
|
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"]);
|
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", () => {
|
it("pivots a metadata-scoped subjectless movement retrieval into documents without inventing a counterparty", () => {
|
||||||
const orgName =
|
const orgName =
|
||||||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
"\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", () => {
|
it("lets raw metadata scope override stale document evidence subject on topic switch", () => {
|
||||||
const orgName =
|
const orgName =
|
||||||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
"\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");
|
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", () => {
|
it("keeps detailed money-breakdown follow-up in business overview without pseudo counterparty anchors", () => {
|
||||||
const orgName =
|
const orgName =
|
||||||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
"\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);
|
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", () => {
|
it("does not turn short entity follow-up into organization switch just because scope already has an active company", () => {
|
||||||
const policy = buildPolicy({
|
const policy = buildPolicy({
|
||||||
resolveAddressToolGateDecision: undefined,
|
resolveAddressToolGateDecision: undefined,
|
||||||
|
|
|
||||||
|
|
@ -1206,6 +1206,67 @@ describe("assistantTransitionPolicy", () => {
|
||||||
period_to: "2020-12-31"
|
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", () => {
|
it("carries resolved entity candidates from grounded entity-resolution discovery into followup context", () => {
|
||||||
const policy = buildPolicy({
|
const policy = buildPolicy({
|
||||||
findLastAddressAssistantItem: () => null,
|
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", () => {
|
it("carries grounded metadata downstream route hints into followup context", () => {
|
||||||
const policy = buildPolicy({
|
const policy = buildPolicy({
|
||||||
findLastAddressAssistantItem: () => null,
|
findLastAddressAssistantItem: () => null,
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,40 @@ describe("counterparty analytics reply builders", () => {
|
||||||
expect(reply.text).not.toContain("Топ-");
|
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", () => {
|
it("explains organization activity age as 1C activity rather than legal age", () => {
|
||||||
const reply = composeFactualReply(
|
const reply = composeFactualReply(
|
||||||
"counterparty_activity_lifecycle",
|
"counterparty_activity_lifecycle",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue