АДРЕСНЫЙ РЕЖИМ - авторан история - базовая версия
This commit is contained in:
@@ -11,6 +11,7 @@ const COUNTERPARTY_PATTERN = /(?:по\s+контрагенту|контраге
|
||||
const CONTRACT_PATTERN = /(?:по\s+(?:договору|контракту)|(?:договор|контракт)(?:у|а)?\s*(?:№|#|n)?|by\s+contract|contract(?:\s*(?:no|number|#|n))?)\s+([^\r\n,.;:]+)/i;
|
||||
const DATE_DMY_PATTERN = /\b(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{2,4})\b/;
|
||||
const DATE_YMD_PATTERN = /\b(20\d{2})[.\/-](\d{1,2})[.\/-](\d{1,2})\b/;
|
||||
const DATE_DMY_MONTH_NAME_PATTERN = /(?:^|[\s,.;:!?()\-])(\d{1,2})\s+([a-zа-яё]+)\s+((?:19|20)\d{2}|\d{2})(?:\s*г(?:од|ода|\\.)?)?(?=$|[\s,.;:!?()\-])/iu;
|
||||
const PERIOD_RANGE_PATTERN_1 = /(?:from|с)\s+(\d{1,4}[.\/-]\d{1,2}[.\/-]\d{1,4})\s+(?:to|по)\s+(\d{1,4}[.\/-]\d{1,2}[.\/-]\d{1,4})/i;
|
||||
const PERIOD_RANGE_PATTERN_2 = /(?:between|за\s+период\s+с)\s+(\d{1,4}[.\/-]\d{1,2}[.\/-]\d{1,4})\s+(?:and|по)\s+(\d{1,4}[.\/-]\d{1,2}[.\/-]\d{1,4})/i;
|
||||
const YEAR_RANGE_PATTERN = /(?:за|for|с|from)?\s*(20\d{2})\s*(?:[-‐‑‒–—―−]|до|to|по)\s*(20\d{2})(?:\s*(?:г(?:од|ода)?\.?|year))?(?=[^\d]|$)/iu;
|
||||
@@ -100,6 +101,36 @@ function extractAsOfDate(text) {
|
||||
const year = yearRaw < 100 ? 2000 + yearRaw : yearRaw;
|
||||
return toIsoDate(year, month, day) ?? undefined;
|
||||
}
|
||||
const dmyByMonthName = text.match(DATE_DMY_MONTH_NAME_PATTERN);
|
||||
if (dmyByMonthName) {
|
||||
const day = Number(dmyByMonthName[1]);
|
||||
const month = resolveMonthByName(String(dmyByMonthName[2] ?? ""));
|
||||
const yearRaw = Number(dmyByMonthName[3]);
|
||||
const year = yearRaw < 100 ? 2000 + yearRaw : yearRaw;
|
||||
if (month) {
|
||||
return toIsoDate(year, month, day) ?? undefined;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
function extractAsOfDateWithCue(text) {
|
||||
const source = String(text ?? "");
|
||||
if (!source) {
|
||||
return undefined;
|
||||
}
|
||||
const numericCue = source.match(/(?:^|[\s,.;:!?()\-])(?:на|до|к|по\s+состоянию\s+на|as\s+of|by)\s+(\d{1,4}[.\/-]\d{1,2}[.\/-]\d{1,4})(?=$|[\s,.;:!?()\-])/iu);
|
||||
if (numericCue) {
|
||||
return parseDateToken(String(numericCue[1] ?? ""));
|
||||
}
|
||||
const monthNameCue = source.match(/(?:^|[\s,.;:!?()\-])(?:на|до|к|по\s+состоянию\s+на|as\s+of|by)\s+(\d{1,2})\s+([a-zа-яё]+)\s+((?:19|20)\d{2})(?:\s*г(?:од|ода|\\.)?)?(?=$|[\s,.;:!?()\-])/iu);
|
||||
if (monthNameCue) {
|
||||
const day = Number(monthNameCue[1]);
|
||||
const month = resolveMonthByName(String(monthNameCue[2] ?? ""));
|
||||
const year = Number(monthNameCue[3]);
|
||||
if (month && Number.isFinite(year) && Number.isFinite(day)) {
|
||||
return toIsoDate(year, month, day) ?? undefined;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
function parseDateToken(token) {
|
||||
@@ -155,6 +186,25 @@ function resolveMonthByName(rawMonthName) {
|
||||
return 12;
|
||||
return undefined;
|
||||
}
|
||||
function deriveQuarterWindowForDate(asOfIso) {
|
||||
const token = String(asOfIso ?? "").trim();
|
||||
const match = token.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const year = Number(match[1]);
|
||||
const month = Number(match[2]);
|
||||
if (!Number.isFinite(year) || !Number.isFinite(month) || month < 1 || month > 12) {
|
||||
return null;
|
||||
}
|
||||
const quarterStartMonth = Math.floor((month - 1) / 3) * 3 + 1;
|
||||
const quarterEndMonth = quarterStartMonth + 2;
|
||||
const quarterEndDay = new Date(Date.UTC(year, quarterEndMonth, 0)).getUTCDate();
|
||||
return {
|
||||
period_from: `${year}-${String(quarterStartMonth).padStart(2, "0")}-01`,
|
||||
period_to: `${year}-${String(quarterEndMonth).padStart(2, "0")}-${String(quarterEndDay).padStart(2, "0")}`
|
||||
};
|
||||
}
|
||||
function extractMonthPeriod(text) {
|
||||
const numericMonthYearMatch = text.match(MONTH_PERIOD_NUMERIC_MONTH_YEAR_PATTERN);
|
||||
if (numericMonthYearMatch) {
|
||||
@@ -532,6 +582,19 @@ function isLikelyCounterpartyToken(rawToken) {
|
||||
"меньше",
|
||||
"платит",
|
||||
"платят",
|
||||
"прогноз",
|
||||
"forecast",
|
||||
"план",
|
||||
"плана",
|
||||
"ндс",
|
||||
"vat",
|
||||
"налог",
|
||||
"оплата",
|
||||
"оплаты",
|
||||
"платеж",
|
||||
"платёж",
|
||||
"платежа",
|
||||
"платежи",
|
||||
"денег",
|
||||
"деньги",
|
||||
"объем",
|
||||
@@ -898,7 +961,8 @@ function extractAddressFilters(userMessage, intent) {
|
||||
intent === "contract_usage_overview" ||
|
||||
intent === "customer_revenue_and_payments" ||
|
||||
intent === "supplier_payouts_profile" ||
|
||||
intent === "contract_usage_and_value";
|
||||
intent === "contract_usage_and_value" ||
|
||||
intent === "vat_payable_forecast";
|
||||
const filters = {
|
||||
sort: "period_desc"
|
||||
};
|
||||
@@ -906,6 +970,8 @@ function extractAddressFilters(userMessage, intent) {
|
||||
filters.limit = 20;
|
||||
}
|
||||
const warnings = [];
|
||||
const explicitAsOfDate = extractAsOfDate(text);
|
||||
const explicitAsOfDateWithCue = extractAsOfDateWithCue(text);
|
||||
const accountMatch = text.match(ACCOUNT_PATTERN);
|
||||
if (accountMatch) {
|
||||
filters.account = String(accountMatch[1]).replace(",", ".");
|
||||
@@ -1011,11 +1077,24 @@ function extractAddressFilters(userMessage, intent) {
|
||||
warnings.push("period_derived_from_year_phrase");
|
||||
}
|
||||
}
|
||||
const vatAsOfDate = explicitAsOfDateWithCue ?? explicitAsOfDate;
|
||||
if (intent === "vat_payable_forecast" && vatAsOfDate && !periodRange.period_from && !periodRange.period_to) {
|
||||
const quarterWindow = deriveQuarterWindowForDate(vatAsOfDate);
|
||||
if (quarterWindow) {
|
||||
filters.period_from = quarterWindow.period_from;
|
||||
warnings.push("period_from_derived_from_quarter_for_vat_forecast");
|
||||
filters.period_to = vatAsOfDate;
|
||||
warnings.push("period_to_derived_from_as_of_date_for_vat_forecast");
|
||||
if (filters.period_from && filters.period_to && filters.period_from > filters.period_to) {
|
||||
filters.period_from = quarterWindow.period_from;
|
||||
warnings.push("period_from_adjusted_for_vat_as_of_window");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isManagementProfileIntent && !filters.period_to && !filters.as_of_date) {
|
||||
filters.period_to = new Date().toISOString().slice(0, 10);
|
||||
warnings.push("period_to_defaulted_today_for_management_profile");
|
||||
}
|
||||
const explicitAsOfDate = extractAsOfDate(text);
|
||||
if (usesAsOfPrimaryWindow(intent) && explicitAsOfDate) {
|
||||
filters.as_of_date = explicitAsOfDate;
|
||||
const periodWasDerivedHeuristically = warnings.includes("period_derived_from_month_phrase") ||
|
||||
|
||||
@@ -442,8 +442,25 @@ function hasFuzzyLexeme(text, lexemeRoots) {
|
||||
return false;
|
||||
}
|
||||
function hasCompactAccountCodeToken(text) {
|
||||
// Match compact account tokens like 60.01 / 62, while avoiding date fragments.
|
||||
return /(?<![\d-])\d{2}(?:[.,]\d{1,2})?(?![\d-])/u.test(text);
|
||||
// Match compact account tokens while reducing false positives on short-year literals like "22 год".
|
||||
const source = String(text ?? "");
|
||||
if (!source) {
|
||||
return false;
|
||||
}
|
||||
// Safe compact form: 60.01 / 62.1
|
||||
if (/(?<![\d-])\d{2}[.,]\d{1,2}(?![\d-])/u.test(source)) {
|
||||
return true;
|
||||
}
|
||||
// Plain two-digit code is accepted only in explicit account context.
|
||||
if (/(?:сч[её]т|account)\D{0,12}\d{2}(?![\d-])/iu.test(source)) {
|
||||
return true;
|
||||
}
|
||||
if (/(?:^|\s)по\s+\d{2}(?=$|[\s,.;:!?])/iu.test(source)) {
|
||||
if (!/(?:^|\s)(?:за|в)\s+\d{2}\s*(?:г(?:од|ода)?|year)\b/iu.test(source)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function hasDocumentsFormingBalanceSignal(text) {
|
||||
if (hasAny(text, DOCUMENTS_FORMING_BALANCE_HINTS)) {
|
||||
@@ -496,6 +513,13 @@ function hasAccountBalanceSignal(text) {
|
||||
const hasFollowupBalanceVerb = /(?:вернись|вернуться|вернуть|back|return)/iu.test(text);
|
||||
return hasAccountLexeme && hasAsOfStyleDate && hasFollowupBalanceVerb;
|
||||
}
|
||||
function hasForecastTaxSignal(text) {
|
||||
const hasForecastLexeme = /(?:прогноз|forecast|план(?:\s+платежа|\s+оплаты)?|прикин(?:уть|ем|у|ь|ул|ули|усь|усь))/iu.test(text);
|
||||
const hasVatLexeme = /(?:ндс|vat)/iu.test(text);
|
||||
const hasTaxLexeme = /(?:ндс|vat|налог)/iu.test(text);
|
||||
const hasVatPayableEstimatePattern = /(?:(?:сколько|скока|скок).{0,48}(?:ндс|vat).{0,48}(?:надо|нужно|к\s+уплате|заплатить|уплатить|платеж|платежа|платежей|платежку)|(?:ндс|vat).{0,48}(?:к\s+уплате|надо|нужно|заплатить|уплатить)|(?:сколько|скока|скок).{0,32}(?:надо|нужно).{0,32}(?:заплатить|уплатить).{0,32}(?:ндс|vat))/iu.test(text);
|
||||
return (hasForecastLexeme && hasTaxLexeme) || (hasVatLexeme && hasVatPayableEstimatePattern);
|
||||
}
|
||||
function hasPeriodCoverageProfileSignal(text) {
|
||||
if (hasAny(text, PERIOD_COVERAGE_PROFILE_HINTS)) {
|
||||
return true;
|
||||
@@ -652,7 +676,9 @@ function hasCustomerRevenueAndPaymentsSignal(text) {
|
||||
const asksIncomingFlow = /(?:приход|поступлен|входящ|зачислен|inflow|incoming)/iu.test(text);
|
||||
const asksDealBudgetRanking = /(?:сделк|deal|бюджет)/iu.test(text) &&
|
||||
/(?:топ|top|сам(?:ый|ая|ое|ые)|крупн|мален|жирн|мелк|больше\s+всего|чаще\s+всего|наибольш|максимальн|минимальн)/iu.test(text);
|
||||
const asksValue = /(?:доходн|выручк|приход|поступлен|входящ|зачислен|оплат|плат(?:еж|ёж|ежн|ежей|ежа|ит|ят)|деньг|денег|чек|сделк|бюджет|занес|занёс|принес|принёс|revenue|inflow|deal)/iu.test(text);
|
||||
const asksRevenueTotal = /(?:сколько|скока|скок).*(?:денег|выручк|доход|заработ|оборот)/iu.test(text);
|
||||
const asksOverallTurnover = /(?:общ(?:ий|ие|ая)\s+оборот|общ(?:ая|ий)\s+выручк|total\s+turnover|turnover\s+total)/iu.test(text);
|
||||
const asksValue = /(?:доходн|выручк|приход|поступлен|входящ|зачислен|оплат|плат(?:еж|ёж|ежн|ежей|ежа|ит|ят)|деньг|денег|заработ|оборот|чек|сделк|бюджет|занес|занёс|принес|принёс|revenue|inflow|deal|turnover)/iu.test(text);
|
||||
const asksRankOrTop = /(?:топ|top|сам(?:ый|ая|ое|ые)|крупн|мален|жирн|мелк|больше\s+всего|чаще\s+всего|наибольш|максимальн)/iu.test(text);
|
||||
const asksCountOnly = /(?:сколько|скока|скок)\s+/iu.test(text) && !asksValue;
|
||||
if (asksCountOnly) {
|
||||
@@ -670,6 +696,9 @@ function hasCustomerRevenueAndPaymentsSignal(text) {
|
||||
if (!hasFuzzySupplierLexeme && asksWhoPays && (asksRankOrTop || hasCounterpartyLexeme)) {
|
||||
return true;
|
||||
}
|
||||
if (!hasFuzzySupplierLexeme && (asksRevenueTotal || asksOverallTurnover)) {
|
||||
return true;
|
||||
}
|
||||
if (asksCounterpartySource && asksValue) {
|
||||
return true;
|
||||
}
|
||||
@@ -1065,6 +1094,13 @@ function hasAccountNumberAnchor(text) {
|
||||
}
|
||||
function resolveAddressIntent(userMessage) {
|
||||
const text = String(userMessage ?? "").trim().toLowerCase();
|
||||
if (hasForecastTaxSignal(text)) {
|
||||
return {
|
||||
intent: "vat_payable_forecast",
|
||||
confidence: "high",
|
||||
reasons: ["forecast_tax_signal_detected"]
|
||||
};
|
||||
}
|
||||
if (hasAny(text, RECEIVABLES_STRONG)) {
|
||||
return {
|
||||
intent: "list_receivables_counterparties",
|
||||
|
||||
+31
-8
@@ -427,7 +427,12 @@ function collectAnalyticsStrings(row) {
|
||||
"Counterparty",
|
||||
"Контрагент",
|
||||
"Contract",
|
||||
"Договор"
|
||||
"Договор",
|
||||
"Organization",
|
||||
"Организация",
|
||||
"ОрганизацияПредставление",
|
||||
"organization",
|
||||
"organization_name"
|
||||
];
|
||||
const collected = [];
|
||||
for (const key of fixedKeys) {
|
||||
@@ -438,7 +443,12 @@ function collectAnalyticsStrings(row) {
|
||||
}
|
||||
for (const [key, rawValue] of Object.entries(row)) {
|
||||
const lowerKey = key.toLowerCase();
|
||||
if (lowerKey.includes("subconto") || lowerKey.includes("субконто") || lowerKey.includes("контраг") || lowerKey.includes("договор")) {
|
||||
if (lowerKey.includes("subconto") ||
|
||||
lowerKey.includes("субконто") ||
|
||||
lowerKey.includes("контраг") ||
|
||||
lowerKey.includes("договор") ||
|
||||
lowerKey.includes("organization") ||
|
||||
lowerKey.includes("организац")) {
|
||||
const value = valueAsString(rawValue).trim();
|
||||
if (value) {
|
||||
collected.push(value);
|
||||
@@ -533,6 +543,14 @@ function applyAddressFilters(rows, filters) {
|
||||
mismatchReason = "contract_anchor_not_matched_in_materialized_rows";
|
||||
}
|
||||
}
|
||||
if (filters.organization && String(filters.organization).trim()) {
|
||||
const needle = String(filters.organization);
|
||||
const before = filtered.length;
|
||||
filtered = filtered.filter((row) => matchesAnchorText(rowSearchableText(row), needle));
|
||||
if (before > 0 && filtered.length === 0 && mismatchReason === null) {
|
||||
mismatchReason = "organization_anchor_not_matched_in_materialized_rows";
|
||||
}
|
||||
}
|
||||
if (filters.document_ref && String(filters.document_ref).trim()) {
|
||||
const needle = String(filters.document_ref);
|
||||
const before = filtered.length;
|
||||
@@ -825,6 +843,11 @@ class AddressQueryService {
|
||||
return null;
|
||||
}
|
||||
const { mode, shape, intent, filters, baseReasons } = decompose;
|
||||
const composeOptionsFromFilters = (filterSet) => ({
|
||||
userMessage,
|
||||
periodFrom: typeof filterSet.period_from === "string" ? filterSet.period_from : undefined,
|
||||
periodTo: typeof filterSet.period_to === "string" ? filterSet.period_to : undefined
|
||||
});
|
||||
let anchor = (0, resolveStage_1.resolvePrimaryAnchor)(intent.intent, filters.extracted_filters);
|
||||
const recipeSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(intent.intent, filters.extracted_filters);
|
||||
if (intent.intent === "unknown") {
|
||||
@@ -1043,7 +1066,7 @@ class AddressQueryService {
|
||||
const recoveredBankRows = applyIntentSpecificFilter("bank_operations_by_contract", filterByAnchors);
|
||||
const recoveredRows = recoveredBankRows.length > 0 ? recoveredBankRows : filterByAnchors;
|
||||
if (recoveredRows.length > 0) {
|
||||
const factual = (0, composeStage_1.composeFactualReply)(intent.intent, recoveredRows, { userMessage });
|
||||
const factual = (0, composeStage_1.composeFactualReply)(intent.intent, recoveredRows, composeOptionsFromFilters(filters.extracted_filters));
|
||||
const recoveryReason = recoveredBankRows.length > 0
|
||||
? "contract_docs_recovered_via_bank_fallback"
|
||||
: "contract_docs_recovered_via_anchor_rows";
|
||||
@@ -1150,7 +1173,7 @@ class AddressQueryService {
|
||||
rowsAnchorMatched: expandedRowsByAnchor.length,
|
||||
rowsMatched: expandedFilteredRows.length
|
||||
});
|
||||
const expandedFactual = (0, composeStage_1.composeFactualReply)(intent.intent, expandedFilteredRows, { userMessage });
|
||||
const expandedFactual = (0, composeStage_1.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"];
|
||||
@@ -1252,7 +1275,7 @@ class AddressQueryService {
|
||||
});
|
||||
const observedWindow = deriveObservedPeriodWindow(broadenedFilteredRows);
|
||||
const broadenedPrefix = composeAutoBroadenedPeriodPrefix(filters.extracted_filters, observedWindow);
|
||||
const broadenedFactual = (0, composeStage_1.composeFactualReply)(intent.intent, broadenedFilteredRows, { userMessage });
|
||||
const broadenedFactual = (0, composeStage_1.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 {
|
||||
@@ -1357,7 +1380,7 @@ class AddressQueryService {
|
||||
rowsAnchorMatched: historicalRowsByAnchor.length,
|
||||
rowsMatched: historicalFilteredRows.length
|
||||
});
|
||||
const historicalFactual = (0, composeStage_1.composeFactualReply)(intent.intent, historicalFilteredRows, { userMessage });
|
||||
const historicalFactual = (0, composeStage_1.composeFactualReply)(intent.intent, historicalFilteredRows, composeOptionsFromFilters(historicalFilters));
|
||||
const historicalPrefix = "Найдены данные в историческом срезе базы по вашему запросу.";
|
||||
const historicalSuggestion = intent.intent === "list_documents_by_counterparty"
|
||||
? "\nЕсли нужно, могу дополнительно показать платежи и договоры по этому контрагенту."
|
||||
@@ -1421,7 +1444,7 @@ class AddressQueryService {
|
||||
(stageStatus === "materialized_but_not_anchor_matched" || stageStatus === "materialized_but_filtered_out_by_recipe")) {
|
||||
const documentBankFallbackRows = applyIntentSpecificFilter(intent.intent, normalizedRows);
|
||||
if (documentBankFallbackRows.length > 0) {
|
||||
const fallbackFactual = (0, composeStage_1.composeFactualReply)(intent.intent, documentBankFallbackRows, { userMessage });
|
||||
const fallbackFactual = (0, composeStage_1.composeFactualReply)(intent.intent, documentBankFallbackRows, composeOptionsFromFilters(filters.extracted_filters));
|
||||
const fallbackPrefix = "По вашему запросу показываю найденные документы и операции в доступном срезе базы.";
|
||||
const fallbackSuggestion = intent.intent === "list_documents_by_counterparty"
|
||||
? "\nЕсли нужно, могу дополнительно сузить период или показать только платежи."
|
||||
@@ -1583,7 +1606,7 @@ class AddressQueryService {
|
||||
reasons: baseReasons
|
||||
});
|
||||
}
|
||||
const factual = (0, composeStage_1.composeFactualReply)(intent.intent, filteredRows, { userMessage });
|
||||
const factual = (0, composeStage_1.composeFactualReply)(intent.intent, filteredRows, composeOptionsFromFilters(filters.extracted_filters));
|
||||
return {
|
||||
handled: true,
|
||||
reply_text: factual.text,
|
||||
|
||||
+137
-14
@@ -2,6 +2,7 @@
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.selectAddressRecipe = selectAddressRecipe;
|
||||
exports.buildAddressRecipePlan = buildAddressRecipePlan;
|
||||
const config_1 = require("../config");
|
||||
const MOVEMENTS_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
Движения.Период КАК Период,
|
||||
@@ -333,6 +334,65 @@ const CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE = `
|
||||
ИЗ
|
||||
Справочник.ДоговорыКонтрагентов КАК Договоры
|
||||
`;
|
||||
const VAT_PAYABLE_FORECAST_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ
|
||||
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||||
"VAT_68_CREDIT" КАК Регистратор,
|
||||
"68" КАК СчетДт,
|
||||
"" КАК СчетКт,
|
||||
СУММА(ВЫБОР
|
||||
КОГДА __VAT68_KT_MATCH__
|
||||
ТОГДА Движения.Сумма
|
||||
ИНАЧЕ 0
|
||||
КОНЕЦ) КАК Сумма
|
||||
ИЗ
|
||||
РегистрБухгалтерии.Хозрасчетный КАК Движения
|
||||
__WHERE_CLAUSE__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ
|
||||
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||||
"VAT_68_DEBIT" КАК Регистратор,
|
||||
"68" КАК СчетДт,
|
||||
"" КАК СчетКт,
|
||||
СУММА(ВЫБОР
|
||||
КОГДА __VAT68_DT_MATCH__
|
||||
ТОГДА Движения.Сумма
|
||||
ИНАЧЕ 0
|
||||
КОНЕЦ) КАК Сумма
|
||||
ИЗ
|
||||
РегистрБухгалтерии.Хозрасчетный КАК Движения
|
||||
__WHERE_CLAUSE__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ
|
||||
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||||
"VAT_19_DEBIT" КАК Регистратор,
|
||||
"19" КАК СчетДт,
|
||||
"" КАК СчетКт,
|
||||
СУММА(ВЫБОР
|
||||
КОГДА __VAT19_DT_MATCH__
|
||||
ТОГДА Движения.Сумма
|
||||
ИНАЧЕ 0
|
||||
КОНЕЦ) КАК Сумма
|
||||
ИЗ
|
||||
РегистрБухгалтерии.Хозрасчетный КАК Движения
|
||||
__WHERE_CLAUSE__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ
|
||||
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||||
"VAT_19_CREDIT" КАК Регистратор,
|
||||
"19" КАК СчетДт,
|
||||
"" КАК СчетКт,
|
||||
СУММА(ВЫБОР
|
||||
КОГДА __VAT19_KT_MATCH__
|
||||
ТОГДА Движения.Сумма
|
||||
ИНАЧЕ 0
|
||||
КОНЕЦ) КАК Сумма
|
||||
ИЗ
|
||||
РегистрБухгалтерии.Хозрасчетный КАК Движения
|
||||
__WHERE_CLAUSE__
|
||||
УПОРЯДОЧИТЬ ПО
|
||||
Регистратор
|
||||
`;
|
||||
const BASE_RECIPES = [
|
||||
{
|
||||
recipe_id: "address_period_coverage_profile_v1",
|
||||
@@ -414,6 +474,16 @@ const BASE_RECIPES = [
|
||||
account_scope_mode: "preferred",
|
||||
query_template: "contract_value_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_vat_payable_forecast_v1",
|
||||
intent: "vat_payable_forecast",
|
||||
purpose: "Estimate VAT payable from factual turnovers on accounts 68 and 19 for selected period",
|
||||
required_filters: [],
|
||||
optional_filters: ["period_from", "period_to", "as_of_date", "organization"],
|
||||
default_limit: 32,
|
||||
account_scope_mode: "preferred",
|
||||
query_template: "vat_payable_forecast_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_contracts_by_counterparty_v1",
|
||||
intent: "list_contracts_by_counterparty",
|
||||
@@ -632,6 +702,50 @@ function buildMovementAccountCondition(filters) {
|
||||
}
|
||||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
||||
}
|
||||
function normalizeAccountPrefixForQuery(value) {
|
||||
const normalized = String(value ?? "")
|
||||
.trim()
|
||||
.replace(",", ".")
|
||||
.replace(/[^0-9.]+/g, "");
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
if (!/^\d{2}(?:\.\d{1,3})*$/.test(normalized)) {
|
||||
return null;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
function accountPrefixVariants(prefix) {
|
||||
const value = normalizeAccountPrefixForQuery(prefix);
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
const variants = new Set([value]);
|
||||
const segments = value.split(".");
|
||||
if (segments.length <= 1) {
|
||||
return Array.from(variants);
|
||||
}
|
||||
const base = segments[0];
|
||||
const normalizedTail = segments.slice(1).map((segment) => {
|
||||
const trimmed = segment.replace(/^0+(?=\d)/, "");
|
||||
return trimmed.length > 0 ? trimmed : "0";
|
||||
});
|
||||
const compact = [base, ...normalizedTail].join(".");
|
||||
if (compact !== value) {
|
||||
variants.add(compact);
|
||||
}
|
||||
return Array.from(variants);
|
||||
}
|
||||
function buildAccountPrefixPredicate(fieldPath, prefixes) {
|
||||
const normalizedPrefixes = Array.from(new Set((prefixes ?? [])
|
||||
.flatMap((item) => accountPrefixVariants(item))
|
||||
.filter((item) => Boolean(item))));
|
||||
if (normalizedPrefixes.length === 0) {
|
||||
return "ЛОЖЬ";
|
||||
}
|
||||
const clauses = normalizedPrefixes.map((prefix) => `ПОДСТРОКА(ЕСТЬNULL(${fieldPath}.Код, ""), 1, ${prefix.length}) = "${prefix}"`);
|
||||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
||||
}
|
||||
function shouldBoostLimitForAllTimeCounterparty(filters) {
|
||||
const hasAnchor = (typeof filters.counterparty === "string" && filters.counterparty.trim().length > 0) ||
|
||||
(typeof filters.contract === "string" && filters.contract.trim().length > 0);
|
||||
@@ -652,6 +766,7 @@ function maxLimitForIntent(intent) {
|
||||
intent === "customer_revenue_and_payments" ||
|
||||
intent === "supplier_payouts_profile" ||
|
||||
intent === "contract_usage_and_value" ||
|
||||
intent === "vat_payable_forecast" ||
|
||||
intent === "list_contracts_by_counterparty" ||
|
||||
intent === "list_documents_by_counterparty" ||
|
||||
intent === "bank_operations_by_counterparty" ||
|
||||
@@ -690,7 +805,8 @@ function buildAddressRecipePlan(recipe, filters) {
|
||||
const isManagementAggregateRecipe = recipe.query_template === "period_profile" ||
|
||||
recipe.query_template === "document_section_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";
|
||||
const baseLimit = typeof filters.limit === "number" && Number.isFinite(filters.limit)
|
||||
? Math.max(1, Math.min(maxLimit, Math.trunc(filters.limit)))
|
||||
: recipe.default_limit;
|
||||
@@ -750,19 +866,26 @@ function buildAddressRecipePlan(recipe, filters) {
|
||||
.replaceAll("__WHERE_IN_VALUE__", buildContractValueWhereClause(filters, "БанкПоступление.Дата", "БанкПоступление.ДоговорКонтрагента"))
|
||||
.replaceAll("__WHERE_OUT_VALUE__", buildContractValueWhereClause(filters, "БанкСписание.Дата", "БанкСписание.ДоговорКонтрагента"))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort))
|
||||
: 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 === "vat_payable_forecast_profile"
|
||||
? VAT_PAYABLE_FORECAST_QUERY_TEMPLATE
|
||||
.replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период"))
|
||||
.replaceAll("__VAT68_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", config_1.VAT_PAYABLE_68_PREFIXES))
|
||||
.replaceAll("__VAT68_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", config_1.VAT_PAYABLE_68_PREFIXES))
|
||||
.replaceAll("__VAT19_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", config_1.VAT_PAYABLE_19_PREFIXES))
|
||||
.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));
|
||||
return {
|
||||
recipe,
|
||||
query,
|
||||
|
||||
@@ -75,6 +75,69 @@ function formatPercent(value, total) {
|
||||
}
|
||||
return `${((value / total) * 100).toFixed(1)}%`;
|
||||
}
|
||||
function formatMoney(value) {
|
||||
if (!Number.isFinite(value)) {
|
||||
return "0.00";
|
||||
}
|
||||
return value.toFixed(2);
|
||||
}
|
||||
function parseIsoDateToken(value) {
|
||||
const source = String(value ?? "").trim();
|
||||
const match = source.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const year = Number(match[1]);
|
||||
const month = Number(match[2]);
|
||||
const day = Number(match[3]);
|
||||
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
|
||||
return null;
|
||||
}
|
||||
if (month < 1 || month > 12 || day < 1 || day > 31) {
|
||||
return null;
|
||||
}
|
||||
return { year, month, day };
|
||||
}
|
||||
function toIsoDate(year, month, day) {
|
||||
return `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
||||
}
|
||||
function formatDateRu(isoDate) {
|
||||
const parsed = parseIsoDateToken(isoDate);
|
||||
if (!parsed) {
|
||||
return isoDate;
|
||||
}
|
||||
return `${String(parsed.day).padStart(2, "0")}.${String(parsed.month).padStart(2, "0")}.${String(parsed.year).padStart(4, "0")}`;
|
||||
}
|
||||
function buildIsoDateWithMonthShift(year, monthOneBased, day, monthShift = 0) {
|
||||
const date = new Date(Date.UTC(year, monthOneBased - 1 + monthShift, day));
|
||||
return date.toISOString().slice(0, 10);
|
||||
}
|
||||
function deriveVatDeadlineCalendar(periodFrom, periodTo) {
|
||||
const reference = parseIsoDateToken(periodTo) ?? parseIsoDateToken(periodFrom);
|
||||
if (!reference) {
|
||||
return null;
|
||||
}
|
||||
const quarterIndex = Math.floor((reference.month - 1) / 3);
|
||||
const quarterNumber = quarterIndex + 1;
|
||||
const quarterStartMonth = quarterIndex * 3 + 1;
|
||||
const quarterEndMonth = quarterStartMonth + 2;
|
||||
const quarterEndDay = new Date(Date.UTC(reference.year, quarterEndMonth, 0)).getUTCDate();
|
||||
const quarterStart = toIsoDate(reference.year, quarterStartMonth, 1);
|
||||
const quarterEnd = toIsoDate(reference.year, quarterEndMonth, quarterEndDay);
|
||||
const declarationDueDate = buildIsoDateWithMonthShift(reference.year, quarterEndMonth, 25, 1);
|
||||
const payment1 = buildIsoDateWithMonthShift(reference.year, quarterEndMonth, 28, 1);
|
||||
const payment2 = buildIsoDateWithMonthShift(reference.year, quarterEndMonth, 28, 2);
|
||||
const payment3 = buildIsoDateWithMonthShift(reference.year, quarterEndMonth, 28, 3);
|
||||
return {
|
||||
periodLabel: `${quarterNumber} кв. ${reference.year}`,
|
||||
quarterStart,
|
||||
quarterEnd,
|
||||
declarationDueDate,
|
||||
paymentDueDates: [payment1, payment2, payment3],
|
||||
windowFrom: periodFrom ?? null,
|
||||
windowTo: periodTo ?? null
|
||||
};
|
||||
}
|
||||
function extractAccountSectionCode(value) {
|
||||
const source = String(value ?? "").trim();
|
||||
if (!source) {
|
||||
@@ -93,6 +156,17 @@ function normalizeQuestionText(value) {
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
function needsVatWhyExplanation(userMessage) {
|
||||
const text = normalizeQuestionText(userMessage);
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
const asksReason = /(?:почему|why|из[-\s]?за\s+чего|как\s+так|reason)/iu.test(text);
|
||||
if (!asksReason) {
|
||||
return false;
|
||||
}
|
||||
return /(?:ндс|vat|прогноз|к\s+уплате|нул|ноль|\b0(?:[.,]0+)?\b)/iu.test(text);
|
||||
}
|
||||
function detectRankingLimit(userMessage, fallback = 20) {
|
||||
const text = normalizeQuestionText(userMessage);
|
||||
if (!text) {
|
||||
@@ -1004,6 +1078,70 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
text: lines.join("\n")
|
||||
};
|
||||
}
|
||||
if (intent === "vat_payable_forecast") {
|
||||
const rowsByMarker = new Map();
|
||||
for (const row of rows) {
|
||||
const marker = String(row.registrator ?? "").trim().toUpperCase();
|
||||
if (!marker) {
|
||||
continue;
|
||||
}
|
||||
const nextValue = (rowsByMarker.get(marker) ?? 0) + (row.amount ?? 0);
|
||||
rowsByMarker.set(marker, nextValue);
|
||||
}
|
||||
const turnover68Credit = rowsByMarker.get("VAT_68_CREDIT") ?? 0;
|
||||
const turnover68Debit = rowsByMarker.get("VAT_68_DEBIT") ?? 0;
|
||||
const turnover19Debit = rowsByMarker.get("VAT_19_DEBIT") ?? 0;
|
||||
const turnover19Credit = rowsByMarker.get("VAT_19_CREDIT") ?? 0;
|
||||
const netVat = turnover68Credit - turnover68Debit;
|
||||
const vatToPay = Math.max(0, netVat);
|
||||
const carryoverOrOverpayment = Math.max(0, -netVat);
|
||||
const totalVatTurnoverAbs = Math.abs(turnover68Credit) + Math.abs(turnover68Debit) + Math.abs(turnover19Debit) + Math.abs(turnover19Credit);
|
||||
const vatActivityDetected = totalVatTurnoverAbs > 0.0000001;
|
||||
const netVatIsEffectivelyZero = Math.abs(netVat) <= 0.005;
|
||||
const explainWhyRequested = needsVatWhyExplanation(options.userMessage);
|
||||
const vatCalendar = deriveVatDeadlineCalendar(options.periodFrom, options.periodTo);
|
||||
const lines = [
|
||||
"Собран прогноз НДС к уплате по фактическим проводкам (НДС-субсчета 68.02*/19*).",
|
||||
`Строк агрегата: ${rows.length}.`,
|
||||
`Оборот по кредиту 68*: ${formatMoney(turnover68Credit)}.`,
|
||||
`Оборот по дебету 68*: ${formatMoney(turnover68Debit)}.`,
|
||||
`Нетто НДС (68 Кт - 68 Дт): ${formatMoney(netVat)}.`,
|
||||
`Прогноз НДС к уплате: ${formatMoney(vatToPay)}.`,
|
||||
`Потенциальный перенос/переплата: ${formatMoney(carryoverOrOverpayment)}.`,
|
||||
`Справочно по 19*: дебет ${formatMoney(turnover19Debit)}, кредит ${formatMoney(turnover19Credit)}.`
|
||||
];
|
||||
if (!vatActivityDetected) {
|
||||
lines.push("В выбранном окне не найдено движений по НДС-субсчетам 68.02*/19*; поэтому оперативный прогноз к уплате равен 0.00.");
|
||||
}
|
||||
else if (vatToPay === 0 && netVatIsEffectivelyZero) {
|
||||
lines.push("В выбранном окне обороты по 68* взаимно перекрылись (нетто близко к нулю), поэтому к уплате 0.00.");
|
||||
}
|
||||
else if (vatToPay === 0 && netVat < 0) {
|
||||
lines.push("В выбранном окне дебет 68* превышает кредит 68*; сумма показана как перенос/переплата, к уплате 0.00.");
|
||||
}
|
||||
if (vatToPay === 0) {
|
||||
lines.push("Чеклист проверки в 1С (почему к уплате 0):", `1) Проверьте ОСВ/анализ счета по 68.02 и 19 за окно ${options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : "расчета"}.`, "2) Проверьте наличие движений в РегистрБухгалтерии.Хозрасчетный по счетам 68.02*/19* (включая субсчета).", "3) Сверьте счета-фактуры, корректировки и момент принятия НДС к вычету (не попали ли в другой период).", "4) Сверьте книгу продаж/покупок и операции Помощника по учету НДС за тот же период.", "5) Убедитесь, что документы проведены, период закрыт корректно и нет неподтвержденных/неперепроведенных документов.");
|
||||
}
|
||||
if (vatCalendar) {
|
||||
const periodWindowLabel = vatCalendar.windowFrom && vatCalendar.windowTo
|
||||
? `${formatDateRu(vatCalendar.windowFrom)}..${formatDateRu(vatCalendar.windowTo)}`
|
||||
: `${formatDateRu(vatCalendar.quarterStart)}..${formatDateRu(vatCalendar.quarterEnd)}`;
|
||||
const [payment1, payment2, payment3] = vatCalendar.paymentDueDates;
|
||||
const installmentRaw = vatToPay / 3;
|
||||
const installmentRounded = Number(installmentRaw.toFixed(2));
|
||||
const installmentThird = Number((vatToPay - installmentRounded * 2).toFixed(2));
|
||||
lines.push(`Период расчета (срез обязательств): ${periodWindowLabel}.`, `Налоговый период: ${vatCalendar.periodLabel}.`, `Срок сдачи декларации: до ${formatDateRu(vatCalendar.declarationDueDate)}.`, `Сроки уплаты: ${formatDateRu(payment1)}, ${formatDateRu(payment2)}, ${formatDateRu(payment3)}.`, `Ориентир по долям к уплате: ${formatMoney(installmentRounded)} / ${formatMoney(installmentRounded)} / ${formatMoney(installmentThird)}.`, "Важно: даже при нулевой сумме к уплате декларация по НДС подается в установленный срок; переносы по выходным/праздникам сверяйте по календарю ФНС/1С.");
|
||||
}
|
||||
if (explainWhyRequested) {
|
||||
lines.push("Почему прогноз к уплате 0: в текущей модели используем формулу max(0, 68 Кт - 68 Дт).", `За период 68 Кт = ${formatMoney(turnover68Credit)}, 68 Дт = ${formatMoney(turnover68Debit)}, разница = ${formatMoney(netVat)}.`, netVat <= 0
|
||||
? "Разница неположительная, поэтому к уплате = 0, а отрицательная часть показана как перенос/переплата."
|
||||
: "Разница положительная, поэтому к уплате берется эта положительная величина.", "Важно: это оперативный прогноз по оборотам НДС-субсчетов 68.02*/19*; финальную сумму налога подтверждают регистры НДС и декларация.");
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_SUMMARY",
|
||||
text: lines.join("\n")
|
||||
};
|
||||
}
|
||||
if (intent === "account_balance_snapshot") {
|
||||
const movementSum = rows.reduce((sum, row) => sum + (row.amount ?? 0), 0);
|
||||
const lines = [
|
||||
|
||||
@@ -247,6 +247,11 @@ function hasAddressFollowupContextSignal(text) {
|
||||
return true;
|
||||
}
|
||||
const tokenCount = normalized.split(/\s+/).filter(Boolean).length;
|
||||
if (tokenCount <= 12 &&
|
||||
/(?:почему|why|из[-\s]?за\s+чего|как\s+так|reason)/iu.test(normalized) &&
|
||||
/(?:ндс|vat|прогноз|к\s+уплате|нул|ноль|\b0(?:[.,]0+)?\b)/iu.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
const hasPeriodLiteral = /\b(?:19|20)\d{2}(?:[./-](?:0?[1-9]|1[0-2]))?\b/.test(normalized);
|
||||
if (tokenCount <= 8 && hasPeriodLiteral) {
|
||||
return true;
|
||||
@@ -264,11 +269,16 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
const previousCounterparty = toNonEmptyString(previous.counterparty);
|
||||
const previousContract = toNonEmptyString(previous.contract);
|
||||
const previousAccount = toNonEmptyString(previous.account);
|
||||
const previousOrganization = toNonEmptyString(previous.organization);
|
||||
const previousAsOfDate = toNonEmptyString(previous.as_of_date);
|
||||
const previousPeriodFrom = toNonEmptyString(previous.period_from);
|
||||
const previousPeriodTo = toNonEmptyString(previous.period_to);
|
||||
const allTimeRequested = hasAllTimeHint(userMessage);
|
||||
const sameDateRequested = hasSameDateHint(userMessage);
|
||||
if (!toNonEmptyString(merged.organization) && previousOrganization) {
|
||||
merged.organization = previousOrganization;
|
||||
reasons.push("organization_from_followup_context");
|
||||
}
|
||||
if (intent === "list_documents_by_counterparty" ||
|
||||
intent === "bank_operations_by_counterparty" ||
|
||||
intent === "list_contracts_by_counterparty") {
|
||||
@@ -350,9 +360,26 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
reasons.push("period_to_cleared_for_lifecycle_followup");
|
||||
}
|
||||
}
|
||||
const hasFollowupSignal = hasAddressFollowupContextSignal(userMessage);
|
||||
const hasExplicitPeriodInMessage = hasExplicitPeriodLiteral(userMessage);
|
||||
const currentHasPeriod = hasExplicitPeriodWindow(merged);
|
||||
const previousHasPeriod = hasExplicitPeriodWindow(previous);
|
||||
if (!currentHasPeriod && previousHasPeriod && hasAddressFollowupContextSignal(userMessage)) {
|
||||
if (intent === "vat_payable_forecast" && previousHasPeriod && hasFollowupSignal && !hasExplicitPeriodInMessage) {
|
||||
const currentPeriodFrom = toNonEmptyString(merged.period_from);
|
||||
const currentPeriodTo = toNonEmptyString(merged.period_to);
|
||||
const todayIso = new Date().toISOString().slice(0, 10);
|
||||
const currentLooksDefaultedToToday = !currentPeriodFrom && currentPeriodTo === todayIso;
|
||||
if (!currentPeriodFrom || currentLooksDefaultedToToday) {
|
||||
if (previousPeriodFrom) {
|
||||
merged.period_from = previousPeriodFrom;
|
||||
}
|
||||
if (previousPeriodTo) {
|
||||
merged.period_to = previousPeriodTo;
|
||||
}
|
||||
reasons.push("period_from_followup_context");
|
||||
}
|
||||
}
|
||||
if (!currentHasPeriod && previousHasPeriod && hasFollowupSignal) {
|
||||
if (previousPeriodFrom) {
|
||||
merged.period_from = previousPeriodFrom;
|
||||
}
|
||||
@@ -477,7 +504,10 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||
function runAddressDecomposeStage(userMessage, followupContext) {
|
||||
const detectedMode = (0, addressQueryClassifier_1.detectAddressQuestionMode)(userMessage);
|
||||
const shape = (0, addressQueryShapeClassifier_1.classifyAddressQueryShape)(userMessage);
|
||||
if (shape.shape === "EXPLAIN_OR_REASON") {
|
||||
const allowExplainAsFollowup = shape.shape === "EXPLAIN_OR_REASON" &&
|
||||
Boolean(followupContext?.previous_intent) &&
|
||||
hasAddressFollowupContextSignal(userMessage);
|
||||
if (shape.shape === "EXPLAIN_OR_REASON" && !allowExplainAsFollowup) {
|
||||
return null;
|
||||
}
|
||||
const detectedIntent = (0, addressIntentResolver_1.resolveAddressIntent)(userMessage);
|
||||
|
||||
+2
-1
@@ -43,7 +43,8 @@ function inferAggregationProfile(intent, shape) {
|
||||
intent === "contract_usage_overview" ||
|
||||
intent === "customer_revenue_and_payments" ||
|
||||
intent === "supplier_payouts_profile" ||
|
||||
intent === "contract_usage_and_value") {
|
||||
intent === "contract_usage_and_value" ||
|
||||
intent === "vat_payable_forecast") {
|
||||
return "management_profile";
|
||||
}
|
||||
if (intent === "account_balance_snapshot" || intent === "documents_forming_balance") {
|
||||
|
||||
+734
-24
@@ -39,6 +39,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AssistantService = void 0;
|
||||
exports.evaluateCoverageForTests = evaluateCoverageForTests;
|
||||
exports.extractSubjectTokensForTests = extractSubjectTokensForTests;
|
||||
exports.resolveAssistantOrchestrationDecision = resolveAssistantOrchestrationDecision;
|
||||
exports.resolveSessionOrganizationScopeContextForTests = resolveSessionOrganizationScopeContextForTests;
|
||||
exports.extractOrganizationFactsFromRowsForTests = extractOrganizationFactsFromRowsForTests;
|
||||
exports.resolveOrganizationNamesByRefsForTests = resolveOrganizationNamesByRefsForTests;
|
||||
exports.resolveLivingAssistantModeDecision = resolveLivingAssistantModeDecision;
|
||||
@@ -1431,9 +1433,24 @@ function compactWhitespace(value) {
|
||||
return value.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
function hasAccountingSignal(text) {
|
||||
const lower = text.toLowerCase();
|
||||
if (/(?:^|[\s,;:])\d{2}(?:\.\d{2})?(?=$|[\s,.;:])/i.test(lower)) {
|
||||
return true;
|
||||
const lower = repairAddressMojibake(String(text ?? "")).toLowerCase();
|
||||
const excludedSpans = [...collectDateSpans(lower), ...collectAmountSpans(lower), ...collectPercentSpans(lower), ...collectContractSpans(lower)];
|
||||
const accountTokenPattern = /\b(?:01|02|07|08|10|13|19|20|21|23|25|26|28|29|41|43|44|50|51|52|55|57|58|60|62|66|67|68|69|70|71|73|75|76|80|81|84|90|91|97)(?:[.,]\d{1,2})?\b/g;
|
||||
let accountMatch = null;
|
||||
while ((accountMatch = accountTokenPattern.exec(lower)) !== null) {
|
||||
const token = String(accountMatch[0] ?? "").trim();
|
||||
if (!token) {
|
||||
continue;
|
||||
}
|
||||
const start = accountMatch.index;
|
||||
const end = start + token.length;
|
||||
if (intersectsAnySpan(start, end, excludedSpans)) {
|
||||
continue;
|
||||
}
|
||||
const hasExplicitSubaccount = /[.,]\d{1,2}/.test(token);
|
||||
if (hasExplicitSubaccount || hasAccountContextAround(lower, start, end) || countTokens(lower) <= 4) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return /(проводк|документ|реализац|поступлен|взаиморасчет|сальдо|остатк|счет|счёт|ндс|амортиз|рбп|контрагент|поставщик|покупател|оплат|банк|выписк|склад|товар|материал|закрыти|период|postavshchik|kontragent|schet|schetu|period|counterparty|supplier|invoice|posting|ledger|account|anomaly|risk)/i.test(lower);
|
||||
}
|
||||
@@ -1819,6 +1836,7 @@ function buildAddressDebugPayload(addressDebug, llmPreDecomposeMeta = null) {
|
||||
sanitized_user_message: llmMeta?.sanitizedUserMessage ?? null,
|
||||
tool_gate_decision: llmMeta?.toolGateDecision ?? null,
|
||||
tool_gate_reason: llmMeta?.toolGateReason ?? null,
|
||||
orchestration_contract_v1: llmMeta?.orchestrationContract ?? null,
|
||||
dialog_continuation_contract_v2: llmMeta?.dialogContinuationContract ?? null,
|
||||
address_retry_audit: llmMeta?.addressRetryAudit ?? null,
|
||||
answer_structure_v11: null,
|
||||
@@ -2394,6 +2412,15 @@ function repairAddressMojibake(value) {
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
function sanitizeOutgoingAssistantText(value, fallback = "Не смог сформировать читаемый ответ. Уточните запрос.") {
|
||||
const repaired = repairAddressMojibake(String(value ?? ""));
|
||||
const sanitized = String((0, answerComposer_1.sanitizeAssistantReplyForUserFacing)(repaired) ?? "").trim();
|
||||
if (sanitized) {
|
||||
return sanitized;
|
||||
}
|
||||
const fallbackText = String(fallback ?? "").trim();
|
||||
return fallbackText || "Не смог сформировать читаемый ответ. Уточните запрос.";
|
||||
}
|
||||
function extractAddressAnchorTokens(value) {
|
||||
const source = repairAddressMojibake(compactWhitespace(String(value ?? "").toLowerCase()));
|
||||
if (!source) {
|
||||
@@ -2632,6 +2659,11 @@ function hasAddressFollowupContextSignal(userMessage) {
|
||||
if (shortFollowup && /^(?:а|и)\s+кто\b/iu.test(text)) {
|
||||
return true;
|
||||
}
|
||||
if (shortFollowup &&
|
||||
/(?:почему|why|из[-\s]?за\s+чего|как\s+так|reason)/iu.test(text) &&
|
||||
/(?:ндс|vat|прогноз|к\s+уплате|нул|ноль|\b0(?:[.,]0+)?\b)/iu.test(text)) {
|
||||
return true;
|
||||
}
|
||||
if (shortFollowup &&
|
||||
/(?:^|\s)по\s+[a-zа-яё][a-zа-яё0-9._-]{1,}(?=$|[\s,.;:!?])/iu.test(text) &&
|
||||
!/(?:по\s+этому|по\s+тому|по\s+нему|по\s+ней|по\s+ним)/iu.test(text)) {
|
||||
@@ -2693,6 +2725,12 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||
previousFilters.counterparty = historicalCounterparty;
|
||||
}
|
||||
}
|
||||
if (!toNonEmptyString(previousFilters.organization)) {
|
||||
const historicalOrganization = findRecentAddressFilterValue(items, "organization");
|
||||
if (historicalOrganization) {
|
||||
previousFilters.organization = historicalOrganization;
|
||||
}
|
||||
}
|
||||
if (!previousIntent && !previousAnchor && Object.keys(previousFilters).length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -3409,6 +3447,7 @@ function resolveAddressToolGateDecision(addressInputMessage, followupContext, ll
|
||||
llmContractIntentConfidence !== "low";
|
||||
const hasLlmCanonicalDataSignal = Boolean(llmPreDecomposeMeta?.llmCanonicalCandidateDetected) &&
|
||||
Boolean(llmPreDecomposeMeta?.applied) &&
|
||||
llmContractMode === "address_query" &&
|
||||
hasStrongDataIntentSignal(repairedInputMessage);
|
||||
const hasLexicalAddressSignal = isAddressLlmPreDecomposeCandidate(addressInputMessage) ||
|
||||
isAddressLlmPreDecomposeCandidate(repairedInputMessage) ||
|
||||
@@ -3461,9 +3500,157 @@ function resolveAddressToolGateDecision(addressInputMessage, followupContext, ll
|
||||
reason: "no_address_signal_after_l0"
|
||||
};
|
||||
}
|
||||
function resolveAssistantOrchestrationDecision(input) {
|
||||
const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? "");
|
||||
const effectiveAddressUserMessage = String(input?.effectiveAddressUserMessage ?? rawUserMessage);
|
||||
const repairedRawUserMessage = repairAddressMojibake(rawUserMessage);
|
||||
const repairedEffectiveAddressUserMessage = repairAddressMojibake(effectiveAddressUserMessage);
|
||||
const followupContext = input?.followupContext ?? null;
|
||||
const llmPreDecomposeMeta = input?.llmPreDecomposeMeta ?? null;
|
||||
const useMock = Boolean(input?.useMock);
|
||||
const dataScopeMetaQuery = hasAssistantDataScopeMetaQuestionSignal(rawUserMessage) ||
|
||||
hasAssistantDataScopeMetaQuestionSignal(repairedRawUserMessage) ||
|
||||
hasAssistantDataScopeMetaQuestionSignal(effectiveAddressUserMessage) ||
|
||||
hasAssistantDataScopeMetaQuestionSignal(repairedEffectiveAddressUserMessage);
|
||||
const capabilityMetaQuery = shouldHandleAsAssistantCapabilityMetaQuery(rawUserMessage) ||
|
||||
shouldHandleAsAssistantCapabilityMetaQuery(repairedRawUserMessage) ||
|
||||
shouldHandleAsAssistantCapabilityMetaQuery(effectiveAddressUserMessage) ||
|
||||
shouldHandleAsAssistantCapabilityMetaQuery(repairedEffectiveAddressUserMessage);
|
||||
const dataRetrievalSignal = hasDataRetrievalRequestSignal(rawUserMessage) ||
|
||||
hasDataRetrievalRequestSignal(repairedRawUserMessage) ||
|
||||
hasDataRetrievalRequestSignal(effectiveAddressUserMessage) ||
|
||||
hasDataRetrievalRequestSignal(repairedEffectiveAddressUserMessage);
|
||||
const modeSample = repairedEffectiveAddressUserMessage || effectiveAddressUserMessage;
|
||||
const modeDetection = (0, addressQueryClassifier_1.detectAddressQuestionMode)(modeSample);
|
||||
const intentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(modeSample);
|
||||
const strongDataSignal = hasStrongDataIntentSignal(rawUserMessage) ||
|
||||
hasStrongDataIntentSignal(repairedRawUserMessage) ||
|
||||
hasStrongDataIntentSignal(effectiveAddressUserMessage) ||
|
||||
hasStrongDataIntentSignal(repairedEffectiveAddressUserMessage) ||
|
||||
hasAccountingSignal(rawUserMessage) ||
|
||||
hasAccountingSignal(repairedRawUserMessage) ||
|
||||
hasAccountingSignal(effectiveAddressUserMessage) ||
|
||||
hasAccountingSignal(repairedEffectiveAddressUserMessage) ||
|
||||
hasDataRetrievalRequestSignal(rawUserMessage) ||
|
||||
hasDataRetrievalRequestSignal(repairedRawUserMessage);
|
||||
const hardMetaMode = dataScopeMetaQuery
|
||||
? "data_scope"
|
||||
: capabilityMetaQuery && !dataRetrievalSignal
|
||||
? "capability"
|
||||
: null;
|
||||
if (hardMetaMode === "data_scope") {
|
||||
return {
|
||||
runAddressLane: false,
|
||||
toolGateDecision: "skip_address_lane",
|
||||
toolGateReason: "assistant_data_scope_query_detected",
|
||||
livingMode: "chat",
|
||||
livingReason: "assistant_data_scope_query_detected",
|
||||
orchestrationContract: {
|
||||
schema_version: "assistant_orchestration_contract_v1",
|
||||
hard_meta_mode: "data_scope",
|
||||
address_mode: modeDetection.mode,
|
||||
address_mode_confidence: modeDetection.confidence,
|
||||
address_intent: intentResolution.intent,
|
||||
address_intent_confidence: intentResolution.confidence,
|
||||
strong_data_signal_detected: strongDataSignal,
|
||||
data_retrieval_signal_detected: dataRetrievalSignal,
|
||||
followup_context_detected: Boolean(followupContext),
|
||||
unsupported_address_intent_fallback_to_deep: false,
|
||||
final_decision: {
|
||||
run_address_lane: false,
|
||||
tool_gate_decision: "skip_address_lane",
|
||||
tool_gate_reason: "assistant_data_scope_query_detected",
|
||||
living_mode: "chat",
|
||||
living_reason: "assistant_data_scope_query_detected"
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
if (hardMetaMode === "capability") {
|
||||
return {
|
||||
runAddressLane: false,
|
||||
toolGateDecision: "skip_address_lane",
|
||||
toolGateReason: "assistant_capability_query_detected",
|
||||
livingMode: "chat",
|
||||
livingReason: "assistant_capability_query_detected",
|
||||
orchestrationContract: {
|
||||
schema_version: "assistant_orchestration_contract_v1",
|
||||
hard_meta_mode: "capability",
|
||||
address_mode: modeDetection.mode,
|
||||
address_mode_confidence: modeDetection.confidence,
|
||||
address_intent: intentResolution.intent,
|
||||
address_intent_confidence: intentResolution.confidence,
|
||||
strong_data_signal_detected: strongDataSignal,
|
||||
data_retrieval_signal_detected: dataRetrievalSignal,
|
||||
followup_context_detected: Boolean(followupContext),
|
||||
unsupported_address_intent_fallback_to_deep: false,
|
||||
final_decision: {
|
||||
run_address_lane: false,
|
||||
tool_gate_decision: "skip_address_lane",
|
||||
tool_gate_reason: "assistant_capability_query_detected",
|
||||
living_mode: "chat",
|
||||
living_reason: "assistant_capability_query_detected"
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
const baseToolGate = resolveAddressToolGateDecision(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage);
|
||||
const unsupportedAddressIntentFallbackToDeep = Boolean(!followupContext &&
|
||||
baseToolGate?.runAddressLane &&
|
||||
modeDetection.mode === "address_query" &&
|
||||
intentResolution.intent === "unknown" &&
|
||||
strongDataSignal);
|
||||
let runAddressLane = Boolean(baseToolGate?.runAddressLane);
|
||||
let toolGateDecision = String(baseToolGate?.decision ?? "skip_address_lane");
|
||||
let toolGateReason = String(baseToolGate?.reason ?? "no_address_signal_after_l0");
|
||||
if (unsupportedAddressIntentFallbackToDeep) {
|
||||
runAddressLane = false;
|
||||
toolGateDecision = "skip_address_lane";
|
||||
toolGateReason = "address_signal_unsupported_intent_fallback_to_deep";
|
||||
}
|
||||
let livingDecision = resolveLivingAssistantModeDecision({
|
||||
userMessage: rawUserMessage,
|
||||
addressLaneTriggered: runAddressLane,
|
||||
useMock,
|
||||
predecomposeMode: llmPreDecomposeMeta?.predecomposeContract?.mode ?? null,
|
||||
predecomposeModeConfidence: llmPreDecomposeMeta?.predecomposeContract?.mode_confidence ?? null
|
||||
});
|
||||
if (unsupportedAddressIntentFallbackToDeep) {
|
||||
livingDecision = {
|
||||
mode: "deep_analysis",
|
||||
reason: "unsupported_address_intent_fallback_to_deep"
|
||||
};
|
||||
}
|
||||
return {
|
||||
runAddressLane,
|
||||
toolGateDecision,
|
||||
toolGateReason,
|
||||
livingMode: livingDecision.mode,
|
||||
livingReason: livingDecision.reason,
|
||||
orchestrationContract: {
|
||||
schema_version: "assistant_orchestration_contract_v1",
|
||||
hard_meta_mode: null,
|
||||
address_mode: modeDetection.mode,
|
||||
address_mode_confidence: modeDetection.confidence,
|
||||
address_intent: intentResolution.intent,
|
||||
address_intent_confidence: intentResolution.confidence,
|
||||
strong_data_signal_detected: strongDataSignal,
|
||||
data_retrieval_signal_detected: dataRetrievalSignal,
|
||||
followup_context_detected: Boolean(followupContext),
|
||||
unsupported_address_intent_fallback_to_deep: unsupportedAddressIntentFallbackToDeep,
|
||||
final_decision: {
|
||||
run_address_lane: runAddressLane,
|
||||
tool_gate_decision: toolGateDecision,
|
||||
tool_gate_reason: toolGateReason,
|
||||
living_mode: livingDecision.mode,
|
||||
living_reason: livingDecision.reason
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
function hasStrongDataIntentSignal(text) {
|
||||
const lower = String(text ?? "").toLowerCase();
|
||||
return /(база|док|документ|проводк|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|оборот|баланс|период|месяц|год|инн|mcp|bank|counterparty|contract|document|ledger|posting|account)/i.test(lower);
|
||||
return /(база|док|документ|проводк|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|оборот|баланс|период|месяц|год|инн|mcp|bank|counterparty|contract|document|ledger|posting|account|организац|компан|контор|фирм)/i.test(lower);
|
||||
}
|
||||
function hasDataRetrievalRequestSignal(text) {
|
||||
const lower = compactWhitespace(String(text ?? "").toLowerCase());
|
||||
@@ -3475,7 +3662,7 @@ function hasDataRetrievalRequestSignal(text) {
|
||||
if (!hasExplicitRetrievalAction && !hasInterrogativeRetrievalAction) {
|
||||
return false;
|
||||
}
|
||||
const hasRetrievalObject = /(1с|база|док|документ|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|период|месяц|год|инн|bank|counterparty|contract|document|account|balance|ledger|posting)/i.test(lower);
|
||||
const hasRetrievalObject = /(1с|база|док|документ|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|период|месяц|год|инн|bank|counterparty|contract|document|account|balance|ledger|posting|организац|компан|контор|фирм|возраст|дата\s+регистрац|регистрац|основан)/i.test(lower);
|
||||
if (!hasRetrievalObject) {
|
||||
return false;
|
||||
}
|
||||
@@ -3485,6 +3672,77 @@ function hasDataRetrievalRequestSignal(text) {
|
||||
const hasMetaCapabilityShape = /(?:мож(?:ем|ешь|ете|но)|уме(?:ешь|ете)|доступ|подключ|чья|как\s+называ(?:ет|ется)|работ(?:ать|аем|аешь|аете)|в\s+тебе|у\s+тебя)/i.test(lower);
|
||||
return !hasMetaCapabilityShape;
|
||||
}
|
||||
function hasOrganizationFactLookupSignal(text) {
|
||||
const repaired = repairAddressMojibake(String(text ?? ""));
|
||||
const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е");
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const hasFactCue = /(?:возраст|сколько\s+лет|дата\s+регистрац|когда\s+(?:зарегистр|создан|основан)|год\s+регистрац|год\s+основан|с\s+какого\s+года|when\s+was\s+(?:it\s+)?(?:registered|founded|created))/i.test(normalized);
|
||||
if (!hasFactCue) {
|
||||
return false;
|
||||
}
|
||||
return /(?:организац|компан|контор|фирм|ооо|ао|зао|ип|альтернатив|лайсвуд|райм|organization|company)/i.test(normalized);
|
||||
}
|
||||
function findLastAssistantLivingChatDebug(items) {
|
||||
if (!Array.isArray(items)) {
|
||||
return null;
|
||||
}
|
||||
for (let index = items.length - 1; index >= 0; index -= 1) {
|
||||
const item = items[index];
|
||||
if (!item || item.role !== "assistant") {
|
||||
continue;
|
||||
}
|
||||
if (item.debug && typeof item.debug === "object") {
|
||||
return item.debug;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function hasOrganizationFactFollowupSignal(userMessage, items) {
|
||||
const repaired = repairAddressMojibake(String(userMessage ?? ""));
|
||||
const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е");
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
if (hasOrganizationFactLookupSignal(normalized)) {
|
||||
return false;
|
||||
}
|
||||
const hasFollowupCue = /(?:^|\s)(?:давай|го|погнали|ок(?:ей)?|хорошо|принято|подтверждаю|запрашивай|запроси|проверь|продолжай|ну\s+давай|да\s+давай)(?=$|[\s,.!?;:])/iu.test(normalized);
|
||||
if (!hasFollowupCue) {
|
||||
return false;
|
||||
}
|
||||
const lastDebug = findLastAssistantLivingChatDebug(items);
|
||||
const lastSource = toNonEmptyString(lastDebug?.living_chat_response_source);
|
||||
const lastGuardReason = toNonEmptyString(lastDebug?.living_chat_grounding_guard_reason);
|
||||
const inOrganizationFactBoundary = lastSource === "deterministic_organization_fact_boundary" ||
|
||||
lastSource === "deterministic_organization_fact_boundary_followup" ||
|
||||
lastGuardReason === "organization_fact_without_live_source_blocked";
|
||||
return inOrganizationFactBoundary;
|
||||
}
|
||||
function shouldEmitOrganizationSelectionReply(userMessage, selectedOrganization) {
|
||||
const selected = normalizeOrganizationScopeValue(selectedOrganization);
|
||||
if (!selected) {
|
||||
return false;
|
||||
}
|
||||
const repaired = repairAddressMojibake(String(userMessage ?? ""));
|
||||
const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е");
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
if (hasOrganizationFactLookupSignal(normalized) || hasDataRetrievalRequestSignal(normalized) || hasStrongDataIntentSignal(normalized)) {
|
||||
return false;
|
||||
}
|
||||
const hasAnalyticalCue = /(?:какой|какая|какие|когда|сколько|кто|почему|зачем|возраст|дата|регистрац|ндс|налог|контракт|договор|документ|операц|оборот|сумм|остат|сальдо|founded|registered|created)/i.test(normalized);
|
||||
if (hasAnalyticalCue) {
|
||||
return false;
|
||||
}
|
||||
const hasSelectionCue = /(?:давай|го|погнали|ок(?:ей)?|хорошо|отлично|берем|выберем|выбираем|переключ(?:им|аем|ай)|фиксир|работаем|обсудим|тогда)\b/i.test(normalized);
|
||||
if (hasSelectionCue) {
|
||||
return true;
|
||||
}
|
||||
return normalized.length <= 36 && !/[?]/.test(String(userMessage ?? ""));
|
||||
}
|
||||
function hasOperationalAdminActionRequestSignal(text) {
|
||||
const lower = compactWhitespace(String(text ?? "").toLowerCase()).replace(/ё/g, "е");
|
||||
const normalized = lower.replace(/\b1\s*[cс]\b/giu, "1с");
|
||||
@@ -3527,18 +3785,28 @@ function hasAssistantCapabilityQuestionSignal(text) {
|
||||
"что ты умеешь",
|
||||
"какой у тебя функционал",
|
||||
"какие у тебя функции",
|
||||
"какие фичи",
|
||||
"что отработано",
|
||||
"что у тебя отработано",
|
||||
"полный список возможностей",
|
||||
"полный список"
|
||||
];
|
||||
if (directCapabilityPhrases.some((phrase) => normalized.includes(phrase))) {
|
||||
return true;
|
||||
}
|
||||
if (/(?:каки[ею].*(?:фич|функц|возможност|отработан)|какого\s+рода\s+ошибк.*ты\s+мож(?:ешь|ете)|какие\s+ошибк.*ты\s+мож(?:ешь|ете))/iu.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
const hasCanVerb = /(?:можешь|можете|умеешь|умеете|можно)/i.test(normalized);
|
||||
const hasControlAction = /(?:настро|установ|подключ|обнов|созда|подготов|сдела|делат|дела)/i.test(normalized);
|
||||
const hasAnalysisAction = /(?:найт|искать|провер|анализ|разоб|объясн|расска|подсказ|показ)/i.test(normalized);
|
||||
const hasCapabilityObject = /(?:1с|1c|док|документ|баз|отчет|отч[её]т|конфигурац|настройк)/i.test(normalized);
|
||||
if (hasCanVerb && hasControlAction && hasCapabilityObject) {
|
||||
return true;
|
||||
}
|
||||
if (hasCanVerb && hasAnalysisAction && !hasDataRetrievalRequestSignal(normalized)) {
|
||||
return true;
|
||||
}
|
||||
const hasCapabilityMetaQuestion = /(?:что|чем)\s+(?:ты\s+)?(?:мож(?:ешь|ете)|уме(?:ешь|ете)|можно)(?=$|[\s,.!?;:])/iu.test(normalized);
|
||||
if (hasCapabilityMetaQuestion && hasCapabilityObject) {
|
||||
return true;
|
||||
@@ -3672,6 +3940,320 @@ function normalizeScopeLabel(value) {
|
||||
function normalizeScopeKey(value) {
|
||||
return repairAddressMojibake(String(value ?? "")).toLowerCase().replace(/ё/g, "е");
|
||||
}
|
||||
const ORGANIZATION_SCOPE_STOPWORDS = new Set([
|
||||
"ооо",
|
||||
"ao",
|
||||
"ао",
|
||||
"зао",
|
||||
"ип",
|
||||
"llc",
|
||||
"ltd",
|
||||
"company",
|
||||
"компания",
|
||||
"организация",
|
||||
"организации",
|
||||
"контора",
|
||||
"конторы",
|
||||
"фирма",
|
||||
"фирмы",
|
||||
"по",
|
||||
"для",
|
||||
"над",
|
||||
"под",
|
||||
"без",
|
||||
"с",
|
||||
"со",
|
||||
"в",
|
||||
"во",
|
||||
"на",
|
||||
"и",
|
||||
"или",
|
||||
"а",
|
||||
"но",
|
||||
"не",
|
||||
"мы",
|
||||
"нам",
|
||||
"наш",
|
||||
"наша",
|
||||
"наше",
|
||||
"наши",
|
||||
"ты",
|
||||
"тебе",
|
||||
"твой",
|
||||
"сейчас",
|
||||
"щас",
|
||||
"тут",
|
||||
"вот",
|
||||
"давай",
|
||||
"го",
|
||||
"погнали",
|
||||
"тогда",
|
||||
"обсудим",
|
||||
"обсуждать",
|
||||
"работать",
|
||||
"работаем",
|
||||
"работаешь",
|
||||
"работаете",
|
||||
"можем",
|
||||
"можно",
|
||||
"какая",
|
||||
"какой",
|
||||
"какие",
|
||||
"чья",
|
||||
"чье",
|
||||
"чьи"
|
||||
]);
|
||||
function normalizeOrganizationScopeValue(value) {
|
||||
const normalized = normalizeScopeLabel(value);
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
const unwrapped = normalized
|
||||
.replace(/^\\+|\\+$/g, "")
|
||||
.replace(/^"+|"+$/g, "")
|
||||
.replace(/^'+|'+$/g, "")
|
||||
.trim();
|
||||
return unwrapped ? unwrapped : null;
|
||||
}
|
||||
function normalizeOrganizationScopeSearchText(value) {
|
||||
const source = normalizeScopeKey(value);
|
||||
return source
|
||||
.replace(/[^a-zа-я0-9]+/giu, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
function tokenizeOrganizationScope(value) {
|
||||
const normalized = normalizeOrganizationScopeSearchText(value);
|
||||
if (!normalized) {
|
||||
return [];
|
||||
}
|
||||
return normalized
|
||||
.split(" ")
|
||||
.map((token) => token.trim())
|
||||
.filter((token) => token.length >= 3 && !ORGANIZATION_SCOPE_STOPWORDS.has(token));
|
||||
}
|
||||
function organizationTokenVariants(token) {
|
||||
const source = String(token ?? "").trim().toLowerCase();
|
||||
if (!source) {
|
||||
return [];
|
||||
}
|
||||
const variants = new Set([source]);
|
||||
const withoutLongEnding = source.replace(/(?:ами|ями|ого|ему|ому|ыми|ими|иях|ях|ах|ей|ой|ом|ем|ам|ям|ую|юю|ая|яя|ое|ее|ые|ие|ов|ев|ий|ый|ой)$/iu, "");
|
||||
if (withoutLongEnding.length >= 4) {
|
||||
variants.add(withoutLongEnding);
|
||||
}
|
||||
const withoutShortEnding = source.replace(/[аеёиоуыэюя]$/iu, "");
|
||||
if (withoutShortEnding.length >= 4) {
|
||||
variants.add(withoutShortEnding);
|
||||
}
|
||||
return Array.from(variants);
|
||||
}
|
||||
function scoreOrganizationMentionInMessage(message, organization) {
|
||||
const messageNorm = normalizeOrganizationScopeSearchText(message);
|
||||
const organizationNorm = normalizeOrganizationScopeSearchText(organization);
|
||||
if (!messageNorm || !organizationNorm) {
|
||||
return 0;
|
||||
}
|
||||
if (messageNorm.includes(organizationNorm)) {
|
||||
return 10_000 + organizationNorm.length;
|
||||
}
|
||||
const organizationTokens = tokenizeOrganizationScope(organizationNorm);
|
||||
if (organizationTokens.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
const messageTokens = tokenizeOrganizationScope(messageNorm);
|
||||
if (messageTokens.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
let matchedTokens = 0;
|
||||
let score = 0;
|
||||
for (const token of organizationTokens) {
|
||||
const variants = organizationTokenVariants(token);
|
||||
let matched = false;
|
||||
let variantScore = 0;
|
||||
for (const variant of variants) {
|
||||
if (!variant) {
|
||||
continue;
|
||||
}
|
||||
if (messageNorm.includes(variant)) {
|
||||
matched = true;
|
||||
variantScore = Math.max(variantScore, variant.length * 5);
|
||||
continue;
|
||||
}
|
||||
const fuzzyMatched = messageTokens.some((messageToken) => {
|
||||
if (messageToken === variant) {
|
||||
return true;
|
||||
}
|
||||
if (messageToken.length >= 5 && variant.length >= 5) {
|
||||
return messageToken.startsWith(variant) || variant.startsWith(messageToken);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (fuzzyMatched) {
|
||||
matched = true;
|
||||
variantScore = Math.max(variantScore, Math.max(20, variant.length * 3));
|
||||
}
|
||||
}
|
||||
if (matched) {
|
||||
matchedTokens += 1;
|
||||
score += variantScore > 0 ? variantScore : 10;
|
||||
}
|
||||
}
|
||||
if (matchedTokens === 0) {
|
||||
return 0;
|
||||
}
|
||||
if (matchedTokens === organizationTokens.length) {
|
||||
score += 400;
|
||||
}
|
||||
else {
|
||||
score += matchedTokens * 50;
|
||||
}
|
||||
return score;
|
||||
}
|
||||
function parseOrganizationsFromDataScopeAssistantText(text) {
|
||||
const source = repairAddressMojibake(String(text ?? ""));
|
||||
if (!source) {
|
||||
return [];
|
||||
}
|
||||
const extracted = [];
|
||||
const singleMatch = source.match(/доступна\s+организация:\s*([^.\n]+)/iu);
|
||||
if (singleMatch) {
|
||||
const value = normalizeOrganizationScopeValue(singleMatch[1]);
|
||||
if (value) {
|
||||
extracted.push(value);
|
||||
}
|
||||
}
|
||||
const multiMatch = source.match(/доступны\s+организац(?:ии|ия)\s*(?:\(\d+\))?:\s*([^.\n]+)/iu);
|
||||
if (multiMatch) {
|
||||
const parts = String(multiMatch[1] ?? "")
|
||||
.split(",")
|
||||
.map((item) => normalizeOrganizationScopeValue(item))
|
||||
.filter(Boolean);
|
||||
extracted.push(...parts);
|
||||
}
|
||||
return Array.from(new Set(extracted));
|
||||
}
|
||||
function mergeKnownOrganizations(values) {
|
||||
const dedup = new Map();
|
||||
for (const raw of Array.isArray(values) ? values : []) {
|
||||
const normalized = normalizeOrganizationScopeValue(raw);
|
||||
if (!normalized) {
|
||||
continue;
|
||||
}
|
||||
const key = normalizeOrganizationScopeSearchText(normalized);
|
||||
if (!key) {
|
||||
continue;
|
||||
}
|
||||
if (!dedup.has(key)) {
|
||||
dedup.set(key, normalized);
|
||||
}
|
||||
}
|
||||
return Array.from(dedup.values()).slice(0, 20);
|
||||
}
|
||||
function extractKnownOrganizationsFromHistory(items) {
|
||||
const collected = [];
|
||||
for (let index = (Array.isArray(items) ? items.length : 0) - 1; index >= 0; index -= 1) {
|
||||
const item = items[index];
|
||||
if (!item || item.role !== "assistant") {
|
||||
continue;
|
||||
}
|
||||
const debug = item.debug && typeof item.debug === "object" ? item.debug : null;
|
||||
if (debug) {
|
||||
const directFromProbe = Array.isArray(debug.living_chat_data_scope_probe_organizations)
|
||||
? debug.living_chat_data_scope_probe_organizations
|
||||
: [];
|
||||
const knownFromDebug = Array.isArray(debug.assistant_known_organizations)
|
||||
? debug.assistant_known_organizations
|
||||
: [];
|
||||
if (directFromProbe.length > 0 || knownFromDebug.length > 0) {
|
||||
collected.push(...directFromProbe, ...knownFromDebug);
|
||||
}
|
||||
}
|
||||
const parsedFromText = parseOrganizationsFromDataScopeAssistantText(item.text);
|
||||
if (parsedFromText.length > 0) {
|
||||
collected.push(...parsedFromText);
|
||||
}
|
||||
if (collected.length >= 20) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return mergeKnownOrganizations(collected);
|
||||
}
|
||||
function findLastAssistantActiveOrganization(items) {
|
||||
for (let index = (Array.isArray(items) ? items.length : 0) - 1; index >= 0; index -= 1) {
|
||||
const item = items[index];
|
||||
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
|
||||
continue;
|
||||
}
|
||||
const direct = normalizeOrganizationScopeValue(item.debug.assistant_active_organization);
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
const selected = normalizeOrganizationScopeValue(item.debug.living_chat_selected_organization);
|
||||
if (selected) {
|
||||
return selected;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function resolveOrganizationSelectionFromMessage(userMessage, knownOrganizations) {
|
||||
const known = mergeKnownOrganizations(knownOrganizations);
|
||||
if (!userMessage || known.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const messageNorm = normalizeOrganizationScopeSearchText(userMessage);
|
||||
if (!messageNorm) {
|
||||
return null;
|
||||
}
|
||||
const scored = known
|
||||
.map((organization) => ({
|
||||
organization,
|
||||
score: scoreOrganizationMentionInMessage(messageNorm, organization)
|
||||
}))
|
||||
.filter((item) => item.score > 0)
|
||||
.sort((a, b) => b.score - a.score || a.organization.length - b.organization.length);
|
||||
if (scored.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const best = scored[0];
|
||||
const second = scored[1];
|
||||
if (best.score < 90) {
|
||||
return null;
|
||||
}
|
||||
if (second && second.score === best.score) {
|
||||
return null;
|
||||
}
|
||||
return best.organization;
|
||||
}
|
||||
function resolveSessionOrganizationScopeContext(userMessage, items) {
|
||||
const knownOrganizations = extractKnownOrganizationsFromHistory(items);
|
||||
const selectedOrganization = resolveOrganizationSelectionFromMessage(userMessage, knownOrganizations);
|
||||
const lastActiveOrganization = findLastAssistantActiveOrganization(items);
|
||||
const activeOrganization = selectedOrganization ?? normalizeOrganizationScopeValue(lastActiveOrganization);
|
||||
return {
|
||||
knownOrganizations,
|
||||
selectedOrganization,
|
||||
activeOrganization
|
||||
};
|
||||
}
|
||||
function mergeFollowupContextWithOrganizationScope(followupContext, organization) {
|
||||
const normalizedOrganization = normalizeOrganizationScopeValue(organization);
|
||||
const base = followupContext && typeof followupContext === "object" ? { ...followupContext } : {};
|
||||
if (!normalizedOrganization) {
|
||||
return followupContext && typeof followupContext === "object" ? base : null;
|
||||
}
|
||||
const previousFilters = base.previous_filters && typeof base.previous_filters === "object"
|
||||
? { ...base.previous_filters }
|
||||
: {};
|
||||
if (!toNonEmptyString(previousFilters.organization)) {
|
||||
previousFilters.organization = normalizedOrganization;
|
||||
}
|
||||
base.previous_filters = previousFilters;
|
||||
return base;
|
||||
}
|
||||
function resolveSessionOrganizationScopeContextForTests(userMessage, items) {
|
||||
return resolveSessionOrganizationScopeContext(userMessage, items);
|
||||
}
|
||||
function normalizeGuidValue(value) {
|
||||
const source = normalizeScopeLabel(value);
|
||||
if (!source) {
|
||||
@@ -4046,6 +4628,26 @@ function buildAssistantDataScopeContractReply(scopeProbe = null) {
|
||||
"Если подключено несколько баз, для автосписка нужен MCP-метод метаданных (перечень баз/организаций); без него можно анализировать только активный контур запросов."
|
||||
].join(" ");
|
||||
}
|
||||
function buildAssistantDataScopeSelectionReply(organization) {
|
||||
const selected = normalizeOrganizationScopeValue(organization) ?? String(organization ?? "").trim();
|
||||
return [
|
||||
`Отлично, фиксирую рабочую организацию: ${selected}.`,
|
||||
"Дальше буду держать этот контур как активный, пока вы не переключите организацию."
|
||||
].join(" ");
|
||||
}
|
||||
function buildAssistantOrganizationFactBoundaryReply(organization) {
|
||||
const selected = normalizeOrganizationScopeValue(organization) ?? String(organization ?? "").trim();
|
||||
if (selected) {
|
||||
return [
|
||||
`По организации ${selected} не буду называть дату/возраст без live-подтвержденного источника.`,
|
||||
"Если нужно, запрошу факт из 1С и верну только подтвержденный ответ."
|
||||
].join(" ");
|
||||
}
|
||||
return [
|
||||
"Не буду называть дату/возраст организации без live-подтвержденного источника.",
|
||||
"Сначала получу факт из 1С, потом дам точный ответ."
|
||||
].join(" ");
|
||||
}
|
||||
function buildAssistantOperationalBoundaryReply() {
|
||||
return [
|
||||
"Понимаю, что ситуация срочная.",
|
||||
@@ -4109,6 +4711,45 @@ function applyLivingChatScriptGuard(chatText, userMessage) {
|
||||
reason: "unexpected_cjk_fragment_fallback"
|
||||
};
|
||||
}
|
||||
function applyLivingChatGroundingGuard(input) {
|
||||
const userMessage = String(input?.userMessage ?? "");
|
||||
const chatText = String(input?.chatText ?? "").trim();
|
||||
const organization = toNonEmptyString(input?.organization);
|
||||
if (!chatText) {
|
||||
return {
|
||||
text: chatText,
|
||||
applied: false,
|
||||
reason: null
|
||||
};
|
||||
}
|
||||
if (!hasOrganizationFactLookupSignal(userMessage)) {
|
||||
return {
|
||||
text: chatText,
|
||||
applied: false,
|
||||
reason: null
|
||||
};
|
||||
}
|
||||
if (/(?:не\s+могу|не\s+вижу|после\s+проверки|live|подтвержден)/i.test(chatText)) {
|
||||
return {
|
||||
text: chatText,
|
||||
applied: false,
|
||||
reason: null
|
||||
};
|
||||
}
|
||||
const hasSpecificUnverifiedFact = /(?:\b\d{1,2}[./-]\d{1,2}[./-](?:\d{2}|\d{4})\b|\b(?:19|20)\d{2}\b|\b\d+\s+лет\b)/i.test(chatText);
|
||||
if (!hasSpecificUnverifiedFact) {
|
||||
return {
|
||||
text: chatText,
|
||||
applied: false,
|
||||
reason: null
|
||||
};
|
||||
}
|
||||
return {
|
||||
text: buildAssistantOrganizationFactBoundaryReply(organization),
|
||||
applied: true,
|
||||
reason: "organization_fact_without_live_source_blocked"
|
||||
};
|
||||
}
|
||||
function resolveLivingAssistantModeDecision(input) {
|
||||
const userMessage = String(input?.userMessage ?? "");
|
||||
if (input?.addressLaneTriggered) {
|
||||
@@ -4187,7 +4828,9 @@ class AssistantService {
|
||||
async handleMessage(payload) {
|
||||
const session = this.sessions.ensureSession(payload.session_id);
|
||||
const sessionId = session.session_id;
|
||||
const userMessage = String(payload.user_message ?? payload.message ?? "").trim();
|
||||
const userMessageRaw = String(payload.user_message ?? payload.message ?? "").trim();
|
||||
const repairedUserMessage = compactWhitespace(repairAddressMojibake(userMessageRaw));
|
||||
const userMessage = repairedUserMessage || userMessageRaw;
|
||||
const userItem = {
|
||||
message_id: `msg-${(0, nanoid_1.nanoid)(10)}`,
|
||||
session_id: sessionId,
|
||||
@@ -4199,13 +4842,23 @@ class AssistantService {
|
||||
debug: null
|
||||
};
|
||||
this.sessions.appendItem(sessionId, userItem);
|
||||
const sessionOrganizationScope = resolveSessionOrganizationScopeContext(userMessage, session.items);
|
||||
const finalizeAddressLaneResponse = (addressLane, effectiveAddressUserMessage, carryoverMeta = null, llmPreDecomposeMeta = null) => {
|
||||
const safeAddressReply = String((0, answerComposer_1.sanitizeAssistantReplyForUserFacing)(addressLane.reply_text) ?? "").trim() || String(addressLane.reply_text ?? "");
|
||||
const safeAddressReply = sanitizeOutgoingAssistantText(addressLane.reply_text);
|
||||
const debug = buildAddressDebugPayload(addressLane.debug, llmPreDecomposeMeta);
|
||||
const followupOffer = buildAddressFollowupOffer(debug);
|
||||
if (followupOffer) {
|
||||
debug.address_followup_offer = followupOffer;
|
||||
}
|
||||
const debugKnownOrganizations = mergeKnownOrganizations(sessionOrganizationScope.knownOrganizations);
|
||||
const debugActiveOrganization = toNonEmptyString(debug?.extracted_filters?.organization) ??
|
||||
toNonEmptyString(sessionOrganizationScope.activeOrganization);
|
||||
if (debugKnownOrganizations.length > 0) {
|
||||
debug.assistant_known_organizations = debugKnownOrganizations;
|
||||
}
|
||||
if (debugActiveOrganization) {
|
||||
debug.assistant_active_organization = debugActiveOrganization;
|
||||
}
|
||||
const assistantItem = {
|
||||
message_id: `msg-${(0, nanoid_1.nanoid)(10)}`,
|
||||
session_id: sessionId,
|
||||
@@ -4311,6 +4964,11 @@ class AssistantService {
|
||||
let livingChatSource = "llm_chat";
|
||||
let livingChatScriptGuardApplied = false;
|
||||
let livingChatScriptGuardReason = null;
|
||||
let livingChatGroundingGuardApplied = false;
|
||||
let livingChatGroundingGuardReason = null;
|
||||
let knownOrganizations = mergeKnownOrganizations(sessionOrganizationScope.knownOrganizations);
|
||||
let selectedOrganization = toNonEmptyString(sessionOrganizationScope.selectedOrganization);
|
||||
let activeOrganization = toNonEmptyString(sessionOrganizationScope.activeOrganization);
|
||||
if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) {
|
||||
chatText = buildAssistantSafetyRefusalReply();
|
||||
livingChatSource = "deterministic_safety_refusal";
|
||||
@@ -4318,10 +4976,35 @@ class AssistantService {
|
||||
else if (dataScopeMetaQuery) {
|
||||
dataScopeProbe = await resolveAssistantDataScopeProbe();
|
||||
chatText = buildAssistantDataScopeContractReply(dataScopeProbe);
|
||||
knownOrganizations = mergeKnownOrganizations([
|
||||
...knownOrganizations,
|
||||
...(Array.isArray(dataScopeProbe?.organizations) ? dataScopeProbe.organizations : [])
|
||||
]);
|
||||
if (!activeOrganization && knownOrganizations.length === 1) {
|
||||
activeOrganization = knownOrganizations[0];
|
||||
}
|
||||
livingChatSource = dataScopeProbe?.status === "resolved"
|
||||
? "deterministic_data_scope_contract_live"
|
||||
: "deterministic_data_scope_contract";
|
||||
}
|
||||
else if ((selectedOrganization || activeOrganization) && hasOrganizationFactLookupSignal(userMessage)) {
|
||||
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
|
||||
chatText = buildAssistantOrganizationFactBoundaryReply(scopedOrganization);
|
||||
activeOrganization = scopedOrganization ?? activeOrganization;
|
||||
livingChatSource = "deterministic_organization_fact_boundary";
|
||||
}
|
||||
else if ((selectedOrganization || activeOrganization) && hasOrganizationFactFollowupSignal(userMessage, session.items)) {
|
||||
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
|
||||
chatText = buildAssistantOrganizationFactBoundaryReply(scopedOrganization);
|
||||
activeOrganization = scopedOrganization ?? activeOrganization;
|
||||
livingChatSource = "deterministic_organization_fact_boundary_followup";
|
||||
}
|
||||
else if (!capabilityMetaQuery && shouldEmitOrganizationSelectionReply(userMessage, selectedOrganization ?? activeOrganization)) {
|
||||
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
|
||||
chatText = buildAssistantDataScopeSelectionReply(scopedOrganization);
|
||||
activeOrganization = scopedOrganization ?? activeOrganization;
|
||||
livingChatSource = "deterministic_data_scope_selection_contract";
|
||||
}
|
||||
else if (capabilityMetaQuery && operationalSignal && !hasAssistantCapabilityQuestionSignal(userMessage)) {
|
||||
chatText = buildAssistantOperationalBoundaryReply();
|
||||
livingChatSource = "deterministic_operational_boundary";
|
||||
@@ -4353,7 +5036,7 @@ class AssistantService {
|
||||
maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900)),
|
||||
temperature: payload.temperature ?? 0.35
|
||||
});
|
||||
chatText = String((0, answerComposer_1.sanitizeAssistantReplyForUserFacing)(chatResponse?.outputText ?? "") ?? "").trim();
|
||||
chatText = sanitizeOutgoingAssistantText(chatResponse?.outputText ?? "", "Понял. Сформулируйте, что именно нужно по данным 1С, и я помогу по шагам.");
|
||||
const scriptGuard = applyLivingChatScriptGuard(chatText, userMessage);
|
||||
chatText = scriptGuard.text;
|
||||
if (scriptGuard.applied) {
|
||||
@@ -4361,6 +5044,17 @@ class AssistantService {
|
||||
livingChatScriptGuardReason = scriptGuard.reason;
|
||||
livingChatSource = "llm_chat_script_guard";
|
||||
}
|
||||
const groundingGuard = applyLivingChatGroundingGuard({
|
||||
userMessage,
|
||||
chatText,
|
||||
organization: activeOrganization ?? selectedOrganization ?? null
|
||||
});
|
||||
chatText = groundingGuard.text;
|
||||
if (groundingGuard.applied) {
|
||||
livingChatGroundingGuardApplied = true;
|
||||
livingChatGroundingGuardReason = groundingGuard.reason;
|
||||
livingChatSource = "llm_chat_grounding_guard";
|
||||
}
|
||||
}
|
||||
if (!chatText) {
|
||||
return null;
|
||||
@@ -4378,16 +5072,25 @@ class AssistantService {
|
||||
living_chat_response_source: livingChatSource,
|
||||
living_chat_script_guard_applied: livingChatScriptGuardApplied,
|
||||
living_chat_script_guard_reason: livingChatScriptGuardReason,
|
||||
living_chat_grounding_guard_applied: livingChatGroundingGuardApplied,
|
||||
living_chat_grounding_guard_reason: livingChatGroundingGuardReason,
|
||||
living_chat_data_scope_probe_status: dataScopeProbe?.status ?? null,
|
||||
living_chat_data_scope_probe_channel: dataScopeProbe?.channel ?? null,
|
||||
living_chat_data_scope_probe_org_count: Array.isArray(dataScopeProbe?.organizations)
|
||||
? dataScopeProbe.organizations.length
|
||||
: 0,
|
||||
living_chat_data_scope_probe_organizations: Array.isArray(dataScopeProbe?.organizations)
|
||||
? mergeKnownOrganizations(dataScopeProbe.organizations)
|
||||
: [],
|
||||
living_chat_data_scope_probe_error: dataScopeProbe?.error ?? null,
|
||||
living_chat_selected_organization: selectedOrganization ?? null,
|
||||
assistant_known_organizations: knownOrganizations,
|
||||
assistant_active_organization: activeOrganization ?? null,
|
||||
address_llm_predecompose_attempted: Boolean(addressRuntimeMeta?.attempted),
|
||||
address_llm_predecompose_applied: Boolean(addressRuntimeMeta?.applied),
|
||||
address_llm_predecompose_reason: addressRuntimeMeta?.reason ?? null,
|
||||
address_llm_predecompose_contract: addressRuntimeMeta?.predecomposeContract ?? null,
|
||||
orchestration_contract_v1: addressRuntimeMeta?.orchestrationContract ?? null,
|
||||
tool_gate_decision: addressRuntimeMeta?.toolGateDecision ?? null,
|
||||
tool_gate_reason: addressRuntimeMeta?.toolGateReason ?? null,
|
||||
normalized: null,
|
||||
@@ -4476,23 +5179,27 @@ class AssistantService {
|
||||
};
|
||||
const addressInputMessage = toNonEmptyString(addressPreDecompose?.effectiveMessage) ?? userMessage;
|
||||
const carryover = resolveAddressFollowupCarryoverContext(userMessage, session.items, addressInputMessage, addressPreDecompose);
|
||||
const toolGate = resolveAddressToolGateDecision(addressInputMessage, carryover?.followupContext ?? null, addressPreDecompose, userMessage);
|
||||
const orchestrationDecision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: userMessage,
|
||||
effectiveAddressUserMessage: addressInputMessage,
|
||||
followupContext: carryover?.followupContext ?? null,
|
||||
llmPreDecomposeMeta: addressPreDecompose,
|
||||
useMock: Boolean(payload.useMock)
|
||||
});
|
||||
const dialogContinuationContract = buildAddressDialogContinuationContractV2(userMessage, addressInputMessage, carryover, addressPreDecompose);
|
||||
const addressRuntimeMeta = {
|
||||
...addressPreDecompose,
|
||||
toolGateDecision: toolGate.decision,
|
||||
toolGateReason: toolGate.reason,
|
||||
dialogContinuationContract
|
||||
toolGateDecision: orchestrationDecision.toolGateDecision,
|
||||
toolGateReason: orchestrationDecision.toolGateReason,
|
||||
dialogContinuationContract,
|
||||
orchestrationContract: orchestrationDecision.orchestrationContract
|
||||
};
|
||||
addressRuntimeMetaForDeep = addressRuntimeMeta;
|
||||
const livingModeDecision = resolveLivingAssistantModeDecision({
|
||||
userMessage,
|
||||
addressLaneTriggered: toolGate.runAddressLane,
|
||||
useMock: Boolean(payload.useMock),
|
||||
predecomposeMode: addressRuntimeMeta?.predecomposeContract?.mode ?? null,
|
||||
predecomposeModeConfidence: addressRuntimeMeta?.predecomposeContract?.mode_confidence ?? null
|
||||
});
|
||||
if (!toolGate.runAddressLane) {
|
||||
const livingModeDecision = {
|
||||
mode: orchestrationDecision.livingMode,
|
||||
reason: orchestrationDecision.livingReason
|
||||
};
|
||||
if (!orchestrationDecision.runAddressLane) {
|
||||
(0, log_1.logJson)({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: "info",
|
||||
@@ -4508,6 +5215,7 @@ class AssistantService {
|
||||
address_llm_predecompose_reason: addressRuntimeMeta?.reason ?? null,
|
||||
address_fallback_rule_hit: addressRuntimeMeta?.fallbackRuleHit ?? null,
|
||||
address_sanitized_user_message: addressRuntimeMeta?.sanitizedUserMessage ?? null,
|
||||
assistant_orchestration_contract_v1: addressRuntimeMeta?.orchestrationContract ?? null,
|
||||
address_tool_gate_decision: addressRuntimeMeta?.toolGateDecision ?? null,
|
||||
address_tool_gate_reason: addressRuntimeMeta?.toolGateReason ?? null,
|
||||
address_llm_predecompose_contract_intent: addressRuntimeMeta?.predecomposeContract?.intent ?? null,
|
||||
@@ -4522,7 +5230,7 @@ class AssistantService {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (toolGate.runAddressLane) {
|
||||
if (orchestrationDecision.runAddressLane) {
|
||||
const shouldPreferContextualLane = Boolean(carryover?.followupContext);
|
||||
const canRetryWithRawUserMessage = compactWhitespace(String(addressInputMessage ?? "").toLowerCase()) !==
|
||||
compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||||
@@ -4563,9 +5271,10 @@ class AssistantService {
|
||||
};
|
||||
};
|
||||
const runAddressLaneAttempt = async (messageUsed, carryMeta) => {
|
||||
if (carryMeta?.followupContext) {
|
||||
const scopedFollowupContext = mergeFollowupContextWithOrganizationScope(carryMeta?.followupContext ?? null, sessionOrganizationScope.activeOrganization);
|
||||
if (scopedFollowupContext) {
|
||||
return this.addressQueryService.tryHandle(messageUsed, {
|
||||
followupContext: carryMeta.followupContext
|
||||
followupContext: scopedFollowupContext
|
||||
});
|
||||
}
|
||||
return this.addressQueryService.tryHandle(messageUsed);
|
||||
@@ -4824,7 +5533,7 @@ class AssistantService {
|
||||
enableProblemCentricAnswerV1: config_1.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1,
|
||||
enableLifecycleAnswerV1: config_1.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1
|
||||
});
|
||||
const safeAssistantReplyBase = (0, answerComposer_1.sanitizeAssistantReplyForUserFacing)(composition.assistant_reply);
|
||||
const safeAssistantReplyBase = sanitizeOutgoingAssistantText(composition.assistant_reply, "Нужны уточнения для надежного ответа.");
|
||||
const safeAssistantReply = String(safeAssistantReplyBase ?? "")
|
||||
.replace(/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json)\b[\s\S]*$/gi, "")
|
||||
.replace(/\b(?:debug_payload_json|technical_breakdown_json)\b[\s\S]*$/gi, "")
|
||||
@@ -4927,6 +5636,7 @@ class AssistantService {
|
||||
address_tool_gate_decision: addressRuntimeMetaForDeep?.toolGateDecision ?? null,
|
||||
address_tool_gate_reason: addressRuntimeMetaForDeep?.toolGateReason ?? null,
|
||||
address_llm_predecompose_contract: addressRuntimeMetaForDeep?.predecomposeContract ?? null,
|
||||
orchestration_contract_v1: addressRuntimeMetaForDeep?.orchestrationContract ?? null,
|
||||
answer_structure_v11: answerStructureV11,
|
||||
investigation_state_snapshot: investigationStateSnapshot,
|
||||
normalized: normalized.normalized
|
||||
|
||||
Reference in New Issue
Block a user