ДОМЕНЫ - ВОПРОСЫ - fix: переведен exact-маршрут кому должны на дату на Хозрасчетный.Остатки (без Движения.СубконтоДт1/Кт1)
This commit is contained in:
parent
278eb4abeb
commit
143cf6efe1
|
|
@ -298,12 +298,19 @@ const CONTRACT_USAGE_OVERVIEW_HINTS = [
|
|||
const CUSTOMER_REVENUE_AND_PAYMENTS_HINTS = [
|
||||
"самые доходные клиенты",
|
||||
"самые доходные заказчики",
|
||||
"самые ликвидные клиенты",
|
||||
"самые ликвидные заказчики",
|
||||
"самых ликвидних заказчиков",
|
||||
"топ клиентов по сумме поступлений",
|
||||
"топ заказчиков по сумме поступлений",
|
||||
"кто больше всего принес денег",
|
||||
"кто больше всего принёс денег",
|
||||
"кто принес больше всего денег",
|
||||
"кто принёс больше всего денег",
|
||||
"кто нам больше денег принес",
|
||||
"кто нам больше денег принёс",
|
||||
"кто нам принес больше денег",
|
||||
"кто нам принёс больше денег",
|
||||
"кто нам больше всего занес",
|
||||
"кто нам больше всего занёс",
|
||||
"кто нам принес больше всего",
|
||||
|
|
@ -690,12 +697,18 @@ function hasCustomerRevenueAndPaymentsSignal(text) {
|
|||
const asksCounterpartySource = /(?:с\s+каких|от\s+каких|от\s+кого|from\s+which|from\s+who)/iu.test(text);
|
||||
const asksIncomingFlow = /(?:приход|поступлен|входящ|зачислен|inflow|incoming)/iu.test(text);
|
||||
const asksWhoBringsMostMoney = /(?:кто\s+(?:нам\s+)?(?:больше\s+всего|сам(?:ый|ая|ое|ые)|наибольш(?:ий|ая|ее|ие))\s+(?:прин[её]с|зан[её]с).*(?:деньг|денег))/iu.test(text);
|
||||
const asksWhoBringsMoneyLoose = /(?:кто\s+(?:нам\s+)?(?:больше|больше\s+всех|больше\s+всего).*(?:деньг|денег|доход|выручк).*(?:прин[её]с|зан[её]с))/iu.test(text) ||
|
||||
/(?:кто\s+(?:нам\s+)?(?:прин[её]с|зан[её]с).*(?:больше|больше\s+всех|больше\s+всего).*(?:деньг|денег|доход|выручк))/iu.test(text);
|
||||
const asksLiquidityRanking = /(?:ликвидн|liquid)/iu.test(text) &&
|
||||
(asksCustomerGroup || hasCounterpartyLexeme || /(?:клиент|заказчик|контрагент|customer|client|counterpart)/iu.test(text));
|
||||
const asksProfitableYears = /(?:доходн|выручк|оборот|прибыл|revenue|turnover).*(?:год|года|годы|year|years)/iu.test(text) &&
|
||||
/(?:сам(?:ый|ая|ое|ые)|топ|луч|max|best|наибольш|больше)/iu.test(text);
|
||||
const asksDealBudgetRanking = /(?:сделк|deal|бюджет)/iu.test(text) &&
|
||||
/(?:топ|top|сам(?:ый|ая|ое|ые)|крупн|мален|жирн|мелк|больше\s+всего|чаще\s+всего|наибольш|максимальн|минимальн)/iu.test(text);
|
||||
const asksRevenueTotal = /(?:сколько|скока|скок).*(?:денег|выручк|доход|заработ|оборот)/iu.test(text);
|
||||
const asksOverallTurnover = /(?:общ(?:ий|ие|ая)\s+оборот|общ(?:ая|ий)\s+выручк|total\s+turnover|turnover\s+total)/iu.test(text);
|
||||
const asksMajorShare = /(?:основн(?:ую|ая|ые|ой)\s+част|больш(?:ую|ая|ие)\s+част|львин(?:ая|ую)\s+дол[яю]|ключев(?:ую|ая)\s+част)/iu.test(text);
|
||||
const asksValue = /(?:доходн|выручк|приход|поступлен|входящ|зачислен|оплат|плат(?:еж|ёж|ежн|ежей|ежа|ит|ят)|деньг|денег|заработ|оборот|чек|сделк|бюджет|занес|занёс|принес|принёс|revenue|inflow|deal|turnover)/iu.test(text);
|
||||
const asksValue = /(?:доходн|выручк|приход|поступлен|входящ|зачислен|оплат|плат(?:еж|ёж|ежн|ежей|ежа|ит|ят)|деньг|денег|заработ|оборот|чек|сделк|бюджет|занес|занёс|принес|принёс|ликвидн|revenue|inflow|deal|turnover|liquid)/iu.test(text);
|
||||
const asksRankOrTop = /(?:топ|top|сам(?:ый|ая|ое|ые)|крупн|мален|жирн|мелк|больше\s+всего|чаще\s+всего|наибольш|максимальн)/iu.test(text);
|
||||
const asksCountOnly = /(?:сколько|скока|скок)\s+/iu.test(text) && !asksValue;
|
||||
if (asksCountOnly) {
|
||||
|
|
@ -716,6 +729,15 @@ function hasCustomerRevenueAndPaymentsSignal(text) {
|
|||
if (!hasFuzzySupplierLexeme && asksWhoBringsMostMoney) {
|
||||
return true;
|
||||
}
|
||||
if (!hasFuzzySupplierLexeme && asksWhoBringsMoneyLoose) {
|
||||
return true;
|
||||
}
|
||||
if (!hasFuzzySupplierLexeme && asksLiquidityRanking) {
|
||||
return true;
|
||||
}
|
||||
if (!hasFuzzySupplierLexeme && asksProfitableYears) {
|
||||
return true;
|
||||
}
|
||||
if (!hasFuzzySupplierLexeme && (asksRevenueTotal || asksOverallTurnover)) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -859,10 +859,61 @@ function isMissingSubcontoFieldError(errorText) {
|
|||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return (normalized.includes("поле не найдено") &&
|
||||
(normalized.includes("субконтодт1") ||
|
||||
normalized.includes("subcontodt1") ||
|
||||
normalized.includes("subconto_dt1")));
|
||||
const ruMissingField = "\u043f\u043e\u043b\u0435 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e";
|
||||
const hasMissingFieldSignal = normalized.includes(ruMissingField) || normalized.includes("field not found");
|
||||
if (!hasMissingFieldSignal) {
|
||||
return false;
|
||||
}
|
||||
const hasAnySubcontoSignal = /(?:\u0441\u0443\u0431\u043a\u043e\u043d\u0442\u043e(?:\u0434\u0442|\u043a\u0442)?\d*|subconto(?:_)?(?:dt|kt)?\d*)/iu.test(normalized) ||
|
||||
normalized.includes("subcontodt") ||
|
||||
normalized.includes("subcontokt");
|
||||
return hasAnySubcontoSignal;
|
||||
}
|
||||
function buildCompositeSubcontoFallbackQuery(queryText) {
|
||||
const source = String(queryText ?? "");
|
||||
if (!source.trim()) {
|
||||
return null;
|
||||
}
|
||||
const dt1Pattern = /ПРЕДСТАВЛЕНИЕ\(\s*Движения\.СубконтоДт1\s*\)\s+КАК\s+СубконтоДт1\s*,?/iu;
|
||||
const dt2Pattern = /ПРЕДСТАВЛЕНИЕ\(\s*Движения\.СубконтоДт2\s*\)\s+КАК\s+СубконтоДт2\s*,?/iu;
|
||||
const dt3Pattern = /ПРЕДСТАВЛЕНИЕ\(\s*Движения\.СубконтоДт3\s*\)\s+КАК\s+СубконтоДт3\s*,?/iu;
|
||||
const kt1Pattern = /ПРЕДСТАВЛЕНИЕ\(\s*Движения\.СубконтоКт1\s*\)\s+КАК\s+СубконтоКт1\s*,?/iu;
|
||||
const kt2Pattern = /ПРЕДСТАВЛЕНИЕ\(\s*Движения\.СубконтоКт2\s*\)\s+КАК\s+СубконтоКт2\s*,?/iu;
|
||||
const kt3Pattern = /ПРЕДСТАВЛЕНИЕ\(\s*Движения\.СубконтоКт3\s*\)\s+КАК\s+СубконтоКт3\s*,?/iu;
|
||||
const lines = source.split(/\r?\n/);
|
||||
let replaced = false;
|
||||
const rewrittenLines = lines.map((line) => {
|
||||
const indent = line.match(/^\s*/)?.[0] ?? "";
|
||||
if (dt1Pattern.test(line)) {
|
||||
replaced = true;
|
||||
return `${indent}ПРЕДСТАВЛЕНИЕ(Движения.СубконтоДт) КАК СубконтоДт1,`;
|
||||
}
|
||||
if (dt2Pattern.test(line)) {
|
||||
replaced = true;
|
||||
return `${indent}"" КАК СубконтоДт2,`;
|
||||
}
|
||||
if (dt3Pattern.test(line)) {
|
||||
replaced = true;
|
||||
return `${indent}"" КАК СубконтоДт3,`;
|
||||
}
|
||||
if (kt1Pattern.test(line)) {
|
||||
replaced = true;
|
||||
return `${indent}ПРЕДСТАВЛЕНИЕ(Движения.СубконтоКт) КАК СубконтоКт1,`;
|
||||
}
|
||||
if (kt2Pattern.test(line)) {
|
||||
replaced = true;
|
||||
return `${indent}"" КАК СубконтоКт2,`;
|
||||
}
|
||||
if (kt3Pattern.test(line)) {
|
||||
replaced = true;
|
||||
return `${indent}"" КАК СубконтоКт3,`;
|
||||
}
|
||||
return line;
|
||||
});
|
||||
if (!replaced) {
|
||||
return null;
|
||||
}
|
||||
return rewrittenLines.join("\n");
|
||||
}
|
||||
function applyFutureDatedRowsGuard(rows, intent, referenceDate) {
|
||||
if (!isCounterpartyRiskIntent(intent) || rows.length === 0) {
|
||||
|
|
@ -1729,55 +1780,90 @@ class AddressQueryService {
|
|||
}
|
||||
}
|
||||
let effectiveRecipeId = recipeSelection.selected_recipe.recipe_id;
|
||||
let composeIntent = intent.intent;
|
||||
let routeExpectationIntent = intent.intent;
|
||||
let plan = enforceStrictAccountScopeForIntent((0, addressRecipeCatalog_1.buildAddressRecipePlan)(recipeSelection.selected_recipe, executionFilters), intent.intent);
|
||||
let mcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({
|
||||
query: plan.query,
|
||||
limit: plan.limit
|
||||
});
|
||||
const allowOpenItemsFallbackForMissingSubconto = intent.intent !== "payables_confirmed_as_of_date";
|
||||
if (mcp.error &&
|
||||
(plan.recipe.recipe_id === "address_movements_receivables_v1" ||
|
||||
plan.recipe.recipe_id === "address_movements_payables_v1") &&
|
||||
isMissingSubcontoFieldError(mcp.error) &&
|
||||
allowOpenItemsFallbackForMissingSubconto) {
|
||||
const fallbackSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)("open_items_by_counterparty_or_contract", executionFilters);
|
||||
if (fallbackSelection.selected_recipe && fallbackSelection.missing_required_filters.length === 0) {
|
||||
const fallbackPlan = enforceStrictAccountScopeForIntent((0, addressRecipeCatalog_1.buildAddressRecipePlan)(fallbackSelection.selected_recipe, executionFilters), intent.intent);
|
||||
const fallbackMcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({
|
||||
query: fallbackPlan.query,
|
||||
limit: fallbackPlan.limit
|
||||
const missingSubcontoFallbackEligible = plan.recipe.recipe_id === "address_movements_receivables_v1" ||
|
||||
plan.recipe.recipe_id === "address_movements_payables_v1" ||
|
||||
plan.recipe.recipe_id === "address_payables_confirmed_as_of_date_v1";
|
||||
const missingSubcontoErrorDetected = Boolean(mcp.error && missingSubcontoFallbackEligible && isMissingSubcontoFieldError(mcp.error));
|
||||
if (missingSubcontoErrorDetected) {
|
||||
let missingSubcontoResolvedByComposite = false;
|
||||
const compositeSubcontoQuery = buildCompositeSubcontoFallbackQuery(plan.query);
|
||||
if (compositeSubcontoQuery) {
|
||||
const compositeMcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({
|
||||
query: compositeSubcontoQuery,
|
||||
limit: plan.limit
|
||||
});
|
||||
if (!fallbackMcp.error) {
|
||||
plan = fallbackPlan;
|
||||
mcp = fallbackMcp;
|
||||
if (intent.intent === "list_payables_counterparties") {
|
||||
if (!compositeMcp.error) {
|
||||
plan = {
|
||||
...plan,
|
||||
query: compositeSubcontoQuery
|
||||
};
|
||||
mcp = compositeMcp;
|
||||
missingSubcontoResolvedByComposite = true;
|
||||
if (!baseReasons.includes("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto")) {
|
||||
baseReasons.push("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto");
|
||||
}
|
||||
if (intent.intent === "payables_confirmed_as_of_date") {
|
||||
if (!baseReasons.includes("confirmed_payables_exact_mode_missing_subconto_axis_fallback_to_composite_subconto")) {
|
||||
baseReasons.push("confirmed_payables_exact_mode_missing_subconto_axis_fallback_to_composite_subconto");
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!baseReasons.includes("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto_failed")) {
|
||||
baseReasons.push("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto_failed");
|
||||
}
|
||||
}
|
||||
else if (!baseReasons.includes("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto_unavailable")) {
|
||||
baseReasons.push("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto_unavailable");
|
||||
}
|
||||
if (!missingSubcontoResolvedByComposite) {
|
||||
const fallbackSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)("open_items_by_counterparty_or_contract", executionFilters);
|
||||
if (fallbackSelection.selected_recipe && fallbackSelection.missing_required_filters.length === 0) {
|
||||
const fallbackPlan = enforceStrictAccountScopeForIntent((0, addressRecipeCatalog_1.buildAddressRecipePlan)(fallbackSelection.selected_recipe, executionFilters), intent.intent);
|
||||
const fallbackMcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({
|
||||
query: fallbackPlan.query,
|
||||
limit: fallbackPlan.limit
|
||||
});
|
||||
if (!fallbackMcp.error) {
|
||||
plan = fallbackPlan;
|
||||
mcp = fallbackMcp;
|
||||
effectiveRecipeId = fallbackSelection.selected_recipe.recipe_id;
|
||||
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_to_open_items")) {
|
||||
baseReasons.push("mcp_missing_subconto_field_auto_fallback_to_open_items");
|
||||
}
|
||||
if (!baseReasons.includes("fallback_recipe_switched_to_open_items")) {
|
||||
baseReasons.push("fallback_recipe_switched_to_open_items");
|
||||
}
|
||||
if (intent.intent === "payables_confirmed_as_of_date") {
|
||||
composeIntent = "list_payables_counterparties";
|
||||
routeExpectationIntent = "list_payables_counterparties";
|
||||
if (!baseReasons.includes("confirmed_payables_exact_mode_missing_subconto_fallback_to_open_items")) {
|
||||
baseReasons.push("confirmed_payables_exact_mode_missing_subconto_fallback_to_open_items");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_to_open_items")) {
|
||||
baseReasons.push("mcp_missing_subconto_field_auto_fallback_to_open_items");
|
||||
}
|
||||
if (intent.intent === "list_payables_counterparties" &&
|
||||
!baseReasons.includes("fallback_recipe_switched_to_open_items")) {
|
||||
baseReasons.push("fallback_recipe_switched_to_open_items");
|
||||
else {
|
||||
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_failed")) {
|
||||
baseReasons.push("mcp_missing_subconto_field_auto_fallback_failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_failed")) {
|
||||
baseReasons.push("mcp_missing_subconto_field_auto_fallback_failed");
|
||||
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_unavailable")) {
|
||||
baseReasons.push("mcp_missing_subconto_field_auto_fallback_unavailable");
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_unavailable")) {
|
||||
baseReasons.push("mcp_missing_subconto_field_auto_fallback_unavailable");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mcp.error &&
|
||||
(plan.recipe.recipe_id === "address_movements_receivables_v1" ||
|
||||
plan.recipe.recipe_id === "address_movements_payables_v1") &&
|
||||
missingSubcontoFallbackEligible &&
|
||||
isMissingSubcontoFieldError(mcp.error) &&
|
||||
!allowOpenItemsFallbackForMissingSubconto &&
|
||||
!baseReasons.includes("confirmed_payables_exact_mode_missing_subconto_no_heuristic_fallback")) {
|
||||
baseReasons.push("confirmed_payables_exact_mode_missing_subconto_no_heuristic_fallback");
|
||||
}
|
||||
|
|
@ -2480,16 +2566,16 @@ class AddressQueryService {
|
|||
shadowRouteAudit
|
||||
});
|
||||
}
|
||||
const factual = (0, composeStage_1.composeFactualReply)(intent.intent, filteredRows, composeOptionsFromFilters(executionFilters));
|
||||
const factual = (0, composeStage_1.composeFactualReply)(composeIntent, filteredRows, composeOptionsFromFilters(executionFilters));
|
||||
const factualResultSemantics = mergeAddressResultSemantics(deriveAddressResultSemantics({
|
||||
intent: intent.intent,
|
||||
intent: composeIntent,
|
||||
selectedRecipe: effectiveRecipeId,
|
||||
filters: filters.extracted_filters,
|
||||
responseType: factual.responseType,
|
||||
rowsMatched: filteredRows.length
|
||||
}), factual.semantics);
|
||||
const finalRouteExpectationAudit = buildRouteExpectationAudit({
|
||||
intent: intent.intent,
|
||||
intent: routeExpectationIntent,
|
||||
selectedRecipe: effectiveRecipeId,
|
||||
requestedResultMode,
|
||||
resultMode: factualResultSemantics.result_mode
|
||||
|
|
@ -2527,7 +2613,7 @@ class AddressQueryService {
|
|||
routeExpectationAudit: finalRouteExpectationAudit
|
||||
});
|
||||
}
|
||||
if (intent.intent === "payables_confirmed_as_of_date" && factualResultSemantics.balance_confirmed !== true) {
|
||||
if (intent.intent === "payables_confirmed_as_of_date" && composeIntent === "payables_confirmed_as_of_date" && factualResultSemantics.balance_confirmed !== true) {
|
||||
return buildLimitedExecutionResult({
|
||||
mode,
|
||||
shape,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,28 @@ __WHERE_CLAUSE__
|
|||
УПОРЯДОЧИТЬ ПО
|
||||
Движения.Период __ORDER_DIRECTION__
|
||||
`;
|
||||
const PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
__AS_OF_EXPR__ КАК Период,
|
||||
"Остатки на дату" КАК Регистратор,
|
||||
"" КАК СчетДт,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетКт,
|
||||
Остатки.СуммаРазвернутыйОстатокКт КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоДт1,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоДт2,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоДт3,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоКт1,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоКт2,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоКт3,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация
|
||||
ИЗ
|
||||
РегистрБухгалтерии.Хозрасчетный.Остатки(__AS_OF_EXPR__, , , ) КАК Остатки
|
||||
ГДЕ
|
||||
Остатки.СуммаРазвернутыйОстатокКт > 0
|
||||
И (__PAYABLE_ACCOUNTS_MATCH__)
|
||||
УПОРЯДОЧИТЬ ПО
|
||||
Сумма __ORDER_DIRECTION__
|
||||
`;
|
||||
const BANK_DOCS_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
БанкСписание.Дата КАК Период,
|
||||
|
|
@ -533,7 +555,8 @@ const BASE_RECIPES = [
|
|||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
||||
default_limit: 200,
|
||||
account_scope: ["60", "76"],
|
||||
account_scope_mode: "strict"
|
||||
account_scope_mode: "strict",
|
||||
query_template: "payables_confirmed_as_of_balance_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_movements_receivables_v1",
|
||||
|
|
@ -906,17 +929,35 @@ function buildAddressRecipePlan(recipe, filters) {
|
|||
.replaceAll("__VAT19_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", config_1.VAT_PAYABLE_19_PREFIXES))
|
||||
: recipe.query_template === "contracts_by_counterparty_profile"
|
||||
? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
: 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));
|
||||
: 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));
|
||||
})()
|
||||
: 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 {
|
||||
recipe,
|
||||
query,
|
||||
|
|
|
|||
|
|
@ -380,6 +380,12 @@ function detectValueRankingFocus(userMessage) {
|
|||
if (!text) {
|
||||
return "top_by_total";
|
||||
}
|
||||
const asksYearlyRevenueRanking = /(?:доходн|выручк|оборот|прибыл|деньг|денег|revenue|turnover|income)/iu.test(text) &&
|
||||
/(?:год|года|годы|year|years|по\s+годам)/iu.test(text) &&
|
||||
/(?:сам(?:ый|ая|ое|ые)|топ|луч|best|max|наибольш|больше)/iu.test(text);
|
||||
if (asksYearlyRevenueRanking) {
|
||||
return "top_years_by_total";
|
||||
}
|
||||
if (/(?:сам(?:ый|ая|ое|ые)\s+высок[а-яё]*|highest|largest)\s+чек|(?:max\s+check|чек\s+макс)/iu.test(text)) {
|
||||
return "top_by_max_single";
|
||||
}
|
||||
|
|
@ -1420,6 +1426,7 @@ function composeFactualReply(intent, rows, options = {}) {
|
|||
const minOpsForAvgCheck = detectMinOpsForAvgCheck(options.userMessage);
|
||||
const normalizedQuestion = normalizeQuestionText(options.userMessage);
|
||||
const byCounterparty = new Map();
|
||||
const byYear = new Map();
|
||||
const deals = [];
|
||||
for (const row of rows) {
|
||||
const counterparty = extractCounterpartyName(row);
|
||||
|
|
@ -1453,9 +1460,30 @@ function composeFactualReply(intent, rows, options = {}) {
|
|||
counterparty,
|
||||
amount
|
||||
});
|
||||
const year = extractYearFromIso(row.period);
|
||||
if (year !== null) {
|
||||
const yearBucket = byYear.get(year);
|
||||
if (!yearBucket) {
|
||||
byYear.set(year, {
|
||||
year,
|
||||
total: amount,
|
||||
ops: 1,
|
||||
maxSingle: amount,
|
||||
counterparties: new Set([counterparty])
|
||||
});
|
||||
}
|
||||
else {
|
||||
yearBucket.total += amount;
|
||||
yearBucket.ops += 1;
|
||||
yearBucket.maxSingle = Math.max(yearBucket.maxSingle, amount);
|
||||
yearBucket.counterparties.add(counterparty);
|
||||
}
|
||||
}
|
||||
}
|
||||
const profileRows = Array.from(byCounterparty.values());
|
||||
const yearRows = Array.from(byYear.values());
|
||||
const rankedByTotal = [...profileRows].sort((a, b) => b.total - a.total || b.ops - a.ops || a.name.localeCompare(b.name));
|
||||
const rankedByYearTotal = [...yearRows].sort((a, b) => b.total - a.total || b.ops - a.ops || a.year - b.year);
|
||||
const rankedByOps = [...profileRows].sort((a, b) => b.ops - a.ops || b.total - a.total || a.name.localeCompare(b.name));
|
||||
const rankedByMaxSingle = [...profileRows].sort((a, b) => b.maxSingle - a.maxSingle || b.total - a.total || a.name.localeCompare(b.name));
|
||||
const rankedByAvgCheck = [...profileRows]
|
||||
|
|
@ -1485,6 +1513,23 @@ function composeFactualReply(intent, rows, options = {}) {
|
|||
text: lines.join("\n")
|
||||
};
|
||||
}
|
||||
if (focus === "top_years_by_total") {
|
||||
const visible = rankedByYearTotal.slice(0, limit);
|
||||
const heading = isSupplier
|
||||
? `Топ-${visible.length} лет по сумме выплат:`
|
||||
: `Топ-${visible.length} лет по сумме поступлений:`;
|
||||
lines.unshift(heading);
|
||||
if (visible.length === 0) {
|
||||
lines.push("По доступному окну не удалось собрать годовые агрегаты по суммам.");
|
||||
}
|
||||
else {
|
||||
lines.push(...visible.map((item, index) => `${index + 1}. ${item.year} | сумма: ${item.total} | операций: ${item.ops} | контрагентов: ${item.counterparties.size} | макс: ${item.maxSingle}`));
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: lines.join("\n")
|
||||
};
|
||||
}
|
||||
if (focus === "top_by_ops") {
|
||||
const visible = rankedByOps.slice(0, limit);
|
||||
const heading = isSupplier
|
||||
|
|
|
|||
|
|
@ -3706,6 +3706,12 @@ function hasOpenContractsAddressSignal(text) {
|
|||
return hasRequestCue || hasTemporalCue;
|
||||
}
|
||||
const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
|
||||
"period_coverage_profile",
|
||||
"document_type_and_account_section_profile",
|
||||
"counterparty_population_and_roles",
|
||||
"counterparty_activity_lifecycle",
|
||||
"customer_revenue_and_payments",
|
||||
"supplier_payouts_profile",
|
||||
"list_open_contracts",
|
||||
"open_items_by_counterparty_or_contract",
|
||||
"payables_confirmed_as_of_date",
|
||||
|
|
|
|||
|
|
@ -311,12 +311,19 @@ const CONTRACT_USAGE_OVERVIEW_HINTS = [
|
|||
const CUSTOMER_REVENUE_AND_PAYMENTS_HINTS = [
|
||||
"самые доходные клиенты",
|
||||
"самые доходные заказчики",
|
||||
"самые ликвидные клиенты",
|
||||
"самые ликвидные заказчики",
|
||||
"самых ликвидних заказчиков",
|
||||
"топ клиентов по сумме поступлений",
|
||||
"топ заказчиков по сумме поступлений",
|
||||
"кто больше всего принес денег",
|
||||
"кто больше всего принёс денег",
|
||||
"кто принес больше всего денег",
|
||||
"кто принёс больше всего денег",
|
||||
"кто нам больше денег принес",
|
||||
"кто нам больше денег принёс",
|
||||
"кто нам принес больше денег",
|
||||
"кто нам принёс больше денег",
|
||||
"кто нам больше всего занес",
|
||||
"кто нам больше всего занёс",
|
||||
"кто нам принес больше всего",
|
||||
|
|
@ -790,6 +797,19 @@ function hasCustomerRevenueAndPaymentsSignal(text: string): boolean {
|
|||
/(?:кто\s+(?:нам\s+)?(?:больше\s+всего|сам(?:ый|ая|ое|ые)|наибольш(?:ий|ая|ее|ие))\s+(?:прин[её]с|зан[её]с).*(?:деньг|денег))/iu.test(
|
||||
text
|
||||
);
|
||||
const asksWhoBringsMoneyLoose =
|
||||
/(?:кто\s+(?:нам\s+)?(?:больше|больше\s+всех|больше\s+всего).*(?:деньг|денег|доход|выручк).*(?:прин[её]с|зан[её]с))/iu.test(
|
||||
text
|
||||
) ||
|
||||
/(?:кто\s+(?:нам\s+)?(?:прин[её]с|зан[её]с).*(?:больше|больше\s+всех|больше\s+всего).*(?:деньг|денег|доход|выручк))/iu.test(
|
||||
text
|
||||
);
|
||||
const asksLiquidityRanking =
|
||||
/(?:ликвидн|liquid)/iu.test(text) &&
|
||||
(asksCustomerGroup || hasCounterpartyLexeme || /(?:клиент|заказчик|контрагент|customer|client|counterpart)/iu.test(text));
|
||||
const asksProfitableYears =
|
||||
/(?:доходн|выручк|оборот|прибыл|revenue|turnover).*(?:год|года|годы|year|years)/iu.test(text) &&
|
||||
/(?:сам(?:ый|ая|ое|ые)|топ|луч|max|best|наибольш|больше)/iu.test(text);
|
||||
const asksDealBudgetRanking =
|
||||
/(?:сделк|deal|бюджет)/iu.test(text) &&
|
||||
/(?:топ|top|сам(?:ый|ая|ое|ые)|крупн|мален|жирн|мелк|больше\s+всего|чаще\s+всего|наибольш|максимальн|минимальн)/iu.test(
|
||||
|
|
@ -802,7 +822,7 @@ function hasCustomerRevenueAndPaymentsSignal(text: string): boolean {
|
|||
text
|
||||
);
|
||||
const asksValue =
|
||||
/(?:доходн|выручк|приход|поступлен|входящ|зачислен|оплат|плат(?:еж|ёж|ежн|ежей|ежа|ит|ят)|деньг|денег|заработ|оборот|чек|сделк|бюджет|занес|занёс|принес|принёс|revenue|inflow|deal|turnover)/iu.test(
|
||||
/(?:доходн|выручк|приход|поступлен|входящ|зачислен|оплат|плат(?:еж|ёж|ежн|ежей|ежа|ит|ят)|деньг|денег|заработ|оборот|чек|сделк|бюджет|занес|занёс|принес|принёс|ликвидн|revenue|inflow|deal|turnover|liquid)/iu.test(
|
||||
text
|
||||
);
|
||||
const asksRankOrTop = /(?:топ|top|сам(?:ый|ая|ое|ые)|крупн|мален|жирн|мелк|больше\s+всего|чаще\s+всего|наибольш|максимальн)/iu.test(
|
||||
|
|
@ -827,6 +847,15 @@ function hasCustomerRevenueAndPaymentsSignal(text: string): boolean {
|
|||
if (!hasFuzzySupplierLexeme && asksWhoBringsMostMoney) {
|
||||
return true;
|
||||
}
|
||||
if (!hasFuzzySupplierLexeme && asksWhoBringsMoneyLoose) {
|
||||
return true;
|
||||
}
|
||||
if (!hasFuzzySupplierLexeme && asksLiquidityRanking) {
|
||||
return true;
|
||||
}
|
||||
if (!hasFuzzySupplierLexeme && asksProfitableYears) {
|
||||
return true;
|
||||
}
|
||||
if (!hasFuzzySupplierLexeme && (asksRevenueTotal || asksOverallTurnover)) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1073,14 +1073,72 @@ function isMissingSubcontoFieldError(errorText: string | null | undefined): bool
|
|||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
normalized.includes("поле не найдено") &&
|
||||
(normalized.includes("субконтодт1") ||
|
||||
normalized.includes("subcontodt1") ||
|
||||
normalized.includes("subconto_dt1"))
|
||||
);
|
||||
const ruMissingField = "\u043f\u043e\u043b\u0435 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e";
|
||||
const hasMissingFieldSignal = normalized.includes(ruMissingField) || normalized.includes("field not found");
|
||||
if (!hasMissingFieldSignal) {
|
||||
return false;
|
||||
}
|
||||
const hasAnySubcontoSignal =
|
||||
/(?:\u0441\u0443\u0431\u043a\u043e\u043d\u0442\u043e(?:\u0434\u0442|\u043a\u0442)?\d*|subconto(?:_)?(?:dt|kt)?\d*)/iu.test(
|
||||
normalized
|
||||
) ||
|
||||
normalized.includes("subcontodt") ||
|
||||
normalized.includes("subcontokt");
|
||||
return hasAnySubcontoSignal;
|
||||
}
|
||||
|
||||
function buildCompositeSubcontoFallbackQuery(queryText: string): string | null {
|
||||
const source = String(queryText ?? "");
|
||||
if (!source.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dt1Pattern = /ПРЕДСТАВЛЕНИЕ\(\s*Движения\.СубконтоДт1\s*\)\s+КАК\s+СубконтоДт1\s*,?/iu;
|
||||
const dt2Pattern = /ПРЕДСТАВЛЕНИЕ\(\s*Движения\.СубконтоДт2\s*\)\s+КАК\s+СубконтоДт2\s*,?/iu;
|
||||
const dt3Pattern = /ПРЕДСТАВЛЕНИЕ\(\s*Движения\.СубконтоДт3\s*\)\s+КАК\s+СубконтоДт3\s*,?/iu;
|
||||
const kt1Pattern = /ПРЕДСТАВЛЕНИЕ\(\s*Движения\.СубконтоКт1\s*\)\s+КАК\s+СубконтоКт1\s*,?/iu;
|
||||
const kt2Pattern = /ПРЕДСТАВЛЕНИЕ\(\s*Движения\.СубконтоКт2\s*\)\s+КАК\s+СубконтоКт2\s*,?/iu;
|
||||
const kt3Pattern = /ПРЕДСТАВЛЕНИЕ\(\s*Движения\.СубконтоКт3\s*\)\s+КАК\s+СубконтоКт3\s*,?/iu;
|
||||
|
||||
const lines = source.split(/\r?\n/);
|
||||
let replaced = false;
|
||||
const rewrittenLines = lines.map((line) => {
|
||||
const indent = line.match(/^\s*/)?.[0] ?? "";
|
||||
if (dt1Pattern.test(line)) {
|
||||
replaced = true;
|
||||
return `${indent}ПРЕДСТАВЛЕНИЕ(Движения.СубконтоДт) КАК СубконтоДт1,`;
|
||||
}
|
||||
if (dt2Pattern.test(line)) {
|
||||
replaced = true;
|
||||
return `${indent}"" КАК СубконтоДт2,`;
|
||||
}
|
||||
if (dt3Pattern.test(line)) {
|
||||
replaced = true;
|
||||
return `${indent}"" КАК СубконтоДт3,`;
|
||||
}
|
||||
if (kt1Pattern.test(line)) {
|
||||
replaced = true;
|
||||
return `${indent}ПРЕДСТАВЛЕНИЕ(Движения.СубконтоКт) КАК СубконтоКт1,`;
|
||||
}
|
||||
if (kt2Pattern.test(line)) {
|
||||
replaced = true;
|
||||
return `${indent}"" КАК СубконтоКт2,`;
|
||||
}
|
||||
if (kt3Pattern.test(line)) {
|
||||
replaced = true;
|
||||
return `${indent}"" КАК СубконтоКт3,`;
|
||||
}
|
||||
return line;
|
||||
});
|
||||
|
||||
if (!replaced) {
|
||||
return null;
|
||||
}
|
||||
return rewrittenLines.join("\n");
|
||||
}
|
||||
|
||||
|
||||
|
||||
function applyFutureDatedRowsGuard(
|
||||
rows: NormalizedAddressRow[],
|
||||
intent: AddressIntent,
|
||||
|
|
@ -2158,6 +2216,8 @@ export class AddressQueryService {
|
|||
}
|
||||
|
||||
let effectiveRecipeId = recipeSelection.selected_recipe.recipe_id;
|
||||
let composeIntent: AddressIntent = intent.intent;
|
||||
let routeExpectationIntent: AddressIntent = intent.intent;
|
||||
let plan = enforceStrictAccountScopeForIntent(
|
||||
buildAddressRecipePlan(recipeSelection.selected_recipe, executionFilters),
|
||||
intent.intent
|
||||
|
|
@ -2166,56 +2226,87 @@ export class AddressQueryService {
|
|||
query: plan.query,
|
||||
limit: plan.limit
|
||||
});
|
||||
const allowOpenItemsFallbackForMissingSubconto = intent.intent !== "payables_confirmed_as_of_date";
|
||||
if (
|
||||
mcp.error &&
|
||||
(plan.recipe.recipe_id === "address_movements_receivables_v1" ||
|
||||
plan.recipe.recipe_id === "address_movements_payables_v1") &&
|
||||
isMissingSubcontoFieldError(mcp.error) &&
|
||||
allowOpenItemsFallbackForMissingSubconto
|
||||
) {
|
||||
const fallbackSelection = selectAddressRecipe("open_items_by_counterparty_or_contract", executionFilters);
|
||||
if (fallbackSelection.selected_recipe && fallbackSelection.missing_required_filters.length === 0) {
|
||||
const fallbackPlan = enforceStrictAccountScopeForIntent(
|
||||
buildAddressRecipePlan(fallbackSelection.selected_recipe, executionFilters),
|
||||
intent.intent
|
||||
);
|
||||
const fallbackMcp = await executeAddressMcpQuery({
|
||||
query: fallbackPlan.query,
|
||||
limit: fallbackPlan.limit
|
||||
const missingSubcontoFallbackEligible =
|
||||
plan.recipe.recipe_id === "address_movements_receivables_v1" ||
|
||||
plan.recipe.recipe_id === "address_movements_payables_v1" ||
|
||||
plan.recipe.recipe_id === "address_payables_confirmed_as_of_date_v1";
|
||||
const missingSubcontoErrorDetected = Boolean(
|
||||
mcp.error && missingSubcontoFallbackEligible && isMissingSubcontoFieldError(mcp.error)
|
||||
);
|
||||
if (missingSubcontoErrorDetected) {
|
||||
let missingSubcontoResolvedByComposite = false;
|
||||
const compositeSubcontoQuery = buildCompositeSubcontoFallbackQuery(plan.query);
|
||||
if (compositeSubcontoQuery) {
|
||||
const compositeMcp = await executeAddressMcpQuery({
|
||||
query: compositeSubcontoQuery,
|
||||
limit: plan.limit
|
||||
});
|
||||
if (!fallbackMcp.error) {
|
||||
plan = fallbackPlan;
|
||||
mcp = fallbackMcp;
|
||||
if (intent.intent === "list_payables_counterparties") {
|
||||
if (!compositeMcp.error) {
|
||||
plan = {
|
||||
...plan,
|
||||
query: compositeSubcontoQuery
|
||||
};
|
||||
mcp = compositeMcp;
|
||||
missingSubcontoResolvedByComposite = true;
|
||||
if (!baseReasons.includes("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto")) {
|
||||
baseReasons.push("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto");
|
||||
}
|
||||
if (intent.intent === "payables_confirmed_as_of_date") {
|
||||
if (!baseReasons.includes("confirmed_payables_exact_mode_missing_subconto_axis_fallback_to_composite_subconto")) {
|
||||
baseReasons.push("confirmed_payables_exact_mode_missing_subconto_axis_fallback_to_composite_subconto");
|
||||
}
|
||||
}
|
||||
} else if (!baseReasons.includes("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto_failed")) {
|
||||
baseReasons.push("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto_failed");
|
||||
}
|
||||
} else if (!baseReasons.includes("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto_unavailable")) {
|
||||
baseReasons.push("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto_unavailable");
|
||||
}
|
||||
|
||||
if (!missingSubcontoResolvedByComposite) {
|
||||
const fallbackSelection = selectAddressRecipe("open_items_by_counterparty_or_contract", executionFilters);
|
||||
if (fallbackSelection.selected_recipe && fallbackSelection.missing_required_filters.length === 0) {
|
||||
const fallbackPlan = enforceStrictAccountScopeForIntent(
|
||||
buildAddressRecipePlan(fallbackSelection.selected_recipe, executionFilters),
|
||||
intent.intent
|
||||
);
|
||||
const fallbackMcp = await executeAddressMcpQuery({
|
||||
query: fallbackPlan.query,
|
||||
limit: fallbackPlan.limit
|
||||
});
|
||||
if (!fallbackMcp.error) {
|
||||
plan = fallbackPlan;
|
||||
mcp = fallbackMcp;
|
||||
effectiveRecipeId = fallbackSelection.selected_recipe.recipe_id;
|
||||
}
|
||||
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_to_open_items")) {
|
||||
baseReasons.push("mcp_missing_subconto_field_auto_fallback_to_open_items");
|
||||
}
|
||||
if (
|
||||
intent.intent === "list_payables_counterparties" &&
|
||||
!baseReasons.includes("fallback_recipe_switched_to_open_items")
|
||||
) {
|
||||
baseReasons.push("fallback_recipe_switched_to_open_items");
|
||||
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_to_open_items")) {
|
||||
baseReasons.push("mcp_missing_subconto_field_auto_fallback_to_open_items");
|
||||
}
|
||||
if (!baseReasons.includes("fallback_recipe_switched_to_open_items")) {
|
||||
baseReasons.push("fallback_recipe_switched_to_open_items");
|
||||
}
|
||||
if (intent.intent === "payables_confirmed_as_of_date") {
|
||||
composeIntent = "list_payables_counterparties";
|
||||
routeExpectationIntent = "list_payables_counterparties";
|
||||
if (!baseReasons.includes("confirmed_payables_exact_mode_missing_subconto_fallback_to_open_items")) {
|
||||
baseReasons.push("confirmed_payables_exact_mode_missing_subconto_fallback_to_open_items");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_failed")) {
|
||||
baseReasons.push("mcp_missing_subconto_field_auto_fallback_failed");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_failed")) {
|
||||
baseReasons.push("mcp_missing_subconto_field_auto_fallback_failed");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_unavailable")) {
|
||||
baseReasons.push("mcp_missing_subconto_field_auto_fallback_unavailable");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
mcp.error &&
|
||||
(plan.recipe.recipe_id === "address_movements_receivables_v1" ||
|
||||
plan.recipe.recipe_id === "address_movements_payables_v1") &&
|
||||
missingSubcontoFallbackEligible &&
|
||||
isMissingSubcontoFieldError(mcp.error) &&
|
||||
!allowOpenItemsFallbackForMissingSubconto &&
|
||||
!baseReasons.includes("confirmed_payables_exact_mode_missing_subconto_no_heuristic_fallback")
|
||||
) {
|
||||
baseReasons.push("confirmed_payables_exact_mode_missing_subconto_no_heuristic_fallback");
|
||||
|
|
@ -3035,10 +3126,10 @@ export class AddressQueryService {
|
|||
});
|
||||
}
|
||||
|
||||
const factual = composeFactualReply(intent.intent, filteredRows, composeOptionsFromFilters(executionFilters));
|
||||
const factual = composeFactualReply(composeIntent, filteredRows, composeOptionsFromFilters(executionFilters));
|
||||
const factualResultSemantics = mergeAddressResultSemantics(
|
||||
deriveAddressResultSemantics({
|
||||
intent: intent.intent,
|
||||
intent: composeIntent,
|
||||
selectedRecipe: effectiveRecipeId,
|
||||
filters: filters.extracted_filters,
|
||||
responseType: factual.responseType,
|
||||
|
|
@ -3047,7 +3138,7 @@ export class AddressQueryService {
|
|||
factual.semantics
|
||||
);
|
||||
const finalRouteExpectationAudit = buildRouteExpectationAudit({
|
||||
intent: intent.intent,
|
||||
intent: routeExpectationIntent,
|
||||
selectedRecipe: effectiveRecipeId,
|
||||
requestedResultMode,
|
||||
resultMode: factualResultSemantics.result_mode
|
||||
|
|
@ -3085,7 +3176,7 @@ export class AddressQueryService {
|
|||
routeExpectationAudit: finalRouteExpectationAudit
|
||||
});
|
||||
}
|
||||
if (intent.intent === "payables_confirmed_as_of_date" && factualResultSemantics.balance_confirmed !== true) {
|
||||
if (intent.intent === "payables_confirmed_as_of_date" && composeIntent === "payables_confirmed_as_of_date" && factualResultSemantics.balance_confirmed !== true) {
|
||||
return buildLimitedExecutionResult({
|
||||
mode,
|
||||
shape,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,29 @@ __WHERE_CLAUSE__
|
|||
Движения.Период __ORDER_DIRECTION__
|
||||
`;
|
||||
|
||||
const PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
__AS_OF_EXPR__ КАК Период,
|
||||
"Остатки на дату" КАК Регистратор,
|
||||
"" КАК СчетДт,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетКт,
|
||||
Остатки.СуммаРазвернутыйОстатокКт КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоДт1,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоДт2,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоДт3,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоКт1,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоКт2,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоКт3,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация
|
||||
ИЗ
|
||||
РегистрБухгалтерии.Хозрасчетный.Остатки(__AS_OF_EXPR__, , , ) КАК Остатки
|
||||
ГДЕ
|
||||
Остатки.СуммаРазвернутыйОстатокКт > 0
|
||||
И (__PAYABLE_ACCOUNTS_MATCH__)
|
||||
УПОРЯДОЧИТЬ ПО
|
||||
Сумма __ORDER_DIRECTION__
|
||||
`;
|
||||
|
||||
const BANK_DOCS_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
БанкСписание.Дата КАК Период,
|
||||
|
|
@ -548,7 +571,8 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
|
|||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
||||
default_limit: 200,
|
||||
account_scope: ["60", "76"],
|
||||
account_scope_mode: "strict"
|
||||
account_scope_mode: "strict",
|
||||
query_template: "payables_confirmed_as_of_balance_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_movements_receivables_v1",
|
||||
|
|
@ -1001,6 +1025,25 @@ export function buildAddressRecipePlan(
|
|||
.replaceAll("__VAT19_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", VAT_PAYABLE_19_PREFIXES))
|
||||
: recipe.query_template === "contracts_by_counterparty_profile"
|
||||
? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
: 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));
|
||||
})()
|
||||
: MOVEMENTS_QUERY_TEMPLATE
|
||||
.replace("__LIMIT__", String(resolvedLimit))
|
||||
.replace("__WHERE_CLAUSE__", (() => {
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ type CounterpartyProfileFocus =
|
|||
type CounterpartyLifecycleFocus = "active_customers_period" | "active_customers_all_time";
|
||||
type ValueRankingFocus =
|
||||
| "top_by_total"
|
||||
| "top_years_by_total"
|
||||
| "top_by_ops"
|
||||
| "top_by_max_single"
|
||||
| "top_by_avg_check_min_ops"
|
||||
|
|
@ -524,6 +525,13 @@ function detectValueRankingFocus(userMessage: string | null | undefined): ValueR
|
|||
if (!text) {
|
||||
return "top_by_total";
|
||||
}
|
||||
const asksYearlyRevenueRanking =
|
||||
/(?:доходн|выручк|оборот|прибыл|деньг|денег|revenue|turnover|income)/iu.test(text) &&
|
||||
/(?:год|года|годы|year|years|по\s+годам)/iu.test(text) &&
|
||||
/(?:сам(?:ый|ая|ое|ые)|топ|луч|best|max|наибольш|больше)/iu.test(text);
|
||||
if (asksYearlyRevenueRanking) {
|
||||
return "top_years_by_total";
|
||||
}
|
||||
if (/(?:сам(?:ый|ая|ое|ые)\s+высок[а-яё]*|highest|largest)\s+чек|(?:max\s+check|чек\s+макс)/iu.test(text)) {
|
||||
return "top_by_max_single";
|
||||
}
|
||||
|
|
@ -1800,6 +1808,16 @@ export function composeFactualReply(
|
|||
lastPeriod: string | null;
|
||||
}
|
||||
>();
|
||||
const byYear = new Map<
|
||||
number,
|
||||
{
|
||||
year: number;
|
||||
total: number;
|
||||
ops: number;
|
||||
maxSingle: number;
|
||||
counterparties: Set<string>;
|
||||
}
|
||||
>();
|
||||
const deals: Array<{ period: string | null; registrator: string; counterparty: string; amount: number }> = [];
|
||||
|
||||
for (const row of rows) {
|
||||
|
|
@ -1834,10 +1852,31 @@ export function composeFactualReply(
|
|||
counterparty,
|
||||
amount
|
||||
});
|
||||
|
||||
const year = extractYearFromIso(row.period);
|
||||
if (year !== null) {
|
||||
const yearBucket = byYear.get(year);
|
||||
if (!yearBucket) {
|
||||
byYear.set(year, {
|
||||
year,
|
||||
total: amount,
|
||||
ops: 1,
|
||||
maxSingle: amount,
|
||||
counterparties: new Set<string>([counterparty])
|
||||
});
|
||||
} else {
|
||||
yearBucket.total += amount;
|
||||
yearBucket.ops += 1;
|
||||
yearBucket.maxSingle = Math.max(yearBucket.maxSingle, amount);
|
||||
yearBucket.counterparties.add(counterparty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const profileRows = Array.from(byCounterparty.values());
|
||||
const yearRows = Array.from(byYear.values());
|
||||
const rankedByTotal = [...profileRows].sort((a, b) => b.total - a.total || b.ops - a.ops || a.name.localeCompare(b.name));
|
||||
const rankedByYearTotal = [...yearRows].sort((a, b) => b.total - a.total || b.ops - a.ops || a.year - b.year);
|
||||
const rankedByOps = [...profileRows].sort((a, b) => b.ops - a.ops || b.total - a.total || a.name.localeCompare(b.name));
|
||||
const rankedByMaxSingle = [...profileRows].sort(
|
||||
(a, b) => b.maxSingle - a.maxSingle || b.total - a.total || a.name.localeCompare(b.name)
|
||||
|
|
@ -1877,6 +1916,28 @@ export function composeFactualReply(
|
|||
};
|
||||
}
|
||||
|
||||
if (focus === "top_years_by_total") {
|
||||
const visible = rankedByYearTotal.slice(0, limit);
|
||||
const heading = isSupplier
|
||||
? `Топ-${visible.length} лет по сумме выплат:`
|
||||
: `Топ-${visible.length} лет по сумме поступлений:`;
|
||||
lines.unshift(heading);
|
||||
if (visible.length === 0) {
|
||||
lines.push("По доступному окну не удалось собрать годовые агрегаты по суммам.");
|
||||
} else {
|
||||
lines.push(
|
||||
...visible.map(
|
||||
(item, index) =>
|
||||
`${index + 1}. ${item.year} | сумма: ${item.total} | операций: ${item.ops} | контрагентов: ${item.counterparties.size} | макс: ${item.maxSingle}`
|
||||
)
|
||||
);
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: lines.join("\n")
|
||||
};
|
||||
}
|
||||
|
||||
if (focus === "top_by_ops") {
|
||||
const visible = rankedByOps.slice(0, limit);
|
||||
const heading = isSupplier
|
||||
|
|
|
|||
|
|
@ -3664,6 +3664,12 @@ function hasOpenContractsAddressSignal(text) {
|
|||
return hasRequestCue || hasTemporalCue;
|
||||
}
|
||||
const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
|
||||
"period_coverage_profile",
|
||||
"document_type_and_account_section_profile",
|
||||
"counterparty_population_and_roles",
|
||||
"counterparty_activity_lifecycle",
|
||||
"customer_revenue_and_payments",
|
||||
"supplier_payouts_profile",
|
||||
"list_open_contracts",
|
||||
"open_items_by_counterparty_or_contract",
|
||||
"payables_confirmed_as_of_date",
|
||||
|
|
|
|||
|
|
@ -129,7 +129,8 @@ export interface AddressRecipeDefinition {
|
|||
| "supplier_payout_profile"
|
||||
| "contract_value_profile"
|
||||
| "contracts_by_counterparty_profile"
|
||||
| "vat_payable_forecast_profile";
|
||||
| "vat_payable_forecast_profile"
|
||||
| "payables_confirmed_as_of_balance_profile";
|
||||
required_filters: Array<keyof AddressFilterSet>;
|
||||
optional_filters: Array<keyof AddressFilterSet>;
|
||||
default_limit: number;
|
||||
|
|
|
|||
|
|
@ -1031,7 +1031,7 @@ describe("address compose stage utf8 headers", () => {
|
|||
|
||||
expect(reply.text).toContain("Профиль договорной базы собран");
|
||||
expect(reply.text).toContain("Всего договоров в базе: 520.");
|
||||
expect(reply.text).toContain("Использованных договоров (есть factual связь с операциями): 148.");
|
||||
expect(reply.text).toContain("<EFBFBD>?Использованных договоров (есть factual связь с операциями): 148.");
|
||||
expect(reply.text).toContain("Неиспользуемых договоров: 372.");
|
||||
});
|
||||
|
||||
|
|
@ -1746,6 +1746,25 @@ describe("address intent resolver expansion (M2.3a)", () => {
|
|||
expect(result.intent).toBe("customer_revenue_and_payments");
|
||||
});
|
||||
|
||||
it("resolves colloquial 'кто нам больше денег принес' wording into customer revenue intent", () => {
|
||||
const result = resolveAddressIntent("\u043a\u0442\u043e \u043d\u0430\u043c \u0431\u043e\u043b\u044c\u0448\u0435 \u0434\u0435\u043d\u0435\u0433 \u043f\u0440\u0438\u043d\u0435\u0441");
|
||||
expect(result.intent).toBe("customer_revenue_and_payments");
|
||||
});
|
||||
|
||||
it("resolves typo 'ликвидних заказчиков' wording into customer revenue intent", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"\u043f\u043e\u043a\u0430\u0436\u0438 \u0441\u0430\u043c\u044b\u0445 \u043b\u0438\u043a\u0432\u0438\u0434\u043d\u0438\u0445 \u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a\u043e\u0432"
|
||||
);
|
||||
expect(result.intent).toBe("customer_revenue_and_payments");
|
||||
});
|
||||
|
||||
it("resolves yearly profitability wording into customer revenue intent", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"\u043a\u0430\u043a\u0438\u0435 \u0441\u0430\u043c\u044b\u0435 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0435 \u0433\u043e\u0434\u0430 \u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u044b"
|
||||
);
|
||||
expect(result.intent).toBe("customer_revenue_and_payments");
|
||||
});
|
||||
|
||||
it("resolves major-share revenue wording into customer revenue intent", () => {
|
||||
const result = resolveAddressIntent("какие контрагенты принесли основную часть нашей выручки за отчетный период?");
|
||||
expect(result.intent).toBe("customer_revenue_and_payments");
|
||||
|
|
@ -2318,7 +2337,7 @@ describe("address filter extraction for balance drilldown", () => {
|
|||
|
||||
it("repairs mojibake phrase before extracting counterparty filters", () => {
|
||||
const result = extractAddressFilters(
|
||||
"Показать документы СВК за 2020 год.",
|
||||
"Показать РґРѕРєСѓР<EFBFBD>?енты РЎР’Рљ Р·Р° 2020 РіРѕРґ.",
|
||||
"list_documents_by_counterparty"
|
||||
);
|
||||
expect(result.extracted_filters.counterparty).toBe("СВК");
|
||||
|
|
@ -2494,25 +2513,35 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
|
|||
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
||||
});
|
||||
|
||||
it("routes 'каму мы должны заплатить за май 2020' into exact confirmed payables flow without heuristic fallback", async () => {
|
||||
it("routes 'каму мы должны заплатить за май 2020' into confirmed payables flow with controlled fallback on schema limits", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle("каму мы должны заплатить за май 2020");
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.debug.detected_intent).toBe("payables_confirmed_as_of_date");
|
||||
expect(result?.debug.requested_result_mode).toBe("confirmed_balance");
|
||||
expect(result?.debug.result_mode).toBe("confirmed_balance");
|
||||
expect(["confirmed_balance", "heuristic_candidates"]).toContain(result?.debug.result_mode);
|
||||
expect(result?.debug.as_of_date_basis).toBe("explicit_as_of_date");
|
||||
expect(result?.debug.selected_recipe).toBe("address_payables_confirmed_as_of_date_v1");
|
||||
expect(["address_payables_confirmed_as_of_date_v1", "address_open_items_by_party_or_contract_v1"]).toContain(result?.debug.selected_recipe);
|
||||
expect(result?.debug.route_expectation_status).toBe("matched");
|
||||
expect(result?.debug.route_expectation_reason).toBe("route_expectation_matched");
|
||||
expect(Array.isArray(result?.debug.reasons)).toBe(true);
|
||||
expect(result?.debug.reasons).not.toContain("confirmed_balance_unavailable_fallback_to_heuristic_candidates");
|
||||
if (result?.debug.result_mode === "heuristic_candidates") {
|
||||
expect(result?.debug.reasons).toContain("confirmed_balance_unavailable_fallback_to_heuristic_candidates");
|
||||
expect(result?.debug.reasons).toContain("confirmed_payables_exact_mode_missing_subconto_fallback_to_open_items");
|
||||
expect(result?.debug.balance_confirmed).toBe(false);
|
||||
} else {
|
||||
expect(result?.debug.reasons).not.toContain("confirmed_balance_unavailable_fallback_to_heuristic_candidates");
|
||||
}
|
||||
expect(["FACTUAL_LIST", "FACTUAL_SUMMARY", "LIMITED_WITH_REASON"]).toContain(result?.response_type);
|
||||
const reply = String(result?.reply_text ?? "");
|
||||
if (result?.response_type === "LIMITED_WITH_REASON") {
|
||||
expect(result?.debug.balance_confirmed).toBe(false);
|
||||
expect(result?.debug.reasons).toContain("exact_payables_mode_limited_response");
|
||||
expect(reply.toLowerCase()).not.toContain("эвристич");
|
||||
} else if (result?.debug.result_mode === "heuristic_candidates") {
|
||||
expect(result?.debug.balance_confirmed).toBe(false);
|
||||
expect(reply).toContain("shortlist");
|
||||
expect(reply.toLowerCase()).toContain("shortlist");
|
||||
} else {
|
||||
expect(result?.debug.balance_confirmed).toBe(true);
|
||||
expect(reply).toContain("Блок 1. Статус результата");
|
||||
|
|
@ -2621,7 +2650,9 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
|
|||
);
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.debug.detected_intent).toBe("list_receivables_counterparties");
|
||||
expect(result?.debug.selected_recipe).toBe("address_movements_receivables_v1");
|
||||
expect(["address_movements_receivables_v1", "address_open_items_by_party_or_contract_v1"]).toContain(
|
||||
result?.debug.selected_recipe
|
||||
);
|
||||
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
|
||||
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
||||
});
|
||||
|
|
@ -2633,7 +2664,9 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
|
|||
);
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.debug.detected_intent).toBe("list_receivables_counterparties");
|
||||
expect(result?.debug.selected_recipe).toBe("address_movements_receivables_v1");
|
||||
expect(["address_movements_receivables_v1", "address_open_items_by_party_or_contract_v1"]).toContain(
|
||||
result?.debug.selected_recipe
|
||||
);
|
||||
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
|
||||
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
||||
});
|
||||
|
|
@ -2731,6 +2764,41 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
|
|||
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
|
||||
});
|
||||
|
||||
it("routes colloquial 'кто нам больше денег принес' into customer value aggregate recipe", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle("\u043a\u0442\u043e \u043d\u0430\u043c \u0431\u043e\u043b\u044c\u0448\u0435 \u0434\u0435\u043d\u0435\u0433 \u043f\u0440\u0438\u043d\u0435\u0441");
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments");
|
||||
expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1");
|
||||
expect(result?.debug.mcp_call_status).not.toBe("skipped");
|
||||
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
|
||||
});
|
||||
|
||||
it("routes typo 'ликвидних заказчиков' into customer value aggregate recipe", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(
|
||||
"\u043f\u043e\u043a\u0430\u0436\u0438 \u0441\u0430\u043c\u044b\u0445 \u043b\u0438\u043a\u0432\u0438\u0434\u043d\u0438\u0445 \u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a\u043e\u0432"
|
||||
);
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments");
|
||||
expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1");
|
||||
expect(result?.debug.mcp_call_status).not.toBe("skipped");
|
||||
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
|
||||
});
|
||||
|
||||
it("routes yearly profitability wording into customer value aggregate recipe", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(
|
||||
"\u043a\u0430\u043a\u0438\u0435 \u0441\u0430\u043c\u044b\u0435 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0435 \u0433\u043e\u0434\u0430 \u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u044b"
|
||||
);
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments");
|
||||
expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1");
|
||||
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
||||
expect(result?.debug.mcp_call_status).not.toBe("skipped");
|
||||
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
|
||||
});
|
||||
|
||||
it("routes typo highest-check wording into customer value aggregate recipe", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle("с каких кликентов самый высокий чек");
|
||||
|
|
@ -3257,13 +3325,13 @@ describe("address decompose stage follow-up carryover", () => {
|
|||
period_to: "2020-12-31"
|
||||
},
|
||||
previous_anchor_type: "counterparty",
|
||||
previous_anchor_value: "ИП Калинин Н.М.",
|
||||
previous_anchor_value: "<EFBFBD>?П Калинин Н.М.",
|
||||
resolved_counterparty_from_display: true
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.mode.mode).toBe("address_query");
|
||||
expect(result?.intent.intent).toBe("customer_revenue_and_payments");
|
||||
expect(result?.filters.extracted_filters.counterparty).toBe("ИП Калинин Н.М.");
|
||||
expect(result?.filters.extracted_filters.counterparty).toBe("<EFBFBD>?П Калинин Н.М.");
|
||||
expect(result?.filters.extracted_filters.period_from).toBe("2020-01-01");
|
||||
expect(result?.filters.extracted_filters.period_to).toBe("2020-12-31");
|
||||
expect(
|
||||
|
|
@ -3372,7 +3440,7 @@ describe("address recipe catalog counterparty filtering", () => {
|
|||
expect(plan.query).toContain("DOC_TYPE_DOCS");
|
||||
expect(plan.query).toContain("SECTION_DT_OPS");
|
||||
expect(plan.query).toContain("SECTION_KT_OPS");
|
||||
expect(plan.query).toContain("СГРУППИРОВАТЬ ПО\n Движения.СчетДт");
|
||||
expect(plan.query).toContain("СГРУПП<EFBFBD>?РОВАТЬ ПО\n Движения.СчетДт");
|
||||
expect(plan.query).not.toContain("ЛЕВ(Движения.СчетДт.Код, 2)");
|
||||
});
|
||||
|
||||
|
|
@ -3445,7 +3513,7 @@ describe("address recipe catalog counterparty filtering", () => {
|
|||
|
||||
expect(plan.recipe.recipe_id).toBe("address_contracts_by_counterparty_v1");
|
||||
expect(plan.query).toContain("Справочник.ДоговорыКонтрагентов");
|
||||
expect(plan.query).toContain("ПРЕДСТАВЛЕНИЕ(Договоры.Владелец)");
|
||||
expect(plan.query).toContain("ПРЕДСТАВЛЕН<EFBFBD>?Е(Договоры.Владелец)");
|
||||
});
|
||||
|
||||
it("selects counterparty lifecycle recipe and keeps activity marker", () => {
|
||||
|
|
@ -3480,7 +3548,7 @@ describe("address recipe catalog counterparty filtering", () => {
|
|||
counterparty: "Жуковка 51",
|
||||
sort: "period_asc"
|
||||
});
|
||||
expect(plan.query).toContain("УПОРЯДОЧИТЬ ПО");
|
||||
expect(plan.query).toContain("УПОРЯДОЧ<EFBFBD>?ТЬ ПО");
|
||||
expect(plan.query).toContain("Период ВОЗР");
|
||||
});
|
||||
|
||||
|
|
@ -3546,7 +3614,7 @@ describe("address recipe catalog counterparty filtering", () => {
|
|||
|
||||
expect(plan.query).toContain("Документ.СписаниеСРасчетногоСчета");
|
||||
expect(plan.query).toContain("Документ.ПоступлениеНаРасчетныйСчет");
|
||||
expect(plan.query).toContain("ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор");
|
||||
expect(plan.query).toContain("ПРЕДСТАВЛЕН<EFBFBD>?Е(БанкПоступление.ДоговорКонтрагента) КАК Договор");
|
||||
});
|
||||
|
||||
it("allows extended limit for open-contracts intent", () => {
|
||||
|
|
@ -3604,8 +3672,8 @@ describe("address recipe catalog counterparty filtering", () => {
|
|||
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Движения.СчетДт.Код, \"\"), 1, 4) = \"68.2\"");
|
||||
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Движения.СчетДт.Код, \"\"), 1, 2) = \"19\"");
|
||||
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Движения.СчетКт.Код, \"\"), 1, 2) = \"19\"");
|
||||
expect(plan.query).not.toContain("ПРЕДСТАВЛЕНИЕ(Движения.СчетКт) ПОДОБНО");
|
||||
expect(plan.query).not.toContain("ПРЕДСТАВЛЕНИЕ(Движения.СчетДт) ПОДОБНО");
|
||||
expect(plan.query).not.toContain("ПРЕДСТАВЛЕН<EFBFBD>?Е(Движения.СчетКт) ПОДОБНО");
|
||||
expect(plan.query).not.toContain("ПРЕДСТАВЛЕН<EFBFBD>?Е(Движения.СчетДт) ПОДОБНО");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -252,6 +252,24 @@ describe("assistant orchestration contract", () => {
|
|||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||||
});
|
||||
|
||||
it("keeps colloquial 'кто нам больше денег принес' in address lane", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "\u043a\u0442\u043e \u043d\u0430\u043c \u0431\u043e\u043b\u044c\u0448\u0435 \u0434\u0435\u043d\u0435\u0433 \u043f\u0440\u0438\u043d\u0435\u0441",
|
||||
effectiveAddressUserMessage: "\u043a\u0442\u043e \u043d\u0430\u043c \u0431\u043e\u043b\u044c\u0448\u0435 \u0434\u0435\u043d\u0435\u0433 \u043f\u0440\u0438\u043d\u0435\u0441",
|
||||
followupContext: null,
|
||||
llmPreDecomposeMeta: null,
|
||||
useMock: false
|
||||
});
|
||||
|
||||
expect(decision.runAddressLane).toBe(true);
|
||||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||||
expect(["address_intent_resolver_detected", "address_mode_classifier_detected", "address_signal_detected"]).toContain(
|
||||
String(decision.toolGateReason)
|
||||
);
|
||||
expect(decision.livingMode).toBe("address_data");
|
||||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||||
});
|
||||
|
||||
it("routes unsupported turnover-by-organization query to deep analysis", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "\u043a\u0430\u043a\u0438\u0435 \u043e\u0431\u043e\u0440\u043e\u0442\u044b \u043f\u043e \u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435 \u0437\u0430 2020 \u0433\u043e\u0434",
|
||||
|
|
@ -381,7 +399,7 @@ describe("assistant orchestration contract", () => {
|
|||
expect(decision.orchestrationContract?.aggregate_analytics_signal_fallback_to_deep).toBe(true);
|
||||
});
|
||||
|
||||
it("routes profitability ranking query to deep analysis instead of address lane", () => {
|
||||
it("keeps profitability ranking query in address lane", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "\u043a\u0430\u043a\u043e\u0439 \u0441\u0430\u043c\u044b\u0439 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0439 \u0433\u043e\u0434?",
|
||||
effectiveAddressUserMessage: "\u043a\u0430\u043a\u043e\u0439 \u0441\u0430\u043c\u044b\u0439 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0439 \u0433\u043e\u0434?",
|
||||
|
|
@ -390,12 +408,14 @@ describe("assistant orchestration contract", () => {
|
|||
useMock: false
|
||||
} as any);
|
||||
|
||||
expect(decision.runAddressLane).toBe(false);
|
||||
expect(decision.toolGateDecision).toBe("skip_address_lane");
|
||||
expect(decision.toolGateReason).toBe("aggregate_analytics_signal_fallback_to_deep");
|
||||
expect(decision.livingMode).toBe("deep_analysis");
|
||||
expect(decision.livingReason).toBe("aggregate_analytics_signal_fallback_to_deep");
|
||||
expect(decision.orchestrationContract?.aggregate_analytics_signal_fallback_to_deep).toBe(true);
|
||||
expect(decision.runAddressLane).toBe(true);
|
||||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||||
expect(["address_intent_resolver_detected", "address_mode_classifier_detected", "address_signal_detected"]).toContain(
|
||||
String(decision.toolGateReason)
|
||||
);
|
||||
expect(decision.livingMode).toBe("address_data");
|
||||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||||
expect(decision.orchestrationContract?.aggregate_analytics_signal_fallback_to_deep).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps unsupported retrieval query in address lane when LLM runtime is unavailable", () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue