АДРЕСНЫЙ РЕЖИМ -Step-5 - feat(assistant): стабилизация свободного LLM-роутинга, прическа маршрутов chat/address, прототип прогноза НДС

This commit is contained in:
2026-04-08 23:40:12 +03:00
parent 71762af575
commit df29798fa2
47 changed files with 11439 additions and 651 deletions
@@ -1037,6 +1037,11 @@ export class AddressQueryService {
return null;
}
const { mode, shape, intent, filters, baseReasons } = decompose;
const composeOptionsFromFilters = (filterSet: AddressFilterSet) => ({
userMessage,
periodFrom: typeof filterSet.period_from === "string" ? filterSet.period_from : undefined,
periodTo: typeof filterSet.period_to === "string" ? filterSet.period_to : undefined
});
let anchor = resolvePrimaryAnchor(intent.intent, filters.extracted_filters);
const recipeSelection = selectAddressRecipe(intent.intent, filters.extracted_filters);
@@ -1273,7 +1278,7 @@ export class AddressQueryService {
const recoveredBankRows = applyIntentSpecificFilter("bank_operations_by_contract", filterByAnchors);
const recoveredRows = recoveredBankRows.length > 0 ? recoveredBankRows : filterByAnchors;
if (recoveredRows.length > 0) {
const factual = composeFactualReply(intent.intent, recoveredRows, { userMessage });
const factual = composeFactualReply(intent.intent, recoveredRows, composeOptionsFromFilters(filters.extracted_filters));
const recoveryReason =
recoveredBankRows.length > 0
? "contract_docs_recovered_via_bank_fallback"
@@ -1392,7 +1397,11 @@ export class AddressQueryService {
rowsAnchorMatched: expandedRowsByAnchor.length,
rowsMatched: expandedFilteredRows.length
});
const expandedFactual = composeFactualReply(intent.intent, expandedFilteredRows, { userMessage });
const expandedFactual = composeFactualReply(
intent.intent,
expandedFilteredRows,
composeOptionsFromFilters(expandedLimitFilters)
);
const expandedPrefix = `Период сохранен. Глубина live-выборки автоматически расширена до ${expandedPlan.limit} строк.`;
const expandedLimitations = [...filters.warnings, "query_limit_auto_expanded_for_anchor_recovery"];
const expandedReasons = [...baseReasons, "query_limit_auto_expanded_for_anchor_recovery"];
@@ -1501,7 +1510,11 @@ export class AddressQueryService {
});
const observedWindow = deriveObservedPeriodWindow(broadenedFilteredRows);
const broadenedPrefix = composeAutoBroadenedPeriodPrefix(filters.extracted_filters, observedWindow);
const broadenedFactual = composeFactualReply(intent.intent, broadenedFilteredRows, { userMessage });
const broadenedFactual = composeFactualReply(
intent.intent,
broadenedFilteredRows,
composeOptionsFromFilters(autoBroadenedFilters)
);
const broadenedLimitations = [...filters.warnings, "period_window_auto_broadened_to_available_data"];
const broadenedReasons = [...baseReasons, "period_window_auto_broadened_to_available_data"];
return {
@@ -1616,14 +1629,21 @@ export class AddressQueryService {
rowsAnchorMatched: historicalRowsByAnchor.length,
rowsMatched: historicalFilteredRows.length
});
const historicalFactual = composeFactualReply(intent.intent, historicalFilteredRows, { userMessage });
const historicalPrefix =
"В последних доступных записях якорь не подтвердился; показаны найденные строки по историческому окну.";
const historicalFactual = composeFactualReply(
intent.intent,
historicalFilteredRows,
composeOptionsFromFilters(historicalFilters)
);
const historicalPrefix = "Найдены данные в историческом срезе базы по вашему запросу.";
const historicalSuggestion =
intent.intent === "list_documents_by_counterparty"
? "\nЕсли нужно, могу дополнительно показать платежи и договоры по этому контрагенту."
: "";
const historicalLimitations = [...filters.warnings, "historical_window_sort_recovery_applied"];
const historicalReasons = [...baseReasons, "historical_window_sort_recovery_applied"];
return {
handled: true,
reply_text: `${historicalPrefix}\n${historicalFactual.text}`,
reply_text: `${historicalPrefix}\n${historicalFactual.text}${historicalSuggestion}`,
reply_type: inferReplyType(historicalFactual.responseType),
response_type: historicalFactual.responseType,
debug: {
@@ -1681,14 +1701,21 @@ export class AddressQueryService {
) {
const documentBankFallbackRows = applyIntentSpecificFilter(intent.intent, normalizedRows);
if (documentBankFallbackRows.length > 0) {
const fallbackFactual = composeFactualReply(intent.intent, documentBankFallbackRows, { userMessage });
const fallbackFactual = composeFactualReply(
intent.intent,
documentBankFallbackRows,
composeOptionsFromFilters(filters.extracted_filters)
);
const fallbackPrefix = "По вашему запросу показываю найденные документы и операции в доступном срезе базы.";
const fallbackSuggestion =
intent.intent === "list_documents_by_counterparty"
? "\nЕсли нужно, могу дополнительно сузить период или показать только платежи."
: "";
const fallbackLimitations = [...filters.warnings, "anchor_not_matched_fallback_rows"];
const fallbackReasons = [...baseReasons, "anchor_not_matched_fallback_rows"];
return {
handled: true,
reply_text:
"Точный якорь не подтвердился в текущем окне live-данных; показаны ближайшие доступные документы/операции по выбранному типу.\n" +
fallbackFactual.text,
reply_text: `${fallbackPrefix}\n${fallbackFactual.text}${fallbackSuggestion}`,
reply_type: inferReplyType(fallbackFactual.responseType),
response_type: fallbackFactual.responseType,
debug: {
@@ -1751,11 +1778,24 @@ export class AddressQueryService {
Array.isArray(filters.warnings) &&
(filters.warnings.includes("counterparty_from_followup_context") ||
filters.warnings.includes("contract_from_followup_context"));
const anchorMismatchByCounterparty =
isAnchorMismatch && String(matchFailureReason ?? "").includes("counterparty_anchor_not_matched");
const anchorMismatchByContract = isAnchorMismatch && String(matchFailureReason ?? "").includes("contract_anchor_not_matched");
const isLowQualityPartyAnchor =
(anchor.anchor_type === "counterparty" || anchor.anchor_type === "contract") &&
isLikelyLowQualityPartyAnchor(anchor.anchor_value_raw);
const anchorMismatchCategory: AddressLimitedReasonCategory =
isFollowupAnchorCarryover || !isLowQualityPartyAnchor ? "empty_match" : "missing_anchor";
const requestedPeriodFrom =
typeof filters.extracted_filters.period_from === "string" ? filters.extracted_filters.period_from : null;
const requestedPeriodTo = typeof filters.extracted_filters.period_to === "string" ? filters.extracted_filters.period_to : null;
const requestedPeriodHint =
requestedPeriodFrom && requestedPeriodTo ? ` (период ${requestedPeriodFrom}..${requestedPeriodTo} сохранен)` : "";
const anchorMismatchCategory: AddressLimitedReasonCategory = isFollowupAnchorCarryover
? "empty_match"
: anchorMismatchByCounterparty || anchorMismatchByContract
? "missing_anchor"
: !isLowQualityPartyAnchor
? "empty_match"
: "missing_anchor";
const category: AddressLimitedReasonCategory = isAnchorMismatch
? anchorMismatchCategory
: isRecipeFilteredOut
@@ -1764,18 +1804,26 @@ export class AddressQueryService {
? "recipe_visibility_gap"
: "empty_match";
const reasonText = isAnchorMismatch
? anchorMismatchCategory === "missing_anchor"
? "якорь контрагента/договора не найден в материализованных live-строках"
: "по указанному якорю и фильтрам в live-выборке нет строк"
? anchorMismatchByCounterparty
? "контрагент по указанному имени/алиасу не найден в materialized live-строках"
: anchorMismatchByContract
? "договор по указанному номеру/названию не найден в materialized live-строках"
: anchorMismatchCategory === "missing_anchor"
? "якорь контрагента/договора не найден в materialized live-строках"
: "по указанному якорю и фильтрам в live-выборке нет строк"
: isRecipeFilteredOut
? "строки по якорю найдены, но отфильтрованы intent-specific recipe"
: isVisibilityGapCandidate
? "в текущем live recipe нет достаточной document/bank видимости после фильтрации"
: "по выбранным фильтрам в live-выборке нет строк";
const nextStep = isAnchorMismatch
? anchorMismatchCategory === "missing_anchor"
? "уточните контрагента точным именем или добавьте ИНН/договор"
: "уточните период или снимите часть фильтров"
? anchorMismatchByCounterparty
? `уточните точное имя контрагента или добавьте ИНН${requestedPeriodHint}`
: anchorMismatchByContract
? `уточните номер/наименование договора${requestedPeriodHint}`
: anchorMismatchCategory === "missing_anchor"
? "уточните контрагента точным именем или добавьте ИНН/договор"
: "уточните период или снимите часть фильтров"
: isRecipeFilteredOut
? "сузьте период, уточните контрагента или документный тип"
: isVisibilityGapCandidate
@@ -1783,9 +1831,13 @@ export class AddressQueryService {
: "уточните период, контрагента, договор или снимите часть фильтров";
const limitations = isAnchorMismatch
? [
anchorMismatchCategory === "missing_anchor"
? "anchor_not_matched_after_materialization"
: "no_rows_for_anchor_after_materialization"
anchorMismatchByCounterparty
? "counterparty_anchor_not_matched_after_materialization"
: anchorMismatchByContract
? "contract_anchor_not_matched_after_materialization"
: anchorMismatchCategory === "missing_anchor"
? "anchor_not_matched_after_materialization"
: "no_rows_for_anchor_after_materialization"
]
: isRecipeFilteredOut
? ["rows_filtered_out_by_recipe_after_anchor_match"]
@@ -1824,7 +1876,7 @@ export class AddressQueryService {
});
}
const factual = composeFactualReply(intent.intent, filteredRows, { userMessage });
const factual = composeFactualReply(intent.intent, filteredRows, composeOptionsFromFilters(filters.extracted_filters));
return {
handled: true,
reply_text: factual.text,