Compare commits
No commits in common. "f1ef5f9d3cf76905ab91b90022fb9afa7340bed8" and "040a55aaea19ab5a02cd0e37503b2b33b1a73f77" have entirely different histories.
f1ef5f9d3c
...
040a55aaea
|
|
@ -1,14 +1,10 @@
|
||||||
# TECH Docs Index
|
# TECH Docs Index
|
||||||
|
|
||||||
Актуальные документы по техническому контуру ассистента:
|
Актуальные документы по operational-контру ассистента:
|
||||||
|
|
||||||
|
1. `assistant_canon.md` - канон поведения ассистента.
|
||||||
|
2. `capabilities_registry.json` - реестр поддерживаемых возможностей.
|
||||||
|
3. `manual_case_decision_schema.json` - схема решений ручной разметки.
|
||||||
|
4. `ui_markup_system.md` - рабочий процесс разметки через GUI.
|
||||||
|
5. `history_colibration.md` - сводка статуса и ближайших задач.
|
||||||
|
|
||||||
1. `ARCH_LAYER_FOUNDATION.md` — архитектурный фундамент: разделение слоев `compute` / `navigation` / `conversational`.
|
|
||||||
2. `STATUS_2026-04-12.md` — текущий статус маршрутов, фиксов и открытых рисков.
|
|
||||||
3. `address_route_baseline_v1.json` — baseline-срез для анти-регресса по ключевым интентам.
|
|
||||||
4. `address_route_expectations_v1.json` — ожидания по `intent -> recipe/result_mode`.
|
|
||||||
5. `capabilities_registry.json` — реестр поддерживаемых capability и границ.
|
|
||||||
6. `assistant_canon.md` — канон поведения ассистента.
|
|
||||||
7. `manual_case_decision_schema.json` — схема ручного решения кейсов.
|
|
||||||
8. `ui_markup_system.md` — правила разметки и UI-процесса.
|
|
||||||
9. `history_colibration.md` — исторический журнал калибровки.
|
|
||||||
10. `PLAN_FIX.md` — долгосрочный план безопасного развития маршрутов.
|
|
||||||
|
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
# Статус проекта на 2026-04-12
|
|
||||||
|
|
||||||
## 1) Что уже стабильно в compute-слое
|
|
||||||
|
|
||||||
- Введены и работают exact-маршруты подтвержденного среза на дату:
|
|
||||||
- `payables_confirmed_as_of_date` (`address_payables_confirmed_as_of_date_v1`)
|
|
||||||
- `receivables_confirmed_as_of_date` (`address_receivables_confirmed_as_of_date_v1`)
|
|
||||||
- `vat_payable_confirmed_as_of_date` (`address_vat_payable_confirmed_as_of_date_v1`)
|
|
||||||
- Для этих интентов зафиксирован expected route/result mode в:
|
|
||||||
- `docs/TECH/address_route_expectations_v1.json`
|
|
||||||
- Режим результата для exact-сценариев закреплен как `confirmed_balance`.
|
|
||||||
|
|
||||||
## 2) Что исправлено в цепных (follow-up) вопросах
|
|
||||||
|
|
||||||
- Исправлен перенос даты среза в коротких продолжениях по долгам:
|
|
||||||
- после вопроса о долгах на дату follow-up по дебиторке наследует `as_of_date`, если новая дата не задана явно.
|
|
||||||
- Добавлен короткий follow-up для НДС:
|
|
||||||
- короткие реплики вида `а ндс?`/`по ндс` теперь корректно идут в VAT exact-route с переносом даты среза из контекста.
|
|
||||||
- Сохранена стратегия LLM-first нормализации с последующим детерминированным compute-роутингом.
|
|
||||||
|
|
||||||
## 3) Что уже покрыто тестами
|
|
||||||
|
|
||||||
- Добавлены/актуализированы тесты на carryover и follow-up:
|
|
||||||
- `llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts`
|
|
||||||
- `llm_normalizer/backend/tests/assistantAddressFollowupContext.test.ts`
|
|
||||||
- Проверен маршрутный baseline:
|
|
||||||
- `llm_normalizer/backend/tests/addressRouteBaseline.test.ts`
|
|
||||||
|
|
||||||
## 4) Известные ограничения (не считать багом расчета)
|
|
||||||
|
|
||||||
- В разговорных нерелевантных репликах (эмоции/брань/односложные сообщения) система может уйти в `clarification_required`; это относится к conversational-слою, не к compute-расчету.
|
|
||||||
- `query_shape` в части exact-кейсов может оставаться `UNKNOWN` при корректном `intent`; расчетный маршрут при этом работает корректно.
|
|
||||||
- Качество бизнес-категоризации контрагентов (особенно по счету 76) требует отдельной донастройки presentation-слоя.
|
|
||||||
|
|
||||||
## 5) Что в приоритете дальше
|
|
||||||
|
|
||||||
1. НДС-контур: усилить доказательную часть расчета "к уплате на дату" и добавить понятную детализацию оснований.
|
|
||||||
2. Цепные вопросы: закрепить перенос контекста между payables/receivables/VAT во всех коротких follow-up формулировках.
|
|
||||||
3. Ответы для UI: довести формат вывода до стабильной блочной структуры без markdown-зависимости.
|
|
||||||
4. Категоризация: отделить поставщиков/заказчиков от банков/госорганов/спецобязательств в итоговой выдаче.
|
|
||||||
|
|
||||||
## 6) Быстрый smoke-check (ручной)
|
|
||||||
|
|
||||||
1. `кому мы должны на сентябрь 2017`
|
|
||||||
2. `а нам кто должен?`
|
|
||||||
3. `кто нам должен на сентябрь 2017`
|
|
||||||
4. `а ндс?`
|
|
||||||
|
|
||||||
Ожидаемое поведение:
|
|
||||||
|
|
||||||
- для 1/3 — `confirmed_balance` в exact-route,
|
|
||||||
- для 2/4 — корректный follow-up с переносом даты среза, без ухода в эвристический shortlist для exact-интентов.
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"schema_version": "address_route_baseline_v1",
|
"schema_version": "address_route_baseline_v1",
|
||||||
"updated_at": "2026-04-12T20:50:00.000Z",
|
"updated_at": "2026-04-12T12:00:00.000Z",
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
{
|
||||||
"intent": "payables_confirmed_as_of_date",
|
"intent": "payables_confirmed_as_of_date",
|
||||||
|
|
@ -14,12 +14,6 @@
|
||||||
"capability_layer": "compute",
|
"capability_layer": "compute",
|
||||||
"capability_route_mode": "exact"
|
"capability_route_mode": "exact"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"intent": "vat_payable_confirmed_as_of_date",
|
|
||||||
"capability_id": "confirmed_vat_payable_as_of_date",
|
|
||||||
"capability_layer": "compute",
|
|
||||||
"capability_route_mode": "exact"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"intent": "list_payables_counterparties",
|
"intent": "list_payables_counterparties",
|
||||||
"capability_id": "payables_candidates_list",
|
"capability_id": "payables_candidates_list",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"schema_version": "address_route_expectations_v1",
|
"schema_version": "address_route_expectations_v1",
|
||||||
"updated_at": "2026-04-12T20:50:00.000Z",
|
"updated_at": "2026-04-12T13:00:00.000Z",
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
{
|
||||||
"intent": "payables_confirmed_as_of_date",
|
"intent": "payables_confirmed_as_of_date",
|
||||||
|
|
@ -14,12 +14,6 @@
|
||||||
"expected_requested_result_modes": ["confirmed_balance"],
|
"expected_requested_result_modes": ["confirmed_balance"],
|
||||||
"expected_result_modes": ["confirmed_balance"]
|
"expected_result_modes": ["confirmed_balance"]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"intent": "vat_payable_confirmed_as_of_date",
|
|
||||||
"expected_selected_recipes": ["address_vat_payable_confirmed_as_of_date_v1"],
|
|
||||||
"expected_requested_result_modes": ["confirmed_balance"],
|
|
||||||
"expected_result_modes": ["confirmed_balance"]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"intent": "list_payables_counterparties",
|
"intent": "list_payables_counterparties",
|
||||||
"expected_selected_recipes": ["address_movements_payables_v1", "address_open_items_by_party_or_contract_v1"],
|
"expected_selected_recipes": ["address_movements_payables_v1", "address_open_items_by_party_or_contract_v1"],
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"schema_version": "capabilities_registry_v1",
|
"schema_version": "capabilities_registry_v1",
|
||||||
"updated_at": "2026-04-12T20:50:00.000Z",
|
"updated_at": "2026-04-09T00:00:00.000Z",
|
||||||
"assistant_mode": "read_only",
|
"assistant_mode": "read_only",
|
||||||
"groups": [
|
"groups": [
|
||||||
{
|
{
|
||||||
|
|
@ -11,7 +11,6 @@
|
||||||
"maturity_status": "partial",
|
"maturity_status": "partial",
|
||||||
"supported_operations": [
|
"supported_operations": [
|
||||||
"vat_period_snapshot",
|
"vat_period_snapshot",
|
||||||
"vat_payable_confirmed_as_of_date",
|
|
||||||
"vat_payable_forecast",
|
"vat_payable_forecast",
|
||||||
"vat_turnover_breakdown"
|
"vat_turnover_breakdown"
|
||||||
],
|
],
|
||||||
|
|
@ -33,7 +32,6 @@
|
||||||
"Почему НДС к уплате ноль?"
|
"Почему НДС к уплате ноль?"
|
||||||
],
|
],
|
||||||
"related_routes": [
|
"related_routes": [
|
||||||
"address_vat_payable_confirmed_as_of_date_v1",
|
|
||||||
"address_vat_payable_forecast_v1"
|
"address_vat_payable_forecast_v1"
|
||||||
],
|
],
|
||||||
"safe_alternatives": [
|
"safe_alternatives": [
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,7 @@ const COMPUTE_EXACT_INTENTS = new Set([
|
||||||
"account_balance_snapshot",
|
"account_balance_snapshot",
|
||||||
"documents_forming_balance",
|
"documents_forming_balance",
|
||||||
"payables_confirmed_as_of_date",
|
"payables_confirmed_as_of_date",
|
||||||
"receivables_confirmed_as_of_date",
|
"receivables_confirmed_as_of_date"
|
||||||
"vat_payable_confirmed_as_of_date"
|
|
||||||
]);
|
]);
|
||||||
const NAVIGATION_INTENTS = new Set([
|
const NAVIGATION_INTENTS = new Set([
|
||||||
"list_documents_by_counterparty",
|
"list_documents_by_counterparty",
|
||||||
|
|
@ -40,9 +39,6 @@ function defaultCapabilityId(intent) {
|
||||||
if (intent === "receivables_confirmed_as_of_date") {
|
if (intent === "receivables_confirmed_as_of_date") {
|
||||||
return "confirmed_receivables_as_of_date";
|
return "confirmed_receivables_as_of_date";
|
||||||
}
|
}
|
||||||
if (intent === "vat_payable_confirmed_as_of_date") {
|
|
||||||
return "confirmed_vat_payable_as_of_date";
|
|
||||||
}
|
|
||||||
if (intent === "list_payables_counterparties") {
|
if (intent === "list_payables_counterparties") {
|
||||||
return "payables_candidates_list";
|
return "payables_candidates_list";
|
||||||
}
|
}
|
||||||
|
|
@ -78,14 +74,6 @@ function resolveCapabilityEnabled(intent) {
|
||||||
: "receivables_confirmed_route_disabled_by_flag"
|
: "receivables_confirmed_route_disabled_by_flag"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (intent === "vat_payable_confirmed_as_of_date") {
|
|
||||||
return {
|
|
||||||
enabled: config_1.FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1,
|
|
||||||
reason: config_1.FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1
|
|
||||||
? "vat_payable_confirmed_route_enabled"
|
|
||||||
: "vat_payable_confirmed_route_disabled_by_flag"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (intent === "list_payables_counterparties") {
|
if (intent === "list_payables_counterparties") {
|
||||||
return {
|
return {
|
||||||
enabled: config_1.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1,
|
enabled: config_1.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1,
|
||||||
|
|
|
||||||
|
|
@ -574,77 +574,6 @@ function isLowQualityCounterpartyAnchorValue(rawValue) {
|
||||||
if (questionCue && (rankingCue || paymentCue)) {
|
if (questionCue && (rankingCue || paymentCue)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const moneyAsOfPhraseCue = /(?:денег|деньг|money|cash)/iu.test(value) &&
|
|
||||||
/(?:на\s+(?:данн(?:ую|ой|ая|ое)|эту|ту)\s+дат|on\s+(?:this|that)\s+date|as\s+of\s+(?:this|that)\s+date)/iu.test(value);
|
|
||||||
if (moneyAsOfPhraseCue) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const hasTemporalCue = /(?:по\s+состоянию|на\s+дат|на\s+конец|за\s+(?:период|месяц|год|квартал)|\b(?:19|20)\d{2}\b|\bянвар|феврал|март|апрел|май|июн|июл|август|сентябр|октябр|ноябр|декабр)/iu.test(value);
|
|
||||||
const hasGenericEntityCue = /(?:компан|организац|контрагент|поставщик|клиент|покупател|дебитор|кредитор|counterparty|company|supplier|customer)/iu.test(value);
|
|
||||||
if (hasTemporalCue && hasGenericEntityCue) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const lowQualityTimeTokens = new Set([
|
|
||||||
"по",
|
|
||||||
"состоянию",
|
|
||||||
"состояние",
|
|
||||||
"на",
|
|
||||||
"дату",
|
|
||||||
"дата",
|
|
||||||
"конец",
|
|
||||||
"период",
|
|
||||||
"месяц",
|
|
||||||
"году",
|
|
||||||
"год",
|
|
||||||
"квартал",
|
|
||||||
"январь",
|
|
||||||
"февраль",
|
|
||||||
"март",
|
|
||||||
"апрель",
|
|
||||||
"май",
|
|
||||||
"июнь",
|
|
||||||
"июль",
|
|
||||||
"август",
|
|
||||||
"сентябрь",
|
|
||||||
"октябрь",
|
|
||||||
"ноябрь",
|
|
||||||
"декабрь"
|
|
||||||
]);
|
|
||||||
const lowQualityGenericTokens = new Set([
|
|
||||||
"деньги",
|
|
||||||
"денег",
|
|
||||||
"деньгам",
|
|
||||||
"деньгами",
|
|
||||||
"денежный",
|
|
||||||
"денежные",
|
|
||||||
"данную",
|
|
||||||
"данной",
|
|
||||||
"данный",
|
|
||||||
"данное",
|
|
||||||
"эту",
|
|
||||||
"этой",
|
|
||||||
"этот",
|
|
||||||
"этом",
|
|
||||||
"ту",
|
|
||||||
"той",
|
|
||||||
"тот",
|
|
||||||
"том",
|
|
||||||
"вцелом",
|
|
||||||
"целом"
|
|
||||||
]);
|
|
||||||
const meaningfulNonTemporalTokens = tokens.filter((token) => isLikelyCounterpartyToken(token) &&
|
|
||||||
!lowQualityTimeTokens.has(token) &&
|
|
||||||
!/^(?:19|20)\d{2}$/.test(token));
|
|
||||||
if (meaningfulNonTemporalTokens.length === 0 && hasTemporalCue) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const meaningfulNonGenericTokens = tokens.filter((token) => isLikelyCounterpartyToken(token) &&
|
|
||||||
!lowQualityTimeTokens.has(token) &&
|
|
||||||
!lowQualityGenericTokens.has(token) &&
|
|
||||||
!/^(?:19|20)\d{2}$/.test(token));
|
|
||||||
if (meaningfulNonGenericTokens.length === 0 && (hasTemporalCue || paymentCue)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const meaningfulTokens = tokens.filter((token) => isLikelyCounterpartyToken(token));
|
const meaningfulTokens = tokens.filter((token) => isLikelyCounterpartyToken(token));
|
||||||
return meaningfulTokens.length === 0;
|
return meaningfulTokens.length === 0;
|
||||||
}
|
}
|
||||||
|
|
@ -822,9 +751,6 @@ function requiredFiltersByIntent(intent) {
|
||||||
if (intent === "receivables_confirmed_as_of_date") {
|
if (intent === "receivables_confirmed_as_of_date") {
|
||||||
return ["as_of_date"];
|
return ["as_of_date"];
|
||||||
}
|
}
|
||||||
if (intent === "vat_payable_confirmed_as_of_date") {
|
|
||||||
return ["as_of_date"];
|
|
||||||
}
|
|
||||||
if (intent === "list_documents_by_counterparty" ||
|
if (intent === "list_documents_by_counterparty" ||
|
||||||
intent === "bank_operations_by_counterparty" ||
|
intent === "bank_operations_by_counterparty" ||
|
||||||
intent === "list_contracts_by_counterparty") {
|
intent === "list_contracts_by_counterparty") {
|
||||||
|
|
@ -839,8 +765,7 @@ function usesAsOfPrimaryWindow(intent) {
|
||||||
return (intent === "open_items_by_counterparty_or_contract" ||
|
return (intent === "open_items_by_counterparty_or_contract" ||
|
||||||
intent === "list_open_contracts" ||
|
intent === "list_open_contracts" ||
|
||||||
intent === "payables_confirmed_as_of_date" ||
|
intent === "payables_confirmed_as_of_date" ||
|
||||||
intent === "receivables_confirmed_as_of_date" ||
|
intent === "receivables_confirmed_as_of_date");
|
||||||
intent === "vat_payable_confirmed_as_of_date");
|
|
||||||
}
|
}
|
||||||
function extractAddressFilters(userMessage, intent) {
|
function extractAddressFilters(userMessage, intent) {
|
||||||
const rawText = String(userMessage ?? "").trim();
|
const rawText = String(userMessage ?? "").trim();
|
||||||
|
|
@ -1003,8 +928,7 @@ function extractAddressFilters(userMessage, intent) {
|
||||||
if ((intent === "account_balance_snapshot" ||
|
if ((intent === "account_balance_snapshot" ||
|
||||||
intent === "documents_forming_balance" ||
|
intent === "documents_forming_balance" ||
|
||||||
intent === "payables_confirmed_as_of_date" ||
|
intent === "payables_confirmed_as_of_date" ||
|
||||||
intent === "receivables_confirmed_as_of_date" ||
|
intent === "receivables_confirmed_as_of_date") &&
|
||||||
intent === "vat_payable_confirmed_as_of_date") &&
|
|
||||||
!filters.as_of_date) {
|
!filters.as_of_date) {
|
||||||
if (filters.period_to) {
|
if (filters.period_to) {
|
||||||
filters.as_of_date = filters.period_to;
|
filters.as_of_date = filters.period_to;
|
||||||
|
|
|
||||||
|
|
@ -385,22 +385,6 @@ const CONTRACT_LIST_BY_COUNTERPARTY_HINTS = [
|
||||||
function hasAny(text, patterns) {
|
function hasAny(text, patterns) {
|
||||||
return patterns.some((item) => text.includes(item));
|
return patterns.some((item) => text.includes(item));
|
||||||
}
|
}
|
||||||
function hasFlexibleReceivablesDebtSignal(text) {
|
|
||||||
const normalized = String(text ?? "");
|
|
||||||
if (!normalized) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return (/(?:кто(?:\s+\S+){0,4}\s+нам(?:\s+\S+){0,4}\s+долж)/iu.test(normalized) ||
|
|
||||||
/(?:нам(?:\s+\S+){0,4}\s+кто(?:\s+\S+){0,4}\s+долж)/iu.test(normalized));
|
|
||||||
}
|
|
||||||
function hasFlexiblePayablesDebtSignal(text) {
|
|
||||||
const normalized = String(text ?? "");
|
|
||||||
if (!normalized) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return (/(?:кому(?:\s+\S+){0,4}\s+мы(?:\s+\S+){0,4}\s+долж)/iu.test(normalized) ||
|
|
||||||
/(?:мы(?:\s+\S+){0,4}\s+кому(?:\s+\S+){0,4}\s+долж)/iu.test(normalized));
|
|
||||||
}
|
|
||||||
function tokenizeText(text) {
|
function tokenizeText(text) {
|
||||||
return String(text ?? "")
|
return String(text ?? "")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
|
@ -551,20 +535,10 @@ function hasAccountBalanceSignal(text) {
|
||||||
}
|
}
|
||||||
function hasForecastTaxSignal(text) {
|
function hasForecastTaxSignal(text) {
|
||||||
const hasForecastLexeme = /(?:прогноз|forecast|план(?:\s+платежа|\s+оплаты)?|прикин(?:уть|ем|у|ь|ул|ули|усь|усь))/iu.test(text);
|
const hasForecastLexeme = /(?:прогноз|forecast|план(?:\s+платежа|\s+оплаты)?|прикин(?:уть|ем|у|ь|ул|ули|усь|усь))/iu.test(text);
|
||||||
const hasTaxLexeme = /(?:ндс|vat|налог)/iu.test(text);
|
|
||||||
return hasForecastLexeme && hasTaxLexeme;
|
|
||||||
}
|
|
||||||
function hasVatPayableConfirmedSignal(text) {
|
|
||||||
const hasVatLexeme = /(?:ндс|vat)/iu.test(text);
|
const hasVatLexeme = /(?:ндс|vat)/iu.test(text);
|
||||||
if (!hasVatLexeme) {
|
const hasTaxLexeme = /(?:ндс|vat|налог)/iu.test(text);
|
||||||
return false;
|
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);
|
||||||
const hasPaymentCue = /(?:к\s+уплате|надо|нужно|заплатить|уплатить|плат[её]ж|платежку|в\s+налогов|в\s+бюджет|должн[аы]?\s+заплатить)/iu.test(text);
|
|
||||||
if (!hasPaymentCue) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const hasDateOrPeriodCue = /(?:на\s+дат|по\s+состоянию|на\s+конец|за\s+(?:\d{4}|январ|феврал|март|апрел|май|июн|июл|август|сентябр|октябр|ноябр|декабр)|квартал|месяц|год|период|\b\d{4}[./-]\d{2}[./-]\d{2}\b)/iu.test(text);
|
|
||||||
return hasDateOrPeriodCue || /(?:сколько|скока|скок)/iu.test(text);
|
|
||||||
}
|
}
|
||||||
function hasPeriodCoverageProfileSignal(text) {
|
function hasPeriodCoverageProfileSignal(text) {
|
||||||
if (hasAny(text, PERIOD_COVERAGE_PROFILE_HINTS)) {
|
if (hasAny(text, PERIOD_COVERAGE_PROFILE_HINTS)) {
|
||||||
|
|
@ -888,7 +862,7 @@ function hasSupplierTailRiskSignal(text) {
|
||||||
return hasSupplier && hasTail && (hasRisk || hasPeriodCue);
|
return hasSupplier && hasTail && (hasRisk || hasPeriodCue);
|
||||||
}
|
}
|
||||||
function hasPayablesDebtLifecycleSignal(text) {
|
function hasPayablesDebtLifecycleSignal(text) {
|
||||||
const hasOweSignal = /(?:кому\s+мы\s+долж(?:ен|ны|эны|эна|эно)?|мы\s+долж(?:ен|ны|эны|эна|эно)?|кому\s+долж(?:ен|ны|эны|эна|эно)?|долж[нэ](?:ы|а|о)?\s+(?:заплат|оплат|перечис)|к\s+оплате|на\s+оплату|who\s+we\s+owe|owe\s+to|payables?|кредитор(?:[а-яё]{0,6})?)/iu.test(text);
|
const hasOweSignal = /(?:кому\s+мы\s+должны|мы\s+должны|кому\s+должны|должн(?:ы|а|о)\s+(?:заплат|оплат|перечис)|к\s+оплате|на\s+оплату|who\s+we\s+owe|owe\s+to|payables?|кредитор(?:ск)?)/iu.test(text);
|
||||||
if (!hasOweSignal) {
|
if (!hasOweSignal) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -900,7 +874,7 @@ function hasPayablesDebtLifecycleSignal(text) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
function hasReceivablesDebtLifecycleSignal(text) {
|
function hasReceivablesDebtLifecycleSignal(text) {
|
||||||
const hasOweUsSignal = /(?:кто\s+нам\s+долж(?:ен|ны|эны|эна|эно)?|кто\s+долж(?:ен|ны|эны|эна|эно)?\s+нам|нам\s+долж(?:ен|ны|эны|эна|эно)?|должник(?:[а-яё]{0,6})?|дебитор(?:[а-яё]{0,6})?|дебиторск(?:[а-яё]{0,6})?|задолж|долг(?:и|ов|а|у)?|к\s+получению|на\s+поступление|к\s+взысканию|who\s+owes\s+us|receivables?)/iu.test(text);
|
const hasOweUsSignal = /(?:кто\s+нам\s+долж(?:ен|ны)?|кто\s+долж(?:ен|ны)?\s+нам|нам\s+долж(?:ен|ны)|должник(?:и|ов|а)?|дебитор(?:ы|ов|ск)?|задолж|долг(?:и|ов|а|у)?|к\s+получению|на\s+поступление|к\s+взысканию|who\s+owes\s+us|receivables?)/iu.test(text);
|
||||||
if (!hasOweUsSignal) {
|
if (!hasOweUsSignal) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -1284,21 +1258,11 @@ function resolveAddressIntent(userMessage) {
|
||||||
reasons: ["forecast_tax_signal_detected"]
|
reasons: ["forecast_tax_signal_detected"]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (hasVatPayableConfirmedSignal(text)) {
|
if (hasAny(text, RECEIVABLES_STRONG)) {
|
||||||
return {
|
const receivablesDebtLifecycleSignal = hasReceivablesDebtLifecycleSignal(text);
|
||||||
intent: "vat_payable_confirmed_as_of_date",
|
|
||||||
confidence: "high",
|
|
||||||
reasons: ["vat_payable_confirmed_signal_detected"]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (hasAny(text, RECEIVABLES_STRONG) || hasFlexibleReceivablesDebtSignal(text)) {
|
|
||||||
const receivablesDebtLifecycleSignal = hasReceivablesDebtLifecycleSignal(text) || hasFlexibleReceivablesDebtSignal(text);
|
|
||||||
const reasons = ["receivables_signal_detected"];
|
const reasons = ["receivables_signal_detected"];
|
||||||
if (receivablesDebtLifecycleSignal) {
|
if (receivablesDebtLifecycleSignal) {
|
||||||
reasons.push("receivables_debt_lifecycle_signal_detected");
|
reasons.push("receivables_debt_lifecycle_signal_detected");
|
||||||
if (hasFlexibleReceivablesDebtSignal(text)) {
|
|
||||||
reasons.push("receivables_signal_detected_flexible_phrase");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
intent: receivablesDebtLifecycleSignal ? "receivables_confirmed_as_of_date" : "list_receivables_counterparties",
|
intent: receivablesDebtLifecycleSignal ? "receivables_confirmed_as_of_date" : "list_receivables_counterparties",
|
||||||
|
|
@ -1306,14 +1270,11 @@ function resolveAddressIntent(userMessage) {
|
||||||
reasons
|
reasons
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (hasAny(text, PAYABLES_STRONG) || hasFlexiblePayablesDebtSignal(text)) {
|
if (hasAny(text, PAYABLES_STRONG)) {
|
||||||
const reasons = ["payables_signal_detected"];
|
const reasons = ["payables_signal_detected"];
|
||||||
const payablesDebtLifecycleSignal = hasPayablesDebtLifecycleSignal(text) || hasFlexiblePayablesDebtSignal(text);
|
const payablesDebtLifecycleSignal = hasPayablesDebtLifecycleSignal(text);
|
||||||
if (payablesDebtLifecycleSignal) {
|
if (payablesDebtLifecycleSignal) {
|
||||||
reasons.push("payables_debt_lifecycle_signal_detected");
|
reasons.push("payables_debt_lifecycle_signal_detected");
|
||||||
if (hasFlexiblePayablesDebtSignal(text)) {
|
|
||||||
reasons.push("payables_signal_detected_flexible_phrase");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
intent: payablesDebtLifecycleSignal ? "payables_confirmed_as_of_date" : "list_payables_counterparties",
|
intent: payablesDebtLifecycleSignal ? "payables_confirmed_as_of_date" : "list_payables_counterparties",
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
};
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.executeAddressMcpQuery = executeAddressMcpQuery;
|
exports.executeAddressMcpQuery = executeAddressMcpQuery;
|
||||||
exports.executeAddressMcpMetadata = executeAddressMcpMetadata;
|
|
||||||
const config_1 = require("../config");
|
const config_1 = require("../config");
|
||||||
const iconv_lite_1 = __importDefault(require("iconv-lite"));
|
const iconv_lite_1 = __importDefault(require("iconv-lite"));
|
||||||
function toStringValue(value) {
|
function toStringValue(value) {
|
||||||
|
|
@ -166,7 +165,7 @@ function parseRowsFromTextTable(source) {
|
||||||
}
|
}
|
||||||
return normalizeMojibakeRows(rows);
|
return normalizeMojibakeRows(rows);
|
||||||
}
|
}
|
||||||
function parseRowsPayload(payload, options = {}) {
|
function parseExecutePayload(payload) {
|
||||||
if (!payload || typeof payload !== "object") {
|
if (!payload || typeof payload !== "object") {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
|
|
@ -209,13 +208,6 @@ function parseRowsPayload(payload, options = {}) {
|
||||||
error: null
|
error: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (source.data && typeof source.data === "object" && options.allowSingleObjectRow) {
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
rows: [normalizeMojibakeValue(source.data)],
|
|
||||||
error: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
rows: [],
|
rows: [],
|
||||||
|
|
@ -269,7 +261,7 @@ async function executeAddressMcpQuery(input) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const payload = responseText.trim() ? JSON.parse(responseText) : {};
|
const payload = responseText.trim() ? JSON.parse(responseText) : {};
|
||||||
const parsed = parseRowsPayload(payload);
|
const parsed = parseExecutePayload(payload);
|
||||||
if (!parsed.ok) {
|
if (!parsed.ok) {
|
||||||
return {
|
return {
|
||||||
fetched_rows: 0,
|
fetched_rows: 0,
|
||||||
|
|
@ -302,90 +294,3 @@ async function executeAddressMcpQuery(input) {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function executeAddressMcpMetadata(input) {
|
|
||||||
const endpoint = buildMcpUrl("/api/get_metadata");
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeout = setTimeout(() => controller.abort(), Math.max(300, config_1.ASSISTANT_MCP_TIMEOUT_MS));
|
|
||||||
try {
|
|
||||||
const body = {};
|
|
||||||
if (typeof input.filter === "string" && input.filter.trim().length > 0) {
|
|
||||||
body.filter = input.filter.trim();
|
|
||||||
}
|
|
||||||
if (typeof input.meta_type === "string" && input.meta_type.trim().length > 0) {
|
|
||||||
body.meta_type = input.meta_type.trim();
|
|
||||||
}
|
|
||||||
else if (Array.isArray(input.meta_type) && input.meta_type.length > 0) {
|
|
||||||
const values = input.meta_type
|
|
||||||
.map((item) => String(item ?? "").trim())
|
|
||||||
.filter((item) => item.length > 0);
|
|
||||||
if (values.length > 0) {
|
|
||||||
body.meta_type = values;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (typeof input.name_mask === "string" && input.name_mask.trim().length > 0) {
|
|
||||||
body.name_mask = input.name_mask.trim();
|
|
||||||
}
|
|
||||||
if (typeof input.limit === "number" && Number.isFinite(input.limit)) {
|
|
||||||
body.limit = Math.max(1, Math.min(1000, Math.trunc(input.limit)));
|
|
||||||
}
|
|
||||||
if (typeof input.offset === "number" && Number.isFinite(input.offset)) {
|
|
||||||
body.offset = Math.max(0, Math.min(1_000_000, Math.trunc(input.offset)));
|
|
||||||
}
|
|
||||||
if (Array.isArray(input.sections) && input.sections.length > 0) {
|
|
||||||
const sections = input.sections
|
|
||||||
.map((item) => String(item ?? "").trim())
|
|
||||||
.filter((item) => item.length > 0);
|
|
||||||
if (sections.length > 0) {
|
|
||||||
body.sections = sections;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (input.extension_name !== undefined) {
|
|
||||||
body.extension_name = input.extension_name;
|
|
||||||
}
|
|
||||||
const response = await fetch(endpoint, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"content-type": "application/json; charset=utf-8"
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
signal: controller.signal
|
|
||||||
});
|
|
||||||
const responseText = await response.text();
|
|
||||||
if (!response.ok) {
|
|
||||||
return {
|
|
||||||
fetched_rows: 0,
|
|
||||||
raw_rows: [],
|
|
||||||
rows: [],
|
|
||||||
error: `MCP HTTP ${response.status}: ${responseText.slice(0, 240)}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const payload = responseText.trim() ? JSON.parse(responseText) : {};
|
|
||||||
const parsed = parseRowsPayload(payload, { allowSingleObjectRow: true });
|
|
||||||
if (!parsed.ok) {
|
|
||||||
return {
|
|
||||||
fetched_rows: 0,
|
|
||||||
raw_rows: [],
|
|
||||||
rows: [],
|
|
||||||
error: parsed.error
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
fetched_rows: parsed.rows.length,
|
|
||||||
raw_rows: parsed.rows,
|
|
||||||
rows: parsed.rows,
|
|
||||||
error: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
|
||||||
return {
|
|
||||||
fetched_rows: 0,
|
|
||||||
raw_rows: [],
|
|
||||||
rows: [],
|
|
||||||
error: `MCP fetch failed: ${message}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ const RESULT_SET_TYPE_BY_INTENT = {
|
||||||
supplier_payouts_profile: "counterparty_list",
|
supplier_payouts_profile: "counterparty_list",
|
||||||
list_payables_counterparties: "counterparty_list",
|
list_payables_counterparties: "counterparty_list",
|
||||||
payables_confirmed_as_of_date: "balance_snapshot",
|
payables_confirmed_as_of_date: "balance_snapshot",
|
||||||
vat_payable_confirmed_as_of_date: "balance_snapshot",
|
|
||||||
receivables_confirmed_as_of_date: "balance_snapshot",
|
receivables_confirmed_as_of_date: "balance_snapshot",
|
||||||
list_receivables_counterparties: "counterparty_list",
|
list_receivables_counterparties: "counterparty_list",
|
||||||
list_contracts_by_counterparty: "contract_list",
|
list_contracts_by_counterparty: "contract_list",
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,6 @@ const ADDRESS_ANCHOR_RECOVERY_LIMIT = 1000;
|
||||||
const ADDRESS_CONFIRMED_PAYABLES_MIN_LIMIT = 200;
|
const ADDRESS_CONFIRMED_PAYABLES_MIN_LIMIT = 200;
|
||||||
const COUNTERPARTY_CATALOG_LOOKUP_LIMIT = 1000;
|
const COUNTERPARTY_CATALOG_LOOKUP_LIMIT = 1000;
|
||||||
const COUNTERPARTY_CATALOG_CACHE_TTL_MS = 120_000;
|
const COUNTERPARTY_CATALOG_CACHE_TTL_MS = 120_000;
|
||||||
const VAT_METADATA_PROBE_LIMIT = 100;
|
|
||||||
const VAT_SOURCE_PROBE_MAX_OBJECTS = 8;
|
|
||||||
const VAT_METADATA_PROBE_TYPES = ["РегистрНакопления", "РегистрСведений", "Документ"];
|
|
||||||
const VAT_METADATA_PROBE_MASKS = ["ндс", "книгапродаж", "книгапокупок", "счетфактур", "вычет", "восстанов"];
|
|
||||||
const PARTY_ANCHOR_STOPWORDS = new Set([
|
const PARTY_ANCHOR_STOPWORDS = new Set([
|
||||||
"ооо",
|
"ооо",
|
||||||
"ао",
|
"ао",
|
||||||
|
|
@ -126,262 +122,6 @@ function valueAsString(value) {
|
||||||
}
|
}
|
||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
function normalizeIsoDateForQuery(value) {
|
|
||||||
if (typeof value !== "string") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const trimmed = value.trim();
|
|
||||||
if (!trimmed) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const match = trimmed.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;
|
|
||||||
}
|
|
||||||
const candidate = new Date(Date.UTC(year, month - 1, day));
|
|
||||||
if (candidate.getUTCFullYear() !== year ||
|
|
||||||
candidate.getUTCMonth() + 1 !== month ||
|
|
||||||
candidate.getUTCDate() !== day) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return `${match[1]}-${match[2]}-${match[3]}`;
|
|
||||||
}
|
|
||||||
function toDateTimeExprForQuery(isoDate) {
|
|
||||||
const match = String(isoDate ?? "").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;
|
|
||||||
}
|
|
||||||
return `ДАТАВРЕМЯ(${year}, ${month}, ${day}, 23, 59, 59)`;
|
|
||||||
}
|
|
||||||
function shouldProbeVatSourcesForForecast(userMessage) {
|
|
||||||
const text = String(userMessage ?? "")
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/ё/g, "е");
|
|
||||||
if (!text.trim()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return /(?:в\s+налогов|почему|из\s+чего|источн|декларац|книга\s+продаж|книга\s+покупок|вычет|восстанов)/iu.test(text);
|
|
||||||
}
|
|
||||||
function detectVatMetadataObjectType(fullName) {
|
|
||||||
const normalized = String(fullName ?? "").trim();
|
|
||||||
if (!normalized) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (normalized.startsWith("Документ.")) {
|
|
||||||
return "document";
|
|
||||||
}
|
|
||||||
if (normalized.startsWith("РегистрНакопления.") || normalized.startsWith("РегистрСведений.")) {
|
|
||||||
return "register";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
function extractVatMetadataObjects(rows) {
|
|
||||||
const out = [];
|
|
||||||
const seen = new Set();
|
|
||||||
for (const row of rows) {
|
|
||||||
const fullName = valueAsString(row.ПолноеИмя ?? row.full_name ?? row.FullName ?? row.Имя ?? row.name ?? row.Name).trim() || null;
|
|
||||||
if (!fullName) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const objectType = detectVatMetadataObjectType(fullName);
|
|
||||||
if (!objectType) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (seen.has(fullName)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
seen.add(fullName);
|
|
||||||
const synonym = valueAsString(row.Синоним ?? row.synonym ?? row.Synonym ?? row.Представление ?? row.presentation).trim() || null;
|
|
||||||
out.push({
|
|
||||||
fullName,
|
|
||||||
synonym,
|
|
||||||
objectType
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
function scoreVatMetadataObject(item) {
|
|
||||||
const fullName = item.fullName.toLowerCase();
|
|
||||||
const synonym = String(item.synonym ?? "").toLowerCase();
|
|
||||||
let score = item.objectType === "register" ? 120 : 80;
|
|
||||||
if (fullName.includes("книгипродаж") || synonym.includes("продаж")) {
|
|
||||||
score += 60;
|
|
||||||
}
|
|
||||||
if (fullName.includes("книгипокупок") || synonym.includes("покуп")) {
|
|
||||||
score += 60;
|
|
||||||
}
|
|
||||||
if (fullName.includes("начислен") || synonym.includes("начислен")) {
|
|
||||||
score += 40;
|
|
||||||
}
|
|
||||||
if (fullName.includes("предъявлен") || synonym.includes("предъявлен")) {
|
|
||||||
score += 40;
|
|
||||||
}
|
|
||||||
if (fullName.includes("оплатындс") || synonym.includes("в бюджет")) {
|
|
||||||
score += 35;
|
|
||||||
}
|
|
||||||
if (fullName.includes("декларац")) {
|
|
||||||
score -= 25;
|
|
||||||
}
|
|
||||||
if (fullName.includes("пояснен")) {
|
|
||||||
score -= 25;
|
|
||||||
}
|
|
||||||
return score;
|
|
||||||
}
|
|
||||||
function buildVatObjectProbeQuery(object, asOfExpr) {
|
|
||||||
if (object.objectType === "document") {
|
|
||||||
return `
|
|
||||||
ВЫБРАТЬ ПЕРВЫЕ 1
|
|
||||||
Док.Дата КАК Период,
|
|
||||||
ПРЕДСТАВЛЕНИЕ(Док.Ссылка) КАК Регистратор,
|
|
||||||
"" КАК СчетДт,
|
|
||||||
"" КАК СчетКт,
|
|
||||||
0 КАК Сумма
|
|
||||||
ИЗ
|
|
||||||
${object.fullName} КАК Док
|
|
||||||
ГДЕ
|
|
||||||
Док.Дата <= ${asOfExpr}
|
|
||||||
УПОРЯДОЧИТЬ ПО
|
|
||||||
Док.Дата УБЫВ
|
|
||||||
`.trim();
|
|
||||||
}
|
|
||||||
return `
|
|
||||||
ВЫБРАТЬ ПЕРВЫЕ 1
|
|
||||||
Движения.Период КАК Период,
|
|
||||||
ПРЕДСТАВЛЕНИЕ(Движения.Регистратор) КАК Регистратор,
|
|
||||||
"" КАК СчетДт,
|
|
||||||
"" КАК СчетКт,
|
|
||||||
0 КАК Сумма
|
|
||||||
ИЗ
|
|
||||||
${object.fullName} КАК Движения
|
|
||||||
ГДЕ
|
|
||||||
Движения.Период <= ${asOfExpr}
|
|
||||||
УПОРЯДОЧИТЬ ПО
|
|
||||||
Движения.Период УБЫВ
|
|
||||||
`.trim();
|
|
||||||
}
|
|
||||||
async function probeVatDirectSources(filters) {
|
|
||||||
const asOfDate = normalizeIsoDateForQuery(filters.as_of_date) ??
|
|
||||||
normalizeIsoDateForQuery(filters.period_to) ??
|
|
||||||
normalizeIsoDateForQuery(filters.period_from);
|
|
||||||
if (!asOfDate) {
|
|
||||||
return {
|
|
||||||
status: "skipped",
|
|
||||||
objectsTotal: 0,
|
|
||||||
documentsTotal: 0,
|
|
||||||
registersTotal: 0,
|
|
||||||
probedSources: [],
|
|
||||||
errors: ["as_of_date_not_resolved_for_vat_probe"]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const asOfExpr = toDateTimeExprForQuery(asOfDate);
|
|
||||||
if (!asOfExpr) {
|
|
||||||
return {
|
|
||||||
status: "skipped",
|
|
||||||
objectsTotal: 0,
|
|
||||||
documentsTotal: 0,
|
|
||||||
registersTotal: 0,
|
|
||||||
probedSources: [],
|
|
||||||
errors: ["as_of_expr_not_resolved_for_vat_probe"]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const metadataRequests = VAT_METADATA_PROBE_TYPES.flatMap((metaType) => VAT_METADATA_PROBE_MASKS.map((nameMask) => ({
|
|
||||||
meta_type: metaType,
|
|
||||||
name_mask: nameMask,
|
|
||||||
limit: VAT_METADATA_PROBE_LIMIT
|
|
||||||
})));
|
|
||||||
const metadataResponses = await Promise.all(metadataRequests.map((request) => (0, addressMcpClient_1.executeAddressMcpMetadata)(request)));
|
|
||||||
const metadataErrors = [];
|
|
||||||
const metadataObjectsBuffer = [];
|
|
||||||
for (const [index, response] of metadataResponses.entries()) {
|
|
||||||
const request = metadataRequests[index];
|
|
||||||
if (response.error) {
|
|
||||||
metadataErrors.push(`${request.meta_type}:${request.name_mask}:${response.error}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
metadataObjectsBuffer.push(...extractVatMetadataObjects(response.rows));
|
|
||||||
}
|
|
||||||
const deduplicatedObjects = new Map();
|
|
||||||
for (const item of metadataObjectsBuffer) {
|
|
||||||
const existing = deduplicatedObjects.get(item.fullName);
|
|
||||||
if (!existing) {
|
|
||||||
deduplicatedObjects.set(item.fullName, item);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!existing.synonym && item.synonym) {
|
|
||||||
deduplicatedObjects.set(item.fullName, {
|
|
||||||
...existing,
|
|
||||||
synonym: item.synonym
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const discoveredMetadataObjects = Array.from(deduplicatedObjects.values()).sort((a, b) => scoreVatMetadataObject(b) - scoreVatMetadataObject(a) || a.fullName.localeCompare(b.fullName, "ru"));
|
|
||||||
const metadataObjects = discoveredMetadataObjects.slice(0, VAT_SOURCE_PROBE_MAX_OBJECTS);
|
|
||||||
const probeRows = [];
|
|
||||||
for (const object of metadataObjects) {
|
|
||||||
const probeQuery = buildVatObjectProbeQuery(object, asOfExpr);
|
|
||||||
const probeResult = await (0, addressMcpClient_1.executeAddressMcpQuery)({
|
|
||||||
query: probeQuery,
|
|
||||||
limit: 1
|
|
||||||
});
|
|
||||||
if (probeResult.error) {
|
|
||||||
probeRows.push({
|
|
||||||
fullName: object.fullName,
|
|
||||||
synonym: object.synonym,
|
|
||||||
objectType: object.objectType,
|
|
||||||
status: "error",
|
|
||||||
rowsFetched: probeResult.fetched_rows,
|
|
||||||
error: probeResult.error
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const firstRow = probeResult.raw_rows[0] ?? null;
|
|
||||||
const lastPeriod = firstRow !== null
|
|
||||||
? valueAsString(firstRow.Период ?? firstRow.period).trim() ||
|
|
||||||
null
|
|
||||||
: null;
|
|
||||||
const sampleRegistrator = firstRow !== null
|
|
||||||
? valueAsString(firstRow.Регистратор ??
|
|
||||||
firstRow.registrator ??
|
|
||||||
firstRow.Registrator).trim() || null
|
|
||||||
: null;
|
|
||||||
probeRows.push({
|
|
||||||
fullName: object.fullName,
|
|
||||||
synonym: object.synonym,
|
|
||||||
objectType: object.objectType,
|
|
||||||
status: probeResult.raw_rows.length > 0 ? "ok" : "empty",
|
|
||||||
rowsFetched: probeResult.fetched_rows,
|
|
||||||
lastPeriod,
|
|
||||||
sampleRegistrator
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const status = metadataResponses.every((item) => item.error) ? "error" : "ok";
|
|
||||||
const allErrors = [
|
|
||||||
...metadataErrors,
|
|
||||||
...probeRows
|
|
||||||
.filter((item) => item.status === "error")
|
|
||||||
.map((item) => `${item.fullName}: ${valueAsString(item.error).slice(0, 120)}`)
|
|
||||||
];
|
|
||||||
return {
|
|
||||||
status,
|
|
||||||
objectsTotal: discoveredMetadataObjects.length,
|
|
||||||
documentsTotal: discoveredMetadataObjects.filter((item) => item.objectType === "document").length,
|
|
||||||
registersTotal: discoveredMetadataObjects.filter((item) => item.objectType === "register").length,
|
|
||||||
probedSources: probeRows,
|
|
||||||
errors: allErrors
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function transliterateCyrillicToLatin(value) {
|
function transliterateCyrillicToLatin(value) {
|
||||||
const map = {
|
const map = {
|
||||||
а: "a",
|
а: "a",
|
||||||
|
|
@ -908,8 +648,7 @@ function isConfirmedBalanceIntent(intent) {
|
||||||
return (intent === "account_balance_snapshot" ||
|
return (intent === "account_balance_snapshot" ||
|
||||||
intent === "documents_forming_balance" ||
|
intent === "documents_forming_balance" ||
|
||||||
intent === "payables_confirmed_as_of_date" ||
|
intent === "payables_confirmed_as_of_date" ||
|
||||||
intent === "receivables_confirmed_as_of_date" ||
|
intent === "receivables_confirmed_as_of_date");
|
||||||
intent === "vat_payable_confirmed_as_of_date");
|
|
||||||
}
|
}
|
||||||
function resolveAsOfDateBasis(filters) {
|
function resolveAsOfDateBasis(filters) {
|
||||||
const asOfDate = normalizeAnalysisDateHint(filters.as_of_date);
|
const asOfDate = normalizeAnalysisDateHint(filters.as_of_date);
|
||||||
|
|
@ -1079,7 +818,7 @@ function enforceStrictAccountScopeForIntent(plan, intent) {
|
||||||
account_scope_mode: "strict"
|
account_scope_mode: "strict"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function resolveExecutionFiltersForConfirmedBalance(filters, analysisDate) {
|
function resolveExecutionFiltersForPayablesConfirmedBalance(filters, analysisDate) {
|
||||||
const explicitAsOf = normalizeAnalysisDateHint(filters.as_of_date);
|
const explicitAsOf = normalizeAnalysisDateHint(filters.as_of_date);
|
||||||
const periodTo = normalizeAnalysisDateHint(filters.period_to);
|
const periodTo = normalizeAnalysisDateHint(filters.period_to);
|
||||||
const derivedAsOf = explicitAsOf ?? periodTo ?? analysisDate ?? null;
|
const derivedAsOf = explicitAsOf ?? periodTo ?? analysisDate ?? null;
|
||||||
|
|
@ -1563,9 +1302,6 @@ function buildLimitedOffers(input) {
|
||||||
else if (input.intent === "receivables_confirmed_as_of_date") {
|
else if (input.intent === "receivables_confirmed_as_of_date") {
|
||||||
offers.push("показать подтвержденный реестр открытой дебиторской задолженности на дату среза по 62/76");
|
offers.push("показать подтвержденный реестр открытой дебиторской задолженности на дату среза по 62/76");
|
||||||
}
|
}
|
||||||
else if (input.intent === "vat_payable_confirmed_as_of_date") {
|
|
||||||
offers.push("показать подтвержденную сумму НДС к уплате на дату среза по счетам 68*");
|
|
||||||
}
|
|
||||||
else if (input.intent === "payables_confirmed_as_of_date") {
|
else if (input.intent === "payables_confirmed_as_of_date") {
|
||||||
offers.push("показать подтвержденный реестр открытых обязательств на дату среза по 60/76");
|
offers.push("показать подтвержденный реестр открытых обязательств на дату среза по 60/76");
|
||||||
}
|
}
|
||||||
|
|
@ -1612,8 +1348,7 @@ function buildLimitedIntentSignalLine(input) {
|
||||||
list_receivables_counterparties: "Сигнал запроса: нужен ранжированный список должников.",
|
list_receivables_counterparties: "Сигнал запроса: нужен ранжированный список должников.",
|
||||||
list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов.",
|
list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов.",
|
||||||
receivables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез дебиторской задолженности на дату.",
|
receivables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез дебиторской задолженности на дату.",
|
||||||
payables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез обязательств к оплате на дату.",
|
payables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез обязательств к оплате на дату."
|
||||||
vat_payable_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез НДС к уплате на дату."
|
|
||||||
};
|
};
|
||||||
const byShape = {
|
const byShape = {
|
||||||
AGGREGATE_LOOKUP: "Сигнал запроса: агрегатный вопрос по периоду/срезу.",
|
AGGREGATE_LOOKUP: "Сигнал запроса: агрегатный вопрос по периоду/срезу.",
|
||||||
|
|
@ -1739,15 +1474,16 @@ function buildLimitedExecutionResult(input) {
|
||||||
});
|
});
|
||||||
const requestedResultMode = resolveRequestedResultMode(input.intent.intent, input.filters);
|
const requestedResultMode = resolveRequestedResultMode(input.intent.intent, input.filters);
|
||||||
const reasonsWithConfirmedFallback = withConfirmedBalanceFallbackReason(input.reasons, requestedResultMode, undefined, resultSemantics.result_mode);
|
const reasonsWithConfirmedFallback = withConfirmedBalanceFallbackReason(input.reasons, requestedResultMode, undefined, resultSemantics.result_mode);
|
||||||
const exactLimitedReason = input.intent.intent === "payables_confirmed_as_of_date"
|
const reasons = (input.intent.intent === "payables_confirmed_as_of_date" || input.intent.intent === "receivables_confirmed_as_of_date") &&
|
||||||
? "exact_payables_mode_limited_response"
|
!reasonsWithConfirmedFallback.includes(input.intent.intent === "payables_confirmed_as_of_date"
|
||||||
: input.intent.intent === "receivables_confirmed_as_of_date"
|
? "exact_payables_mode_limited_response"
|
||||||
? "exact_receivables_mode_limited_response"
|
: "exact_receivables_mode_limited_response")
|
||||||
: input.intent.intent === "vat_payable_confirmed_as_of_date"
|
? [
|
||||||
? "exact_vat_payable_mode_limited_response"
|
...reasonsWithConfirmedFallback,
|
||||||
: null;
|
input.intent.intent === "payables_confirmed_as_of_date"
|
||||||
const reasons = exactLimitedReason && !reasonsWithConfirmedFallback.includes(exactLimitedReason)
|
? "exact_payables_mode_limited_response"
|
||||||
? [...reasonsWithConfirmedFallback, exactLimitedReason]
|
: "exact_receivables_mode_limited_response"
|
||||||
|
]
|
||||||
: reasonsWithConfirmedFallback;
|
: reasonsWithConfirmedFallback;
|
||||||
const routeExpectationAudit = input.routeExpectationAudit ??
|
const routeExpectationAudit = input.routeExpectationAudit ??
|
||||||
buildRouteExpectationAudit({
|
buildRouteExpectationAudit({
|
||||||
|
|
@ -1855,20 +1591,13 @@ class AddressQueryService {
|
||||||
const confirmedBalancePayablesIntent = (intent.intent === "list_payables_counterparties" || intent.intent === "payables_confirmed_as_of_date") &&
|
const confirmedBalancePayablesIntent = (intent.intent === "list_payables_counterparties" || intent.intent === "payables_confirmed_as_of_date") &&
|
||||||
requestedResultMode === "confirmed_balance";
|
requestedResultMode === "confirmed_balance";
|
||||||
const confirmedBalanceReceivablesIntent = intent.intent === "receivables_confirmed_as_of_date" && requestedResultMode === "confirmed_balance";
|
const confirmedBalanceReceivablesIntent = intent.intent === "receivables_confirmed_as_of_date" && requestedResultMode === "confirmed_balance";
|
||||||
const confirmedBalanceVatPayableIntent = intent.intent === "vat_payable_confirmed_as_of_date" && requestedResultMode === "confirmed_balance";
|
|
||||||
const payablesConfirmedExecution = confirmedBalancePayablesIntent
|
const payablesConfirmedExecution = confirmedBalancePayablesIntent
|
||||||
? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate)
|
? resolveExecutionFiltersForPayablesConfirmedBalance(filters.extracted_filters, analysisDate)
|
||||||
: null;
|
: null;
|
||||||
const receivablesConfirmedExecution = confirmedBalanceReceivablesIntent
|
const receivablesConfirmedExecution = confirmedBalanceReceivablesIntent
|
||||||
? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate)
|
? resolveExecutionFiltersForPayablesConfirmedBalance(filters.extracted_filters, analysisDate)
|
||||||
: null;
|
: null;
|
||||||
const vatPayableConfirmedExecution = confirmedBalanceVatPayableIntent
|
const executionFilters = payablesConfirmedExecution?.executionFilters ?? receivablesConfirmedExecution?.executionFilters ?? filters.extracted_filters;
|
||||||
? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate)
|
|
||||||
: null;
|
|
||||||
const executionFilters = payablesConfirmedExecution?.executionFilters ??
|
|
||||||
receivablesConfirmedExecution?.executionFilters ??
|
|
||||||
vatPayableConfirmedExecution?.executionFilters ??
|
|
||||||
filters.extracted_filters;
|
|
||||||
if (payablesConfirmedExecution?.asOfDerived &&
|
if (payablesConfirmedExecution?.asOfDerived &&
|
||||||
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)) {
|
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)) {
|
||||||
if (!filters.warnings.includes("as_of_date_derived_for_confirmed_payables")) {
|
if (!filters.warnings.includes("as_of_date_derived_for_confirmed_payables")) {
|
||||||
|
|
@ -1887,15 +1616,6 @@ class AddressQueryService {
|
||||||
baseReasons.push("as_of_date_derived_for_confirmed_receivables");
|
baseReasons.push("as_of_date_derived_for_confirmed_receivables");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (vatPayableConfirmedExecution?.asOfDerived &&
|
|
||||||
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)) {
|
|
||||||
if (!filters.warnings.includes("as_of_date_derived_for_confirmed_vat_payable")) {
|
|
||||||
filters.warnings.push("as_of_date_derived_for_confirmed_vat_payable");
|
|
||||||
}
|
|
||||||
if (!baseReasons.includes("as_of_date_derived_for_confirmed_vat_payable")) {
|
|
||||||
baseReasons.push("as_of_date_derived_for_confirmed_vat_payable");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const capabilityDecision = (0, addressCapabilityPolicy_1.resolveAddressCapabilityRouteDecision)(intent.intent);
|
const capabilityDecision = (0, addressCapabilityPolicy_1.resolveAddressCapabilityRouteDecision)(intent.intent);
|
||||||
const capabilityAudit = buildCapabilityAudit(intent.intent);
|
const capabilityAudit = buildCapabilityAudit(intent.intent);
|
||||||
const shadowRouteAudit = buildShadowRouteAudit({
|
const shadowRouteAudit = buildShadowRouteAudit({
|
||||||
|
|
@ -1926,15 +1646,12 @@ class AddressQueryService {
|
||||||
shadowRouteAudit
|
shadowRouteAudit
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const composeOptionsFromFilters = (filterSet, options = {}) => ({
|
const composeOptionsFromFilters = (filterSet) => ({
|
||||||
userMessage,
|
userMessage,
|
||||||
periodFrom: typeof filterSet.period_from === "string" ? filterSet.period_from : undefined,
|
periodFrom: typeof filterSet.period_from === "string" ? filterSet.period_from : undefined,
|
||||||
periodTo: typeof filterSet.period_to === "string" ? filterSet.period_to : undefined,
|
periodTo: typeof filterSet.period_to === "string" ? filterSet.period_to : undefined,
|
||||||
asOfDate: typeof filterSet.as_of_date === "string" ? filterSet.as_of_date : undefined,
|
asOfDate: typeof filterSet.as_of_date === "string" ? filterSet.as_of_date : undefined,
|
||||||
requestedResultMode,
|
requestedResultMode
|
||||||
vatDirectSourceProbe: options.vatDirectSourceProbe ?? undefined,
|
|
||||||
emphasizeNumbers: options.emphasizeNumbers ?? undefined,
|
|
||||||
useRubCurrency: options.useRubCurrency ?? undefined
|
|
||||||
});
|
});
|
||||||
const futureGuardReferenceDate = resolveFutureGuardReferenceDate(analysisDate, executionFilters);
|
const futureGuardReferenceDate = resolveFutureGuardReferenceDate(analysisDate, executionFilters);
|
||||||
let anchor = (0, resolveStage_1.resolvePrimaryAnchor)(intent.intent, filters.extracted_filters);
|
let anchor = (0, resolveStage_1.resolvePrimaryAnchor)(intent.intent, filters.extracted_filters);
|
||||||
|
|
@ -1969,10 +1686,6 @@ class AddressQueryService {
|
||||||
!baseReasons.includes("confirmed_balance_exact_receivables_intent")) {
|
!baseReasons.includes("confirmed_balance_exact_receivables_intent")) {
|
||||||
baseReasons.push("confirmed_balance_exact_receivables_intent");
|
baseReasons.push("confirmed_balance_exact_receivables_intent");
|
||||||
}
|
}
|
||||||
if (intent.intent === "vat_payable_confirmed_as_of_date" &&
|
|
||||||
!baseReasons.includes("confirmed_balance_exact_vat_payable_intent")) {
|
|
||||||
baseReasons.push("confirmed_balance_exact_vat_payable_intent");
|
|
||||||
}
|
|
||||||
if (requestedResultMode === "confirmed_balance" &&
|
if (requestedResultMode === "confirmed_balance" &&
|
||||||
recipeIntent === "open_items_by_counterparty_or_contract" &&
|
recipeIntent === "open_items_by_counterparty_or_contract" &&
|
||||||
!baseReasons.includes("confirmed_balance_unavailable_fallback_to_heuristic_candidates")) {
|
!baseReasons.includes("confirmed_balance_unavailable_fallback_to_heuristic_candidates")) {
|
||||||
|
|
@ -2898,29 +2611,7 @@ class AddressQueryService {
|
||||||
shadowRouteAudit
|
shadowRouteAudit
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const vatProbeRequired = composeIntent === "vat_payable_confirmed_as_of_date" ||
|
const factual = (0, composeStage_1.composeFactualReply)(composeIntent, filteredRows, composeOptionsFromFilters(executionFilters));
|
||||||
(composeIntent === "vat_payable_forecast" && shouldProbeVatSourcesForForecast(userMessage));
|
|
||||||
const vatDirectSourceProbe = vatProbeRequired ? await probeVatDirectSources(executionFilters) : null;
|
|
||||||
const shouldEmphasizeNumbers = composeIntent === "vat_payable_forecast" ||
|
|
||||||
composeIntent === "vat_payable_confirmed_as_of_date" ||
|
|
||||||
composeIntent === "payables_confirmed_as_of_date" ||
|
|
||||||
composeIntent === "receivables_confirmed_as_of_date";
|
|
||||||
const shouldUseRubCurrency = composeIntent === "vat_payable_forecast";
|
|
||||||
const factual = (0, composeStage_1.composeFactualReply)(composeIntent, filteredRows, composeOptionsFromFilters(executionFilters, {
|
|
||||||
vatDirectSourceProbe,
|
|
||||||
emphasizeNumbers: shouldEmphasizeNumbers,
|
|
||||||
useRubCurrency: shouldUseRubCurrency
|
|
||||||
}));
|
|
||||||
const vatProbeLimitations = vatProbeRequired && vatDirectSourceProbe
|
|
||||||
? vatDirectSourceProbe.status === "error"
|
|
||||||
? ["vat_source_probe_error"]
|
|
||||||
: vatDirectSourceProbe.status === "skipped"
|
|
||||||
? ["vat_source_probe_skipped"]
|
|
||||||
: vatDirectSourceProbe.errors.length > 0
|
|
||||||
? ["vat_source_probe_partial_errors"]
|
|
||||||
: []
|
|
||||||
: [];
|
|
||||||
const factualLimitations = [...filters.warnings, ...vatProbeLimitations];
|
|
||||||
const factualResultSemantics = mergeAddressResultSemantics(deriveAddressResultSemantics({
|
const factualResultSemantics = mergeAddressResultSemantics(deriveAddressResultSemantics({
|
||||||
intent: composeIntent,
|
intent: composeIntent,
|
||||||
selectedRecipe: effectiveRecipeId,
|
selectedRecipe: effectiveRecipeId,
|
||||||
|
|
@ -2967,15 +2658,10 @@ class AddressQueryService {
|
||||||
routeExpectationAudit: finalRouteExpectationAudit
|
routeExpectationAudit: finalRouteExpectationAudit
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const exactConfirmedIntent = (intent.intent === "payables_confirmed_as_of_date" && composeIntent === "payables_confirmed_as_of_date") ||
|
if (((intent.intent === "payables_confirmed_as_of_date" && composeIntent === "payables_confirmed_as_of_date") ||
|
||||||
(intent.intent === "receivables_confirmed_as_of_date" && composeIntent === "receivables_confirmed_as_of_date") ||
|
(intent.intent === "receivables_confirmed_as_of_date" && composeIntent === "receivables_confirmed_as_of_date")) &&
|
||||||
(intent.intent === "vat_payable_confirmed_as_of_date" && composeIntent === "vat_payable_confirmed_as_of_date");
|
factualResultSemantics.balance_confirmed !== true) {
|
||||||
if (exactConfirmedIntent && factualResultSemantics.balance_confirmed !== true) {
|
const exactModeName = intent.intent === "payables_confirmed_as_of_date" ? "payables" : "receivables";
|
||||||
const exactModeName = intent.intent === "payables_confirmed_as_of_date"
|
|
||||||
? "payables"
|
|
||||||
: intent.intent === "receivables_confirmed_as_of_date"
|
|
||||||
? "receivables"
|
|
||||||
: "vat_payable";
|
|
||||||
return buildLimitedExecutionResult({
|
return buildLimitedExecutionResult({
|
||||||
mode,
|
mode,
|
||||||
shape,
|
shape,
|
||||||
|
|
@ -3000,9 +2686,7 @@ class AddressQueryService {
|
||||||
materializationDropReason: rowDiagnostics.materializationDropReason,
|
materializationDropReason: rowDiagnostics.materializationDropReason,
|
||||||
category: "recipe_visibility_gap",
|
category: "recipe_visibility_gap",
|
||||||
reasonText: `exact ${exactModeName} mode: confirmed balance was not proven for the requested as-of slice`,
|
reasonText: `exact ${exactModeName} mode: confirmed balance was not proven for the requested as-of slice`,
|
||||||
nextStep: intent.intent === "vat_payable_confirmed_as_of_date"
|
nextStep: "specify as_of_date/counterparty or enable detailed settlement registers for exact confirmed balance",
|
||||||
? "specify as_of_date/organization or provide VAT settlement registers to prove exact VAT payable balance"
|
|
||||||
: "specify as_of_date/counterparty or enable detailed settlement registers for exact confirmed balance",
|
|
||||||
limitations: [`exact_${exactModeName}_mode_unconfirmed_output_blocked`],
|
limitations: [`exact_${exactModeName}_mode_unconfirmed_output_blocked`],
|
||||||
reasons: [...baseReasons, `exact_${exactModeName}_mode_unconfirmed_output_blocked`],
|
reasons: [...baseReasons, `exact_${exactModeName}_mode_unconfirmed_output_blocked`],
|
||||||
capabilityAudit,
|
capabilityAudit,
|
||||||
|
|
@ -3069,7 +2753,7 @@ class AddressQueryService {
|
||||||
route_expectation_expected_requested_result_modes: finalRouteExpectationAudit.expectedRequestedResultModes,
|
route_expectation_expected_requested_result_modes: finalRouteExpectationAudit.expectedRequestedResultModes,
|
||||||
route_expectation_expected_result_modes: finalRouteExpectationAudit.expectedResultModes,
|
route_expectation_expected_result_modes: finalRouteExpectationAudit.expectedResultModes,
|
||||||
...factualResultSemantics,
|
...factualResultSemantics,
|
||||||
limitations: factualLimitations,
|
limitations: filters.warnings,
|
||||||
reasons: withConfirmedBalanceFallbackReason(reasonsWithRouteExpectation, requestedResultMode, factual.semantics, factualResultSemantics.result_mode)
|
reasons: withConfirmedBalanceFallbackReason(reasonsWithRouteExpectation, requestedResultMode, factual.semantics, factualResultSemantics.result_mode)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -66,28 +66,6 @@ const RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
||||||
УПОРЯДОЧИТЬ ПО
|
УПОРЯДОЧИТЬ ПО
|
||||||
Сумма __ORDER_DIRECTION__
|
Сумма __ORDER_DIRECTION__
|
||||||
`;
|
`;
|
||||||
const VAT_PAYABLE_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
|
||||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
|
||||||
__AS_OF_EXPR__ КАК Период,
|
|
||||||
"Остатки на дату" КАК Регистратор,
|
|
||||||
"" КАК СчетДт,
|
|
||||||
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетКт,
|
|
||||||
Остатки.СуммаРазвернутыйОстатокКт КАК Сумма,
|
|
||||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоДт1,
|
|
||||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоДт2,
|
|
||||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоДт3,
|
|
||||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоКт1,
|
|
||||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоКт2,
|
|
||||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоКт3,
|
|
||||||
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация
|
|
||||||
ИЗ
|
|
||||||
РегистрБухгалтерии.Хозрасчетный.Остатки(__AS_OF_EXPR__, , , ) КАК Остатки
|
|
||||||
ГДЕ
|
|
||||||
Остатки.СуммаРазвернутыйОстатокКт > 0
|
|
||||||
И (__VAT_PAYABLE_ACCOUNTS_MATCH__)
|
|
||||||
УПОРЯДОЧИТЬ ПО
|
|
||||||
Сумма __ORDER_DIRECTION__
|
|
||||||
`;
|
|
||||||
const BANK_DOCS_QUERY_TEMPLATE = `
|
const BANK_DOCS_QUERY_TEMPLATE = `
|
||||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||||
БанкСписание.Дата КАК Период,
|
БанкСписание.Дата КАК Период,
|
||||||
|
|
@ -571,17 +549,6 @@ const BASE_RECIPES = [
|
||||||
account_scope_mode: "preferred",
|
account_scope_mode: "preferred",
|
||||||
query_template: "vat_payable_forecast_profile"
|
query_template: "vat_payable_forecast_profile"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
recipe_id: "address_vat_payable_confirmed_as_of_date_v1",
|
|
||||||
intent: "vat_payable_confirmed_as_of_date",
|
|
||||||
purpose: "Build confirmed VAT payable snapshot as-of date from balances on VAT payable accounts",
|
|
||||||
required_filters: ["as_of_date"],
|
|
||||||
optional_filters: ["period_from", "period_to", "organization", "limit", "sort"],
|
|
||||||
default_limit: 200,
|
|
||||||
account_scope: ["68"],
|
|
||||||
account_scope_mode: "strict",
|
|
||||||
query_template: "vat_payable_confirmed_as_of_balance_profile"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
recipe_id: "address_contracts_by_counterparty_v1",
|
recipe_id: "address_contracts_by_counterparty_v1",
|
||||||
intent: "list_contracts_by_counterparty",
|
intent: "list_contracts_by_counterparty",
|
||||||
|
|
@ -993,27 +960,27 @@ function buildAddressRecipePlan(recipe, filters) {
|
||||||
.replaceAll("__VAT68_DT_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_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", config_1.VAT_PAYABLE_19_PREFIXES))
|
||||||
.replaceAll("__VAT19_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", config_1.VAT_PAYABLE_19_PREFIXES))
|
.replaceAll("__VAT19_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", config_1.VAT_PAYABLE_19_PREFIXES))
|
||||||
: recipe.query_template === "vat_payable_confirmed_as_of_balance_profile"
|
: recipe.query_template === "contracts_by_counterparty_profile"
|
||||||
? (() => {
|
? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
: recipe.query_template === "payables_confirmed_as_of_balance_profile"
|
||||||
? toDateTimeExpr(filters.as_of_date, true)
|
? (() => {
|
||||||
: null) ??
|
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
? toDateTimeExpr(filters.as_of_date, true)
|
||||||
? toDateTimeExpr(filters.period_to, true)
|
|
||||||
: null) ??
|
: null) ??
|
||||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||||
? toDateTimeExpr(filters.period_from, true)
|
? toDateTimeExpr(filters.period_to, true)
|
||||||
: null) ??
|
: null) ??
|
||||||
"ТЕКУЩАЯДАТА()";
|
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||||
return VAT_PAYABLE_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
? toDateTimeExpr(filters.period_from, true)
|
||||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
: null) ??
|
||||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
"ТЕКУЩАЯДАТА()";
|
||||||
.replaceAll("__VAT_PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", config_1.VAT_PAYABLE_68_PREFIXES))
|
return PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||||
})()
|
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||||
: recipe.query_template === "contracts_by_counterparty_profile"
|
.replaceAll("__PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
|
||||||
? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit))
|
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||||
: recipe.query_template === "payables_confirmed_as_of_balance_profile"
|
})()
|
||||||
|
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
|
||||||
? (() => {
|
? (() => {
|
||||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||||
? toDateTimeExpr(filters.as_of_date, true)
|
? toDateTimeExpr(filters.as_of_date, true)
|
||||||
|
|
@ -1025,41 +992,23 @@ function buildAddressRecipePlan(recipe, filters) {
|
||||||
? toDateTimeExpr(filters.period_from, true)
|
? toDateTimeExpr(filters.period_from, true)
|
||||||
: null) ??
|
: null) ??
|
||||||
"ТЕКУЩАЯДАТА()";
|
"ТЕКУЩАЯДАТА()";
|
||||||
return PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
return RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||||
.replaceAll("__PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
|
.replaceAll("__RECEIVABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||||
})()
|
})()
|
||||||
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
|
: MOVEMENTS_QUERY_TEMPLATE
|
||||||
? (() => {
|
.replace("__LIMIT__", String(resolvedLimit))
|
||||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
.replace("__WHERE_CLAUSE__", (() => {
|
||||||
? toDateTimeExpr(filters.as_of_date, true)
|
const extraConditions = [];
|
||||||
: null) ??
|
const accountCondition = buildMovementAccountCondition(filters);
|
||||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
if (accountCondition) {
|
||||||
? toDateTimeExpr(filters.period_to, true)
|
extraConditions.push(accountCondition);
|
||||||
: null) ??
|
}
|
||||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
return buildWhereClause(filters, "Движения.Период", extraConditions);
|
||||||
? toDateTimeExpr(filters.period_from, true)
|
})())
|
||||||
: null) ??
|
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||||
"ТЕКУЩАЯДАТА()";
|
|
||||||
return RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
|
||||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
|
||||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
|
||||||
.replaceAll("__RECEIVABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
|
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
|
||||||
})()
|
|
||||||
: MOVEMENTS_QUERY_TEMPLATE
|
|
||||||
.replace("__LIMIT__", String(resolvedLimit))
|
|
||||||
.replace("__WHERE_CLAUSE__", (() => {
|
|
||||||
const extraConditions = [];
|
|
||||||
const accountCondition = buildMovementAccountCondition(filters);
|
|
||||||
if (accountCondition) {
|
|
||||||
extraConditions.push(accountCondition);
|
|
||||||
}
|
|
||||||
return buildWhereClause(filters, "Движения.Период", extraConditions);
|
|
||||||
})())
|
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
|
||||||
return {
|
return {
|
||||||
recipe,
|
recipe,
|
||||||
query,
|
query,
|
||||||
|
|
|
||||||
|
|
@ -101,35 +101,8 @@ function formatNumberWithDots(value, fractionDigits = 0) {
|
||||||
function formatMoneyRub(value) {
|
function formatMoneyRub(value) {
|
||||||
return `${formatNumberWithDots(value, 2)} ₽`;
|
return `${formatNumberWithDots(value, 2)} ₽`;
|
||||||
}
|
}
|
||||||
function formatVatProbeStatusRu(status) {
|
|
||||||
if (status === "ok") {
|
|
||||||
return "есть движения";
|
|
||||||
}
|
|
||||||
if (status === "empty") {
|
|
||||||
return "движения не найдены";
|
|
||||||
}
|
|
||||||
return "ошибка запроса";
|
|
||||||
}
|
|
||||||
function emphasizeNumericTokens(line) {
|
function emphasizeNumericTokens(line) {
|
||||||
if (!line) {
|
return line;
|
||||||
return line;
|
|
||||||
}
|
|
||||||
const chunks = line.split(/(`[^`]*`)/g);
|
|
||||||
return chunks
|
|
||||||
.map((chunk, index) => {
|
|
||||||
if (index % 2 === 1) {
|
|
||||||
return chunk;
|
|
||||||
}
|
|
||||||
return chunk.replace(/\b-?(?:\d{1,3}(?:[.\s]\d{3})+|\d+)(?:[.,]\d+)?\b/g, (match, offset, source) => {
|
|
||||||
const before = offset > 0 ? source[offset - 1] : "";
|
|
||||||
const after = offset + match.length < source.length ? source[offset + match.length] : "";
|
|
||||||
if (before === "*" || after === "*") {
|
|
||||||
return match;
|
|
||||||
}
|
|
||||||
return `**${match}**`;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.join("");
|
|
||||||
}
|
}
|
||||||
function parseIsoDateToken(value) {
|
function parseIsoDateToken(value) {
|
||||||
const source = String(value ?? "").trim();
|
const source = String(value ?? "").trim();
|
||||||
|
|
@ -162,21 +135,6 @@ function buildIsoDateWithMonthShift(year, monthOneBased, day, monthShift = 0) {
|
||||||
const date = new Date(Date.UTC(year, monthOneBased - 1 + monthShift, day));
|
const date = new Date(Date.UTC(year, monthOneBased - 1 + monthShift, day));
|
||||||
return date.toISOString().slice(0, 10);
|
return date.toISOString().slice(0, 10);
|
||||||
}
|
}
|
||||||
function shiftIsoDateToNextBusinessDay(isoDate) {
|
|
||||||
const parsed = parseIsoDateToken(isoDate);
|
|
||||||
if (!parsed) {
|
|
||||||
return isoDate;
|
|
||||||
}
|
|
||||||
const date = new Date(Date.UTC(parsed.year, parsed.month - 1, parsed.day));
|
|
||||||
for (let guard = 0; guard < 10; guard += 1) {
|
|
||||||
const dayOfWeek = date.getUTCDay();
|
|
||||||
if (dayOfWeek !== 0 && dayOfWeek !== 6) {
|
|
||||||
return date.toISOString().slice(0, 10);
|
|
||||||
}
|
|
||||||
date.setUTCDate(date.getUTCDate() + 1);
|
|
||||||
}
|
|
||||||
return isoDate;
|
|
||||||
}
|
|
||||||
function deriveVatDeadlineCalendar(periodFrom, periodTo) {
|
function deriveVatDeadlineCalendar(periodFrom, periodTo) {
|
||||||
const reference = parseIsoDateToken(periodTo) ?? parseIsoDateToken(periodFrom);
|
const reference = parseIsoDateToken(periodTo) ?? parseIsoDateToken(periodFrom);
|
||||||
if (!reference) {
|
if (!reference) {
|
||||||
|
|
@ -189,10 +147,10 @@ function deriveVatDeadlineCalendar(periodFrom, periodTo) {
|
||||||
const quarterEndDay = new Date(Date.UTC(reference.year, quarterEndMonth, 0)).getUTCDate();
|
const quarterEndDay = new Date(Date.UTC(reference.year, quarterEndMonth, 0)).getUTCDate();
|
||||||
const quarterStart = toIsoDate(reference.year, quarterStartMonth, 1);
|
const quarterStart = toIsoDate(reference.year, quarterStartMonth, 1);
|
||||||
const quarterEnd = toIsoDate(reference.year, quarterEndMonth, quarterEndDay);
|
const quarterEnd = toIsoDate(reference.year, quarterEndMonth, quarterEndDay);
|
||||||
const declarationDueDate = shiftIsoDateToNextBusinessDay(buildIsoDateWithMonthShift(reference.year, quarterEndMonth, 25, 1));
|
const declarationDueDate = buildIsoDateWithMonthShift(reference.year, quarterEndMonth, 25, 1);
|
||||||
const payment1 = shiftIsoDateToNextBusinessDay(buildIsoDateWithMonthShift(reference.year, quarterEndMonth, 28, 1));
|
const payment1 = buildIsoDateWithMonthShift(reference.year, quarterEndMonth, 28, 1);
|
||||||
const payment2 = shiftIsoDateToNextBusinessDay(buildIsoDateWithMonthShift(reference.year, quarterEndMonth, 28, 2));
|
const payment2 = buildIsoDateWithMonthShift(reference.year, quarterEndMonth, 28, 2);
|
||||||
const payment3 = shiftIsoDateToNextBusinessDay(buildIsoDateWithMonthShift(reference.year, quarterEndMonth, 28, 3));
|
const payment3 = buildIsoDateWithMonthShift(reference.year, quarterEndMonth, 28, 3);
|
||||||
return {
|
return {
|
||||||
periodLabel: `${quarterNumber} кв. ${reference.year}`,
|
periodLabel: `${quarterNumber} кв. ${reference.year}`,
|
||||||
quarterStart,
|
quarterStart,
|
||||||
|
|
@ -312,13 +270,6 @@ function needsVatWhyExplanation(userMessage) {
|
||||||
}
|
}
|
||||||
return /(?:ндс|vat|прогноз|к\s+уплате|нул|ноль|\b0(?:[.,]0+)?\b)/iu.test(text);
|
return /(?:ндс|vat|прогноз|к\s+уплате|нул|ноль|\b0(?:[.,]0+)?\b)/iu.test(text);
|
||||||
}
|
}
|
||||||
function needsVatCalendarDetails(userMessage) {
|
|
||||||
const text = normalizeQuestionText(userMessage);
|
|
||||||
if (!text) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return /(?:срок|когда|дата\s+уплат|декларац|дол(?:я|ями)|по\s+частям|платежн(?:ый|ого)\s+график)/iu.test(text);
|
|
||||||
}
|
|
||||||
function detectRankingLimit(userMessage, fallback = 20) {
|
function detectRankingLimit(userMessage, fallback = 20) {
|
||||||
const text = normalizeQuestionText(userMessage);
|
const text = normalizeQuestionText(userMessage);
|
||||||
if (!text) {
|
if (!text) {
|
||||||
|
|
@ -1196,8 +1147,6 @@ function contractCandidatesFromRows(rows) {
|
||||||
return uniqueStrings(candidates);
|
return uniqueStrings(candidates);
|
||||||
}
|
}
|
||||||
function composeFactualReply(intent, rows, options = {}) {
|
function composeFactualReply(intent, rows, options = {}) {
|
||||||
const applyNumericEmphasis = (line) => (options.emphasizeNumbers ? emphasizeNumericTokens(line) : line);
|
|
||||||
const joinLines = (lines) => lines.map(applyNumericEmphasis).join("\n");
|
|
||||||
if (intent === "document_type_and_account_section_profile") {
|
if (intent === "document_type_and_account_section_profile") {
|
||||||
const rowsByMarker = new Map();
|
const rowsByMarker = new Map();
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
|
|
@ -1977,54 +1926,30 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
const vatActivityDetected = totalVatTurnoverAbs > 0.0000001;
|
const vatActivityDetected = totalVatTurnoverAbs > 0.0000001;
|
||||||
const netVatIsEffectivelyZero = Math.abs(netVat) <= 0.005;
|
const netVatIsEffectivelyZero = Math.abs(netVat) <= 0.005;
|
||||||
const explainWhyRequested = needsVatWhyExplanation(options.userMessage);
|
const explainWhyRequested = needsVatWhyExplanation(options.userMessage);
|
||||||
const shouldShowCalendarDetails = needsVatCalendarDetails(options.userMessage);
|
|
||||||
const vatCalendar = deriveVatDeadlineCalendar(options.periodFrom, options.periodTo);
|
const vatCalendar = deriveVatDeadlineCalendar(options.periodFrom, options.periodTo);
|
||||||
const formatForecastMoney = (value) => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
|
||||||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
|
||||||
const periodWindowLabel = options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null;
|
|
||||||
const lines = [
|
const lines = [
|
||||||
`Собран прогноз НДС к уплате: ${formatForecastMoney(vatToPay)}.`,
|
"Собран прогноз НДС к уплате по фактическим проводкам (НДС-субсчета 68.02*/19*).",
|
||||||
`Потенциальный перенос/переплата: ${formatForecastMoney(carryoverOrOverpayment)}.`,
|
`Строк агрегата: ${rows.length}.`,
|
||||||
`Период оценки: ${periodWindowLabel ?? "не задан (использован доступный срез)"}.`,
|
`Оборот по кредиту 68*: ${formatMoney(turnover68Credit)}.`,
|
||||||
"Режим результата: предварительная оценка по проводкам 68.02*/19* (не подтвержденная сумма налога по декларации).",
|
`Оборот по дебету 68*: ${formatMoney(turnover68Debit)}.`,
|
||||||
"",
|
`Нетто НДС (68 Кт - 68 Дт): ${formatMoney(netVat)}.`,
|
||||||
"База расчета:",
|
`Прогноз НДС к уплате: ${formatMoney(vatToPay)}.`,
|
||||||
`- Строк агрегата: ${formatNumberWithDots(rows.length)}.`,
|
`Потенциальный перенос/переплата: ${formatMoney(carryoverOrOverpayment)}.`,
|
||||||
`- Оборот по кредиту 68*: ${formatForecastMoney(turnover68Credit)}.`,
|
`Справочно по 19*: дебет ${formatMoney(turnover19Debit)}, кредит ${formatMoney(turnover19Credit)}.`
|
||||||
`- Оборот по дебету 68*: ${formatForecastMoney(turnover68Debit)}.`,
|
|
||||||
`- Нетто НДС (68 Кт - 68 Дт): ${formatForecastMoney(netVat)}.`,
|
|
||||||
`- Справочно по 19*: дебет ${formatForecastMoney(turnover19Debit)}, кредит ${formatForecastMoney(turnover19Credit)}.`
|
|
||||||
];
|
];
|
||||||
if (vatProbe && vatProbe.status === "ok") {
|
|
||||||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
|
||||||
lines.push("", "Покрытие VAT-источников через MCP:", `- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`);
|
|
||||||
if (vatProbe.probedSources.length > 0) {
|
|
||||||
lines.push(...vatProbe.probedSources.slice(0, 6).map((item, index) => {
|
|
||||||
const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName;
|
|
||||||
return `${index + 1}. ${name} | ${formatVatProbeStatusRu(item.status)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}`;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
if (vatProbe.errors.length > 0) {
|
|
||||||
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
|
||||||
}
|
|
||||||
lines.push("- Сумма прогноза выше рассчитана строго по оборотам 68.02*/19*; прямые VAT-источники показаны для проверки покрытия.");
|
|
||||||
}
|
|
||||||
else if (vatProbe && vatProbe.status === "error") {
|
|
||||||
lines.push("", "Покрытие VAT-источников через MCP: probe завершился ошибкой, поэтому использован только базовый контур 68.02*/19*.");
|
|
||||||
}
|
|
||||||
if (!vatActivityDetected) {
|
if (!vatActivityDetected) {
|
||||||
lines.push(`В выбранном окне не найдено движений по НДС-субсчетам 68.02*/19*; поэтому оперативный прогноз к уплате равен ${formatForecastMoney(0)}.`);
|
lines.push("В выбранном окне не найдено движений по НДС-субсчетам 68.02*/19*; поэтому оперативный прогноз к уплате равен 0.00.");
|
||||||
}
|
}
|
||||||
else if (vatToPay === 0 && netVatIsEffectivelyZero) {
|
else if (vatToPay === 0 && netVatIsEffectivelyZero) {
|
||||||
lines.push(`В выбранном окне обороты по 68* взаимно перекрылись (нетто близко к нулю), поэтому к уплате ${formatForecastMoney(0)}.`);
|
lines.push("В выбранном окне обороты по 68* взаимно перекрылись (нетто близко к нулю), поэтому к уплате 0.00.");
|
||||||
}
|
}
|
||||||
else if (vatToPay === 0 && netVat < 0) {
|
else if (vatToPay === 0 && netVat < 0) {
|
||||||
lines.push(`В выбранном окне дебет 68* превышает кредит 68*; сумма показана как перенос/переплата, к уплате ${formatForecastMoney(0)}.`);
|
lines.push("В выбранном окне дебет 68* превышает кредит 68*; сумма показана как перенос/переплата, к уплате 0.00.");
|
||||||
}
|
}
|
||||||
if (vatToPay === 0) {
|
if (vatToPay === 0) {
|
||||||
lines.push("", "Чеклист проверки в 1С (почему к уплате 0):", `1) Проверьте ОСВ/анализ счета по 68.02 и 19 за окно ${periodWindowLabel ?? "расчета"}.`, "2) Проверьте наличие движений в РегистрБухгалтерии.Хозрасчетный по счетам 68.02*/19* (включая субсчета).", "3) Сверьте счета-фактуры, корректировки и момент принятия НДС к вычету (не попали ли в другой период).", "4) Сверьте книгу продаж/покупок и операции Помощника по учету НДС за тот же период.", "5) Убедитесь, что документы проведены, период закрыт корректно и нет неподтвержденных/неперепроведенных документов.");
|
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 && shouldShowCalendarDetails) {
|
if (vatCalendar) {
|
||||||
const periodWindowLabel = vatCalendar.windowFrom && vatCalendar.windowTo
|
const periodWindowLabel = vatCalendar.windowFrom && vatCalendar.windowTo
|
||||||
? `${formatDateRu(vatCalendar.windowFrom)}..${formatDateRu(vatCalendar.windowTo)}`
|
? `${formatDateRu(vatCalendar.windowFrom)}..${formatDateRu(vatCalendar.windowTo)}`
|
||||||
: `${formatDateRu(vatCalendar.quarterStart)}..${formatDateRu(vatCalendar.quarterEnd)}`;
|
: `${formatDateRu(vatCalendar.quarterStart)}..${formatDateRu(vatCalendar.quarterEnd)}`;
|
||||||
|
|
@ -2032,107 +1957,16 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
const installmentRaw = vatToPay / 3;
|
const installmentRaw = vatToPay / 3;
|
||||||
const installmentRounded = Number(installmentRaw.toFixed(2));
|
const installmentRounded = Number(installmentRaw.toFixed(2));
|
||||||
const installmentThird = Number((vatToPay - installmentRounded * 2).toFixed(2));
|
const installmentThird = Number((vatToPay - installmentRounded * 2).toFixed(2));
|
||||||
lines.push("", `Период расчета (срез обязательств): ${periodWindowLabel}.`, `Налоговый период: ${vatCalendar.periodLabel}.`, `Срок сдачи декларации: до ${formatDateRu(vatCalendar.declarationDueDate)}.`, `Сроки уплаты: ${formatDateRu(payment1)}, ${formatDateRu(payment2)}, ${formatDateRu(payment3)}.`, `Ориентир по долям к уплате: ${formatForecastMoney(installmentRounded)} / ${formatForecastMoney(installmentRounded)} / ${formatForecastMoney(installmentThird)}.`, "Важно: даже при нулевой сумме к уплате декларация по НДС подается в установленный срок; переносы по выходным/праздникам сверяйте по календарю ФНС/1С.");
|
lines.push(`Период расчета (срез обязательств): ${periodWindowLabel}.`, `Налоговый период: ${vatCalendar.periodLabel}.`, `Срок сдачи декларации: до ${formatDateRu(vatCalendar.declarationDueDate)}.`, `Сроки уплаты: ${formatDateRu(payment1)}, ${formatDateRu(payment2)}, ${formatDateRu(payment3)}.`, `Ориентир по долям к уплате: ${formatMoney(installmentRounded)} / ${formatMoney(installmentRounded)} / ${formatMoney(installmentThird)}.`, "Важно: даже при нулевой сумме к уплате декларация по НДС подается в установленный срок; переносы по выходным/праздникам сверяйте по календарю ФНС/1С.");
|
||||||
}
|
}
|
||||||
if (explainWhyRequested) {
|
if (explainWhyRequested) {
|
||||||
lines.push("", "Почему прогноз к уплате 0: в текущей модели используем формулу max(0, 68 Кт - 68 Дт).", `За период 68 Кт = ${formatForecastMoney(turnover68Credit)}, 68 Дт = ${formatForecastMoney(turnover68Debit)}, разница = ${formatForecastMoney(netVat)}.`, netVat <= 0
|
lines.push("Почему прогноз к уплате 0: в текущей модели используем формулу max(0, 68 Кт - 68 Дт).", `За период 68 Кт = ${formatMoney(turnover68Credit)}, 68 Дт = ${formatMoney(turnover68Debit)}, разница = ${formatMoney(netVat)}.`, netVat <= 0
|
||||||
? "Разница неположительная, поэтому к уплате = 0, а отрицательная часть показана как перенос/переплата."
|
? "Разница неположительная, поэтому к уплате = 0, а отрицательная часть показана как перенос/переплата."
|
||||||
: "Разница положительная, поэтому к уплате берется эта положительная величина.", "Важно: это оперативный прогноз по оборотам НДС-субсчетов 68.02*/19*; финальную сумму налога подтверждают регистры НДС и декларация.");
|
: "Разница положительная, поэтому к уплате берется эта положительная величина.", "Важно: это оперативный прогноз по оборотам НДС-субсчетов 68.02*/19*; финальную сумму налога подтверждают регистры НДС и декларация.");
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
responseType: "FACTUAL_SUMMARY",
|
responseType: "FACTUAL_SUMMARY",
|
||||||
text: joinLines(lines)
|
text: lines.join("\n")
|
||||||
};
|
|
||||||
}
|
|
||||||
if (intent === "vat_payable_confirmed_as_of_date") {
|
|
||||||
const asOfDate = resolvePayablesAsOfDate(options);
|
|
||||||
const confirmedRows = rows.filter((row) => {
|
|
||||||
const amount = row.amount ?? 0;
|
|
||||||
if (!Number.isFinite(amount) || amount <= 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const section = extractAccountSectionCode(row.account_kt);
|
|
||||||
return section === "68";
|
|
||||||
});
|
|
||||||
const byAccount = new Map();
|
|
||||||
for (const row of confirmedRows) {
|
|
||||||
const account = String(row.account_kt ?? "").trim() || "68*";
|
|
||||||
const registrator = String(row.registrator ?? "").trim();
|
|
||||||
const amount = row.amount ?? 0;
|
|
||||||
const current = byAccount.get(account);
|
|
||||||
if (!current) {
|
|
||||||
byAccount.set(account, {
|
|
||||||
account,
|
|
||||||
total: amount,
|
|
||||||
operations: 1,
|
|
||||||
lastPeriod: row.period,
|
|
||||||
refs: registrator ? new Set([registrator]) : new Set()
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
current.total += amount;
|
|
||||||
current.operations += 1;
|
|
||||||
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
|
|
||||||
current.lastPeriod = row.period;
|
|
||||||
}
|
|
||||||
if (registrator) {
|
|
||||||
current.refs.add(registrator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const accountRows = Array.from(byAccount.values())
|
|
||||||
.filter((item) => Number.isFinite(item.total) && item.total > 0)
|
|
||||||
.sort((a, b) => b.total - a.total || b.operations - a.operations || a.account.localeCompare(b.account, "ru"));
|
|
||||||
const totalVatPayable = accountRows.reduce((sum, item) => sum + item.total, 0);
|
|
||||||
const lines = [
|
|
||||||
`Итого подтвержденный НДС к уплате на ${formatDateRu(asOfDate)}: ${formatMoneyRub(totalVatPayable)}.`,
|
|
||||||
"",
|
|
||||||
"Блок 1. Статус результата",
|
|
||||||
"- Результат: подтвержденный срез НДС к уплате по состоянию на дату.",
|
|
||||||
"",
|
|
||||||
"Блок 2. Что учтено",
|
|
||||||
`- Дата среза: ${formatDateRu(asOfDate)}.`,
|
|
||||||
"- Контур: остатки по счетам НДС к уплате (68*)."
|
|
||||||
];
|
|
||||||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
|
||||||
if (vatProbe && vatProbe.status === "ok") {
|
|
||||||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
|
||||||
lines.push("", "Блок 2.1. MCP-проверка VAT-источников", `- VAT-объектов в метаданных 1С: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Пробных прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`);
|
|
||||||
if (vatProbe.probedSources.length > 0) {
|
|
||||||
lines.push(...vatProbe.probedSources.slice(0, 4).map((item, index) => {
|
|
||||||
const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName;
|
|
||||||
const suffix = item.status === "ok"
|
|
||||||
? `${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.sampleRegistrator ? ` | пример: ${item.sampleRegistrator}` : ""}`
|
|
||||||
: item.status === "error" && item.error
|
|
||||||
? ` | ошибка: ${item.error}`
|
|
||||||
: "";
|
|
||||||
return `${index + 1}. ${name} | ${formatVatProbeStatusRu(item.status)}${suffix}`;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
if (vatProbe.errors.length > 0) {
|
|
||||||
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (vatProbe && vatProbe.status === "error") {
|
|
||||||
lines.push("", "Блок 2.1. MCP-проверка VAT-источников", "- Probe VAT-источников завершился ошибкой, поэтому срез подтвержден по доступному бухгалтерскому источнику (68*).");
|
|
||||||
}
|
|
||||||
lines.push("", "Блок 3. Сводка", `- Строк в выборке: ${formatNumberWithDots(rows.length)}.`, `- Подтвержденных позиций по НДС: ${formatNumberWithDots(accountRows.length)}.`, "", "Блок 4. Подтвержденные позиции");
|
|
||||||
if (accountRows.length > 0) {
|
|
||||||
lines.push(...accountRows.slice(0, 12).map((item, index) => {
|
|
||||||
const refs = Array.from(item.refs).slice(0, 2).join("; ");
|
|
||||||
return `${index + 1}. ${item.account} | остаток НДС к уплате: ${formatMoneyRub(item.total)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${refs ? ` | source refs: ${refs}` : ""}`;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
lines.push("- Подтвержденный остаток НДС к уплате на дату среза не найден.");
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
responseType: "FACTUAL_LIST",
|
|
||||||
text: joinLines(lines),
|
|
||||||
semantics: {
|
|
||||||
result_mode: "confirmed_balance",
|
|
||||||
evidence_strength: "strong",
|
|
||||||
balance_confirmed: true
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (intent === "account_balance_snapshot") {
|
if (intent === "account_balance_snapshot") {
|
||||||
|
|
@ -2248,7 +2082,7 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
||||||
text: joinLines(lines),
|
text: lines.map(emphasizeNumericTokens).join("\n"),
|
||||||
semantics: {
|
semantics: {
|
||||||
result_mode: "confirmed_balance",
|
result_mode: "confirmed_balance",
|
||||||
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
|
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
|
||||||
|
|
@ -2315,7 +2149,7 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
||||||
text: joinLines(lines),
|
text: lines.map(emphasizeNumericTokens).join("\n"),
|
||||||
semantics: {
|
semantics: {
|
||||||
result_mode: "confirmed_balance",
|
result_mode: "confirmed_balance",
|
||||||
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
|
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
|
||||||
|
|
@ -2437,7 +2271,7 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
];
|
];
|
||||||
return {
|
return {
|
||||||
responseType: "FACTUAL_LIST",
|
responseType: "FACTUAL_LIST",
|
||||||
text: joinLines(lines),
|
text: lines.map(emphasizeNumericTokens).join("\n"),
|
||||||
semantics: {
|
semantics: {
|
||||||
result_mode: "confirmed_balance",
|
result_mode: "confirmed_balance",
|
||||||
evidence_strength: "strong",
|
evidence_strength: "strong",
|
||||||
|
|
@ -2448,7 +2282,7 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
const fallbackLines = buildHeuristicLines(true);
|
const fallbackLines = buildHeuristicLines(true);
|
||||||
return {
|
return {
|
||||||
responseType: "FACTUAL_LIST",
|
responseType: "FACTUAL_LIST",
|
||||||
text: joinLines(fallbackLines),
|
text: fallbackLines.map(emphasizeNumericTokens).join("\n"),
|
||||||
semantics: {
|
semantics: {
|
||||||
result_mode: "heuristic_candidates",
|
result_mode: "heuristic_candidates",
|
||||||
evidence_strength: counterparties.length > 0 ? "medium" : "weak",
|
evidence_strength: counterparties.length > 0 ? "medium" : "weak",
|
||||||
|
|
@ -2459,7 +2293,7 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
const lines = buildHeuristicLines(false);
|
const lines = buildHeuristicLines(false);
|
||||||
return {
|
return {
|
||||||
responseType: "FACTUAL_LIST",
|
responseType: "FACTUAL_LIST",
|
||||||
text: joinLines(lines),
|
text: lines.map(emphasizeNumericTokens).join("\n"),
|
||||||
semantics: {
|
semantics: {
|
||||||
result_mode: "heuristic_candidates",
|
result_mode: "heuristic_candidates",
|
||||||
evidence_strength: counterparties.length > 0 ? "medium" : "weak",
|
evidence_strength: counterparties.length > 0 ? "medium" : "weak",
|
||||||
|
|
|
||||||
|
|
@ -30,12 +30,6 @@ function hasExplicitPeriodLiteral(text) {
|
||||||
function hasOpenItemsHint(text) {
|
function hasOpenItemsHint(text) {
|
||||||
return /(?:open\s+items|unclosed\s+items|хвост|висят|незакрыт|не\s+закрыт|открыт|долг|задолж|позиц)/iu.test(String(text ?? ""));
|
return /(?:open\s+items|unclosed\s+items|хвост|висят|незакрыт|не\s+закрыт|открыт|долг|задолж|позиц)/iu.test(String(text ?? ""));
|
||||||
}
|
}
|
||||||
function hasVatCue(text) {
|
|
||||||
return /(?:^|[\s,.;:!?()\-])(?:ндс|vat)(?=$|[\s,.;:!?()\-])/iu.test(String(text ?? ""));
|
|
||||||
}
|
|
||||||
function hasVatForecastCue(text) {
|
|
||||||
return /(?:прогноз|forecast|прикин|оцен|план)/iu.test(String(text ?? ""));
|
|
||||||
}
|
|
||||||
function hasDocumentSignal(text) {
|
function hasDocumentSignal(text) {
|
||||||
return /(?:док(?:и|умент|ументы|ументов|ументами)|docs?|documents?|doki|docy|doci)/iu.test(String(text ?? ""));
|
return /(?:док(?:и|умент|ументы|ументов|ументами)|docs?|documents?|doki|docy|doci)/iu.test(String(text ?? ""));
|
||||||
}
|
}
|
||||||
|
|
@ -355,23 +349,11 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
reasons.push("as_of_date_from_followup_context");
|
reasons.push("as_of_date_from_followup_context");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!sameDateRequested && !hasExplicitPeriodLiteral(userMessage)) {
|
|
||||||
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
|
|
||||||
const currentAsOfDate = toNonEmptyString(merged.as_of_date);
|
|
||||||
const todayIso = new Date().toISOString().slice(0, 10);
|
|
||||||
const currentLooksDefaultedToToday = currentAsOfDate === todayIso;
|
|
||||||
if (inheritedAsOfDate && (!currentAsOfDate || currentLooksDefaultedToToday) && currentAsOfDate !== inheritedAsOfDate) {
|
|
||||||
merged.as_of_date = inheritedAsOfDate;
|
|
||||||
reasons.push("as_of_date_from_followup_context");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (intent === "open_items_by_counterparty_or_contract" ||
|
if (intent === "open_items_by_counterparty_or_contract" ||
|
||||||
intent === "list_open_contracts" ||
|
intent === "list_open_contracts" ||
|
||||||
intent === "payables_confirmed_as_of_date" ||
|
intent === "payables_confirmed_as_of_date" ||
|
||||||
intent === "receivables_confirmed_as_of_date" ||
|
intent === "receivables_confirmed_as_of_date") {
|
||||||
intent === "vat_payable_confirmed_as_of_date") {
|
|
||||||
const hasFollowupSignalForConfirmed = hasAddressFollowupContextSignal(userMessage);
|
|
||||||
const inheritedContract = previousContract ?? (followupContext.previous_anchor_type === "contract" ? previousAnchorValue : null);
|
const inheritedContract = previousContract ?? (followupContext.previous_anchor_type === "contract" ? previousAnchorValue : null);
|
||||||
const currentContract = toNonEmptyString(merged.contract);
|
const currentContract = toNonEmptyString(merged.contract);
|
||||||
const shouldInheritContract = !currentContract ||
|
const shouldInheritContract = !currentContract ||
|
||||||
|
|
@ -398,16 +380,6 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
reasons.push("as_of_date_from_followup_context");
|
reasons.push("as_of_date_from_followup_context");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!sameDateRequested && hasFollowupSignalForConfirmed && !hasExplicitPeriodLiteral(userMessage)) {
|
|
||||||
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
|
|
||||||
const currentAsOfDate = toNonEmptyString(merged.as_of_date);
|
|
||||||
const todayIso = new Date().toISOString().slice(0, 10);
|
|
||||||
const currentLooksDefaultedToToday = currentAsOfDate === todayIso;
|
|
||||||
if (inheritedAsOfDate && (!currentAsOfDate || currentLooksDefaultedToToday) && currentAsOfDate !== inheritedAsOfDate) {
|
|
||||||
merged.as_of_date = inheritedAsOfDate;
|
|
||||||
reasons.push("as_of_date_from_followup_context");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (allTimeRequested) {
|
if (allTimeRequested) {
|
||||||
if (toNonEmptyString(merged.period_from) || toNonEmptyString(merged.period_to)) {
|
if (toNonEmptyString(merged.period_from) || toNonEmptyString(merged.period_to)) {
|
||||||
|
|
@ -464,7 +436,6 @@ function resolveMissingRequiredFilters(intent, filters) {
|
||||||
documents_forming_balance: ["account", "as_of_date"],
|
documents_forming_balance: ["account", "as_of_date"],
|
||||||
payables_confirmed_as_of_date: ["as_of_date"],
|
payables_confirmed_as_of_date: ["as_of_date"],
|
||||||
receivables_confirmed_as_of_date: ["as_of_date"],
|
receivables_confirmed_as_of_date: ["as_of_date"],
|
||||||
vat_payable_confirmed_as_of_date: ["as_of_date"],
|
|
||||||
list_documents_by_counterparty: ["counterparty"],
|
list_documents_by_counterparty: ["counterparty"],
|
||||||
bank_operations_by_counterparty: ["counterparty"],
|
bank_operations_by_counterparty: ["counterparty"],
|
||||||
list_contracts_by_counterparty: ["counterparty"],
|
list_contracts_by_counterparty: ["counterparty"],
|
||||||
|
|
@ -495,17 +466,6 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||||
const hasPreviousContract = Boolean(previousContract ?? previousContractFromAnchor);
|
const hasPreviousContract = Boolean(previousContract ?? previousContractFromAnchor);
|
||||||
const hasPreviousCounterparty = Boolean(previousCounterparty ?? previousCounterpartyFromAnchor);
|
const hasPreviousCounterparty = Boolean(previousCounterparty ?? previousCounterpartyFromAnchor);
|
||||||
const hasAnyPartyAnchor = hasPreviousContract || hasPreviousCounterparty;
|
const hasAnyPartyAnchor = hasPreviousContract || hasPreviousCounterparty;
|
||||||
const isVatFollowup = hasVatCue(normalizedMessage);
|
|
||||||
if (detectedIntent.intent === "unknown" && isVatFollowup) {
|
|
||||||
const vatIntent = hasVatForecastCue(normalizedMessage)
|
|
||||||
? "vat_payable_forecast"
|
|
||||||
: "vat_payable_confirmed_as_of_date";
|
|
||||||
return {
|
|
||||||
intent: vatIntent,
|
|
||||||
confidence: "low",
|
|
||||||
reasons: [...detectedIntent.reasons, "intent_adjusted_to_vat_followup_context"]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (hasOpenItemsHint(normalizedMessage) && hasAnyPartyAnchor) {
|
if (hasOpenItemsHint(normalizedMessage) && hasAnyPartyAnchor) {
|
||||||
return {
|
return {
|
||||||
intent: "open_items_by_counterparty_or_contract",
|
intent: "open_items_by_counterparty_or_contract",
|
||||||
|
|
|
||||||
|
|
@ -94,8 +94,7 @@ function inferAggregationProfile(intent, shape) {
|
||||||
if (intent === "account_balance_snapshot" ||
|
if (intent === "account_balance_snapshot" ||
|
||||||
intent === "documents_forming_balance" ||
|
intent === "documents_forming_balance" ||
|
||||||
intent === "payables_confirmed_as_of_date" ||
|
intent === "payables_confirmed_as_of_date" ||
|
||||||
intent === "receivables_confirmed_as_of_date" ||
|
intent === "receivables_confirmed_as_of_date") {
|
||||||
intent === "vat_payable_confirmed_as_of_date") {
|
|
||||||
return "balance_snapshot";
|
return "balance_snapshot";
|
||||||
}
|
}
|
||||||
if (intent === "open_items_by_counterparty_or_contract" ||
|
if (intent === "open_items_by_counterparty_or_contract" ||
|
||||||
|
|
|
||||||
|
|
@ -2033,7 +2033,7 @@ function textMojibakeScoreForAddress(value) {
|
||||||
const source = String(value ?? "");
|
const source = String(value ?? "");
|
||||||
const cyrillic = (source.match(/[А-Яа-яЁё]/g) ?? []).length;
|
const cyrillic = (source.match(/[А-Яа-яЁё]/g) ?? []).length;
|
||||||
const latin = (source.match(/[A-Za-z]/g) ?? []).length;
|
const latin = (source.match(/[A-Za-z]/g) ?? []).length;
|
||||||
const hardMarkers = (source.match(/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ<EFBFBD>?’“”•–—™љ›њќћџ]/g) ?? []).length;
|
const hardMarkers = (source.match(/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ‘’“”•–—™љ›њќћџ]/g) ?? []).length;
|
||||||
const pairMarkers = (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length;
|
const pairMarkers = (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length;
|
||||||
const doubleEncodedMarkers = (source.match(/(?:Г[Ђ-џ]|В[Ђ-џ]|Ã.|Â.)/gu) ?? []).length;
|
const doubleEncodedMarkers = (source.match(/(?:Г[Ђ-џ]|В[Ђ-џ]|Ã.|Â.)/gu) ?? []).length;
|
||||||
return cyrillic + latin - hardMarkers * 3 - pairMarkers * 2 - doubleEncodedMarkers * 2;
|
return cyrillic + latin - hardMarkers * 3 - pairMarkers * 2 - doubleEncodedMarkers * 2;
|
||||||
|
|
@ -2043,7 +2043,7 @@ function looksLikeMojibakeForAddress(value) {
|
||||||
if (!source.trim()) {
|
if (!source.trim()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ<EFBFBD>?’“”•–—™љ›њќћџ]/.test(source)) {
|
if (/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ‘’“”•–—™љ›њќћџ]/.test(source)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if ((source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length >= 2) {
|
if ((source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length >= 2) {
|
||||||
|
|
@ -2248,7 +2248,7 @@ function normalizeCounterpartyForFollowupMatch(value) {
|
||||||
return compactWhitespace(repairAddressMojibake(String(value ?? ""))
|
return compactWhitespace(repairAddressMojibake(String(value ?? ""))
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/ё/g, "е")
|
.replace(/ё/g, "е")
|
||||||
.replace(/[«»"'`“”„’<EFBFBD>?]/g, " ")
|
.replace(/[«»"'`“”„’‘]/g, " ")
|
||||||
.replace(/[^a-zа-я0-9\s._-]+/giu, " "));
|
.replace(/[^a-zа-я0-9\s._-]+/giu, " "));
|
||||||
}
|
}
|
||||||
function normalizeCounterpartyTokenForFollowupMatch(value) {
|
function normalizeCounterpartyTokenForFollowupMatch(value) {
|
||||||
|
|
@ -2294,7 +2294,7 @@ function extractDisplayedAddressEntityCandidates(replyText, entityType = "unknow
|
||||||
if (parts.length >= 2 && /^\d{4}-\d{2}-\d{2}/.test(parts[0] ?? "")) {
|
if (parts.length >= 2 && /^\d{4}-\d{2}-\d{2}/.test(parts[0] ?? "")) {
|
||||||
counterpartyCandidate = parts[1] ?? counterpartyCandidate;
|
counterpartyCandidate = parts[1] ?? counterpartyCandidate;
|
||||||
}
|
}
|
||||||
const cleanedCandidate = compactWhitespace(counterpartyCandidate.replace(/^["'«»“”„`’<EFBFBD>?]+|["'«»“”„`’<>?]+$/gu, ""));
|
const cleanedCandidate = compactWhitespace(counterpartyCandidate.replace(/^["'«»“”„`’‘]+|["'«»“”„`’‘]+$/gu, ""));
|
||||||
if (!cleanedCandidate || cleanedCandidate.length < 2) {
|
if (!cleanedCandidate || cleanedCandidate.length < 2) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -2558,133 +2558,61 @@ function isImplicitAddressContinuationByLlm(userMessage, llmPreDecomposeMeta) {
|
||||||
return tokenCount > 0 && tokenCount <= 4;
|
return tokenCount > 0 && tokenCount <= 4;
|
||||||
}
|
}
|
||||||
function hasAddressFollowupContextSignal(userMessage) {
|
function hasAddressFollowupContextSignal(userMessage) {
|
||||||
const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
|
||||||
const repaired = repairAddressMojibake(String(userMessage ?? ""));
|
const repaired = repairAddressMojibake(String(userMessage ?? ""));
|
||||||
const repairedText = compactWhitespace(repaired.toLowerCase());
|
const text = compactWhitespace(repaired.toLowerCase());
|
||||||
const samples = [rawText, repairedText].filter((item) => item.length > 0);
|
if (!text) {
|
||||||
if (samples.length === 0) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const hasAny = (pattern) => samples.some((sample) => pattern.test(sample));
|
if (hasStandaloneAddressTopicSignal(text)) {
|
||||||
const hasMarker = () => samples.some((sample) => hasFollowupMarker(sample));
|
|
||||||
const hasPointer = () => samples.some((sample) => hasReferentialPointer(sample));
|
|
||||||
const minTokens = samples.reduce((min, sample) => Math.min(min, countTokens(sample)), Number.POSITIVE_INFINITY);
|
|
||||||
const shortFollowup = minTokens <= 8;
|
|
||||||
const ultraShortFollowup = minTokens <= 3;
|
|
||||||
const debtRoleSwapToReceivables = shortFollowup &&
|
|
||||||
(/^(?:\u0430|a|\u0438|i)\s+(?:\u043d\u0430\u043c\s+)?\u043a\u0442\u043e(?=$|[\s,.;:!?])/iu.test(rawText) ||
|
|
||||||
/^(?:р°|a|рё|i)\s+(?:рЅр°рј\s+)?рєс‚рѕ(?=$|[\s,.;:!?])/iu.test(rawText));
|
|
||||||
if (debtRoleSwapToReceivables) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const debtRoleSwapToPayables = shortFollowup &&
|
|
||||||
(/^(?:\u0430|a|\u0438|i)\s+(?:\u043c\u044b\s+)?\u043a\u043e\u043c\u0443(?=$|[\s,.;:!?])/iu.test(rawText) ||
|
|
||||||
/^(?:р°|a|рё|i)\s+(?:рјс‹\s+)?рєрѕрјсѓ(?=$|[\s,.;:!?])/iu.test(rawText));
|
|
||||||
if (debtRoleSwapToPayables) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const shortContinuationCue = ultraShortFollowup &&
|
|
||||||
(/^(?:\u0434\u0430\u0432\u0430\u0439|\u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0439|\u043f\u043e\u043a\u0430\u0437\u044b\u0432\u044b\u0430\u0439|\u0435\u0449[\u0435\u0451]|also|again|go|ok|okay)(?=$|[\s,.;:!?])/iu.test(rawText) ||
|
|
||||||
/^(?:рґр°рір°р№|рїрѕрєр°р·с‹рір°р№|рїрѕрєр°р·с‹ріс‹р°р№|рµс‰[рµс‘]|also|again|go|ok|okay)(?=$|[\s,.;:!?])/iu.test(rawText));
|
|
||||||
if (shortContinuationCue) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const shortVatCue = ultraShortFollowup &&
|
|
||||||
/^(?:(?:\u0430|\u0438)\s+)?(?:(?:\u043f\u043e|po)\s+)?(?:\u043d\u0434\u0441|vat)(?=$|[\s,.;:!?])/iu.test(rawText);
|
|
||||||
if (shortVatCue) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (shortFollowup && hasAny(/^(?:а|a|и|i)\s+(?:нам\s+)?кто(?=$|[\s,.;:!?])/iu)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (shortFollowup && hasAny(/^(?:а|a|и|i)\s+(?:мы\s+)?кому(?=$|[\s,.;:!?])/iu)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (ultraShortFollowup && hasAny(/^(?:давай|показывай|показывыай|ещ[её]|also|again|go|ok|okay)(?=$|[\s,.;:!?])/iu)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (hasStandaloneAddressTopicSignal(rawText || repairedText)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (shouldHandleAsAssistantCapabilityMetaQuery(rawText || repairedText)) {
|
if (shouldHandleAsAssistantCapabilityMetaQuery(text)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (hasAny(/(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+всю\s+истори(?:ю|и)|за\s+любой\s+период|for\s+all\s+time|all\s+time|for\s+entire\s+period|entire\s+period|for\s+any\s+period|any\s+period)/iu)) {
|
if (/(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+всю\s+истори(?:ю|и)|за\s+любой\s+период|for\s+all\s+time|all\s+time|for\s+entire\s+period|entire\s+period|for\s+any\s+period|any\s+period)/iu.test(text)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (hasPointer()) {
|
if (hasReferentialPointer(text)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (hasAny(/(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|same\s+date|the\s+same\s+date|as\s+of\s+same\s+date)/iu)) {
|
if (/(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|same\s+date|the\s+same\s+date|as\s+of\s+same\s+date)/iu.test(text)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (hasAny(/(?:кроме|помимо)\s+(?:этого|этой|этот|эту|этих|этого\s+документа|этого\s+договора|этого\s+контрагента)/iu)) {
|
const shortFollowup = countTokens(text) <= 8;
|
||||||
|
if (/(?:кроме|помимо)\s+(?:этого|этой|этот|эту|этих|этого\s+документа|этого\s+договора|этого\s+контрагента)/iu.test(text)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (hasAny(/(?:есть\s+ещ[её]|что\s+ещ[её]|ещ[её]\s+что|ещ[её]\s+что-?то|остал(?:ось|ось\?)|друг(?:ое|ие))/iu) && minTokens <= 12) {
|
if (/(?:есть\s+ещ[её]|что\s+ещ[её]|ещ[её]\s+что|ещ[её]\s+что-?то|остал(?:ось|ось\?)|друг(?:ое|ие))/iu.test(text) && countTokens(text) <= 12) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (shortFollowup && hasMarker()) {
|
if (shortFollowup && hasFollowupMarker(text)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (shortFollowup && hasAny(/(?:^|\s)(?:также|тоже|also|same|again|ещ[её]|теперь|then|now)(?=$|[\s,.;:!?])/iu)) {
|
if (shortFollowup && /(?:^|\s)(?:также|тоже|also|same|again|ещ[её]|теперь|then|now)(?=$|[\s,.;:!?])/iu.test(text)) {
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (shortFollowup && hasAny(/(?:кто\s+из\s+(?:них|этих|тех)|кто\s+нов(?:ые|ых|ый)|кто\s+потом\s+исчез|кто\s+был\s+(?:только|ровно)\s+один\s+раз)/iu)) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (shortFollowup &&
|
if (shortFollowup &&
|
||||||
hasAny(/(?:почему|why|из[-\s]?за\s+чего|как\s+так|reason)/iu) &&
|
/(?:кто\s+из\s+(?:них|этих|тех)|кто\s+нов(?:ые|ых|ый)|кто\s+потом\s+исчез|кто\s+был\s+(?:только|ровно)\s+один\s+раз)/iu.test(text)) {
|
||||||
hasAny(/(?:ндс|vat|прогноз|к\s+уплате|нул|ноль|\b0(?:[.,]0+)?\b)/iu)) {
|
return true;
|
||||||
|
}
|
||||||
|
if (shortFollowup && /^(?:а|и)\s+кто\b/iu.test(text)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (shortFollowup &&
|
if (shortFollowup &&
|
||||||
hasAny(/(?:^|\s)по\s+[a-zа-яё][a-zа-яё0-9._-]{1,}(?=$|[\s,.;:!?])/iu) &&
|
/(?:почему|why|из[-\s]?за\s+чего|как\s+так|reason)/iu.test(text) &&
|
||||||
!hasAny(/(?:по\s+этому|по\s+тому|по\s+нему|по\s+ней|по\s+ним)/iu)) {
|
/(?:ндс|vat|прогноз|к\s+уплате|нул|ноль|\b0(?:[.,]0+)?\b)/iu.test(text)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (shortFollowup && samples.some((sample) => hasPeriodLiteral(sample))) {
|
if (shortFollowup &&
|
||||||
|
/(?:^|\s)по\s+[a-zа-яё][a-zа-яё0-9._-]{1,}(?=$|[\s,.;:!?])/iu.test(text) &&
|
||||||
|
!/(?:по\s+этому|по\s+тому|по\s+нему|по\s+ней|по\s+ним)/iu.test(text)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (shortFollowup && hasPeriodLiteral(text)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
function hasShortDebtMirrorFollowupSignal(userMessage) {
|
|
||||||
const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
|
||||||
const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
|
|
||||||
const samples = [rawText, repairedText].filter((item) => item.length > 0);
|
|
||||||
if (samples.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const minTokens = samples.reduce((min, sample) => Math.min(min, countTokens(sample)), Number.POSITIVE_INFINITY);
|
|
||||||
if (minTokens > 8) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return samples.some((sample) => /^(?:а|a|и|i)\s+(?:нам\s+)?кто(?=$|[\s,.;:!?])/iu.test(sample) ||
|
|
||||||
/^(?:а|a|и|i)\s+(?:мы\s+)?кому(?=$|[\s,.;:!?])/iu.test(sample) ||
|
|
||||||
/^(?:р°|a|рё|i)\s+(?:рЅр°рј\s+)?рєс‚рѕ(?=$|[\s,.;:!?])/iu.test(sample) ||
|
|
||||||
/^(?:р°|a|рё|i)\s+(?:рјс‹\s+)?рєрѕрјсѓ(?=$|[\s,.;:!?])/iu.test(sample));
|
|
||||||
}
|
|
||||||
function resolveDebtRoleSwapFollowupIntent(userMessage, previousIntent) {
|
|
||||||
const normalized = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
|
||||||
if (!normalized || countTokens(normalized) > 10) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const hasReceivablesCue = /(?:нам\s+кто\s+долж|кто\s+нам\s+долж|кто\s+долж[а-яё]*\s+нам|дебитор|к\s+получению|к\s+взысканию|receivable)/iu.test(normalized) ||
|
|
||||||
/^(?:а|a|и|i)\s+(?:нам\s+)?кто(?=$|[\s,.;:!?])/iu.test(normalized) ||
|
|
||||||
/^(?:р°|a|рё|i)\s+(?:рЅр°рј\s+)?рєс‚рѕ(?=$|[\s,.;:!?])/iu.test(normalized);
|
|
||||||
const hasPayablesCue = /(?:мы\s+кому\s+долж|кому\s+мы\s+долж|кому\s+долж[а-яё]*\s+мы|кредитор|к\s+уплате|payable)/iu.test(normalized) ||
|
|
||||||
/^(?:а|a|и|i)\s+(?:мы\s+)?кому(?=$|[\s,.;:!?])/iu.test(normalized) ||
|
|
||||||
/^(?:р°|a|рё|i)\s+(?:рјс‹\s+)?рєрѕрјсѓ(?=$|[\s,.;:!?])/iu.test(normalized);
|
|
||||||
if ((previousIntent === "payables_confirmed_as_of_date" || previousIntent === "list_payables_counterparties") &&
|
|
||||||
hasReceivablesCue) {
|
|
||||||
return "receivables_confirmed_as_of_date";
|
|
||||||
}
|
|
||||||
if ((previousIntent === "receivables_confirmed_as_of_date" || previousIntent === "list_receivables_counterparties") &&
|
|
||||||
hasPayablesCue) {
|
|
||||||
return "payables_confirmed_as_of_date";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMessage = null, llmPreDecomposeMeta = null) {
|
function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMessage = null, llmPreDecomposeMeta = null) {
|
||||||
const previousAddressItem = findLastAddressAssistantItem(items);
|
const previousAddressItem = findLastAddressAssistantItem(items);
|
||||||
const previousAddressDebug = previousAddressItem?.debug ?? null;
|
const previousAddressDebug = previousAddressItem?.debug ?? null;
|
||||||
|
|
@ -2693,15 +2621,9 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
Boolean(followupOffer?.enabled) &&
|
Boolean(followupOffer?.enabled) &&
|
||||||
(isImplicitAddressContinuationByLlm(userMessage, llmPreDecomposeMeta) ||
|
(isImplicitAddressContinuationByLlm(userMessage, llmPreDecomposeMeta) ||
|
||||||
(toNonEmptyString(alternateMessage) ? isImplicitAddressContinuationByLlm(alternateMessage, llmPreDecomposeMeta) : false));
|
(toNonEmptyString(alternateMessage) ? isImplicitAddressContinuationByLlm(alternateMessage, llmPreDecomposeMeta) : false));
|
||||||
const sourceIntentHint = toNonEmptyString(previousAddressDebug?.detected_intent);
|
const hasPrimaryFollowupSignal = hasAddressFollowupContextSignal(userMessage);
|
||||||
const debtRoleSwapPrimary = sourceIntentHint ? resolveDebtRoleSwapFollowupIntent(userMessage, sourceIntentHint) : null;
|
|
||||||
const debtRoleSwapAlternate = sourceIntentHint && toNonEmptyString(alternateMessage)
|
|
||||||
? resolveDebtRoleSwapFollowupIntent(String(alternateMessage ?? ""), sourceIntentHint)
|
|
||||||
: null;
|
|
||||||
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
|
|
||||||
const hasPrimaryFollowupSignal = hasAddressFollowupContextSignal(userMessage) || Boolean(debtRoleSwapPrimary);
|
|
||||||
const hasAlternateFollowupSignal = toNonEmptyString(alternateMessage)
|
const hasAlternateFollowupSignal = toNonEmptyString(alternateMessage)
|
||||||
? hasAddressFollowupContextSignal(alternateMessage) || Boolean(debtRoleSwapAlternate)
|
? hasAddressFollowupContextSignal(alternateMessage)
|
||||||
: false;
|
: false;
|
||||||
const hasPrimaryIndexReferenceSignal = extractDisplayedEntityIndexMention(userMessage) !== null;
|
const hasPrimaryIndexReferenceSignal = extractDisplayedEntityIndexMention(userMessage) !== null;
|
||||||
const hasAlternateIndexReferenceSignal = toNonEmptyString(alternateMessage)
|
const hasAlternateIndexReferenceSignal = toNonEmptyString(alternateMessage)
|
||||||
|
|
@ -2710,11 +2632,7 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
const hasIndexReferenceSignal = hasPrimaryIndexReferenceSignal || hasAlternateIndexReferenceSignal;
|
const hasIndexReferenceSignal = hasPrimaryIndexReferenceSignal || hasAlternateIndexReferenceSignal;
|
||||||
const hasStandaloneAddressTopic = hasStandaloneAddressTopicSignal(userMessage) ||
|
const hasStandaloneAddressTopic = hasStandaloneAddressTopicSignal(userMessage) ||
|
||||||
(toNonEmptyString(alternateMessage) ? hasStandaloneAddressTopicSignal(alternateMessage) : false);
|
(toNonEmptyString(alternateMessage) ? hasStandaloneAddressTopicSignal(alternateMessage) : false);
|
||||||
if (hasStandaloneAddressTopic &&
|
if (hasStandaloneAddressTopic && !hasImplicitContinuationSignal && !hasIndexReferenceSignal) {
|
||||||
!hasPrimaryFollowupSignal &&
|
|
||||||
!hasAlternateFollowupSignal &&
|
|
||||||
!hasImplicitContinuationSignal &&
|
|
||||||
!hasIndexReferenceSignal) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!hasPrimaryFollowupSignal && !hasAlternateFollowupSignal && !hasImplicitContinuationSignal && !hasIndexReferenceSignal) {
|
if (!hasPrimaryFollowupSignal && !hasAlternateFollowupSignal && !hasImplicitContinuationSignal && !hasIndexReferenceSignal) {
|
||||||
|
|
@ -2726,9 +2644,6 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
const sourceIntent = toNonEmptyString(previousAddressDebug.detected_intent);
|
const sourceIntent = toNonEmptyString(previousAddressDebug.detected_intent);
|
||||||
let previousIntent = sourceIntent;
|
let previousIntent = sourceIntent;
|
||||||
let followupSelectionMode = "carry_previous_intent";
|
let followupSelectionMode = "carry_previous_intent";
|
||||||
if (debtRoleSwapIntent) {
|
|
||||||
previousIntent = debtRoleSwapIntent;
|
|
||||||
}
|
|
||||||
if (hasImplicitContinuationSignal) {
|
if (hasImplicitContinuationSignal) {
|
||||||
const suggestedIntent = Array.isArray(followupOffer?.suggested_intents)
|
const suggestedIntent = Array.isArray(followupOffer?.suggested_intents)
|
||||||
? toNonEmptyString(followupOffer.suggested_intents[0])
|
? toNonEmptyString(followupOffer.suggested_intents[0])
|
||||||
|
|
@ -3234,13 +3149,6 @@ function hasSameDateAccountFollowupSignalForPredecompose(text) {
|
||||||
/(?:^|\s)по\s+\d{2}(?:[.,]\d{1,2})?(?=$|[\s,.;:!?])/iu.test(source) ||
|
/(?:^|\s)по\s+\d{2}(?:[.,]\d{1,2})?(?=$|[\s,.;:!?])/iu.test(source) ||
|
||||||
/\b\d{2}(?:[.,]\d{1,2})\b/u.test(source));
|
/\b\d{2}(?:[.,]\d{1,2})\b/u.test(source));
|
||||||
}
|
}
|
||||||
function hasPredecomposeDiagnosticUncertaintyLead(text) {
|
|
||||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
|
||||||
if (!normalized) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return /^(?:неясно|не\s+ясно|непонятно|не\s+понятно|unclear|not\s+clear|ambiguous|unknown)(?=$|[\s,.;:!?])/iu.test(normalized);
|
|
||||||
}
|
|
||||||
function attachAddressPredecomposeContract(meta, sourceMessage) {
|
function attachAddressPredecomposeContract(meta, sourceMessage) {
|
||||||
const canonicalMessage = toNonEmptyString(meta?.effectiveMessage) ?? String(sourceMessage ?? "");
|
const canonicalMessage = toNonEmptyString(meta?.effectiveMessage) ?? String(sourceMessage ?? "");
|
||||||
const predecomposeContract = (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({
|
const predecomposeContract = (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({
|
||||||
|
|
@ -3342,20 +3250,6 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
||||||
const candidateIntentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(candidate);
|
const candidateIntentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(candidate);
|
||||||
const sourceIntentKnown = sourceIntentResolution.intent !== "unknown";
|
const sourceIntentKnown = sourceIntentResolution.intent !== "unknown";
|
||||||
const candidateIntentKnown = candidateIntentResolution.intent !== "unknown";
|
const candidateIntentKnown = candidateIntentResolution.intent !== "unknown";
|
||||||
const candidateStartsWithDiagnosticUncertainty = hasPredecomposeDiagnosticUncertaintyLead(candidate);
|
|
||||||
if (candidateStartsWithDiagnosticUncertainty && sourceIntentKnown) {
|
|
||||||
return attachAddressPredecomposeContract({
|
|
||||||
...baseMeta,
|
|
||||||
attempted: true,
|
|
||||||
applied: false,
|
|
||||||
traceId: normalized?.trace_id ?? null,
|
|
||||||
llmCanonicalCandidateDetected: true,
|
|
||||||
effectiveMessage: userMessage,
|
|
||||||
reason: "normalized_fragment_rejected_diagnostic_rewrite",
|
|
||||||
fallbackRuleHit: null,
|
|
||||||
sanitizedUserMessage
|
|
||||||
}, userMessage);
|
|
||||||
}
|
|
||||||
const intentConflict = sourceIntentKnown &&
|
const intentConflict = sourceIntentKnown &&
|
||||||
candidateIntentKnown &&
|
candidateIntentKnown &&
|
||||||
sourceIntentResolution.intent !== candidateIntentResolution.intent;
|
sourceIntentResolution.intent !== candidateIntentResolution.intent;
|
||||||
|
|
@ -3622,8 +3516,6 @@ function resolveAddressToolGateDecision(addressInputMessage, followupContext, ll
|
||||||
isAddressLlmPreDecomposeCandidate(repairedInputMessage) ||
|
isAddressLlmPreDecomposeCandidate(repairedInputMessage) ||
|
||||||
hasAccountingSignal(addressInputMessage) ||
|
hasAccountingSignal(addressInputMessage) ||
|
||||||
hasAccountingSignal(repairedInputMessage) ||
|
hasAccountingSignal(repairedInputMessage) ||
|
||||||
hasShortDebtMirrorFollowupSignal(rawMessageForGate) ||
|
|
||||||
hasShortDebtMirrorFollowupSignal(repairedInputMessage) ||
|
|
||||||
sameDateAccountFollowupSignal;
|
sameDateAccountFollowupSignal;
|
||||||
const hasUnsupportedLowConfidencePredecomposeSignal = llmContractMode === "unsupported" &&
|
const hasUnsupportedLowConfidencePredecomposeSignal = llmContractMode === "unsupported" &&
|
||||||
(llmContractModeConfidence === "low" || llmContractModeConfidence === "medium") &&
|
(llmContractModeConfidence === "low" || llmContractModeConfidence === "medium") &&
|
||||||
|
|
@ -3641,7 +3533,6 @@ function resolveAddressToolGateDecision(addressInputMessage, followupContext, ll
|
||||||
!followupContext &&
|
!followupContext &&
|
||||||
!hasClassifierSignal &&
|
!hasClassifierSignal &&
|
||||||
!hasIntentSignal &&
|
!hasIntentSignal &&
|
||||||
!hasLexicalAddressSignal &&
|
|
||||||
!strongDataSignalFromRawMessage &&
|
!strongDataSignalFromRawMessage &&
|
||||||
!strongDataSignalFromEffectiveMessage) {
|
!strongDataSignalFromEffectiveMessage) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -3834,8 +3725,7 @@ const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
|
||||||
"list_contracts_by_counterparty",
|
"list_contracts_by_counterparty",
|
||||||
"contract_usage_overview",
|
"contract_usage_overview",
|
||||||
"contract_usage_and_value",
|
"contract_usage_and_value",
|
||||||
"vat_payable_forecast",
|
"vat_payable_forecast"
|
||||||
"vat_payable_confirmed_as_of_date"
|
|
||||||
]);
|
]);
|
||||||
function resolveAssistantOrchestrationDecision(input) {
|
function resolveAssistantOrchestrationDecision(input) {
|
||||||
const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? "");
|
const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? "");
|
||||||
|
|
@ -3916,11 +3806,7 @@ function resolveAssistantOrchestrationDecision(input) {
|
||||||
const explicitAddressFollowupSignal = hasAddressFollowupContextSignal(rawUserMessage) ||
|
const explicitAddressFollowupSignal = hasAddressFollowupContextSignal(rawUserMessage) ||
|
||||||
hasAddressFollowupContextSignal(repairedRawUserMessage) ||
|
hasAddressFollowupContextSignal(repairedRawUserMessage) ||
|
||||||
hasAddressFollowupContextSignal(effectiveAddressUserMessage) ||
|
hasAddressFollowupContextSignal(effectiveAddressUserMessage) ||
|
||||||
hasAddressFollowupContextSignal(repairedEffectiveAddressUserMessage) ||
|
hasAddressFollowupContextSignal(repairedEffectiveAddressUserMessage);
|
||||||
hasShortDebtMirrorFollowupSignal(rawUserMessage) ||
|
|
||||||
hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) ||
|
|
||||||
hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) ||
|
|
||||||
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage);
|
|
||||||
const effectiveAddressFollowupSignal = explicitAddressFollowupSignal && !dangerOrCoercionSignal;
|
const effectiveAddressFollowupSignal = explicitAddressFollowupSignal && !dangerOrCoercionSignal;
|
||||||
const deterministicNonDomainGuard = Boolean(!dataScopeMetaQuery &&
|
const deterministicNonDomainGuard = Boolean(!dataScopeMetaQuery &&
|
||||||
!capabilityMetaQuery &&
|
!capabilityMetaQuery &&
|
||||||
|
|
@ -4035,11 +3921,7 @@ function resolveAssistantOrchestrationDecision(input) {
|
||||||
hasAddressFollowupContextSignal(rawUserMessage) ||
|
hasAddressFollowupContextSignal(rawUserMessage) ||
|
||||||
hasAddressFollowupContextSignal(effectiveAddressUserMessage) ||
|
hasAddressFollowupContextSignal(effectiveAddressUserMessage) ||
|
||||||
hasAddressFollowupContextSignal(repairedRawUserMessage) ||
|
hasAddressFollowupContextSignal(repairedRawUserMessage) ||
|
||||||
hasAddressFollowupContextSignal(repairedEffectiveAddressUserMessage) ||
|
hasAddressFollowupContextSignal(repairedEffectiveAddressUserMessage));
|
||||||
hasShortDebtMirrorFollowupSignal(rawUserMessage) ||
|
|
||||||
hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) ||
|
|
||||||
hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) ||
|
|
||||||
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage));
|
|
||||||
const supportedAddressIntentDetected = !strictDeepInvestigationCueDetected &&
|
const supportedAddressIntentDetected = !strictDeepInvestigationCueDetected &&
|
||||||
Boolean((intentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(intentResolution.intent)) ||
|
Boolean((intentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(intentResolution.intent)) ||
|
||||||
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) ||
|
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) ||
|
||||||
|
|
@ -5057,14 +4939,14 @@ async function resolveAssistantDataScopeProbe() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const catalogQueryCandidates = [
|
const catalogQueryCandidates = [
|
||||||
"ВЫБРАТЬ ПЕРВЫЕ 20 ПРЕДСТАВЛЕН<EFBFBD>?Е(Организации.Ссылка) КАК Организация <20>?З Справочник.Организации КАК Организации",
|
"ВЫБРАТЬ ПЕРВЫЕ 20 ПРЕДСТАВЛЕНИЕ(Организации.Ссылка) КАК Организация ИЗ Справочник.Организации КАК Организации",
|
||||||
"ВЫБРАТЬ ПЕРВЫЕ 20 Организации.Наименование КАК Организация <EFBFBD>?З Справочник.Организации КАК Организации",
|
"ВЫБРАТЬ ПЕРВЫЕ 20 Организации.Наименование КАК Организация ИЗ Справочник.Организации КАК Организации",
|
||||||
"ВЫБРАТЬ ПЕРВЫЕ 20 Организации.НаименованиеПолное КАК Организация <EFBFBD>?З Справочник.Организации КАК Организации",
|
"ВЫБРАТЬ ПЕРВЫЕ 20 Организации.НаименованиеПолное КАК Организация ИЗ Справочник.Организации КАК Организации",
|
||||||
"ВЫБРАТЬ ПЕРВЫЕ 100 Организации.Ссылка КАК Организация, ПРЕДСТАВЛЕН<EFBFBD>?Е(Организации.Ссылка) КАК ОрганизацияПредставление <20>?З Справочник.Организации КАК Организации"
|
"ВЫБРАТЬ ПЕРВЫЕ 100 Организации.Ссылка КАК Организация, ПРЕДСТАВЛЕНИЕ(Организации.Ссылка) КАК ОрганизацияПредставление ИЗ Справочник.Организации КАК Организации"
|
||||||
];
|
];
|
||||||
const movementProbeCandidates = [
|
const movementProbeCandidates = [
|
||||||
"ВЫБРАТЬ ПЕРВЫЕ 60 Движения.Организация КАК Организация, ПРЕДСТАВЛЕН<EFBFBD>?Е(Движения.Организация) КАК ОрганизацияПредставление <EFBFBD>?З РегистрБухгалтерии.Хозрасчетный КАК Движения УПОРЯДОЧ<EFBFBD>?ТЬ ПО Движения.Период УБЫВ",
|
"ВЫБРАТЬ ПЕРВЫЕ 60 Движения.Организация КАК Организация, ПРЕДСТАВЛЕНИЕ(Движения.Организация) КАК ОрганизацияПредставление ИЗ РегистрБухгалтерии.Хозрасчетный КАК Движения УПОРЯДОЧИТЬ ПО Движения.Период УБЫВ",
|
||||||
"ВЫБРАТЬ ПЕРВЫЕ 60 Движения.Организация КАК Организация <EFBFBD>?З РегистрБухгалтерии.Хозрасчетный КАК Движения"
|
"ВЫБРАТЬ ПЕРВЫЕ 60 Движения.Организация КАК Организация ИЗ РегистрБухгалтерии.Хозрасчетный КАК Движения"
|
||||||
];
|
];
|
||||||
let lastError = null;
|
let lastError = null;
|
||||||
const catalogFacts = { names: [], refs: [], pairs: [] };
|
const catalogFacts = { names: [], refs: [], pairs: [] };
|
||||||
|
|
@ -5195,7 +5077,7 @@ function buildAssistantOperationalBoundaryReply() {
|
||||||
return [
|
return [
|
||||||
"Понимаю, что ситуация срочная.",
|
"Понимаю, что ситуация срочная.",
|
||||||
"Я не могу сам настраивать 1С или менять базу/конфигурацию.",
|
"Я не могу сам настраивать 1С или менять базу/конфигурацию.",
|
||||||
"Могу помочь безопасно: разберем симптомы и подготовим точные шаги для вашего 1С/<EFBFBD>?Т-админа."
|
"Могу помочь безопасно: разберем симптомы и подготовим точные шаги для вашего 1С/ИТ-админа."
|
||||||
].join(" ");
|
].join(" ");
|
||||||
}
|
}
|
||||||
function buildAssistantSafetyRefusalReply() {
|
function buildAssistantSafetyRefusalReply() {
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,7 @@ const COMPUTE_EXACT_INTENTS = new Set<AddressIntent>([
|
||||||
"account_balance_snapshot",
|
"account_balance_snapshot",
|
||||||
"documents_forming_balance",
|
"documents_forming_balance",
|
||||||
"payables_confirmed_as_of_date",
|
"payables_confirmed_as_of_date",
|
||||||
"receivables_confirmed_as_of_date",
|
"receivables_confirmed_as_of_date"
|
||||||
"vat_payable_confirmed_as_of_date"
|
|
||||||
]);
|
]);
|
||||||
const NAVIGATION_INTENTS = new Set<AddressIntent>([
|
const NAVIGATION_INTENTS = new Set<AddressIntent>([
|
||||||
"list_documents_by_counterparty",
|
"list_documents_by_counterparty",
|
||||||
|
|
@ -63,9 +62,6 @@ function defaultCapabilityId(intent: AddressIntent): string {
|
||||||
if (intent === "receivables_confirmed_as_of_date") {
|
if (intent === "receivables_confirmed_as_of_date") {
|
||||||
return "confirmed_receivables_as_of_date";
|
return "confirmed_receivables_as_of_date";
|
||||||
}
|
}
|
||||||
if (intent === "vat_payable_confirmed_as_of_date") {
|
|
||||||
return "confirmed_vat_payable_as_of_date";
|
|
||||||
}
|
|
||||||
if (intent === "list_payables_counterparties") {
|
if (intent === "list_payables_counterparties") {
|
||||||
return "payables_candidates_list";
|
return "payables_candidates_list";
|
||||||
}
|
}
|
||||||
|
|
@ -102,14 +98,6 @@ function resolveCapabilityEnabled(intent: AddressIntent): { enabled: boolean; re
|
||||||
: "receivables_confirmed_route_disabled_by_flag"
|
: "receivables_confirmed_route_disabled_by_flag"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (intent === "vat_payable_confirmed_as_of_date") {
|
|
||||||
return {
|
|
||||||
enabled: FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1,
|
|
||||||
reason: FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1
|
|
||||||
? "vat_payable_confirmed_route_enabled"
|
|
||||||
: "vat_payable_confirmed_route_disabled_by_flag"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (intent === "list_payables_counterparties") {
|
if (intent === "list_payables_counterparties") {
|
||||||
return {
|
return {
|
||||||
enabled: FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1,
|
enabled: FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1,
|
||||||
|
|
|
||||||
|
|
@ -643,92 +643,6 @@ function isLowQualityCounterpartyAnchorValue(rawValue: string): boolean {
|
||||||
if (questionCue && (rankingCue || paymentCue)) {
|
if (questionCue && (rankingCue || paymentCue)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const moneyAsOfPhraseCue =
|
|
||||||
/(?:денег|деньг|money|cash)/iu.test(value) &&
|
|
||||||
/(?:на\s+(?:данн(?:ую|ой|ая|ое)|эту|ту)\s+дат|on\s+(?:this|that)\s+date|as\s+of\s+(?:this|that)\s+date)/iu.test(
|
|
||||||
value
|
|
||||||
);
|
|
||||||
if (moneyAsOfPhraseCue) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const hasTemporalCue =
|
|
||||||
/(?:по\s+состоянию|на\s+дат|на\s+конец|за\s+(?:период|месяц|год|квартал)|\b(?:19|20)\d{2}\b|\bянвар|феврал|март|апрел|май|июн|июл|август|сентябр|октябр|ноябр|декабр)/iu.test(
|
|
||||||
value
|
|
||||||
);
|
|
||||||
const hasGenericEntityCue =
|
|
||||||
/(?:компан|организац|контрагент|поставщик|клиент|покупател|дебитор|кредитор|counterparty|company|supplier|customer)/iu.test(
|
|
||||||
value
|
|
||||||
);
|
|
||||||
if (hasTemporalCue && hasGenericEntityCue) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const lowQualityTimeTokens = new Set([
|
|
||||||
"по",
|
|
||||||
"состоянию",
|
|
||||||
"состояние",
|
|
||||||
"на",
|
|
||||||
"дату",
|
|
||||||
"дата",
|
|
||||||
"конец",
|
|
||||||
"период",
|
|
||||||
"месяц",
|
|
||||||
"году",
|
|
||||||
"год",
|
|
||||||
"квартал",
|
|
||||||
"январь",
|
|
||||||
"февраль",
|
|
||||||
"март",
|
|
||||||
"апрель",
|
|
||||||
"май",
|
|
||||||
"июнь",
|
|
||||||
"июль",
|
|
||||||
"август",
|
|
||||||
"сентябрь",
|
|
||||||
"октябрь",
|
|
||||||
"ноябрь",
|
|
||||||
"декабрь"
|
|
||||||
]);
|
|
||||||
const lowQualityGenericTokens = new Set([
|
|
||||||
"деньги",
|
|
||||||
"денег",
|
|
||||||
"деньгам",
|
|
||||||
"деньгами",
|
|
||||||
"денежный",
|
|
||||||
"денежные",
|
|
||||||
"данную",
|
|
||||||
"данной",
|
|
||||||
"данный",
|
|
||||||
"данное",
|
|
||||||
"эту",
|
|
||||||
"этой",
|
|
||||||
"этот",
|
|
||||||
"этом",
|
|
||||||
"ту",
|
|
||||||
"той",
|
|
||||||
"тот",
|
|
||||||
"том",
|
|
||||||
"вцелом",
|
|
||||||
"целом"
|
|
||||||
]);
|
|
||||||
const meaningfulNonTemporalTokens = tokens.filter(
|
|
||||||
(token) =>
|
|
||||||
isLikelyCounterpartyToken(token) &&
|
|
||||||
!lowQualityTimeTokens.has(token) &&
|
|
||||||
!/^(?:19|20)\d{2}$/.test(token)
|
|
||||||
);
|
|
||||||
if (meaningfulNonTemporalTokens.length === 0 && hasTemporalCue) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const meaningfulNonGenericTokens = tokens.filter(
|
|
||||||
(token) =>
|
|
||||||
isLikelyCounterpartyToken(token) &&
|
|
||||||
!lowQualityTimeTokens.has(token) &&
|
|
||||||
!lowQualityGenericTokens.has(token) &&
|
|
||||||
!/^(?:19|20)\d{2}$/.test(token)
|
|
||||||
);
|
|
||||||
if (meaningfulNonGenericTokens.length === 0 && (hasTemporalCue || paymentCue)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const meaningfulTokens = tokens.filter((token) => isLikelyCounterpartyToken(token));
|
const meaningfulTokens = tokens.filter((token) => isLikelyCounterpartyToken(token));
|
||||||
return meaningfulTokens.length === 0;
|
return meaningfulTokens.length === 0;
|
||||||
}
|
}
|
||||||
|
|
@ -929,9 +843,6 @@ function requiredFiltersByIntent(intent: AddressIntent): Array<keyof AddressFilt
|
||||||
if (intent === "receivables_confirmed_as_of_date") {
|
if (intent === "receivables_confirmed_as_of_date") {
|
||||||
return ["as_of_date"];
|
return ["as_of_date"];
|
||||||
}
|
}
|
||||||
if (intent === "vat_payable_confirmed_as_of_date") {
|
|
||||||
return ["as_of_date"];
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
intent === "list_documents_by_counterparty" ||
|
intent === "list_documents_by_counterparty" ||
|
||||||
intent === "bank_operations_by_counterparty" ||
|
intent === "bank_operations_by_counterparty" ||
|
||||||
|
|
@ -950,8 +861,7 @@ function usesAsOfPrimaryWindow(intent: AddressIntent): boolean {
|
||||||
intent === "open_items_by_counterparty_or_contract" ||
|
intent === "open_items_by_counterparty_or_contract" ||
|
||||||
intent === "list_open_contracts" ||
|
intent === "list_open_contracts" ||
|
||||||
intent === "payables_confirmed_as_of_date" ||
|
intent === "payables_confirmed_as_of_date" ||
|
||||||
intent === "receivables_confirmed_as_of_date" ||
|
intent === "receivables_confirmed_as_of_date"
|
||||||
intent === "vat_payable_confirmed_as_of_date"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1140,8 +1050,7 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
||||||
(intent === "account_balance_snapshot" ||
|
(intent === "account_balance_snapshot" ||
|
||||||
intent === "documents_forming_balance" ||
|
intent === "documents_forming_balance" ||
|
||||||
intent === "payables_confirmed_as_of_date" ||
|
intent === "payables_confirmed_as_of_date" ||
|
||||||
intent === "receivables_confirmed_as_of_date" ||
|
intent === "receivables_confirmed_as_of_date") &&
|
||||||
intent === "vat_payable_confirmed_as_of_date") &&
|
|
||||||
!filters.as_of_date
|
!filters.as_of_date
|
||||||
) {
|
) {
|
||||||
if (filters.period_to) {
|
if (filters.period_to) {
|
||||||
|
|
|
||||||
|
|
@ -403,28 +403,6 @@ function hasAny(text: string, patterns: string[]): boolean {
|
||||||
return patterns.some((item) => text.includes(item));
|
return patterns.some((item) => text.includes(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasFlexibleReceivablesDebtSignal(text: string): boolean {
|
|
||||||
const normalized = String(text ?? "");
|
|
||||||
if (!normalized) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
/(?:кто(?:\s+\S+){0,4}\s+нам(?:\s+\S+){0,4}\s+долж)/iu.test(normalized) ||
|
|
||||||
/(?:нам(?:\s+\S+){0,4}\s+кто(?:\s+\S+){0,4}\s+долж)/iu.test(normalized)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasFlexiblePayablesDebtSignal(text: string): boolean {
|
|
||||||
const normalized = String(text ?? "");
|
|
||||||
if (!normalized) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
/(?:кому(?:\s+\S+){0,4}\s+мы(?:\s+\S+){0,4}\s+долж)/iu.test(normalized) ||
|
|
||||||
/(?:мы(?:\s+\S+){0,4}\s+кому(?:\s+\S+){0,4}\s+долж)/iu.test(normalized)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function tokenizeText(text: string): string[] {
|
function tokenizeText(text: string): string[] {
|
||||||
return String(text ?? "")
|
return String(text ?? "")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
|
@ -598,27 +576,13 @@ function hasAccountBalanceSignal(text: string): boolean {
|
||||||
function hasForecastTaxSignal(text: string): boolean {
|
function hasForecastTaxSignal(text: string): boolean {
|
||||||
const hasForecastLexeme =
|
const hasForecastLexeme =
|
||||||
/(?:прогноз|forecast|план(?:\s+платежа|\s+оплаты)?|прикин(?:уть|ем|у|ь|ул|ули|усь|усь))/iu.test(text);
|
/(?:прогноз|forecast|план(?:\s+платежа|\s+оплаты)?|прикин(?:уть|ем|у|ь|ул|ули|усь|усь))/iu.test(text);
|
||||||
const hasTaxLexeme = /(?:ндс|vat|налог)/iu.test(text);
|
|
||||||
return hasForecastLexeme && hasTaxLexeme;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasVatPayableConfirmedSignal(text: string): boolean {
|
|
||||||
const hasVatLexeme = /(?:ндс|vat)/iu.test(text);
|
const hasVatLexeme = /(?:ндс|vat)/iu.test(text);
|
||||||
if (!hasVatLexeme) {
|
const hasTaxLexeme = /(?:ндс|vat|налог)/iu.test(text);
|
||||||
return false;
|
const hasVatPayableEstimatePattern =
|
||||||
}
|
/(?:(?:сколько|скока|скок).{0,48}(?:ндс|vat).{0,48}(?:надо|нужно|к\s+уплате|заплатить|уплатить|платеж|платежа|платежей|платежку)|(?:ндс|vat).{0,48}(?:к\s+уплате|надо|нужно|заплатить|уплатить)|(?:сколько|скока|скок).{0,32}(?:надо|нужно).{0,32}(?:заплатить|уплатить).{0,32}(?:ндс|vat))/iu.test(
|
||||||
const hasPaymentCue =
|
|
||||||
/(?:к\s+уплате|надо|нужно|заплатить|уплатить|плат[её]ж|платежку|в\s+налогов|в\s+бюджет|должн[аы]?\s+заплатить)/iu.test(
|
|
||||||
text
|
text
|
||||||
);
|
);
|
||||||
if (!hasPaymentCue) {
|
return (hasForecastLexeme && hasTaxLexeme) || (hasVatLexeme && hasVatPayableEstimatePattern);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const hasDateOrPeriodCue =
|
|
||||||
/(?:на\s+дат|по\s+состоянию|на\s+конец|за\s+(?:\d{4}|январ|феврал|март|апрел|май|июн|июл|август|сентябр|октябр|ноябр|декабр)|квартал|месяц|год|период|\b\d{4}[./-]\d{2}[./-]\d{2}\b)/iu.test(
|
|
||||||
text
|
|
||||||
);
|
|
||||||
return hasDateOrPeriodCue || /(?:сколько|скока|скок)/iu.test(text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasPeriodCoverageProfileSignal(text: string): boolean {
|
function hasPeriodCoverageProfileSignal(text: string): boolean {
|
||||||
|
|
@ -1042,7 +1006,7 @@ function hasSupplierTailRiskSignal(text: string): boolean {
|
||||||
|
|
||||||
function hasPayablesDebtLifecycleSignal(text: string): boolean {
|
function hasPayablesDebtLifecycleSignal(text: string): boolean {
|
||||||
const hasOweSignal =
|
const hasOweSignal =
|
||||||
/(?:кому\s+мы\s+долж(?:ен|ны|эны|эна|эно)?|мы\s+долж(?:ен|ны|эны|эна|эно)?|кому\s+долж(?:ен|ны|эны|эна|эно)?|долж[нэ](?:ы|а|о)?\s+(?:заплат|оплат|перечис)|к\s+оплате|на\s+оплату|who\s+we\s+owe|owe\s+to|payables?|кредитор(?:[а-яё]{0,6})?)/iu.test(
|
/(?:кому\s+мы\s+должны|мы\s+должны|кому\s+должны|должн(?:ы|а|о)\s+(?:заплат|оплат|перечис)|к\s+оплате|на\s+оплату|who\s+we\s+owe|owe\s+to|payables?|кредитор(?:ск)?)/iu.test(
|
||||||
text
|
text
|
||||||
);
|
);
|
||||||
if (!hasOweSignal) {
|
if (!hasOweSignal) {
|
||||||
|
|
@ -1058,7 +1022,7 @@ function hasPayablesDebtLifecycleSignal(text: string): boolean {
|
||||||
|
|
||||||
function hasReceivablesDebtLifecycleSignal(text: string): boolean {
|
function hasReceivablesDebtLifecycleSignal(text: string): boolean {
|
||||||
const hasOweUsSignal =
|
const hasOweUsSignal =
|
||||||
/(?:кто\s+нам\s+долж(?:ен|ны|эны|эна|эно)?|кто\s+долж(?:ен|ны|эны|эна|эно)?\s+нам|нам\s+долж(?:ен|ны|эны|эна|эно)?|должник(?:[а-яё]{0,6})?|дебитор(?:[а-яё]{0,6})?|дебиторск(?:[а-яё]{0,6})?|задолж|долг(?:и|ов|а|у)?|к\s+получению|на\s+поступление|к\s+взысканию|who\s+owes\s+us|receivables?)/iu.test(
|
/(?:кто\s+нам\s+долж(?:ен|ны)?|кто\s+долж(?:ен|ны)?\s+нам|нам\s+долж(?:ен|ны)|должник(?:и|ов|а)?|дебитор(?:ы|ов|ск)?|задолж|долг(?:и|ов|а|у)?|к\s+получению|на\s+поступление|к\s+взысканию|who\s+owes\s+us|receivables?)/iu.test(
|
||||||
text
|
text
|
||||||
);
|
);
|
||||||
if (!hasOweUsSignal) {
|
if (!hasOweUsSignal) {
|
||||||
|
|
@ -1511,23 +1475,11 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasVatPayableConfirmedSignal(text)) {
|
if (hasAny(text, RECEIVABLES_STRONG)) {
|
||||||
return {
|
const receivablesDebtLifecycleSignal = hasReceivablesDebtLifecycleSignal(text);
|
||||||
intent: "vat_payable_confirmed_as_of_date",
|
|
||||||
confidence: "high",
|
|
||||||
reasons: ["vat_payable_confirmed_signal_detected"]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasAny(text, RECEIVABLES_STRONG) || hasFlexibleReceivablesDebtSignal(text)) {
|
|
||||||
const receivablesDebtLifecycleSignal =
|
|
||||||
hasReceivablesDebtLifecycleSignal(text) || hasFlexibleReceivablesDebtSignal(text);
|
|
||||||
const reasons = ["receivables_signal_detected"];
|
const reasons = ["receivables_signal_detected"];
|
||||||
if (receivablesDebtLifecycleSignal) {
|
if (receivablesDebtLifecycleSignal) {
|
||||||
reasons.push("receivables_debt_lifecycle_signal_detected");
|
reasons.push("receivables_debt_lifecycle_signal_detected");
|
||||||
if (hasFlexibleReceivablesDebtSignal(text)) {
|
|
||||||
reasons.push("receivables_signal_detected_flexible_phrase");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
intent: receivablesDebtLifecycleSignal ? "receivables_confirmed_as_of_date" : "list_receivables_counterparties",
|
intent: receivablesDebtLifecycleSignal ? "receivables_confirmed_as_of_date" : "list_receivables_counterparties",
|
||||||
|
|
@ -1536,15 +1488,11 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasAny(text, PAYABLES_STRONG) || hasFlexiblePayablesDebtSignal(text)) {
|
if (hasAny(text, PAYABLES_STRONG)) {
|
||||||
const reasons = ["payables_signal_detected"];
|
const reasons = ["payables_signal_detected"];
|
||||||
const payablesDebtLifecycleSignal =
|
const payablesDebtLifecycleSignal = hasPayablesDebtLifecycleSignal(text);
|
||||||
hasPayablesDebtLifecycleSignal(text) || hasFlexiblePayablesDebtSignal(text);
|
|
||||||
if (payablesDebtLifecycleSignal) {
|
if (payablesDebtLifecycleSignal) {
|
||||||
reasons.push("payables_debt_lifecycle_signal_detected");
|
reasons.push("payables_debt_lifecycle_signal_detected");
|
||||||
if (hasFlexiblePayablesDebtSignal(text)) {
|
|
||||||
reasons.push("payables_signal_detected_flexible_phrase");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
intent: payablesDebtLifecycleSignal ? "payables_confirmed_as_of_date" : "list_payables_counterparties",
|
intent: payablesDebtLifecycleSignal ? "payables_confirmed_as_of_date" : "list_payables_counterparties",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import {
|
import {
|
||||||
ASSISTANT_MCP_CHANNEL,
|
ASSISTANT_MCP_CHANNEL,
|
||||||
ASSISTANT_MCP_PROXY_URL,
|
ASSISTANT_MCP_PROXY_URL,
|
||||||
ASSISTANT_MCP_TIMEOUT_MS
|
ASSISTANT_MCP_TIMEOUT_MS
|
||||||
|
|
@ -17,13 +17,6 @@ export interface AddressMcpQueryResult {
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddressMcpMetadataRowsResult {
|
|
||||||
fetched_rows: number;
|
|
||||||
raw_rows: Array<Record<string, unknown>>;
|
|
||||||
rows: Array<Record<string, unknown>>;
|
|
||||||
error: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toStringValue(value: unknown): string {
|
function toStringValue(value: unknown): string {
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
return "";
|
return "";
|
||||||
|
|
@ -195,12 +188,7 @@ function parseRowsFromTextTable(source: string): Array<Record<string, unknown>>
|
||||||
return normalizeMojibakeRows(rows);
|
return normalizeMojibakeRows(rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseRowsPayload(
|
function parseExecutePayload(payload: unknown): AddressMcpQueryResult {
|
||||||
payload: unknown,
|
|
||||||
options: {
|
|
||||||
allowSingleObjectRow?: boolean;
|
|
||||||
} = {}
|
|
||||||
): AddressMcpQueryResult {
|
|
||||||
if (!payload || typeof payload !== "object") {
|
if (!payload || typeof payload !== "object") {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
|
|
@ -252,14 +240,6 @@ function parseRowsPayload(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (source.data && typeof source.data === "object" && options.allowSingleObjectRow) {
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
rows: [normalizeMojibakeValue(source.data) as Record<string, unknown>],
|
|
||||||
error: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
rows: [],
|
rows: [],
|
||||||
|
|
@ -332,7 +312,7 @@ export async function executeAddressMcpQuery(input: {
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = responseText.trim() ? (JSON.parse(responseText) as unknown) : {};
|
const payload = responseText.trim() ? (JSON.parse(responseText) as unknown) : {};
|
||||||
const parsed = parseRowsPayload(payload);
|
const parsed = parseExecutePayload(payload);
|
||||||
if (!parsed.ok) {
|
if (!parsed.ok) {
|
||||||
return {
|
return {
|
||||||
fetched_rows: 0,
|
fetched_rows: 0,
|
||||||
|
|
@ -364,100 +344,3 @@ export async function executeAddressMcpQuery(input: {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function executeAddressMcpMetadata(input: {
|
|
||||||
filter?: string;
|
|
||||||
meta_type?: string | string[];
|
|
||||||
name_mask?: string;
|
|
||||||
limit?: number;
|
|
||||||
offset?: number;
|
|
||||||
sections?: string[];
|
|
||||||
extension_name?: string | null;
|
|
||||||
}): Promise<AddressMcpMetadataRowsResult> {
|
|
||||||
const endpoint = buildMcpUrl("/api/get_metadata");
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeout = setTimeout(() => controller.abort(), Math.max(300, ASSISTANT_MCP_TIMEOUT_MS));
|
|
||||||
try {
|
|
||||||
const body: Record<string, unknown> = {};
|
|
||||||
if (typeof input.filter === "string" && input.filter.trim().length > 0) {
|
|
||||||
body.filter = input.filter.trim();
|
|
||||||
}
|
|
||||||
if (typeof input.meta_type === "string" && input.meta_type.trim().length > 0) {
|
|
||||||
body.meta_type = input.meta_type.trim();
|
|
||||||
} else if (Array.isArray(input.meta_type) && input.meta_type.length > 0) {
|
|
||||||
const values = input.meta_type
|
|
||||||
.map((item) => String(item ?? "").trim())
|
|
||||||
.filter((item) => item.length > 0);
|
|
||||||
if (values.length > 0) {
|
|
||||||
body.meta_type = values;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (typeof input.name_mask === "string" && input.name_mask.trim().length > 0) {
|
|
||||||
body.name_mask = input.name_mask.trim();
|
|
||||||
}
|
|
||||||
if (typeof input.limit === "number" && Number.isFinite(input.limit)) {
|
|
||||||
body.limit = Math.max(1, Math.min(1000, Math.trunc(input.limit)));
|
|
||||||
}
|
|
||||||
if (typeof input.offset === "number" && Number.isFinite(input.offset)) {
|
|
||||||
body.offset = Math.max(0, Math.min(1_000_000, Math.trunc(input.offset)));
|
|
||||||
}
|
|
||||||
if (Array.isArray(input.sections) && input.sections.length > 0) {
|
|
||||||
const sections = input.sections
|
|
||||||
.map((item) => String(item ?? "").trim())
|
|
||||||
.filter((item) => item.length > 0);
|
|
||||||
if (sections.length > 0) {
|
|
||||||
body.sections = sections;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (input.extension_name !== undefined) {
|
|
||||||
body.extension_name = input.extension_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(endpoint, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"content-type": "application/json; charset=utf-8"
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
signal: controller.signal
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseText = await response.text();
|
|
||||||
if (!response.ok) {
|
|
||||||
return {
|
|
||||||
fetched_rows: 0,
|
|
||||||
raw_rows: [],
|
|
||||||
rows: [],
|
|
||||||
error: `MCP HTTP ${response.status}: ${responseText.slice(0, 240)}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = responseText.trim() ? (JSON.parse(responseText) as unknown) : {};
|
|
||||||
const parsed = parseRowsPayload(payload, { allowSingleObjectRow: true });
|
|
||||||
if (!parsed.ok) {
|
|
||||||
return {
|
|
||||||
fetched_rows: 0,
|
|
||||||
raw_rows: [],
|
|
||||||
rows: [],
|
|
||||||
error: parsed.error
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
fetched_rows: parsed.rows.length,
|
|
||||||
raw_rows: parsed.rows,
|
|
||||||
rows: parsed.rows,
|
|
||||||
error: null
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
|
||||||
return {
|
|
||||||
fetched_rows: 0,
|
|
||||||
raw_rows: [],
|
|
||||||
rows: [],
|
|
||||||
error: `MCP fetch failed: ${message}`
|
|
||||||
};
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@ const RESULT_SET_TYPE_BY_INTENT: Partial<Record<AddressIntent, AddressResultSetT
|
||||||
supplier_payouts_profile: "counterparty_list",
|
supplier_payouts_profile: "counterparty_list",
|
||||||
list_payables_counterparties: "counterparty_list",
|
list_payables_counterparties: "counterparty_list",
|
||||||
payables_confirmed_as_of_date: "balance_snapshot",
|
payables_confirmed_as_of_date: "balance_snapshot",
|
||||||
vat_payable_confirmed_as_of_date: "balance_snapshot",
|
|
||||||
receivables_confirmed_as_of_date: "balance_snapshot",
|
receivables_confirmed_as_of_date: "balance_snapshot",
|
||||||
list_receivables_counterparties: "counterparty_list",
|
list_receivables_counterparties: "counterparty_list",
|
||||||
list_contracts_by_counterparty: "contract_list",
|
list_contracts_by_counterparty: "contract_list",
|
||||||
|
|
|
||||||
|
|
@ -27,16 +27,10 @@ import {
|
||||||
selectAddressRecipe,
|
selectAddressRecipe,
|
||||||
type AddressRecipeExecutionPlan
|
type AddressRecipeExecutionPlan
|
||||||
} from "./addressRecipeCatalog";
|
} from "./addressRecipeCatalog";
|
||||||
import { executeAddressMcpMetadata, executeAddressMcpQuery } from "./addressMcpClient";
|
import { executeAddressMcpQuery } from "./addressMcpClient";
|
||||||
import { runAddressDecomposeStage, type AddressFollowupContext } from "./address_runtime/decomposeStage";
|
import { runAddressDecomposeStage, type AddressFollowupContext } from "./address_runtime/decomposeStage";
|
||||||
import { resolvePrimaryAnchor, refineAnchorFromRows, type AnchorResolutionDebug } from "./address_runtime/resolveStage";
|
import { resolvePrimaryAnchor, refineAnchorFromRows, type AnchorResolutionDebug } from "./address_runtime/resolveStage";
|
||||||
import {
|
import { composeFactualReply, inferReplyType, type ComposeReplySemantics } from "./address_runtime/composeStage";
|
||||||
composeFactualReply,
|
|
||||||
inferReplyType,
|
|
||||||
type ComposeReplySemantics,
|
|
||||||
type VatDirectSourceProbeItem,
|
|
||||||
type VatDirectSourceProbeSummary
|
|
||||||
} from "./address_runtime/composeStage";
|
|
||||||
import {
|
import {
|
||||||
isCapabilityRouteBlocked,
|
isCapabilityRouteBlocked,
|
||||||
resolveAddressCapabilityRouteDecision,
|
resolveAddressCapabilityRouteDecision,
|
||||||
|
|
@ -86,10 +80,6 @@ const ADDRESS_ANCHOR_RECOVERY_LIMIT = 1000;
|
||||||
const ADDRESS_CONFIRMED_PAYABLES_MIN_LIMIT = 200;
|
const ADDRESS_CONFIRMED_PAYABLES_MIN_LIMIT = 200;
|
||||||
const COUNTERPARTY_CATALOG_LOOKUP_LIMIT = 1000;
|
const COUNTERPARTY_CATALOG_LOOKUP_LIMIT = 1000;
|
||||||
const COUNTERPARTY_CATALOG_CACHE_TTL_MS = 120_000;
|
const COUNTERPARTY_CATALOG_CACHE_TTL_MS = 120_000;
|
||||||
const VAT_METADATA_PROBE_LIMIT = 100;
|
|
||||||
const VAT_SOURCE_PROBE_MAX_OBJECTS = 8;
|
|
||||||
const VAT_METADATA_PROBE_TYPES = ["РегистрНакопления", "РегистрСведений", "Документ"] as const;
|
|
||||||
const VAT_METADATA_PROBE_MASKS = ["ндс", "книгапродаж", "книгапокупок", "счетфактур", "вычет", "восстанов"] as const;
|
|
||||||
const PARTY_ANCHOR_STOPWORDS = new Set([
|
const PARTY_ANCHOR_STOPWORDS = new Set([
|
||||||
"ооо",
|
"ооо",
|
||||||
"ао",
|
"ао",
|
||||||
|
|
@ -140,12 +130,6 @@ const ACCOUNT_ALIAS_MAP: Record<string, string[]> = {
|
||||||
"62": ["покупатель", "покупателями", "расчеты с покупателями"],
|
"62": ["покупатель", "покупателями", "расчеты с покупателями"],
|
||||||
"76": ["прочие расчеты", "прочими дебиторами и кредиторами"]
|
"76": ["прочие расчеты", "прочими дебиторами и кредиторами"]
|
||||||
};
|
};
|
||||||
|
|
||||||
interface VatMetadataObject {
|
|
||||||
fullName: string;
|
|
||||||
synonym: string | null;
|
|
||||||
objectType: "document" | "register";
|
|
||||||
}
|
|
||||||
const COUNTERPARTY_CATALOG_LOOKUP_QUERY_TEMPLATE = `
|
const COUNTERPARTY_CATALOG_LOOKUP_QUERY_TEMPLATE = `
|
||||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||||
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||||||
|
|
@ -217,293 +201,6 @@ function valueAsString(value: unknown): string {
|
||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeIsoDateForQuery(value: unknown): string | null {
|
|
||||||
if (typeof value !== "string") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const trimmed = value.trim();
|
|
||||||
if (!trimmed) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const match = trimmed.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;
|
|
||||||
}
|
|
||||||
const candidate = new Date(Date.UTC(year, month - 1, day));
|
|
||||||
if (
|
|
||||||
candidate.getUTCFullYear() !== year ||
|
|
||||||
candidate.getUTCMonth() + 1 !== month ||
|
|
||||||
candidate.getUTCDate() !== day
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return `${match[1]}-${match[2]}-${match[3]}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toDateTimeExprForQuery(isoDate: string): string | null {
|
|
||||||
const match = String(isoDate ?? "").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;
|
|
||||||
}
|
|
||||||
return `ДАТАВРЕМЯ(${year}, ${month}, ${day}, 23, 59, 59)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldProbeVatSourcesForForecast(userMessage: string): boolean {
|
|
||||||
const text = String(userMessage ?? "")
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/ё/g, "е");
|
|
||||||
if (!text.trim()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return /(?:в\s+налогов|почему|из\s+чего|источн|декларац|книга\s+продаж|книга\s+покупок|вычет|восстанов)/iu.test(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
function detectVatMetadataObjectType(fullName: string): VatMetadataObject["objectType"] | null {
|
|
||||||
const normalized = String(fullName ?? "").trim();
|
|
||||||
if (!normalized) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (normalized.startsWith("Документ.")) {
|
|
||||||
return "document";
|
|
||||||
}
|
|
||||||
if (normalized.startsWith("РегистрНакопления.") || normalized.startsWith("РегистрСведений.")) {
|
|
||||||
return "register";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractVatMetadataObjects(rows: Array<Record<string, unknown>>): VatMetadataObject[] {
|
|
||||||
const out: VatMetadataObject[] = [];
|
|
||||||
const seen = new Set<string>();
|
|
||||||
for (const row of rows) {
|
|
||||||
const fullName =
|
|
||||||
valueAsString(row.ПолноеИмя ?? row.full_name ?? row.FullName ?? row.Имя ?? row.name ?? row.Name).trim() || null;
|
|
||||||
if (!fullName) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const objectType = detectVatMetadataObjectType(fullName);
|
|
||||||
if (!objectType) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (seen.has(fullName)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
seen.add(fullName);
|
|
||||||
const synonym =
|
|
||||||
valueAsString(row.Синоним ?? row.synonym ?? row.Synonym ?? row.Представление ?? row.presentation).trim() || null;
|
|
||||||
out.push({
|
|
||||||
fullName,
|
|
||||||
synonym,
|
|
||||||
objectType
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function scoreVatMetadataObject(item: VatMetadataObject): number {
|
|
||||||
const fullName = item.fullName.toLowerCase();
|
|
||||||
const synonym = String(item.synonym ?? "").toLowerCase();
|
|
||||||
let score = item.objectType === "register" ? 120 : 80;
|
|
||||||
if (fullName.includes("книгипродаж") || synonym.includes("продаж")) {
|
|
||||||
score += 60;
|
|
||||||
}
|
|
||||||
if (fullName.includes("книгипокупок") || synonym.includes("покуп")) {
|
|
||||||
score += 60;
|
|
||||||
}
|
|
||||||
if (fullName.includes("начислен") || synonym.includes("начислен")) {
|
|
||||||
score += 40;
|
|
||||||
}
|
|
||||||
if (fullName.includes("предъявлен") || synonym.includes("предъявлен")) {
|
|
||||||
score += 40;
|
|
||||||
}
|
|
||||||
if (fullName.includes("оплатындс") || synonym.includes("в бюджет")) {
|
|
||||||
score += 35;
|
|
||||||
}
|
|
||||||
if (fullName.includes("декларац")) {
|
|
||||||
score -= 25;
|
|
||||||
}
|
|
||||||
if (fullName.includes("пояснен")) {
|
|
||||||
score -= 25;
|
|
||||||
}
|
|
||||||
return score;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildVatObjectProbeQuery(object: VatMetadataObject, asOfExpr: string): string {
|
|
||||||
if (object.objectType === "document") {
|
|
||||||
return `
|
|
||||||
ВЫБРАТЬ ПЕРВЫЕ 1
|
|
||||||
Док.Дата КАК Период,
|
|
||||||
ПРЕДСТАВЛЕНИЕ(Док.Ссылка) КАК Регистратор,
|
|
||||||
"" КАК СчетДт,
|
|
||||||
"" КАК СчетКт,
|
|
||||||
0 КАК Сумма
|
|
||||||
ИЗ
|
|
||||||
${object.fullName} КАК Док
|
|
||||||
ГДЕ
|
|
||||||
Док.Дата <= ${asOfExpr}
|
|
||||||
УПОРЯДОЧИТЬ ПО
|
|
||||||
Док.Дата УБЫВ
|
|
||||||
`.trim();
|
|
||||||
}
|
|
||||||
return `
|
|
||||||
ВЫБРАТЬ ПЕРВЫЕ 1
|
|
||||||
Движения.Период КАК Период,
|
|
||||||
ПРЕДСТАВЛЕНИЕ(Движения.Регистратор) КАК Регистратор,
|
|
||||||
"" КАК СчетДт,
|
|
||||||
"" КАК СчетКт,
|
|
||||||
0 КАК Сумма
|
|
||||||
ИЗ
|
|
||||||
${object.fullName} КАК Движения
|
|
||||||
ГДЕ
|
|
||||||
Движения.Период <= ${asOfExpr}
|
|
||||||
УПОРЯДОЧИТЬ ПО
|
|
||||||
Движения.Период УБЫВ
|
|
||||||
`.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function probeVatDirectSources(filters: AddressFilterSet): Promise<VatDirectSourceProbeSummary> {
|
|
||||||
const asOfDate =
|
|
||||||
normalizeIsoDateForQuery(filters.as_of_date) ??
|
|
||||||
normalizeIsoDateForQuery(filters.period_to) ??
|
|
||||||
normalizeIsoDateForQuery(filters.period_from);
|
|
||||||
if (!asOfDate) {
|
|
||||||
return {
|
|
||||||
status: "skipped",
|
|
||||||
objectsTotal: 0,
|
|
||||||
documentsTotal: 0,
|
|
||||||
registersTotal: 0,
|
|
||||||
probedSources: [],
|
|
||||||
errors: ["as_of_date_not_resolved_for_vat_probe"]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const asOfExpr = toDateTimeExprForQuery(asOfDate);
|
|
||||||
if (!asOfExpr) {
|
|
||||||
return {
|
|
||||||
status: "skipped",
|
|
||||||
objectsTotal: 0,
|
|
||||||
documentsTotal: 0,
|
|
||||||
registersTotal: 0,
|
|
||||||
probedSources: [],
|
|
||||||
errors: ["as_of_expr_not_resolved_for_vat_probe"]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadataRequests: Array<{ meta_type: string; name_mask: string; limit: number }> = VAT_METADATA_PROBE_TYPES.flatMap(
|
|
||||||
(metaType) =>
|
|
||||||
VAT_METADATA_PROBE_MASKS.map((nameMask) => ({
|
|
||||||
meta_type: metaType,
|
|
||||||
name_mask: nameMask,
|
|
||||||
limit: VAT_METADATA_PROBE_LIMIT
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
const metadataResponses = await Promise.all(metadataRequests.map((request) => executeAddressMcpMetadata(request)));
|
|
||||||
|
|
||||||
const metadataErrors: string[] = [];
|
|
||||||
const metadataObjectsBuffer: VatMetadataObject[] = [];
|
|
||||||
for (const [index, response] of metadataResponses.entries()) {
|
|
||||||
const request = metadataRequests[index];
|
|
||||||
if (response.error) {
|
|
||||||
metadataErrors.push(`${request.meta_type}:${request.name_mask}:${response.error}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
metadataObjectsBuffer.push(...extractVatMetadataObjects(response.rows));
|
|
||||||
}
|
|
||||||
|
|
||||||
const deduplicatedObjects = new Map<string, VatMetadataObject>();
|
|
||||||
for (const item of metadataObjectsBuffer) {
|
|
||||||
const existing = deduplicatedObjects.get(item.fullName);
|
|
||||||
if (!existing) {
|
|
||||||
deduplicatedObjects.set(item.fullName, item);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!existing.synonym && item.synonym) {
|
|
||||||
deduplicatedObjects.set(item.fullName, {
|
|
||||||
...existing,
|
|
||||||
synonym: item.synonym
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const discoveredMetadataObjects = Array.from(deduplicatedObjects.values()).sort(
|
|
||||||
(a, b) => scoreVatMetadataObject(b) - scoreVatMetadataObject(a) || a.fullName.localeCompare(b.fullName, "ru")
|
|
||||||
);
|
|
||||||
const metadataObjects = discoveredMetadataObjects.slice(0, VAT_SOURCE_PROBE_MAX_OBJECTS);
|
|
||||||
|
|
||||||
const probeRows: VatDirectSourceProbeItem[] = [];
|
|
||||||
for (const object of metadataObjects) {
|
|
||||||
const probeQuery = buildVatObjectProbeQuery(object, asOfExpr);
|
|
||||||
const probeResult = await executeAddressMcpQuery({
|
|
||||||
query: probeQuery,
|
|
||||||
limit: 1
|
|
||||||
});
|
|
||||||
if (probeResult.error) {
|
|
||||||
probeRows.push({
|
|
||||||
fullName: object.fullName,
|
|
||||||
synonym: object.synonym,
|
|
||||||
objectType: object.objectType,
|
|
||||||
status: "error",
|
|
||||||
rowsFetched: probeResult.fetched_rows,
|
|
||||||
error: probeResult.error
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstRow = probeResult.raw_rows[0] ?? null;
|
|
||||||
const lastPeriod =
|
|
||||||
firstRow !== null
|
|
||||||
? valueAsString((firstRow as Record<string, unknown>).Период ?? (firstRow as Record<string, unknown>).period).trim() ||
|
|
||||||
null
|
|
||||||
: null;
|
|
||||||
const sampleRegistrator =
|
|
||||||
firstRow !== null
|
|
||||||
? valueAsString(
|
|
||||||
(firstRow as Record<string, unknown>).Регистратор ??
|
|
||||||
(firstRow as Record<string, unknown>).registrator ??
|
|
||||||
(firstRow as Record<string, unknown>).Registrator
|
|
||||||
).trim() || null
|
|
||||||
: null;
|
|
||||||
probeRows.push({
|
|
||||||
fullName: object.fullName,
|
|
||||||
synonym: object.synonym,
|
|
||||||
objectType: object.objectType,
|
|
||||||
status: probeResult.raw_rows.length > 0 ? "ok" : "empty",
|
|
||||||
rowsFetched: probeResult.fetched_rows,
|
|
||||||
lastPeriod,
|
|
||||||
sampleRegistrator
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const status: VatDirectSourceProbeSummary["status"] = metadataResponses.every((item) => item.error) ? "error" : "ok";
|
|
||||||
const allErrors = [
|
|
||||||
...metadataErrors,
|
|
||||||
...probeRows
|
|
||||||
.filter((item) => item.status === "error")
|
|
||||||
.map((item) => `${item.fullName}: ${valueAsString(item.error).slice(0, 120)}`)
|
|
||||||
];
|
|
||||||
|
|
||||||
return {
|
|
||||||
status,
|
|
||||||
objectsTotal: discoveredMetadataObjects.length,
|
|
||||||
documentsTotal: discoveredMetadataObjects.filter((item) => item.objectType === "document").length,
|
|
||||||
registersTotal: discoveredMetadataObjects.filter((item) => item.objectType === "register").length,
|
|
||||||
probedSources: probeRows,
|
|
||||||
errors: allErrors
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function transliterateCyrillicToLatin(value: string): string {
|
function transliterateCyrillicToLatin(value: string): string {
|
||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
а: "a",
|
а: "a",
|
||||||
|
|
@ -1105,8 +802,7 @@ function isConfirmedBalanceIntent(intent: AddressIntent): boolean {
|
||||||
intent === "account_balance_snapshot" ||
|
intent === "account_balance_snapshot" ||
|
||||||
intent === "documents_forming_balance" ||
|
intent === "documents_forming_balance" ||
|
||||||
intent === "payables_confirmed_as_of_date" ||
|
intent === "payables_confirmed_as_of_date" ||
|
||||||
intent === "receivables_confirmed_as_of_date" ||
|
intent === "receivables_confirmed_as_of_date"
|
||||||
intent === "vat_payable_confirmed_as_of_date"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1327,7 +1023,7 @@ function enforceStrictAccountScopeForIntent(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveExecutionFiltersForConfirmedBalance(
|
function resolveExecutionFiltersForPayablesConfirmedBalance(
|
||||||
filters: AddressFilterSet,
|
filters: AddressFilterSet,
|
||||||
analysisDate: string | null
|
analysisDate: string | null
|
||||||
): {
|
): {
|
||||||
|
|
@ -1940,8 +1636,6 @@ function buildLimitedOffers(input: {
|
||||||
offers.push("показать контрагентов с максимальными хвостами дебиторки по 62/76");
|
offers.push("показать контрагентов с максимальными хвостами дебиторки по 62/76");
|
||||||
} else if (input.intent === "receivables_confirmed_as_of_date") {
|
} else if (input.intent === "receivables_confirmed_as_of_date") {
|
||||||
offers.push("показать подтвержденный реестр открытой дебиторской задолженности на дату среза по 62/76");
|
offers.push("показать подтвержденный реестр открытой дебиторской задолженности на дату среза по 62/76");
|
||||||
} else if (input.intent === "vat_payable_confirmed_as_of_date") {
|
|
||||||
offers.push("показать подтвержденную сумму НДС к уплате на дату среза по счетам 68*");
|
|
||||||
} else if (input.intent === "payables_confirmed_as_of_date") {
|
} else if (input.intent === "payables_confirmed_as_of_date") {
|
||||||
offers.push("показать подтвержденный реестр открытых обязательств на дату среза по 60/76");
|
offers.push("показать подтвержденный реестр открытых обязательств на дату среза по 60/76");
|
||||||
} else if (input.intent === "list_payables_counterparties") {
|
} else if (input.intent === "list_payables_counterparties") {
|
||||||
|
|
@ -1995,8 +1689,7 @@ function buildLimitedIntentSignalLine(input: {
|
||||||
list_receivables_counterparties: "Сигнал запроса: нужен ранжированный список должников.",
|
list_receivables_counterparties: "Сигнал запроса: нужен ранжированный список должников.",
|
||||||
list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов.",
|
list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов.",
|
||||||
receivables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез дебиторской задолженности на дату.",
|
receivables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез дебиторской задолженности на дату.",
|
||||||
payables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез обязательств к оплате на дату.",
|
payables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез обязательств к оплате на дату."
|
||||||
vat_payable_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез НДС к уплате на дату."
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const byShape: Partial<Record<AddressQueryShapeDetection["shape"], string>> = {
|
const byShape: Partial<Record<AddressQueryShapeDetection["shape"], string>> = {
|
||||||
|
|
@ -2194,17 +1887,19 @@ function buildLimitedExecutionResult(input: {
|
||||||
undefined,
|
undefined,
|
||||||
resultSemantics.result_mode
|
resultSemantics.result_mode
|
||||||
);
|
);
|
||||||
const exactLimitedReason =
|
|
||||||
input.intent.intent === "payables_confirmed_as_of_date"
|
|
||||||
? "exact_payables_mode_limited_response"
|
|
||||||
: input.intent.intent === "receivables_confirmed_as_of_date"
|
|
||||||
? "exact_receivables_mode_limited_response"
|
|
||||||
: input.intent.intent === "vat_payable_confirmed_as_of_date"
|
|
||||||
? "exact_vat_payable_mode_limited_response"
|
|
||||||
: null;
|
|
||||||
const reasons =
|
const reasons =
|
||||||
exactLimitedReason && !reasonsWithConfirmedFallback.includes(exactLimitedReason)
|
(input.intent.intent === "payables_confirmed_as_of_date" || input.intent.intent === "receivables_confirmed_as_of_date") &&
|
||||||
? [...reasonsWithConfirmedFallback, exactLimitedReason]
|
!reasonsWithConfirmedFallback.includes(
|
||||||
|
input.intent.intent === "payables_confirmed_as_of_date"
|
||||||
|
? "exact_payables_mode_limited_response"
|
||||||
|
: "exact_receivables_mode_limited_response"
|
||||||
|
)
|
||||||
|
? [
|
||||||
|
...reasonsWithConfirmedFallback,
|
||||||
|
input.intent.intent === "payables_confirmed_as_of_date"
|
||||||
|
? "exact_payables_mode_limited_response"
|
||||||
|
: "exact_receivables_mode_limited_response"
|
||||||
|
]
|
||||||
: reasonsWithConfirmedFallback;
|
: reasonsWithConfirmedFallback;
|
||||||
const routeExpectationAudit =
|
const routeExpectationAudit =
|
||||||
input.routeExpectationAudit ??
|
input.routeExpectationAudit ??
|
||||||
|
|
@ -2319,23 +2014,15 @@ export class AddressQueryService {
|
||||||
requestedResultMode === "confirmed_balance";
|
requestedResultMode === "confirmed_balance";
|
||||||
const confirmedBalanceReceivablesIntent =
|
const confirmedBalanceReceivablesIntent =
|
||||||
intent.intent === "receivables_confirmed_as_of_date" && requestedResultMode === "confirmed_balance";
|
intent.intent === "receivables_confirmed_as_of_date" && requestedResultMode === "confirmed_balance";
|
||||||
const confirmedBalanceVatPayableIntent =
|
|
||||||
intent.intent === "vat_payable_confirmed_as_of_date" && requestedResultMode === "confirmed_balance";
|
|
||||||
const payablesConfirmedExecution =
|
const payablesConfirmedExecution =
|
||||||
confirmedBalancePayablesIntent
|
confirmedBalancePayablesIntent
|
||||||
? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate)
|
? resolveExecutionFiltersForPayablesConfirmedBalance(filters.extracted_filters, analysisDate)
|
||||||
: null;
|
: null;
|
||||||
const receivablesConfirmedExecution = confirmedBalanceReceivablesIntent
|
const receivablesConfirmedExecution = confirmedBalanceReceivablesIntent
|
||||||
? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate)
|
? resolveExecutionFiltersForPayablesConfirmedBalance(filters.extracted_filters, analysisDate)
|
||||||
: null;
|
|
||||||
const vatPayableConfirmedExecution = confirmedBalanceVatPayableIntent
|
|
||||||
? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate)
|
|
||||||
: null;
|
: null;
|
||||||
const executionFilters =
|
const executionFilters =
|
||||||
payablesConfirmedExecution?.executionFilters ??
|
payablesConfirmedExecution?.executionFilters ?? receivablesConfirmedExecution?.executionFilters ?? filters.extracted_filters;
|
||||||
receivablesConfirmedExecution?.executionFilters ??
|
|
||||||
vatPayableConfirmedExecution?.executionFilters ??
|
|
||||||
filters.extracted_filters;
|
|
||||||
if (
|
if (
|
||||||
payablesConfirmedExecution?.asOfDerived &&
|
payablesConfirmedExecution?.asOfDerived &&
|
||||||
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)
|
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)
|
||||||
|
|
@ -2358,17 +2045,6 @@ export class AddressQueryService {
|
||||||
baseReasons.push("as_of_date_derived_for_confirmed_receivables");
|
baseReasons.push("as_of_date_derived_for_confirmed_receivables");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
vatPayableConfirmedExecution?.asOfDerived &&
|
|
||||||
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)
|
|
||||||
) {
|
|
||||||
if (!filters.warnings.includes("as_of_date_derived_for_confirmed_vat_payable")) {
|
|
||||||
filters.warnings.push("as_of_date_derived_for_confirmed_vat_payable");
|
|
||||||
}
|
|
||||||
if (!baseReasons.includes("as_of_date_derived_for_confirmed_vat_payable")) {
|
|
||||||
baseReasons.push("as_of_date_derived_for_confirmed_vat_payable");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const capabilityDecision = resolveAddressCapabilityRouteDecision(intent.intent);
|
const capabilityDecision = resolveAddressCapabilityRouteDecision(intent.intent);
|
||||||
const capabilityAudit = buildCapabilityAudit(intent.intent);
|
const capabilityAudit = buildCapabilityAudit(intent.intent);
|
||||||
const shadowRouteAudit = buildShadowRouteAudit({
|
const shadowRouteAudit = buildShadowRouteAudit({
|
||||||
|
|
@ -2399,22 +2075,12 @@ export class AddressQueryService {
|
||||||
shadowRouteAudit
|
shadowRouteAudit
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const composeOptionsFromFilters = (
|
const composeOptionsFromFilters = (filterSet: AddressFilterSet) => ({
|
||||||
filterSet: AddressFilterSet,
|
|
||||||
options: {
|
|
||||||
vatDirectSourceProbe?: VatDirectSourceProbeSummary | null;
|
|
||||||
emphasizeNumbers?: boolean;
|
|
||||||
useRubCurrency?: boolean;
|
|
||||||
} = {}
|
|
||||||
) => ({
|
|
||||||
userMessage,
|
userMessage,
|
||||||
periodFrom: typeof filterSet.period_from === "string" ? filterSet.period_from : undefined,
|
periodFrom: typeof filterSet.period_from === "string" ? filterSet.period_from : undefined,
|
||||||
periodTo: typeof filterSet.period_to === "string" ? filterSet.period_to : undefined,
|
periodTo: typeof filterSet.period_to === "string" ? filterSet.period_to : undefined,
|
||||||
asOfDate: typeof filterSet.as_of_date === "string" ? filterSet.as_of_date : undefined,
|
asOfDate: typeof filterSet.as_of_date === "string" ? filterSet.as_of_date : undefined,
|
||||||
requestedResultMode,
|
requestedResultMode
|
||||||
vatDirectSourceProbe: options.vatDirectSourceProbe ?? undefined,
|
|
||||||
emphasizeNumbers: options.emphasizeNumbers ?? undefined,
|
|
||||||
useRubCurrency: options.useRubCurrency ?? undefined
|
|
||||||
});
|
});
|
||||||
const futureGuardReferenceDate = resolveFutureGuardReferenceDate(analysisDate, executionFilters);
|
const futureGuardReferenceDate = resolveFutureGuardReferenceDate(analysisDate, executionFilters);
|
||||||
let anchor = resolvePrimaryAnchor(intent.intent, filters.extracted_filters);
|
let anchor = resolvePrimaryAnchor(intent.intent, filters.extracted_filters);
|
||||||
|
|
@ -2454,12 +2120,6 @@ export class AddressQueryService {
|
||||||
) {
|
) {
|
||||||
baseReasons.push("confirmed_balance_exact_receivables_intent");
|
baseReasons.push("confirmed_balance_exact_receivables_intent");
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
intent.intent === "vat_payable_confirmed_as_of_date" &&
|
|
||||||
!baseReasons.includes("confirmed_balance_exact_vat_payable_intent")
|
|
||||||
) {
|
|
||||||
baseReasons.push("confirmed_balance_exact_vat_payable_intent");
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
requestedResultMode === "confirmed_balance" &&
|
requestedResultMode === "confirmed_balance" &&
|
||||||
recipeIntent === "open_items_by_counterparty_or_contract" &&
|
recipeIntent === "open_items_by_counterparty_or_contract" &&
|
||||||
|
|
@ -3517,36 +3177,7 @@ export class AddressQueryService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const vatProbeRequired =
|
const factual = composeFactualReply(composeIntent, filteredRows, composeOptionsFromFilters(executionFilters));
|
||||||
composeIntent === "vat_payable_confirmed_as_of_date" ||
|
|
||||||
(composeIntent === "vat_payable_forecast" && shouldProbeVatSourcesForForecast(userMessage));
|
|
||||||
const vatDirectSourceProbe = vatProbeRequired ? await probeVatDirectSources(executionFilters) : null;
|
|
||||||
const shouldEmphasizeNumbers =
|
|
||||||
composeIntent === "vat_payable_forecast" ||
|
|
||||||
composeIntent === "vat_payable_confirmed_as_of_date" ||
|
|
||||||
composeIntent === "payables_confirmed_as_of_date" ||
|
|
||||||
composeIntent === "receivables_confirmed_as_of_date";
|
|
||||||
const shouldUseRubCurrency = composeIntent === "vat_payable_forecast";
|
|
||||||
const factual = composeFactualReply(
|
|
||||||
composeIntent,
|
|
||||||
filteredRows,
|
|
||||||
composeOptionsFromFilters(executionFilters, {
|
|
||||||
vatDirectSourceProbe,
|
|
||||||
emphasizeNumbers: shouldEmphasizeNumbers,
|
|
||||||
useRubCurrency: shouldUseRubCurrency
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const vatProbeLimitations =
|
|
||||||
vatProbeRequired && vatDirectSourceProbe
|
|
||||||
? vatDirectSourceProbe.status === "error"
|
|
||||||
? ["vat_source_probe_error"]
|
|
||||||
: vatDirectSourceProbe.status === "skipped"
|
|
||||||
? ["vat_source_probe_skipped"]
|
|
||||||
: vatDirectSourceProbe.errors.length > 0
|
|
||||||
? ["vat_source_probe_partial_errors"]
|
|
||||||
: []
|
|
||||||
: [];
|
|
||||||
const factualLimitations = [...filters.warnings, ...vatProbeLimitations];
|
|
||||||
const factualResultSemantics = mergeAddressResultSemantics(
|
const factualResultSemantics = mergeAddressResultSemantics(
|
||||||
deriveAddressResultSemantics({
|
deriveAddressResultSemantics({
|
||||||
intent: composeIntent,
|
intent: composeIntent,
|
||||||
|
|
@ -3596,17 +3227,12 @@ export class AddressQueryService {
|
||||||
routeExpectationAudit: finalRouteExpectationAudit
|
routeExpectationAudit: finalRouteExpectationAudit
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const exactConfirmedIntent =
|
if (
|
||||||
(intent.intent === "payables_confirmed_as_of_date" && composeIntent === "payables_confirmed_as_of_date") ||
|
((intent.intent === "payables_confirmed_as_of_date" && composeIntent === "payables_confirmed_as_of_date") ||
|
||||||
(intent.intent === "receivables_confirmed_as_of_date" && composeIntent === "receivables_confirmed_as_of_date") ||
|
(intent.intent === "receivables_confirmed_as_of_date" && composeIntent === "receivables_confirmed_as_of_date")) &&
|
||||||
(intent.intent === "vat_payable_confirmed_as_of_date" && composeIntent === "vat_payable_confirmed_as_of_date");
|
factualResultSemantics.balance_confirmed !== true
|
||||||
if (exactConfirmedIntent && factualResultSemantics.balance_confirmed !== true) {
|
) {
|
||||||
const exactModeName =
|
const exactModeName = intent.intent === "payables_confirmed_as_of_date" ? "payables" : "receivables";
|
||||||
intent.intent === "payables_confirmed_as_of_date"
|
|
||||||
? "payables"
|
|
||||||
: intent.intent === "receivables_confirmed_as_of_date"
|
|
||||||
? "receivables"
|
|
||||||
: "vat_payable";
|
|
||||||
return buildLimitedExecutionResult({
|
return buildLimitedExecutionResult({
|
||||||
mode,
|
mode,
|
||||||
shape,
|
shape,
|
||||||
|
|
@ -3631,10 +3257,7 @@ export class AddressQueryService {
|
||||||
materializationDropReason: rowDiagnostics.materializationDropReason,
|
materializationDropReason: rowDiagnostics.materializationDropReason,
|
||||||
category: "recipe_visibility_gap",
|
category: "recipe_visibility_gap",
|
||||||
reasonText: `exact ${exactModeName} mode: confirmed balance was not proven for the requested as-of slice`,
|
reasonText: `exact ${exactModeName} mode: confirmed balance was not proven for the requested as-of slice`,
|
||||||
nextStep:
|
nextStep: "specify as_of_date/counterparty or enable detailed settlement registers for exact confirmed balance",
|
||||||
intent.intent === "vat_payable_confirmed_as_of_date"
|
|
||||||
? "specify as_of_date/organization or provide VAT settlement registers to prove exact VAT payable balance"
|
|
||||||
: "specify as_of_date/counterparty or enable detailed settlement registers for exact confirmed balance",
|
|
||||||
limitations: [`exact_${exactModeName}_mode_unconfirmed_output_blocked`],
|
limitations: [`exact_${exactModeName}_mode_unconfirmed_output_blocked`],
|
||||||
reasons: [...baseReasons, `exact_${exactModeName}_mode_unconfirmed_output_blocked`],
|
reasons: [...baseReasons, `exact_${exactModeName}_mode_unconfirmed_output_blocked`],
|
||||||
capabilityAudit,
|
capabilityAudit,
|
||||||
|
|
@ -3702,7 +3325,7 @@ export class AddressQueryService {
|
||||||
route_expectation_expected_requested_result_modes: finalRouteExpectationAudit.expectedRequestedResultModes,
|
route_expectation_expected_requested_result_modes: finalRouteExpectationAudit.expectedRequestedResultModes,
|
||||||
route_expectation_expected_result_modes: finalRouteExpectationAudit.expectedResultModes,
|
route_expectation_expected_result_modes: finalRouteExpectationAudit.expectedResultModes,
|
||||||
...factualResultSemantics,
|
...factualResultSemantics,
|
||||||
limitations: factualLimitations,
|
limitations: filters.warnings,
|
||||||
reasons: withConfirmedBalanceFallbackReason(
|
reasons: withConfirmedBalanceFallbackReason(
|
||||||
reasonsWithRouteExpectation,
|
reasonsWithRouteExpectation,
|
||||||
requestedResultMode,
|
requestedResultMode,
|
||||||
|
|
|
||||||
|
|
@ -72,29 +72,6 @@ const RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
||||||
Сумма __ORDER_DIRECTION__
|
Сумма __ORDER_DIRECTION__
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const VAT_PAYABLE_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
|
||||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
|
||||||
__AS_OF_EXPR__ КАК Период,
|
|
||||||
"Остатки на дату" КАК Регистратор,
|
|
||||||
"" КАК СчетДт,
|
|
||||||
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетКт,
|
|
||||||
Остатки.СуммаРазвернутыйОстатокКт КАК Сумма,
|
|
||||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоДт1,
|
|
||||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоДт2,
|
|
||||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоДт3,
|
|
||||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоКт1,
|
|
||||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоКт2,
|
|
||||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоКт3,
|
|
||||||
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация
|
|
||||||
ИЗ
|
|
||||||
РегистрБухгалтерии.Хозрасчетный.Остатки(__AS_OF_EXPR__, , , ) КАК Остатки
|
|
||||||
ГДЕ
|
|
||||||
Остатки.СуммаРазвернутыйОстатокКт > 0
|
|
||||||
И (__VAT_PAYABLE_ACCOUNTS_MATCH__)
|
|
||||||
УПОРЯДОЧИТЬ ПО
|
|
||||||
Сумма __ORDER_DIRECTION__
|
|
||||||
`;
|
|
||||||
|
|
||||||
const BANK_DOCS_QUERY_TEMPLATE = `
|
const BANK_DOCS_QUERY_TEMPLATE = `
|
||||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||||
БанкСписание.Дата КАК Период,
|
БанкСписание.Дата КАК Период,
|
||||||
|
|
@ -589,17 +566,6 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
|
||||||
account_scope_mode: "preferred",
|
account_scope_mode: "preferred",
|
||||||
query_template: "vat_payable_forecast_profile"
|
query_template: "vat_payable_forecast_profile"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
recipe_id: "address_vat_payable_confirmed_as_of_date_v1",
|
|
||||||
intent: "vat_payable_confirmed_as_of_date",
|
|
||||||
purpose: "Build confirmed VAT payable snapshot as-of date from balances on VAT payable accounts",
|
|
||||||
required_filters: ["as_of_date"],
|
|
||||||
optional_filters: ["period_from", "period_to", "organization", "limit", "sort"],
|
|
||||||
default_limit: 200,
|
|
||||||
account_scope: ["68"],
|
|
||||||
account_scope_mode: "strict",
|
|
||||||
query_template: "vat_payable_confirmed_as_of_balance_profile"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
recipe_id: "address_contracts_by_counterparty_v1",
|
recipe_id: "address_contracts_by_counterparty_v1",
|
||||||
intent: "list_contracts_by_counterparty",
|
intent: "list_contracts_by_counterparty",
|
||||||
|
|
@ -1091,28 +1057,6 @@ export function buildAddressRecipePlan(
|
||||||
.replaceAll("__VAT68_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", VAT_PAYABLE_68_PREFIXES))
|
.replaceAll("__VAT68_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", VAT_PAYABLE_68_PREFIXES))
|
||||||
.replaceAll("__VAT19_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", VAT_PAYABLE_19_PREFIXES))
|
.replaceAll("__VAT19_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", VAT_PAYABLE_19_PREFIXES))
|
||||||
.replaceAll("__VAT19_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", VAT_PAYABLE_19_PREFIXES))
|
.replaceAll("__VAT19_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", VAT_PAYABLE_19_PREFIXES))
|
||||||
: recipe.query_template === "vat_payable_confirmed_as_of_balance_profile"
|
|
||||||
? (() => {
|
|
||||||
const asOfExpr =
|
|
||||||
(typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
|
||||||
? toDateTimeExpr(filters.as_of_date, true)
|
|
||||||
: null) ??
|
|
||||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
|
||||||
? toDateTimeExpr(filters.period_to, true)
|
|
||||||
: null) ??
|
|
||||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
|
||||||
? toDateTimeExpr(filters.period_from, true)
|
|
||||||
: null) ??
|
|
||||||
"ТЕКУЩАЯДАТА()";
|
|
||||||
return VAT_PAYABLE_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
|
||||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
|
||||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
|
||||||
.replaceAll(
|
|
||||||
"__VAT_PAYABLE_ACCOUNTS_MATCH__",
|
|
||||||
buildAccountPrefixPredicate("Остатки.Счет", VAT_PAYABLE_68_PREFIXES)
|
|
||||||
)
|
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
|
||||||
})()
|
|
||||||
: recipe.query_template === "contracts_by_counterparty_profile"
|
: recipe.query_template === "contracts_by_counterparty_profile"
|
||||||
? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit))
|
? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||||
: recipe.query_template === "payables_confirmed_as_of_balance_profile"
|
: recipe.query_template === "payables_confirmed_as_of_balance_profile"
|
||||||
|
|
|
||||||
|
|
@ -14,35 +14,12 @@ export interface ComposeStageRow {
|
||||||
analytics: string[];
|
analytics: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VatDirectSourceProbeItem {
|
|
||||||
fullName: string;
|
|
||||||
synonym?: string | null;
|
|
||||||
objectType: "document" | "register";
|
|
||||||
status: "ok" | "empty" | "error";
|
|
||||||
rowsFetched: number;
|
|
||||||
lastPeriod?: string | null;
|
|
||||||
sampleRegistrator?: string | null;
|
|
||||||
error?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VatDirectSourceProbeSummary {
|
|
||||||
status: "ok" | "error" | "skipped";
|
|
||||||
objectsTotal: number;
|
|
||||||
documentsTotal: number;
|
|
||||||
registersTotal: number;
|
|
||||||
probedSources: VatDirectSourceProbeItem[];
|
|
||||||
errors: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ComposeFactualReplyOptions {
|
interface ComposeFactualReplyOptions {
|
||||||
userMessage?: string;
|
userMessage?: string;
|
||||||
periodFrom?: string;
|
periodFrom?: string;
|
||||||
periodTo?: string;
|
periodTo?: string;
|
||||||
asOfDate?: string;
|
asOfDate?: string;
|
||||||
requestedResultMode?: AddressResultMode;
|
requestedResultMode?: AddressResultMode;
|
||||||
vatDirectSourceProbe?: VatDirectSourceProbeSummary | null;
|
|
||||||
emphasizeNumbers?: boolean;
|
|
||||||
useRubCurrency?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComposeReplySemantics {
|
export interface ComposeReplySemantics {
|
||||||
|
|
@ -198,36 +175,8 @@ function formatMoneyRub(value: number): string {
|
||||||
return `${formatNumberWithDots(value, 2)} ₽`;
|
return `${formatNumberWithDots(value, 2)} ₽`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatVatProbeStatusRu(status: VatDirectSourceProbeItem["status"]): string {
|
|
||||||
if (status === "ok") {
|
|
||||||
return "есть движения";
|
|
||||||
}
|
|
||||||
if (status === "empty") {
|
|
||||||
return "движения не найдены";
|
|
||||||
}
|
|
||||||
return "ошибка запроса";
|
|
||||||
}
|
|
||||||
|
|
||||||
function emphasizeNumericTokens(line: string): string {
|
function emphasizeNumericTokens(line: string): string {
|
||||||
if (!line) {
|
return line;
|
||||||
return line;
|
|
||||||
}
|
|
||||||
const chunks = line.split(/(`[^`]*`)/g);
|
|
||||||
return chunks
|
|
||||||
.map((chunk, index) => {
|
|
||||||
if (index % 2 === 1) {
|
|
||||||
return chunk;
|
|
||||||
}
|
|
||||||
return chunk.replace(/\b-?(?:\d{1,3}(?:[.\s]\d{3})+|\d+)(?:[.,]\d+)?\b/g, (match, offset, source) => {
|
|
||||||
const before = offset > 0 ? source[offset - 1] : "";
|
|
||||||
const after = offset + match.length < source.length ? source[offset + match.length] : "";
|
|
||||||
if (before === "*" || after === "*") {
|
|
||||||
return match;
|
|
||||||
}
|
|
||||||
return `**${match}**`;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.join("");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseIsoDateToken(value: string | null | undefined): { year: number; month: number; day: number } | null {
|
function parseIsoDateToken(value: string | null | undefined): { year: number; month: number; day: number } | null {
|
||||||
|
|
@ -270,22 +219,6 @@ function buildIsoDateWithMonthShift(
|
||||||
return date.toISOString().slice(0, 10);
|
return date.toISOString().slice(0, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
function shiftIsoDateToNextBusinessDay(isoDate: string): string {
|
|
||||||
const parsed = parseIsoDateToken(isoDate);
|
|
||||||
if (!parsed) {
|
|
||||||
return isoDate;
|
|
||||||
}
|
|
||||||
const date = new Date(Date.UTC(parsed.year, parsed.month - 1, parsed.day));
|
|
||||||
for (let guard = 0; guard < 10; guard += 1) {
|
|
||||||
const dayOfWeek = date.getUTCDay();
|
|
||||||
if (dayOfWeek !== 0 && dayOfWeek !== 6) {
|
|
||||||
return date.toISOString().slice(0, 10);
|
|
||||||
}
|
|
||||||
date.setUTCDate(date.getUTCDate() + 1);
|
|
||||||
}
|
|
||||||
return isoDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
function deriveVatDeadlineCalendar(
|
function deriveVatDeadlineCalendar(
|
||||||
periodFrom: string | null | undefined,
|
periodFrom: string | null | undefined,
|
||||||
periodTo: string | null | undefined
|
periodTo: string | null | undefined
|
||||||
|
|
@ -310,12 +243,10 @@ function deriveVatDeadlineCalendar(
|
||||||
const quarterEndDay = new Date(Date.UTC(reference.year, quarterEndMonth, 0)).getUTCDate();
|
const quarterEndDay = new Date(Date.UTC(reference.year, quarterEndMonth, 0)).getUTCDate();
|
||||||
const quarterStart = toIsoDate(reference.year, quarterStartMonth, 1);
|
const quarterStart = toIsoDate(reference.year, quarterStartMonth, 1);
|
||||||
const quarterEnd = toIsoDate(reference.year, quarterEndMonth, quarterEndDay);
|
const quarterEnd = toIsoDate(reference.year, quarterEndMonth, quarterEndDay);
|
||||||
const declarationDueDate = shiftIsoDateToNextBusinessDay(
|
const declarationDueDate = buildIsoDateWithMonthShift(reference.year, quarterEndMonth, 25, 1);
|
||||||
buildIsoDateWithMonthShift(reference.year, quarterEndMonth, 25, 1)
|
const payment1 = buildIsoDateWithMonthShift(reference.year, quarterEndMonth, 28, 1);
|
||||||
);
|
const payment2 = buildIsoDateWithMonthShift(reference.year, quarterEndMonth, 28, 2);
|
||||||
const payment1 = shiftIsoDateToNextBusinessDay(buildIsoDateWithMonthShift(reference.year, quarterEndMonth, 28, 1));
|
const payment3 = buildIsoDateWithMonthShift(reference.year, quarterEndMonth, 28, 3);
|
||||||
const payment2 = shiftIsoDateToNextBusinessDay(buildIsoDateWithMonthShift(reference.year, quarterEndMonth, 28, 2));
|
|
||||||
const payment3 = shiftIsoDateToNextBusinessDay(buildIsoDateWithMonthShift(reference.year, quarterEndMonth, 28, 3));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
periodLabel: `${quarterNumber} кв. ${reference.year}`,
|
periodLabel: `${quarterNumber} кв. ${reference.year}`,
|
||||||
|
|
@ -453,14 +384,6 @@ function needsVatWhyExplanation(userMessage: string | null | undefined): boolean
|
||||||
return /(?:ндс|vat|прогноз|к\s+уплате|нул|ноль|\b0(?:[.,]0+)?\b)/iu.test(text);
|
return /(?:ндс|vat|прогноз|к\s+уплате|нул|ноль|\b0(?:[.,]0+)?\b)/iu.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
function needsVatCalendarDetails(userMessage: string | null | undefined): boolean {
|
|
||||||
const text = normalizeQuestionText(userMessage);
|
|
||||||
if (!text) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return /(?:срок|когда|дата\s+уплат|декларац|дол(?:я|ями)|по\s+частям|платежн(?:ый|ого)\s+график)/iu.test(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
function detectRankingLimit(userMessage: string | null | undefined, fallback = 20): number {
|
function detectRankingLimit(userMessage: string | null | undefined, fallback = 20): number {
|
||||||
const text = normalizeQuestionText(userMessage);
|
const text = normalizeQuestionText(userMessage);
|
||||||
if (!text) {
|
if (!text) {
|
||||||
|
|
@ -1541,9 +1464,6 @@ export function composeFactualReply(
|
||||||
rows: ComposeStageRow[],
|
rows: ComposeStageRow[],
|
||||||
options: ComposeFactualReplyOptions = {}
|
options: ComposeFactualReplyOptions = {}
|
||||||
): { responseType: AddressResponseType; text: string; semantics?: ComposeReplySemantics } {
|
): { responseType: AddressResponseType; text: string; semantics?: ComposeReplySemantics } {
|
||||||
const applyNumericEmphasis = (line: string): string => (options.emphasizeNumbers ? emphasizeNumericTokens(line) : line);
|
|
||||||
const joinLines = (lines: string[]): string => lines.map(applyNumericEmphasis).join("\n");
|
|
||||||
|
|
||||||
if (intent === "document_type_and_account_section_profile") {
|
if (intent === "document_type_and_account_section_profile") {
|
||||||
const rowsByMarker = new Map<string, ComposeStageRow[]>();
|
const rowsByMarker = new Map<string, ComposeStageRow[]>();
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
|
|
@ -2522,72 +2442,32 @@ export function composeFactualReply(
|
||||||
const vatActivityDetected = totalVatTurnoverAbs > 0.0000001;
|
const vatActivityDetected = totalVatTurnoverAbs > 0.0000001;
|
||||||
const netVatIsEffectivelyZero = Math.abs(netVat) <= 0.005;
|
const netVatIsEffectivelyZero = Math.abs(netVat) <= 0.005;
|
||||||
const explainWhyRequested = needsVatWhyExplanation(options.userMessage);
|
const explainWhyRequested = needsVatWhyExplanation(options.userMessage);
|
||||||
const shouldShowCalendarDetails = needsVatCalendarDetails(options.userMessage);
|
|
||||||
const vatCalendar = deriveVatDeadlineCalendar(options.periodFrom, options.periodTo);
|
const vatCalendar = deriveVatDeadlineCalendar(options.periodFrom, options.periodTo);
|
||||||
const formatForecastMoney = (value: number): string => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
|
||||||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
|
||||||
const periodWindowLabel =
|
|
||||||
options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null;
|
|
||||||
|
|
||||||
const lines = [
|
const lines = [
|
||||||
`Собран прогноз НДС к уплате: ${formatForecastMoney(vatToPay)}.`,
|
"Собран прогноз НДС к уплате по фактическим проводкам (НДС-субсчета 68.02*/19*).",
|
||||||
`Потенциальный перенос/переплата: ${formatForecastMoney(carryoverOrOverpayment)}.`,
|
`Строк агрегата: ${rows.length}.`,
|
||||||
`Период оценки: ${periodWindowLabel ?? "не задан (использован доступный срез)"}.`,
|
`Оборот по кредиту 68*: ${formatMoney(turnover68Credit)}.`,
|
||||||
"Режим результата: предварительная оценка по проводкам 68.02*/19* (не подтвержденная сумма налога по декларации).",
|
`Оборот по дебету 68*: ${formatMoney(turnover68Debit)}.`,
|
||||||
"",
|
`Нетто НДС (68 Кт - 68 Дт): ${formatMoney(netVat)}.`,
|
||||||
"База расчета:",
|
`Прогноз НДС к уплате: ${formatMoney(vatToPay)}.`,
|
||||||
`- Строк агрегата: ${formatNumberWithDots(rows.length)}.`,
|
`Потенциальный перенос/переплата: ${formatMoney(carryoverOrOverpayment)}.`,
|
||||||
`- Оборот по кредиту 68*: ${formatForecastMoney(turnover68Credit)}.`,
|
`Справочно по 19*: дебет ${formatMoney(turnover19Debit)}, кредит ${formatMoney(turnover19Credit)}.`
|
||||||
`- Оборот по дебету 68*: ${formatForecastMoney(turnover68Debit)}.`,
|
|
||||||
`- Нетто НДС (68 Кт - 68 Дт): ${formatForecastMoney(netVat)}.`,
|
|
||||||
`- Справочно по 19*: дебет ${formatForecastMoney(turnover19Debit)}, кредит ${formatForecastMoney(turnover19Credit)}.`
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (vatProbe && vatProbe.status === "ok") {
|
|
||||||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
|
||||||
lines.push(
|
|
||||||
"",
|
|
||||||
"Покрытие VAT-источников через MCP:",
|
|
||||||
`- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`,
|
|
||||||
`- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`,
|
|
||||||
`- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`
|
|
||||||
);
|
|
||||||
if (vatProbe.probedSources.length > 0) {
|
|
||||||
lines.push(
|
|
||||||
...vatProbe.probedSources.slice(0, 6).map((item, index) => {
|
|
||||||
const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName;
|
|
||||||
return `${index + 1}. ${name} | ${formatVatProbeStatusRu(item.status)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}`;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (vatProbe.errors.length > 0) {
|
|
||||||
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
|
||||||
}
|
|
||||||
lines.push("- Сумма прогноза выше рассчитана строго по оборотам 68.02*/19*; прямые VAT-источники показаны для проверки покрытия.");
|
|
||||||
} else if (vatProbe && vatProbe.status === "error") {
|
|
||||||
lines.push("", "Покрытие VAT-источников через MCP: probe завершился ошибкой, поэтому использован только базовый контур 68.02*/19*.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!vatActivityDetected) {
|
if (!vatActivityDetected) {
|
||||||
lines.push(
|
lines.push(
|
||||||
`В выбранном окне не найдено движений по НДС-субсчетам 68.02*/19*; поэтому оперативный прогноз к уплате равен ${formatForecastMoney(
|
"В выбранном окне не найдено движений по НДС-субсчетам 68.02*/19*; поэтому оперативный прогноз к уплате равен 0.00."
|
||||||
0
|
|
||||||
)}.`
|
|
||||||
);
|
);
|
||||||
} else if (vatToPay === 0 && netVatIsEffectivelyZero) {
|
} else if (vatToPay === 0 && netVatIsEffectivelyZero) {
|
||||||
lines.push(
|
lines.push("В выбранном окне обороты по 68* взаимно перекрылись (нетто близко к нулю), поэтому к уплате 0.00.");
|
||||||
`В выбранном окне обороты по 68* взаимно перекрылись (нетто близко к нулю), поэтому к уплате ${formatForecastMoney(0)}.`
|
|
||||||
);
|
|
||||||
} else if (vatToPay === 0 && netVat < 0) {
|
} else if (vatToPay === 0 && netVat < 0) {
|
||||||
lines.push(
|
lines.push("В выбранном окне дебет 68* превышает кредит 68*; сумма показана как перенос/переплата, к уплате 0.00.");
|
||||||
`В выбранном окне дебет 68* превышает кредит 68*; сумма показана как перенос/переплата, к уплате ${formatForecastMoney(0)}.`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (vatToPay === 0) {
|
if (vatToPay === 0) {
|
||||||
lines.push(
|
lines.push(
|
||||||
"",
|
|
||||||
"Чеклист проверки в 1С (почему к уплате 0):",
|
"Чеклист проверки в 1С (почему к уплате 0):",
|
||||||
`1) Проверьте ОСВ/анализ счета по 68.02 и 19 за окно ${periodWindowLabel ?? "расчета"}.`,
|
`1) Проверьте ОСВ/анализ счета по 68.02 и 19 за окно ${options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : "расчета"}.`,
|
||||||
"2) Проверьте наличие движений в РегистрБухгалтерии.Хозрасчетный по счетам 68.02*/19* (включая субсчета).",
|
"2) Проверьте наличие движений в РегистрБухгалтерии.Хозрасчетный по счетам 68.02*/19* (включая субсчета).",
|
||||||
"3) Сверьте счета-фактуры, корректировки и момент принятия НДС к вычету (не попали ли в другой период).",
|
"3) Сверьте счета-фактуры, корректировки и момент принятия НДС к вычету (не попали ли в другой период).",
|
||||||
"4) Сверьте книгу продаж/покупок и операции Помощника по учету НДС за тот же период.",
|
"4) Сверьте книгу продаж/покупок и операции Помощника по учету НДС за тот же период.",
|
||||||
|
|
@ -2595,7 +2475,7 @@ export function composeFactualReply(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vatCalendar && shouldShowCalendarDetails) {
|
if (vatCalendar) {
|
||||||
const periodWindowLabel =
|
const periodWindowLabel =
|
||||||
vatCalendar.windowFrom && vatCalendar.windowTo
|
vatCalendar.windowFrom && vatCalendar.windowTo
|
||||||
? `${formatDateRu(vatCalendar.windowFrom)}..${formatDateRu(vatCalendar.windowTo)}`
|
? `${formatDateRu(vatCalendar.windowFrom)}..${formatDateRu(vatCalendar.windowTo)}`
|
||||||
|
|
@ -2605,20 +2485,18 @@ export function composeFactualReply(
|
||||||
const installmentRounded = Number(installmentRaw.toFixed(2));
|
const installmentRounded = Number(installmentRaw.toFixed(2));
|
||||||
const installmentThird = Number((vatToPay - installmentRounded * 2).toFixed(2));
|
const installmentThird = Number((vatToPay - installmentRounded * 2).toFixed(2));
|
||||||
lines.push(
|
lines.push(
|
||||||
"",
|
|
||||||
`Период расчета (срез обязательств): ${periodWindowLabel}.`,
|
`Период расчета (срез обязательств): ${periodWindowLabel}.`,
|
||||||
`Налоговый период: ${vatCalendar.periodLabel}.`,
|
`Налоговый период: ${vatCalendar.periodLabel}.`,
|
||||||
`Срок сдачи декларации: до ${formatDateRu(vatCalendar.declarationDueDate)}.`,
|
`Срок сдачи декларации: до ${formatDateRu(vatCalendar.declarationDueDate)}.`,
|
||||||
`Сроки уплаты: ${formatDateRu(payment1)}, ${formatDateRu(payment2)}, ${formatDateRu(payment3)}.`,
|
`Сроки уплаты: ${formatDateRu(payment1)}, ${formatDateRu(payment2)}, ${formatDateRu(payment3)}.`,
|
||||||
`Ориентир по долям к уплате: ${formatForecastMoney(installmentRounded)} / ${formatForecastMoney(installmentRounded)} / ${formatForecastMoney(installmentThird)}.`,
|
`Ориентир по долям к уплате: ${formatMoney(installmentRounded)} / ${formatMoney(installmentRounded)} / ${formatMoney(installmentThird)}.`,
|
||||||
"Важно: даже при нулевой сумме к уплате декларация по НДС подается в установленный срок; переносы по выходным/праздникам сверяйте по календарю ФНС/1С."
|
"Важно: даже при нулевой сумме к уплате декларация по НДС подается в установленный срок; переносы по выходным/праздникам сверяйте по календарю ФНС/1С."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (explainWhyRequested) {
|
if (explainWhyRequested) {
|
||||||
lines.push(
|
lines.push(
|
||||||
"",
|
|
||||||
"Почему прогноз к уплате 0: в текущей модели используем формулу max(0, 68 Кт - 68 Дт).",
|
"Почему прогноз к уплате 0: в текущей модели используем формулу max(0, 68 Кт - 68 Дт).",
|
||||||
`За период 68 Кт = ${formatForecastMoney(turnover68Credit)}, 68 Дт = ${formatForecastMoney(turnover68Debit)}, разница = ${formatForecastMoney(netVat)}.`,
|
`За период 68 Кт = ${formatMoney(turnover68Credit)}, 68 Дт = ${formatMoney(turnover68Debit)}, разница = ${formatMoney(netVat)}.`,
|
||||||
netVat <= 0
|
netVat <= 0
|
||||||
? "Разница неположительная, поэтому к уплате = 0, а отрицательная часть показана как перенос/переплата."
|
? "Разница неположительная, поэтому к уплате = 0, а отрицательная часть показана как перенос/переплата."
|
||||||
: "Разница положительная, поэтому к уплате берется эта положительная величина.",
|
: "Разница положительная, поэтому к уплате берется эта положительная величина.",
|
||||||
|
|
@ -2628,136 +2506,7 @@ export function composeFactualReply(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
responseType: "FACTUAL_SUMMARY",
|
responseType: "FACTUAL_SUMMARY",
|
||||||
text: joinLines(lines)
|
text: lines.join("\n")
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (intent === "vat_payable_confirmed_as_of_date") {
|
|
||||||
const asOfDate = resolvePayablesAsOfDate(options);
|
|
||||||
const confirmedRows = rows.filter((row) => {
|
|
||||||
const amount = row.amount ?? 0;
|
|
||||||
if (!Number.isFinite(amount) || amount <= 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const section = extractAccountSectionCode(row.account_kt);
|
|
||||||
return section === "68";
|
|
||||||
});
|
|
||||||
|
|
||||||
const byAccount = new Map<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
account: string;
|
|
||||||
total: number;
|
|
||||||
operations: number;
|
|
||||||
lastPeriod: string | null;
|
|
||||||
refs: Set<string>;
|
|
||||||
}
|
|
||||||
>();
|
|
||||||
|
|
||||||
for (const row of confirmedRows) {
|
|
||||||
const account = String(row.account_kt ?? "").trim() || "68*";
|
|
||||||
const registrator = String(row.registrator ?? "").trim();
|
|
||||||
const amount = row.amount ?? 0;
|
|
||||||
const current = byAccount.get(account);
|
|
||||||
if (!current) {
|
|
||||||
byAccount.set(account, {
|
|
||||||
account,
|
|
||||||
total: amount,
|
|
||||||
operations: 1,
|
|
||||||
lastPeriod: row.period,
|
|
||||||
refs: registrator ? new Set([registrator]) : new Set()
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
current.total += amount;
|
|
||||||
current.operations += 1;
|
|
||||||
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
|
|
||||||
current.lastPeriod = row.period;
|
|
||||||
}
|
|
||||||
if (registrator) {
|
|
||||||
current.refs.add(registrator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const accountRows = Array.from(byAccount.values())
|
|
||||||
.filter((item) => Number.isFinite(item.total) && item.total > 0)
|
|
||||||
.sort((a, b) => b.total - a.total || b.operations - a.operations || a.account.localeCompare(b.account, "ru"));
|
|
||||||
const totalVatPayable = accountRows.reduce((sum, item) => sum + item.total, 0);
|
|
||||||
|
|
||||||
const lines: string[] = [
|
|
||||||
`Итого подтвержденный НДС к уплате на ${formatDateRu(asOfDate)}: ${formatMoneyRub(totalVatPayable)}.`,
|
|
||||||
"",
|
|
||||||
"Блок 1. Статус результата",
|
|
||||||
"- Результат: подтвержденный срез НДС к уплате по состоянию на дату.",
|
|
||||||
"",
|
|
||||||
"Блок 2. Что учтено",
|
|
||||||
`- Дата среза: ${formatDateRu(asOfDate)}.`,
|
|
||||||
"- Контур: остатки по счетам НДС к уплате (68*)."
|
|
||||||
];
|
|
||||||
|
|
||||||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
|
||||||
if (vatProbe && vatProbe.status === "ok") {
|
|
||||||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
|
||||||
lines.push(
|
|
||||||
"",
|
|
||||||
"Блок 2.1. MCP-проверка VAT-источников",
|
|
||||||
`- VAT-объектов в метаданных 1С: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`,
|
|
||||||
`- Пробных прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`,
|
|
||||||
`- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`
|
|
||||||
);
|
|
||||||
if (vatProbe.probedSources.length > 0) {
|
|
||||||
lines.push(
|
|
||||||
...vatProbe.probedSources.slice(0, 4).map((item, index) => {
|
|
||||||
const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName;
|
|
||||||
const suffix =
|
|
||||||
item.status === "ok"
|
|
||||||
? `${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.sampleRegistrator ? ` | пример: ${item.sampleRegistrator}` : ""}`
|
|
||||||
: item.status === "error" && item.error
|
|
||||||
? ` | ошибка: ${item.error}`
|
|
||||||
: "";
|
|
||||||
return `${index + 1}. ${name} | ${formatVatProbeStatusRu(item.status)}${suffix}`;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (vatProbe.errors.length > 0) {
|
|
||||||
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
|
||||||
}
|
|
||||||
} else if (vatProbe && vatProbe.status === "error") {
|
|
||||||
lines.push(
|
|
||||||
"",
|
|
||||||
"Блок 2.1. MCP-проверка VAT-источников",
|
|
||||||
"- Probe VAT-источников завершился ошибкой, поэтому срез подтвержден по доступному бухгалтерскому источнику (68*)."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.push(
|
|
||||||
"",
|
|
||||||
"Блок 3. Сводка",
|
|
||||||
`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`,
|
|
||||||
`- Подтвержденных позиций по НДС: ${formatNumberWithDots(accountRows.length)}.`,
|
|
||||||
"",
|
|
||||||
"Блок 4. Подтвержденные позиции"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (accountRows.length > 0) {
|
|
||||||
lines.push(
|
|
||||||
...accountRows.slice(0, 12).map((item, index) => {
|
|
||||||
const refs = Array.from(item.refs).slice(0, 2).join("; ");
|
|
||||||
return `${index + 1}. ${item.account} | остаток НДС к уплате: ${formatMoneyRub(item.total)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${refs ? ` | source refs: ${refs}` : ""}`;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
lines.push("- Подтвержденный остаток НДС к уплате на дату среза не найден.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
responseType: "FACTUAL_LIST",
|
|
||||||
text: joinLines(lines),
|
|
||||||
semantics: {
|
|
||||||
result_mode: "confirmed_balance",
|
|
||||||
evidence_strength: "strong",
|
|
||||||
balance_confirmed: true
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2892,7 +2641,7 @@ export function composeFactualReply(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
||||||
text: joinLines(lines),
|
text: lines.map(emphasizeNumericTokens).join("\n"),
|
||||||
semantics: {
|
semantics: {
|
||||||
result_mode: "confirmed_balance",
|
result_mode: "confirmed_balance",
|
||||||
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
|
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
|
||||||
|
|
@ -2972,7 +2721,7 @@ export function composeFactualReply(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
||||||
text: joinLines(lines),
|
text: lines.map(emphasizeNumericTokens).join("\n"),
|
||||||
semantics: {
|
semantics: {
|
||||||
result_mode: "confirmed_balance",
|
result_mode: "confirmed_balance",
|
||||||
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
|
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
|
||||||
|
|
@ -3119,7 +2868,7 @@ export function composeFactualReply(
|
||||||
];
|
];
|
||||||
return {
|
return {
|
||||||
responseType: "FACTUAL_LIST",
|
responseType: "FACTUAL_LIST",
|
||||||
text: joinLines(lines),
|
text: lines.map(emphasizeNumericTokens).join("\n"),
|
||||||
semantics: {
|
semantics: {
|
||||||
result_mode: "confirmed_balance",
|
result_mode: "confirmed_balance",
|
||||||
evidence_strength: "strong",
|
evidence_strength: "strong",
|
||||||
|
|
@ -3131,7 +2880,7 @@ export function composeFactualReply(
|
||||||
const fallbackLines = buildHeuristicLines(true);
|
const fallbackLines = buildHeuristicLines(true);
|
||||||
return {
|
return {
|
||||||
responseType: "FACTUAL_LIST",
|
responseType: "FACTUAL_LIST",
|
||||||
text: joinLines(fallbackLines),
|
text: fallbackLines.map(emphasizeNumericTokens).join("\n"),
|
||||||
semantics: {
|
semantics: {
|
||||||
result_mode: "heuristic_candidates",
|
result_mode: "heuristic_candidates",
|
||||||
evidence_strength: counterparties.length > 0 ? "medium" : "weak",
|
evidence_strength: counterparties.length > 0 ? "medium" : "weak",
|
||||||
|
|
@ -3143,7 +2892,7 @@ export function composeFactualReply(
|
||||||
const lines = buildHeuristicLines(false);
|
const lines = buildHeuristicLines(false);
|
||||||
return {
|
return {
|
||||||
responseType: "FACTUAL_LIST",
|
responseType: "FACTUAL_LIST",
|
||||||
text: joinLines(lines),
|
text: lines.map(emphasizeNumericTokens).join("\n"),
|
||||||
semantics: {
|
semantics: {
|
||||||
result_mode: "heuristic_candidates",
|
result_mode: "heuristic_candidates",
|
||||||
evidence_strength: counterparties.length > 0 ? "medium" : "weak",
|
evidence_strength: counterparties.length > 0 ? "medium" : "weak",
|
||||||
|
|
|
||||||
|
|
@ -66,14 +66,6 @@ function hasOpenItemsHint(text: string): boolean {
|
||||||
return /(?:open\s+items|unclosed\s+items|хвост|висят|незакрыт|не\s+закрыт|открыт|долг|задолж|позиц)/iu.test(String(text ?? ""));
|
return /(?:open\s+items|unclosed\s+items|хвост|висят|незакрыт|не\s+закрыт|открыт|долг|задолж|позиц)/iu.test(String(text ?? ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasVatCue(text: string): boolean {
|
|
||||||
return /(?:^|[\s,.;:!?()\-])(?:ндс|vat)(?=$|[\s,.;:!?()\-])/iu.test(String(text ?? ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasVatForecastCue(text: string): boolean {
|
|
||||||
return /(?:прогноз|forecast|прикин|оцен|план)/iu.test(String(text ?? ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasDocumentSignal(text: string): boolean {
|
function hasDocumentSignal(text: string): boolean {
|
||||||
return /(?:док(?:и|умент|ументы|ументов|ументами)|docs?|documents?|doki|docy|doci)/iu.test(String(text ?? ""));
|
return /(?:док(?:и|умент|ументы|ументов|ументами)|docs?|documents?|doki|docy|doci)/iu.test(String(text ?? ""));
|
||||||
}
|
}
|
||||||
|
|
@ -445,26 +437,14 @@ function mergeFollowupFilters(
|
||||||
reasons.push("as_of_date_from_followup_context");
|
reasons.push("as_of_date_from_followup_context");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!sameDateRequested && !hasExplicitPeriodLiteral(userMessage)) {
|
|
||||||
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
|
|
||||||
const currentAsOfDate = toNonEmptyString(merged.as_of_date);
|
|
||||||
const todayIso = new Date().toISOString().slice(0, 10);
|
|
||||||
const currentLooksDefaultedToToday = currentAsOfDate === todayIso;
|
|
||||||
if (inheritedAsOfDate && (!currentAsOfDate || currentLooksDefaultedToToday) && currentAsOfDate !== inheritedAsOfDate) {
|
|
||||||
merged.as_of_date = inheritedAsOfDate;
|
|
||||||
reasons.push("as_of_date_from_followup_context");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
intent === "open_items_by_counterparty_or_contract" ||
|
intent === "open_items_by_counterparty_or_contract" ||
|
||||||
intent === "list_open_contracts" ||
|
intent === "list_open_contracts" ||
|
||||||
intent === "payables_confirmed_as_of_date" ||
|
intent === "payables_confirmed_as_of_date" ||
|
||||||
intent === "receivables_confirmed_as_of_date" ||
|
intent === "receivables_confirmed_as_of_date"
|
||||||
intent === "vat_payable_confirmed_as_of_date"
|
|
||||||
) {
|
) {
|
||||||
const hasFollowupSignalForConfirmed = hasAddressFollowupContextSignal(userMessage);
|
|
||||||
const inheritedContract = previousContract ?? (followupContext.previous_anchor_type === "contract" ? previousAnchorValue : null);
|
const inheritedContract = previousContract ?? (followupContext.previous_anchor_type === "contract" ? previousAnchorValue : null);
|
||||||
const currentContract = toNonEmptyString(merged.contract);
|
const currentContract = toNonEmptyString(merged.contract);
|
||||||
const shouldInheritContract =
|
const shouldInheritContract =
|
||||||
|
|
@ -494,16 +474,6 @@ function mergeFollowupFilters(
|
||||||
reasons.push("as_of_date_from_followup_context");
|
reasons.push("as_of_date_from_followup_context");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!sameDateRequested && hasFollowupSignalForConfirmed && !hasExplicitPeriodLiteral(userMessage)) {
|
|
||||||
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
|
|
||||||
const currentAsOfDate = toNonEmptyString(merged.as_of_date);
|
|
||||||
const todayIso = new Date().toISOString().slice(0, 10);
|
|
||||||
const currentLooksDefaultedToToday = currentAsOfDate === todayIso;
|
|
||||||
if (inheritedAsOfDate && (!currentAsOfDate || currentLooksDefaultedToToday) && currentAsOfDate !== inheritedAsOfDate) {
|
|
||||||
merged.as_of_date = inheritedAsOfDate;
|
|
||||||
reasons.push("as_of_date_from_followup_context");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allTimeRequested) {
|
if (allTimeRequested) {
|
||||||
|
|
@ -569,7 +539,6 @@ function resolveMissingRequiredFilters(intent: AddressIntent, filters: AddressFi
|
||||||
documents_forming_balance: ["account", "as_of_date"],
|
documents_forming_balance: ["account", "as_of_date"],
|
||||||
payables_confirmed_as_of_date: ["as_of_date"],
|
payables_confirmed_as_of_date: ["as_of_date"],
|
||||||
receivables_confirmed_as_of_date: ["as_of_date"],
|
receivables_confirmed_as_of_date: ["as_of_date"],
|
||||||
vat_payable_confirmed_as_of_date: ["as_of_date"],
|
|
||||||
list_documents_by_counterparty: ["counterparty"],
|
list_documents_by_counterparty: ["counterparty"],
|
||||||
bank_operations_by_counterparty: ["counterparty"],
|
bank_operations_by_counterparty: ["counterparty"],
|
||||||
list_contracts_by_counterparty: ["counterparty"],
|
list_contracts_by_counterparty: ["counterparty"],
|
||||||
|
|
@ -608,18 +577,6 @@ function deriveIntentWithFollowupContext(
|
||||||
const hasPreviousContract = Boolean(previousContract ?? previousContractFromAnchor);
|
const hasPreviousContract = Boolean(previousContract ?? previousContractFromAnchor);
|
||||||
const hasPreviousCounterparty = Boolean(previousCounterparty ?? previousCounterpartyFromAnchor);
|
const hasPreviousCounterparty = Boolean(previousCounterparty ?? previousCounterpartyFromAnchor);
|
||||||
const hasAnyPartyAnchor = hasPreviousContract || hasPreviousCounterparty;
|
const hasAnyPartyAnchor = hasPreviousContract || hasPreviousCounterparty;
|
||||||
const isVatFollowup = hasVatCue(normalizedMessage);
|
|
||||||
|
|
||||||
if (detectedIntent.intent === "unknown" && isVatFollowup) {
|
|
||||||
const vatIntent: AddressIntent = hasVatForecastCue(normalizedMessage)
|
|
||||||
? "vat_payable_forecast"
|
|
||||||
: "vat_payable_confirmed_as_of_date";
|
|
||||||
return {
|
|
||||||
intent: vatIntent,
|
|
||||||
confidence: "low",
|
|
||||||
reasons: [...detectedIntent.reasons, "intent_adjusted_to_vat_followup_context"]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasOpenItemsHint(normalizedMessage) && hasAnyPartyAnchor) {
|
if (hasOpenItemsHint(normalizedMessage) && hasAnyPartyAnchor) {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -193,8 +193,7 @@ function inferAggregationProfile(intent: AddressIntent, shape: AddressQueryShape
|
||||||
intent === "account_balance_snapshot" ||
|
intent === "account_balance_snapshot" ||
|
||||||
intent === "documents_forming_balance" ||
|
intent === "documents_forming_balance" ||
|
||||||
intent === "payables_confirmed_as_of_date" ||
|
intent === "payables_confirmed_as_of_date" ||
|
||||||
intent === "receivables_confirmed_as_of_date" ||
|
intent === "receivables_confirmed_as_of_date"
|
||||||
intent === "vat_payable_confirmed_as_of_date"
|
|
||||||
) {
|
) {
|
||||||
return "balance_snapshot";
|
return "balance_snapshot";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1990,7 +1990,7 @@ function textMojibakeScoreForAddress(value) {
|
||||||
const source = String(value ?? "");
|
const source = String(value ?? "");
|
||||||
const cyrillic = (source.match(/[А-Яа-яЁё]/g) ?? []).length;
|
const cyrillic = (source.match(/[А-Яа-яЁё]/g) ?? []).length;
|
||||||
const latin = (source.match(/[A-Za-z]/g) ?? []).length;
|
const latin = (source.match(/[A-Za-z]/g) ?? []).length;
|
||||||
const hardMarkers = (source.match(/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ<EFBFBD>?’“”•–—™љ›њќћџ]/g) ?? []).length;
|
const hardMarkers = (source.match(/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ‘’“”•–—™љ›њќћџ]/g) ?? []).length;
|
||||||
const pairMarkers = (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length;
|
const pairMarkers = (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length;
|
||||||
const doubleEncodedMarkers = (source.match(/(?:Г[Ђ-џ]|В[Ђ-џ]|Ã.|Â.)/gu) ?? []).length;
|
const doubleEncodedMarkers = (source.match(/(?:Г[Ђ-џ]|В[Ђ-џ]|Ã.|Â.)/gu) ?? []).length;
|
||||||
return cyrillic + latin - hardMarkers * 3 - pairMarkers * 2 - doubleEncodedMarkers * 2;
|
return cyrillic + latin - hardMarkers * 3 - pairMarkers * 2 - doubleEncodedMarkers * 2;
|
||||||
|
|
@ -2000,7 +2000,7 @@ function looksLikeMojibakeForAddress(value) {
|
||||||
if (!source.trim()) {
|
if (!source.trim()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ<EFBFBD>?’“”•–—™љ›њќћџ]/.test(source)) {
|
if (/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ‘’“”•–—™љ›њќћџ]/.test(source)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if ((source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length >= 2) {
|
if ((source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length >= 2) {
|
||||||
|
|
@ -2205,7 +2205,7 @@ function normalizeCounterpartyForFollowupMatch(value) {
|
||||||
return compactWhitespace(repairAddressMojibake(String(value ?? ""))
|
return compactWhitespace(repairAddressMojibake(String(value ?? ""))
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/ё/g, "е")
|
.replace(/ё/g, "е")
|
||||||
.replace(/[«»"'`“”„’<EFBFBD>?]/g, " ")
|
.replace(/[«»"'`“”„’‘]/g, " ")
|
||||||
.replace(/[^a-zа-я0-9\s._-]+/giu, " "));
|
.replace(/[^a-zа-я0-9\s._-]+/giu, " "));
|
||||||
}
|
}
|
||||||
function normalizeCounterpartyTokenForFollowupMatch(value) {
|
function normalizeCounterpartyTokenForFollowupMatch(value) {
|
||||||
|
|
@ -2251,7 +2251,7 @@ function extractDisplayedAddressEntityCandidates(replyText, entityType = "unknow
|
||||||
if (parts.length >= 2 && /^\d{4}-\d{2}-\d{2}/.test(parts[0] ?? "")) {
|
if (parts.length >= 2 && /^\d{4}-\d{2}-\d{2}/.test(parts[0] ?? "")) {
|
||||||
counterpartyCandidate = parts[1] ?? counterpartyCandidate;
|
counterpartyCandidate = parts[1] ?? counterpartyCandidate;
|
||||||
}
|
}
|
||||||
const cleanedCandidate = compactWhitespace(counterpartyCandidate.replace(/^["'«»“”„`’<EFBFBD>?]+|["'«»“”„`’<EFBFBD>?]+$/gu, ""));
|
const cleanedCandidate = compactWhitespace(counterpartyCandidate.replace(/^["'«»“”„`’‘]+|["'«»“”„`’‘]+$/gu, ""));
|
||||||
if (!cleanedCandidate || cleanedCandidate.length < 2) {
|
if (!cleanedCandidate || cleanedCandidate.length < 2) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -2515,133 +2515,61 @@ function isImplicitAddressContinuationByLlm(userMessage, llmPreDecomposeMeta) {
|
||||||
return tokenCount > 0 && tokenCount <= 4;
|
return tokenCount > 0 && tokenCount <= 4;
|
||||||
}
|
}
|
||||||
function hasAddressFollowupContextSignal(userMessage) {
|
function hasAddressFollowupContextSignal(userMessage) {
|
||||||
const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
|
||||||
const repaired = repairAddressMojibake(String(userMessage ?? ""));
|
const repaired = repairAddressMojibake(String(userMessage ?? ""));
|
||||||
const repairedText = compactWhitespace(repaired.toLowerCase());
|
const text = compactWhitespace(repaired.toLowerCase());
|
||||||
const samples = [rawText, repairedText].filter((item) => item.length > 0);
|
if (!text) {
|
||||||
if (samples.length === 0) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const hasAny = (pattern) => samples.some((sample) => pattern.test(sample));
|
if (hasStandaloneAddressTopicSignal(text)) {
|
||||||
const hasMarker = () => samples.some((sample) => hasFollowupMarker(sample));
|
|
||||||
const hasPointer = () => samples.some((sample) => hasReferentialPointer(sample));
|
|
||||||
const minTokens = samples.reduce((min, sample) => Math.min(min, countTokens(sample)), Number.POSITIVE_INFINITY);
|
|
||||||
const shortFollowup = minTokens <= 8;
|
|
||||||
const ultraShortFollowup = minTokens <= 3;
|
|
||||||
const debtRoleSwapToReceivables = shortFollowup &&
|
|
||||||
(/^(?:\u0430|a|\u0438|i)\s+(?:\u043d\u0430\u043c\s+)?\u043a\u0442\u043e(?=$|[\s,.;:!?])/iu.test(rawText) ||
|
|
||||||
/^(?:р°|a|рё|i)\s+(?:рЅр°рј\s+)?рєс‚рѕ(?=$|[\s,.;:!?])/iu.test(rawText));
|
|
||||||
if (debtRoleSwapToReceivables) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const debtRoleSwapToPayables = shortFollowup &&
|
|
||||||
(/^(?:\u0430|a|\u0438|i)\s+(?:\u043c\u044b\s+)?\u043a\u043e\u043c\u0443(?=$|[\s,.;:!?])/iu.test(rawText) ||
|
|
||||||
/^(?:р°|a|рё|i)\s+(?:рјс‹\s+)?рєрѕрјсѓ(?=$|[\s,.;:!?])/iu.test(rawText));
|
|
||||||
if (debtRoleSwapToPayables) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const shortContinuationCue = ultraShortFollowup &&
|
|
||||||
(/^(?:\u0434\u0430\u0432\u0430\u0439|\u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0439|\u043f\u043e\u043a\u0430\u0437\u044b\u0432\u044b\u0430\u0439|\u0435\u0449[\u0435\u0451]|also|again|go|ok|okay)(?=$|[\s,.;:!?])/iu.test(rawText) ||
|
|
||||||
/^(?:рґр°рір°р№|рїрѕрєр°р·с‹рір°р№|рїрѕрєр°р·с‹ріс‹р°р№|рµс‰[рµс‘]|also|again|go|ok|okay)(?=$|[\s,.;:!?])/iu.test(rawText));
|
|
||||||
if (shortContinuationCue) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const shortVatCue = ultraShortFollowup &&
|
|
||||||
/^(?:(?:\u0430|\u0438)\s+)?(?:(?:\u043f\u043e|po)\s+)?(?:\u043d\u0434\u0441|vat)(?=$|[\s,.;:!?])/iu.test(rawText);
|
|
||||||
if (shortVatCue) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (shortFollowup && hasAny(/^(?:а|a|и|i)\s+(?:нам\s+)?кто(?=$|[\s,.;:!?])/iu)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (shortFollowup && hasAny(/^(?:а|a|и|i)\s+(?:мы\s+)?кому(?=$|[\s,.;:!?])/iu)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (ultraShortFollowup && hasAny(/^(?:давай|показывай|показывыай|ещ[её]|also|again|go|ok|okay)(?=$|[\s,.;:!?])/iu)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (hasStandaloneAddressTopicSignal(rawText || repairedText)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (shouldHandleAsAssistantCapabilityMetaQuery(rawText || repairedText)) {
|
if (shouldHandleAsAssistantCapabilityMetaQuery(text)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (hasAny(/(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+всю\s+истори(?:ю|и)|за\s+любой\s+период|for\s+all\s+time|all\s+time|for\s+entire\s+period|entire\s+period|for\s+any\s+period|any\s+period)/iu)) {
|
if (/(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+всю\s+истори(?:ю|и)|за\s+любой\s+период|for\s+all\s+time|all\s+time|for\s+entire\s+period|entire\s+period|for\s+any\s+period|any\s+period)/iu.test(text)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (hasPointer()) {
|
if (hasReferentialPointer(text)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (hasAny(/(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|same\s+date|the\s+same\s+date|as\s+of\s+same\s+date)/iu)) {
|
if (/(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|same\s+date|the\s+same\s+date|as\s+of\s+same\s+date)/iu.test(text)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (hasAny(/(?:кроме|помимо)\s+(?:этого|этой|этот|эту|этих|этого\s+документа|этого\s+договора|этого\s+контрагента)/iu)) {
|
const shortFollowup = countTokens(text) <= 8;
|
||||||
|
if (/(?:кроме|помимо)\s+(?:этого|этой|этот|эту|этих|этого\s+документа|этого\s+договора|этого\s+контрагента)/iu.test(text)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (hasAny(/(?:есть\s+ещ[её]|что\s+ещ[её]|ещ[её]\s+что|ещ[её]\s+что-?то|остал(?:ось|ось\?)|друг(?:ое|ие))/iu) && minTokens <= 12) {
|
if (/(?:есть\s+ещ[её]|что\s+ещ[её]|ещ[её]\s+что|ещ[её]\s+что-?то|остал(?:ось|ось\?)|друг(?:ое|ие))/iu.test(text) && countTokens(text) <= 12) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (shortFollowup && hasMarker()) {
|
if (shortFollowup && hasFollowupMarker(text)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (shortFollowup && hasAny(/(?:^|\s)(?:также|тоже|also|same|again|ещ[её]|теперь|then|now)(?=$|[\s,.;:!?])/iu)) {
|
if (shortFollowup && /(?:^|\s)(?:также|тоже|also|same|again|ещ[её]|теперь|then|now)(?=$|[\s,.;:!?])/iu.test(text)) {
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (shortFollowup && hasAny(/(?:кто\s+из\s+(?:них|этих|тех)|кто\s+нов(?:ые|ых|ый)|кто\s+потом\s+исчез|кто\s+был\s+(?:только|ровно)\s+один\s+раз)/iu)) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (shortFollowup &&
|
if (shortFollowup &&
|
||||||
hasAny(/(?:почему|why|из[-\s]?за\s+чего|как\s+так|reason)/iu) &&
|
/(?:кто\s+из\s+(?:них|этих|тех)|кто\s+нов(?:ые|ых|ый)|кто\s+потом\s+исчез|кто\s+был\s+(?:только|ровно)\s+один\s+раз)/iu.test(text)) {
|
||||||
hasAny(/(?:ндс|vat|прогноз|к\s+уплате|нул|ноль|\b0(?:[.,]0+)?\b)/iu)) {
|
return true;
|
||||||
|
}
|
||||||
|
if (shortFollowup && /^(?:а|и)\s+кто\b/iu.test(text)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (shortFollowup &&
|
if (shortFollowup &&
|
||||||
hasAny(/(?:^|\s)по\s+[a-zа-яё][a-zа-яё0-9._-]{1,}(?=$|[\s,.;:!?])/iu) &&
|
/(?:почему|why|из[-\s]?за\s+чего|как\s+так|reason)/iu.test(text) &&
|
||||||
!hasAny(/(?:по\s+этому|по\s+тому|по\s+нему|по\s+ней|по\s+ним)/iu)) {
|
/(?:ндс|vat|прогноз|к\s+уплате|нул|ноль|\b0(?:[.,]0+)?\b)/iu.test(text)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (shortFollowup && samples.some((sample) => hasPeriodLiteral(sample))) {
|
if (shortFollowup &&
|
||||||
|
/(?:^|\s)по\s+[a-zа-яё][a-zа-яё0-9._-]{1,}(?=$|[\s,.;:!?])/iu.test(text) &&
|
||||||
|
!/(?:по\s+этому|по\s+тому|по\s+нему|по\s+ней|по\s+ним)/iu.test(text)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (shortFollowup && hasPeriodLiteral(text)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
function hasShortDebtMirrorFollowupSignal(userMessage) {
|
|
||||||
const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
|
||||||
const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
|
|
||||||
const samples = [rawText, repairedText].filter((item) => item.length > 0);
|
|
||||||
if (samples.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const minTokens = samples.reduce((min, sample) => Math.min(min, countTokens(sample)), Number.POSITIVE_INFINITY);
|
|
||||||
if (minTokens > 8) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return samples.some((sample) => /^(?:а|a|и|i)\s+(?:нам\s+)?кто(?=$|[\s,.;:!?])/iu.test(sample) ||
|
|
||||||
/^(?:а|a|и|i)\s+(?:мы\s+)?кому(?=$|[\s,.;:!?])/iu.test(sample) ||
|
|
||||||
/^(?:р°|a|рё|i)\s+(?:рЅр°рј\s+)?рєс‚рѕ(?=$|[\s,.;:!?])/iu.test(sample) ||
|
|
||||||
/^(?:р°|a|рё|i)\s+(?:рјс‹\s+)?рєрѕрјсѓ(?=$|[\s,.;:!?])/iu.test(sample));
|
|
||||||
}
|
|
||||||
function resolveDebtRoleSwapFollowupIntent(userMessage, previousIntent) {
|
|
||||||
const normalized = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
|
||||||
if (!normalized || countTokens(normalized) > 10) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const hasReceivablesCue = /(?:нам\s+кто\s+долж|кто\s+нам\s+долж|кто\s+долж[а-яё]*\s+нам|дебитор|к\s+получению|к\s+взысканию|receivable)/iu.test(normalized) ||
|
|
||||||
/^(?:а|a|и|i)\s+(?:нам\s+)?кто(?=$|[\s,.;:!?])/iu.test(normalized) ||
|
|
||||||
/^(?:р°|a|рё|i)\s+(?:рЅр°рј\s+)?рєс‚рѕ(?=$|[\s,.;:!?])/iu.test(normalized);
|
|
||||||
const hasPayablesCue = /(?:мы\s+кому\s+долж|кому\s+мы\s+долж|кому\s+долж[а-яё]*\s+мы|кредитор|к\s+уплате|payable)/iu.test(normalized) ||
|
|
||||||
/^(?:а|a|и|i)\s+(?:мы\s+)?кому(?=$|[\s,.;:!?])/iu.test(normalized) ||
|
|
||||||
/^(?:р°|a|рё|i)\s+(?:рјс‹\s+)?рєрѕрјсѓ(?=$|[\s,.;:!?])/iu.test(normalized);
|
|
||||||
if ((previousIntent === "payables_confirmed_as_of_date" || previousIntent === "list_payables_counterparties") &&
|
|
||||||
hasReceivablesCue) {
|
|
||||||
return "receivables_confirmed_as_of_date";
|
|
||||||
}
|
|
||||||
if ((previousIntent === "receivables_confirmed_as_of_date" || previousIntent === "list_receivables_counterparties") &&
|
|
||||||
hasPayablesCue) {
|
|
||||||
return "payables_confirmed_as_of_date";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMessage = null, llmPreDecomposeMeta = null) {
|
function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMessage = null, llmPreDecomposeMeta = null) {
|
||||||
const previousAddressItem = findLastAddressAssistantItem(items);
|
const previousAddressItem = findLastAddressAssistantItem(items);
|
||||||
const previousAddressDebug = previousAddressItem?.debug ?? null;
|
const previousAddressDebug = previousAddressItem?.debug ?? null;
|
||||||
|
|
@ -2650,15 +2578,9 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
Boolean(followupOffer?.enabled) &&
|
Boolean(followupOffer?.enabled) &&
|
||||||
(isImplicitAddressContinuationByLlm(userMessage, llmPreDecomposeMeta) ||
|
(isImplicitAddressContinuationByLlm(userMessage, llmPreDecomposeMeta) ||
|
||||||
(toNonEmptyString(alternateMessage) ? isImplicitAddressContinuationByLlm(alternateMessage, llmPreDecomposeMeta) : false));
|
(toNonEmptyString(alternateMessage) ? isImplicitAddressContinuationByLlm(alternateMessage, llmPreDecomposeMeta) : false));
|
||||||
const sourceIntentHint = toNonEmptyString(previousAddressDebug?.detected_intent);
|
const hasPrimaryFollowupSignal = hasAddressFollowupContextSignal(userMessage);
|
||||||
const debtRoleSwapPrimary = sourceIntentHint ? resolveDebtRoleSwapFollowupIntent(userMessage, sourceIntentHint) : null;
|
|
||||||
const debtRoleSwapAlternate = sourceIntentHint && toNonEmptyString(alternateMessage)
|
|
||||||
? resolveDebtRoleSwapFollowupIntent(String(alternateMessage ?? ""), sourceIntentHint)
|
|
||||||
: null;
|
|
||||||
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
|
|
||||||
const hasPrimaryFollowupSignal = hasAddressFollowupContextSignal(userMessage) || Boolean(debtRoleSwapPrimary);
|
|
||||||
const hasAlternateFollowupSignal = toNonEmptyString(alternateMessage)
|
const hasAlternateFollowupSignal = toNonEmptyString(alternateMessage)
|
||||||
? hasAddressFollowupContextSignal(alternateMessage) || Boolean(debtRoleSwapAlternate)
|
? hasAddressFollowupContextSignal(alternateMessage)
|
||||||
: false;
|
: false;
|
||||||
const hasPrimaryIndexReferenceSignal = extractDisplayedEntityIndexMention(userMessage) !== null;
|
const hasPrimaryIndexReferenceSignal = extractDisplayedEntityIndexMention(userMessage) !== null;
|
||||||
const hasAlternateIndexReferenceSignal = toNonEmptyString(alternateMessage)
|
const hasAlternateIndexReferenceSignal = toNonEmptyString(alternateMessage)
|
||||||
|
|
@ -2667,11 +2589,7 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
const hasIndexReferenceSignal = hasPrimaryIndexReferenceSignal || hasAlternateIndexReferenceSignal;
|
const hasIndexReferenceSignal = hasPrimaryIndexReferenceSignal || hasAlternateIndexReferenceSignal;
|
||||||
const hasStandaloneAddressTopic = hasStandaloneAddressTopicSignal(userMessage) ||
|
const hasStandaloneAddressTopic = hasStandaloneAddressTopicSignal(userMessage) ||
|
||||||
(toNonEmptyString(alternateMessage) ? hasStandaloneAddressTopicSignal(alternateMessage) : false);
|
(toNonEmptyString(alternateMessage) ? hasStandaloneAddressTopicSignal(alternateMessage) : false);
|
||||||
if (hasStandaloneAddressTopic &&
|
if (hasStandaloneAddressTopic && !hasImplicitContinuationSignal && !hasIndexReferenceSignal) {
|
||||||
!hasPrimaryFollowupSignal &&
|
|
||||||
!hasAlternateFollowupSignal &&
|
|
||||||
!hasImplicitContinuationSignal &&
|
|
||||||
!hasIndexReferenceSignal) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!hasPrimaryFollowupSignal && !hasAlternateFollowupSignal && !hasImplicitContinuationSignal && !hasIndexReferenceSignal) {
|
if (!hasPrimaryFollowupSignal && !hasAlternateFollowupSignal && !hasImplicitContinuationSignal && !hasIndexReferenceSignal) {
|
||||||
|
|
@ -2683,9 +2601,6 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
const sourceIntent = toNonEmptyString(previousAddressDebug.detected_intent);
|
const sourceIntent = toNonEmptyString(previousAddressDebug.detected_intent);
|
||||||
let previousIntent = sourceIntent;
|
let previousIntent = sourceIntent;
|
||||||
let followupSelectionMode = "carry_previous_intent";
|
let followupSelectionMode = "carry_previous_intent";
|
||||||
if (debtRoleSwapIntent) {
|
|
||||||
previousIntent = debtRoleSwapIntent;
|
|
||||||
}
|
|
||||||
if (hasImplicitContinuationSignal) {
|
if (hasImplicitContinuationSignal) {
|
||||||
const suggestedIntent = Array.isArray(followupOffer?.suggested_intents)
|
const suggestedIntent = Array.isArray(followupOffer?.suggested_intents)
|
||||||
? toNonEmptyString(followupOffer.suggested_intents[0])
|
? toNonEmptyString(followupOffer.suggested_intents[0])
|
||||||
|
|
@ -3191,13 +3106,6 @@ function hasSameDateAccountFollowupSignalForPredecompose(text) {
|
||||||
/(?:^|\s)по\s+\d{2}(?:[.,]\d{1,2})?(?=$|[\s,.;:!?])/iu.test(source) ||
|
/(?:^|\s)по\s+\d{2}(?:[.,]\d{1,2})?(?=$|[\s,.;:!?])/iu.test(source) ||
|
||||||
/\b\d{2}(?:[.,]\d{1,2})\b/u.test(source));
|
/\b\d{2}(?:[.,]\d{1,2})\b/u.test(source));
|
||||||
}
|
}
|
||||||
function hasPredecomposeDiagnosticUncertaintyLead(text) {
|
|
||||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
|
||||||
if (!normalized) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return /^(?:неясно|не\s+ясно|непонятно|не\s+понятно|unclear|not\s+clear|ambiguous|unknown)(?=$|[\s,.;:!?])/iu.test(normalized);
|
|
||||||
}
|
|
||||||
function attachAddressPredecomposeContract(meta, sourceMessage) {
|
function attachAddressPredecomposeContract(meta, sourceMessage) {
|
||||||
const canonicalMessage = toNonEmptyString(meta?.effectiveMessage) ?? String(sourceMessage ?? "");
|
const canonicalMessage = toNonEmptyString(meta?.effectiveMessage) ?? String(sourceMessage ?? "");
|
||||||
const predecomposeContract = (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({
|
const predecomposeContract = (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({
|
||||||
|
|
@ -3299,20 +3207,6 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
||||||
const candidateIntentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(candidate);
|
const candidateIntentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(candidate);
|
||||||
const sourceIntentKnown = sourceIntentResolution.intent !== "unknown";
|
const sourceIntentKnown = sourceIntentResolution.intent !== "unknown";
|
||||||
const candidateIntentKnown = candidateIntentResolution.intent !== "unknown";
|
const candidateIntentKnown = candidateIntentResolution.intent !== "unknown";
|
||||||
const candidateStartsWithDiagnosticUncertainty = hasPredecomposeDiagnosticUncertaintyLead(candidate);
|
|
||||||
if (candidateStartsWithDiagnosticUncertainty && sourceIntentKnown) {
|
|
||||||
return attachAddressPredecomposeContract({
|
|
||||||
...baseMeta,
|
|
||||||
attempted: true,
|
|
||||||
applied: false,
|
|
||||||
traceId: normalized?.trace_id ?? null,
|
|
||||||
llmCanonicalCandidateDetected: true,
|
|
||||||
effectiveMessage: userMessage,
|
|
||||||
reason: "normalized_fragment_rejected_diagnostic_rewrite",
|
|
||||||
fallbackRuleHit: null,
|
|
||||||
sanitizedUserMessage
|
|
||||||
}, userMessage);
|
|
||||||
}
|
|
||||||
const intentConflict = sourceIntentKnown &&
|
const intentConflict = sourceIntentKnown &&
|
||||||
candidateIntentKnown &&
|
candidateIntentKnown &&
|
||||||
sourceIntentResolution.intent !== candidateIntentResolution.intent;
|
sourceIntentResolution.intent !== candidateIntentResolution.intent;
|
||||||
|
|
@ -3579,8 +3473,6 @@ function resolveAddressToolGateDecision(addressInputMessage, followupContext, ll
|
||||||
isAddressLlmPreDecomposeCandidate(repairedInputMessage) ||
|
isAddressLlmPreDecomposeCandidate(repairedInputMessage) ||
|
||||||
hasAccountingSignal(addressInputMessage) ||
|
hasAccountingSignal(addressInputMessage) ||
|
||||||
hasAccountingSignal(repairedInputMessage) ||
|
hasAccountingSignal(repairedInputMessage) ||
|
||||||
hasShortDebtMirrorFollowupSignal(rawMessageForGate) ||
|
|
||||||
hasShortDebtMirrorFollowupSignal(repairedInputMessage) ||
|
|
||||||
sameDateAccountFollowupSignal;
|
sameDateAccountFollowupSignal;
|
||||||
const hasUnsupportedLowConfidencePredecomposeSignal = llmContractMode === "unsupported" &&
|
const hasUnsupportedLowConfidencePredecomposeSignal = llmContractMode === "unsupported" &&
|
||||||
(llmContractModeConfidence === "low" || llmContractModeConfidence === "medium") &&
|
(llmContractModeConfidence === "low" || llmContractModeConfidence === "medium") &&
|
||||||
|
|
@ -3599,7 +3491,6 @@ function resolveAddressToolGateDecision(addressInputMessage, followupContext, ll
|
||||||
!followupContext &&
|
!followupContext &&
|
||||||
!hasClassifierSignal &&
|
!hasClassifierSignal &&
|
||||||
!hasIntentSignal &&
|
!hasIntentSignal &&
|
||||||
!hasLexicalAddressSignal &&
|
|
||||||
!strongDataSignalFromRawMessage &&
|
!strongDataSignalFromRawMessage &&
|
||||||
!strongDataSignalFromEffectiveMessage) {
|
!strongDataSignalFromEffectiveMessage) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -3792,8 +3683,7 @@ const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
|
||||||
"list_contracts_by_counterparty",
|
"list_contracts_by_counterparty",
|
||||||
"contract_usage_overview",
|
"contract_usage_overview",
|
||||||
"contract_usage_and_value",
|
"contract_usage_and_value",
|
||||||
"vat_payable_forecast",
|
"vat_payable_forecast"
|
||||||
"vat_payable_confirmed_as_of_date"
|
|
||||||
]);
|
]);
|
||||||
export function resolveAssistantOrchestrationDecision(input) {
|
export function resolveAssistantOrchestrationDecision(input) {
|
||||||
const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? "");
|
const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? "");
|
||||||
|
|
@ -3874,11 +3764,7 @@ export function resolveAssistantOrchestrationDecision(input) {
|
||||||
const explicitAddressFollowupSignal = hasAddressFollowupContextSignal(rawUserMessage) ||
|
const explicitAddressFollowupSignal = hasAddressFollowupContextSignal(rawUserMessage) ||
|
||||||
hasAddressFollowupContextSignal(repairedRawUserMessage) ||
|
hasAddressFollowupContextSignal(repairedRawUserMessage) ||
|
||||||
hasAddressFollowupContextSignal(effectiveAddressUserMessage) ||
|
hasAddressFollowupContextSignal(effectiveAddressUserMessage) ||
|
||||||
hasAddressFollowupContextSignal(repairedEffectiveAddressUserMessage) ||
|
hasAddressFollowupContextSignal(repairedEffectiveAddressUserMessage);
|
||||||
hasShortDebtMirrorFollowupSignal(rawUserMessage) ||
|
|
||||||
hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) ||
|
|
||||||
hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) ||
|
|
||||||
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage);
|
|
||||||
const effectiveAddressFollowupSignal = explicitAddressFollowupSignal && !dangerOrCoercionSignal;
|
const effectiveAddressFollowupSignal = explicitAddressFollowupSignal && !dangerOrCoercionSignal;
|
||||||
const deterministicNonDomainGuard = Boolean(!dataScopeMetaQuery &&
|
const deterministicNonDomainGuard = Boolean(!dataScopeMetaQuery &&
|
||||||
!capabilityMetaQuery &&
|
!capabilityMetaQuery &&
|
||||||
|
|
@ -3993,11 +3879,7 @@ export function resolveAssistantOrchestrationDecision(input) {
|
||||||
hasAddressFollowupContextSignal(rawUserMessage) ||
|
hasAddressFollowupContextSignal(rawUserMessage) ||
|
||||||
hasAddressFollowupContextSignal(effectiveAddressUserMessage) ||
|
hasAddressFollowupContextSignal(effectiveAddressUserMessage) ||
|
||||||
hasAddressFollowupContextSignal(repairedRawUserMessage) ||
|
hasAddressFollowupContextSignal(repairedRawUserMessage) ||
|
||||||
hasAddressFollowupContextSignal(repairedEffectiveAddressUserMessage) ||
|
hasAddressFollowupContextSignal(repairedEffectiveAddressUserMessage));
|
||||||
hasShortDebtMirrorFollowupSignal(rawUserMessage) ||
|
|
||||||
hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) ||
|
|
||||||
hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) ||
|
|
||||||
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage));
|
|
||||||
const supportedAddressIntentDetected = !strictDeepInvestigationCueDetected &&
|
const supportedAddressIntentDetected = !strictDeepInvestigationCueDetected &&
|
||||||
Boolean((intentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(intentResolution.intent)) ||
|
Boolean((intentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(intentResolution.intent)) ||
|
||||||
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) ||
|
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) ||
|
||||||
|
|
@ -5014,14 +4896,14 @@ async function resolveAssistantDataScopeProbe() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const catalogQueryCandidates = [
|
const catalogQueryCandidates = [
|
||||||
"ВЫБРАТЬ ПЕРВЫЕ 20 ПРЕДСТАВЛЕН<EFBFBD>?Е(Организации.Ссылка) КАК Организация <20>?З Справочник.Организации КАК Организации",
|
"ВЫБРАТЬ ПЕРВЫЕ 20 ПРЕДСТАВЛЕНИЕ(Организации.Ссылка) КАК Организация ИЗ Справочник.Организации КАК Организации",
|
||||||
"ВЫБРАТЬ ПЕРВЫЕ 20 Организации.Наименование КАК Организация <EFBFBD>?З Справочник.Организации КАК Организации",
|
"ВЫБРАТЬ ПЕРВЫЕ 20 Организации.Наименование КАК Организация ИЗ Справочник.Организации КАК Организации",
|
||||||
"ВЫБРАТЬ ПЕРВЫЕ 20 Организации.НаименованиеПолное КАК Организация <EFBFBD>?З Справочник.Организации КАК Организации",
|
"ВЫБРАТЬ ПЕРВЫЕ 20 Организации.НаименованиеПолное КАК Организация ИЗ Справочник.Организации КАК Организации",
|
||||||
"ВЫБРАТЬ ПЕРВЫЕ 100 Организации.Ссылка КАК Организация, ПРЕДСТАВЛЕН<EFBFBD>?Е(Организации.Ссылка) КАК ОрганизацияПредставление <20>?З Справочник.Организации КАК Организации"
|
"ВЫБРАТЬ ПЕРВЫЕ 100 Организации.Ссылка КАК Организация, ПРЕДСТАВЛЕНИЕ(Организации.Ссылка) КАК ОрганизацияПредставление ИЗ Справочник.Организации КАК Организации"
|
||||||
];
|
];
|
||||||
const movementProbeCandidates = [
|
const movementProbeCandidates = [
|
||||||
"ВЫБРАТЬ ПЕРВЫЕ 60 Движения.Организация КАК Организация, ПРЕДСТАВЛЕН<EFBFBD>?Е(Движения.Организация) КАК ОрганизацияПредставление <EFBFBD>?З РегистрБухгалтерии.Хозрасчетный КАК Движения УПОРЯДОЧ<EFBFBD>?ТЬ ПО Движения.Период УБЫВ",
|
"ВЫБРАТЬ ПЕРВЫЕ 60 Движения.Организация КАК Организация, ПРЕДСТАВЛЕНИЕ(Движения.Организация) КАК ОрганизацияПредставление ИЗ РегистрБухгалтерии.Хозрасчетный КАК Движения УПОРЯДОЧИТЬ ПО Движения.Период УБЫВ",
|
||||||
"ВЫБРАТЬ ПЕРВЫЕ 60 Движения.Организация КАК Организация <EFBFBD>?З РегистрБухгалтерии.Хозрасчетный КАК Движения"
|
"ВЫБРАТЬ ПЕРВЫЕ 60 Движения.Организация КАК Организация ИЗ РегистрБухгалтерии.Хозрасчетный КАК Движения"
|
||||||
];
|
];
|
||||||
let lastError = null;
|
let lastError = null;
|
||||||
const catalogFacts = { names: [], refs: [], pairs: [] };
|
const catalogFacts = { names: [], refs: [], pairs: [] };
|
||||||
|
|
@ -5152,7 +5034,7 @@ function buildAssistantOperationalBoundaryReply() {
|
||||||
return [
|
return [
|
||||||
"Понимаю, что ситуация срочная.",
|
"Понимаю, что ситуация срочная.",
|
||||||
"Я не могу сам настраивать 1С или менять базу/конфигурацию.",
|
"Я не могу сам настраивать 1С или менять базу/конфигурацию.",
|
||||||
"Могу помочь безопасно: разберем симптомы и подготовим точные шаги для вашего 1С/<EFBFBD>?Т-админа."
|
"Могу помочь безопасно: разберем симптомы и подготовим точные шаги для вашего 1С/ИТ-админа."
|
||||||
].join(" ");
|
].join(" ");
|
||||||
}
|
}
|
||||||
function buildAssistantSafetyRefusalReply() {
|
function buildAssistantSafetyRefusalReply() {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ export type AddressIntent =
|
||||||
| "supplier_payouts_profile"
|
| "supplier_payouts_profile"
|
||||||
| "contract_usage_and_value"
|
| "contract_usage_and_value"
|
||||||
| "vat_payable_forecast"
|
| "vat_payable_forecast"
|
||||||
| "vat_payable_confirmed_as_of_date"
|
|
||||||
| "list_contracts_by_counterparty"
|
| "list_contracts_by_counterparty"
|
||||||
| "list_open_contracts"
|
| "list_open_contracts"
|
||||||
| "list_payables_counterparties"
|
| "list_payables_counterparties"
|
||||||
|
|
@ -132,7 +131,6 @@ export interface AddressRecipeDefinition {
|
||||||
| "contract_value_profile"
|
| "contract_value_profile"
|
||||||
| "contracts_by_counterparty_profile"
|
| "contracts_by_counterparty_profile"
|
||||||
| "vat_payable_forecast_profile"
|
| "vat_payable_forecast_profile"
|
||||||
| "vat_payable_confirmed_as_of_balance_profile"
|
|
||||||
| "payables_confirmed_as_of_balance_profile"
|
| "payables_confirmed_as_of_balance_profile"
|
||||||
| "receivables_confirmed_as_of_balance_profile";
|
| "receivables_confirmed_as_of_balance_profile";
|
||||||
required_filters: Array<keyof AddressFilterSet>;
|
required_filters: Array<keyof AddressFilterSet>;
|
||||||
|
|
|
||||||
|
|
@ -24,15 +24,6 @@ describe("address capability policy", () => {
|
||||||
expect(isCapabilityRouteBlocked(decision)).toBe(false);
|
expect(isCapabilityRouteBlocked(decision)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("maps confirmed VAT payable intent to compute exact capability", () => {
|
|
||||||
const decision = resolveAddressCapabilityRouteDecision("vat_payable_confirmed_as_of_date");
|
|
||||||
expect(decision.capability_id).toBe("confirmed_vat_payable_as_of_date");
|
|
||||||
expect(decision.capability_layer).toBe("compute");
|
|
||||||
expect(decision.capability_route_mode).toBe("exact");
|
|
||||||
expect(decision.capability_route_enabled).toBe(true);
|
|
||||||
expect(isCapabilityRouteBlocked(decision)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("maps document drilldown intent to navigation capability", () => {
|
it("maps document drilldown intent to navigation capability", () => {
|
||||||
const decision = resolveAddressCapabilityRouteDecision("list_documents_by_contract");
|
const decision = resolveAddressCapabilityRouteDecision("list_documents_by_contract");
|
||||||
expect(decision.capability_id).toBe("documents_drilldown");
|
expect(decision.capability_id).toBe("documents_drilldown");
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import { resolveAddressIntent } from "../src/services/addressIntentResolver";
|
|
||||||
|
|
||||||
describe("debt lifecycle typo tolerance", () => {
|
|
||||||
it("routes payables typo phrasing to exact confirmed payables intent", () => {
|
|
||||||
const result = resolveAddressIntent("кому мы должэны на май 2021");
|
|
||||||
expect(result.intent).toBe("payables_confirmed_as_of_date");
|
|
||||||
expect(result.reasons).toContain("payables_debt_lifecycle_signal_detected");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("routes receivables typo phrasing to exact confirmed receivables intent", () => {
|
|
||||||
const result = resolveAddressIntent("кто нам должэны на июль 2020");
|
|
||||||
expect(result.intent).toBe("receivables_confirmed_as_of_date");
|
|
||||||
expect(result.reasons).toContain("receivables_debt_lifecycle_signal_detected");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import { executeAddressMcpMetadata, executeAddressMcpQuery } from "../src/services/addressMcpClient";
|
import { executeAddressMcpQuery } from "../src/services/addressMcpClient";
|
||||||
|
|
||||||
const ORIGINAL_FETCH = globalThis.fetch;
|
const ORIGINAL_FETCH = globalThis.fetch;
|
||||||
|
|
||||||
|
|
@ -44,33 +44,4 @@ describe("address MCP encoding repair", () => {
|
||||||
expect(result.rows[0]?.["Контрагент"]).toBe("Группа СВК");
|
expect(result.rows[0]?.["Контрагент"]).toBe("Группа СВК");
|
||||||
expect(result.rows[0]?.["Регистратор"]).toContain("Поступление");
|
expect(result.rows[0]?.["Регистратор"]).toContain("Поступление");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("parses get_metadata text-table payload", async () => {
|
|
||||||
const payload = {
|
|
||||||
success: true,
|
|
||||||
data: `[2]{"FullName","Synonym"}:
|
|
||||||
Document.VATDoc,VAT document
|
|
||||||
Register.VATRegister,VAT register`
|
|
||||||
};
|
|
||||||
|
|
||||||
globalThis.fetch = vi.fn(async () =>
|
|
||||||
new Response(JSON.stringify(payload), {
|
|
||||||
status: 200,
|
|
||||||
headers: {
|
|
||||||
"content-type": "application/json"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
) as typeof fetch;
|
|
||||||
|
|
||||||
const result = await executeAddressMcpMetadata({
|
|
||||||
meta_type: "Документ",
|
|
||||||
name_mask: "ндс",
|
|
||||||
limit: 10
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.error).toBeNull();
|
|
||||||
expect(result.fetched_rows).toBe(2);
|
|
||||||
expect(result.rows[0]?.["FullName"]).toBe("Document.VATDoc");
|
|
||||||
expect(result.rows[0]?.["Synonym"]).toBe("VAT document");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1256,7 +1256,6 @@ describe("address compose stage utf8 headers", () => {
|
||||||
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
||||||
expect(reply.text).toContain("Почему прогноз к уплате 0");
|
expect(reply.text).toContain("Почему прогноз к уплате 0");
|
||||||
expect(reply.text).toContain("max(0, 68 Кт - 68 Дт)");
|
expect(reply.text).toContain("max(0, 68 Кт - 68 Дт)");
|
||||||
expect(reply.text).toContain("Собран прогноз НДС к уплате: 0.00.");
|
|
||||||
expect(reply.text).toContain("За период 68 Кт = 9126.00, 68 Дт = 115342.00, разница = -106216.00.");
|
expect(reply.text).toContain("За период 68 Кт = 9126.00, 68 Дт = 115342.00, разница = -106216.00.");
|
||||||
expect(reply.text).toContain("Разница неположительная");
|
expect(reply.text).toContain("Разница неположительная");
|
||||||
expect(reply.text).toContain("оперативный прогноз по оборотам НДС-субсчетов 68.02*/19*");
|
expect(reply.text).toContain("оперативный прогноз по оборотам НДС-субсчетов 68.02*/19*");
|
||||||
|
|
@ -1284,7 +1283,7 @@ describe("address compose stage utf8 headers", () => {
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
userMessage: "какие сроки уплаты и сдачи декларации по НДС по состоянию на 15 марта 2020 года",
|
userMessage: "сколько НДС нужно заплатить по состоянию на 15 марта 2020 года",
|
||||||
periodFrom: "2020-01-01",
|
periodFrom: "2020-01-01",
|
||||||
periodTo: "2020-03-15"
|
periodTo: "2020-03-15"
|
||||||
}
|
}
|
||||||
|
|
@ -1293,8 +1292,8 @@ describe("address compose stage utf8 headers", () => {
|
||||||
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
||||||
expect(reply.text).toContain("Период расчета (срез обязательств): 01.01.2020..15.03.2020.");
|
expect(reply.text).toContain("Период расчета (срез обязательств): 01.01.2020..15.03.2020.");
|
||||||
expect(reply.text).toContain("Налоговый период: 1 кв. 2020.");
|
expect(reply.text).toContain("Налоговый период: 1 кв. 2020.");
|
||||||
expect(reply.text).toContain("Срок сдачи декларации: до 27.04.2020.");
|
expect(reply.text).toContain("Срок сдачи декларации: до 25.04.2020.");
|
||||||
expect(reply.text).toContain("Сроки уплаты: 28.04.2020, 28.05.2020, 29.06.2020.");
|
expect(reply.text).toContain("Сроки уплаты: 28.04.2020, 28.05.2020, 28.06.2020.");
|
||||||
expect(reply.text).toContain("Ориентир по долям к уплате: 100.00 / 100.00 / 100.00.");
|
expect(reply.text).toContain("Ориентир по долям к уплате: 100.00 / 100.00 / 100.00.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1312,7 +1311,7 @@ describe("address compose stage utf8 headers", () => {
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
userMessage: "когда платить НДС за 4 квартал 2020",
|
userMessage: "прогноз НДС на 31 декабря 2020",
|
||||||
periodFrom: "2020-10-01",
|
periodFrom: "2020-10-01",
|
||||||
periodTo: "2020-12-31"
|
periodTo: "2020-12-31"
|
||||||
}
|
}
|
||||||
|
|
@ -1321,7 +1320,7 @@ describe("address compose stage utf8 headers", () => {
|
||||||
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
||||||
expect(reply.text).toContain("Налоговый период: 4 кв. 2020.");
|
expect(reply.text).toContain("Налоговый период: 4 кв. 2020.");
|
||||||
expect(reply.text).toContain("Срок сдачи декларации: до 25.01.2021.");
|
expect(reply.text).toContain("Срок сдачи декларации: до 25.01.2021.");
|
||||||
expect(reply.text).toContain("Сроки уплаты: 28.01.2021, 01.03.2021, 29.03.2021.");
|
expect(reply.text).toContain("Сроки уплаты: 28.01.2021, 28.02.2021, 28.03.2021.");
|
||||||
expect(reply.text).toContain("Ориентир по долям к уплате: 30.00 / 30.00 / 30.00.");
|
expect(reply.text).toContain("Ориентир по долям к уплате: 30.00 / 30.00 / 30.00.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1370,7 +1369,7 @@ describe("address compose stage utf8 headers", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
||||||
expect(reply.text).toContain("Собран прогноз НДС к уплате: 0.00.");
|
expect(reply.text).toContain("Прогноз НДС к уплате: 0.00.");
|
||||||
expect(reply.text).toContain("не найдено движений по НДС-субсчетам 68.02*/19*");
|
expect(reply.text).toContain("не найдено движений по НДС-субсчетам 68.02*/19*");
|
||||||
expect(reply.text).toContain("Чеклист проверки в 1С (почему к уплате 0):");
|
expect(reply.text).toContain("Чеклист проверки в 1С (почему к уплате 0):");
|
||||||
expect(reply.text).toContain("Проверьте наличие движений в РегистрБухгалтерии.Хозрасчетный");
|
expect(reply.text).toContain("Проверьте наличие движений в РегистрБухгалтерии.Хозрасчетный");
|
||||||
|
|
@ -1405,160 +1404,10 @@ describe("address compose stage utf8 headers", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
||||||
expect(reply.text).toContain("Собран прогноз НДС к уплате: 0.00.");
|
expect(reply.text).toContain("Прогноз НДС к уплате: 0.00.");
|
||||||
expect(reply.text).toContain("обороты по 68* взаимно перекрылись");
|
expect(reply.text).toContain("обороты по 68* взаимно перекрылись");
|
||||||
expect(reply.text).toContain("Чеклист проверки в 1С (почему к уплате 0):");
|
expect(reply.text).toContain("Чеклист проверки в 1С (почему к уплате 0):");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("adds MCP VAT source coverage block for VAT forecast response", () => {
|
|
||||||
const reply = composeFactualReply(
|
|
||||||
"vat_payable_forecast",
|
|
||||||
[
|
|
||||||
{
|
|
||||||
period: "2019-12-31T23:59:59Z",
|
|
||||||
registrator: "VAT_68_CREDIT",
|
|
||||||
account_dt: "68",
|
|
||||||
account_kt: "",
|
|
||||||
amount: 1000,
|
|
||||||
analytics: []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
{
|
|
||||||
userMessage: "прикинь ндс за декабрь 2019",
|
|
||||||
periodFrom: "2019-12-01",
|
|
||||||
periodTo: "2019-12-31",
|
|
||||||
vatDirectSourceProbe: {
|
|
||||||
status: "ok",
|
|
||||||
objectsTotal: 5,
|
|
||||||
documentsTotal: 2,
|
|
||||||
registersTotal: 3,
|
|
||||||
probedSources: [
|
|
||||||
{
|
|
||||||
fullName: "РегистрНакопления.НДСПродажи",
|
|
||||||
objectType: "register",
|
|
||||||
status: "ok",
|
|
||||||
rowsFetched: 1,
|
|
||||||
lastPeriod: "2019-12-31T23:59:59Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fullName: "РегистрНакопления.НДСПокупки",
|
|
||||||
objectType: "register",
|
|
||||||
status: "empty",
|
|
||||||
rowsFetched: 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
errors: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
|
||||||
expect(reply.text).toContain("Покрытие VAT-источников через MCP");
|
|
||||||
expect(reply.text).toContain("Найдено VAT-объектов: 5");
|
|
||||||
expect(reply.text).toContain("РегистрНакопления.НДСПродажи");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("formats VAT forecast amounts in rubles and emphasizes numbers when requested", () => {
|
|
||||||
const reply = composeFactualReply(
|
|
||||||
"vat_payable_forecast",
|
|
||||||
[
|
|
||||||
{
|
|
||||||
period: "2019-12-31T23:59:59Z",
|
|
||||||
registrator: "VAT_68_CREDIT",
|
|
||||||
account_dt: "68",
|
|
||||||
account_kt: "",
|
|
||||||
amount: 1234567.89,
|
|
||||||
analytics: []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
{
|
|
||||||
userMessage: "прикинь ндс",
|
|
||||||
useRubCurrency: true,
|
|
||||||
emphasizeNumbers: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(reply.text).toContain("**1.234.567,89** ₽");
|
|
||||||
expect(reply.text).toContain("Собран прогноз НДС к уплате:");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("adds MCP VAT source probe block for confirmed VAT as-of response", () => {
|
|
||||||
const reply = composeFactualReply(
|
|
||||||
"vat_payable_confirmed_as_of_date",
|
|
||||||
[
|
|
||||||
{
|
|
||||||
period: "2020-03-31T23:59:59Z",
|
|
||||||
registrator: "Остатки на дату",
|
|
||||||
account_dt: null,
|
|
||||||
account_kt: "68.02",
|
|
||||||
amount: 123456.78,
|
|
||||||
analytics: []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
{
|
|
||||||
asOfDate: "2020-03-31",
|
|
||||||
vatDirectSourceProbe: {
|
|
||||||
status: "ok",
|
|
||||||
objectsTotal: 3,
|
|
||||||
documentsTotal: 1,
|
|
||||||
registersTotal: 2,
|
|
||||||
probedSources: [
|
|
||||||
{
|
|
||||||
fullName: "РегистрНакопления.НДСНачисленный",
|
|
||||||
objectType: "register",
|
|
||||||
status: "ok",
|
|
||||||
rowsFetched: 1,
|
|
||||||
lastPeriod: "2020-03-31T23:59:59Z",
|
|
||||||
sampleRegistrator: "Отражение начисления НДС 0001"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fullName: "Документ.РегистрацияОплатыНДСВБюджет",
|
|
||||||
objectType: "document",
|
|
||||||
status: "empty",
|
|
||||||
rowsFetched: 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
errors: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(reply.responseType).toBe("FACTUAL_LIST");
|
|
||||||
expect(reply.text).toContain("Блок 2.1. MCP-проверка VAT-источников");
|
|
||||||
expect(reply.text).toContain("VAT-объектов в метаданных 1С: 3");
|
|
||||||
expect(reply.text).toContain("Источников с движениями до даты среза: 1");
|
|
||||||
expect(reply.text).toContain("РегистрНакопления.НДСНачисленный");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("adds VAT probe error note for confirmed VAT as-of response", () => {
|
|
||||||
const reply = composeFactualReply(
|
|
||||||
"vat_payable_confirmed_as_of_date",
|
|
||||||
[
|
|
||||||
{
|
|
||||||
period: "2020-03-31T23:59:59Z",
|
|
||||||
registrator: "Остатки на дату",
|
|
||||||
account_dt: null,
|
|
||||||
account_kt: "68.02",
|
|
||||||
amount: 1000,
|
|
||||||
analytics: []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
{
|
|
||||||
asOfDate: "2020-03-31",
|
|
||||||
vatDirectSourceProbe: {
|
|
||||||
status: "error",
|
|
||||||
objectsTotal: 0,
|
|
||||||
documentsTotal: 0,
|
|
||||||
registersTotal: 0,
|
|
||||||
probedSources: [],
|
|
||||||
errors: ["metadata timeout"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(reply.responseType).toBe("FACTUAL_LIST");
|
|
||||||
expect(reply.text).toContain("Probe VAT-источников завершился ошибкой");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("address intent resolver expansion (M2.3a)", () => {
|
describe("address intent resolver expansion (M2.3a)", () => {
|
||||||
|
|
@ -2006,12 +1855,6 @@ describe("address intent resolver expansion (M2.3a)", () => {
|
||||||
expect(result.reasons).toContain("payables_debt_lifecycle_signal_detected");
|
expect(result.reasons).toContain("payables_debt_lifecycle_signal_detected");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resolves repair phrasing 'кто нам в целом должен' as receivables debt lifecycle intent", () => {
|
|
||||||
const result = resolveAddressIntent("нет вопрос кто нам в целом должен на денег на эту дату");
|
|
||||||
expect(result.intent).toBe("receivables_confirmed_as_of_date");
|
|
||||||
expect(result.reasons).toContain("receivables_debt_lifecycle_signal_detected");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps out-of-scope supplier control wording as unknown intent", () => {
|
it("keeps out-of-scope supplier control wording as unknown intent", () => {
|
||||||
const result = resolveAddressIntent(
|
const result = resolveAddressIntent(
|
||||||
"Какие поставщики у нас уже пару месяцев сдают акты без приходок. Может, их надо проконтролировать отдельно чтоб не засорять бухгалтерию дальше?"
|
"Какие поставщики у нас уже пару месяцев сдают акты без приходок. Может, их надо проконтролировать отдельно чтоб не засорять бухгалтерию дальше?"
|
||||||
|
|
@ -2179,15 +2022,6 @@ describe("address filter extraction for balance drilldown", () => {
|
||||||
expect(extracted.warnings).toContain("counterparty_anchor_dropped_low_quality");
|
expect(extracted.warnings).toContain("counterparty_anchor_dropped_low_quality");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("drops pseudo-counterparty 'деньги на данную дату' from diagnostic rewrite phrase", () => {
|
|
||||||
const extracted = extractAddressFilters(
|
|
||||||
"Неясно, кто должен компании деньги на данную дату.",
|
|
||||||
"unknown"
|
|
||||||
);
|
|
||||||
expect(extracted.extracted_filters.counterparty).toBeUndefined();
|
|
||||||
expect(extracted.warnings).toContain("counterparty_anchor_dropped_low_quality");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not capture narrative filler as counterparty in broad docs-vs-money question", () => {
|
it("does not capture narrative filler as counterparty in broad docs-vs-money question", () => {
|
||||||
const extracted = extractAddressFilters(
|
const extracted = extractAddressFilters(
|
||||||
"В каких случаях мы видим ситуацию, когда документы есть, а денег нет и пока не предвидится?",
|
"В каких случаях мы видим ситуацию, когда документы есть, а денег нет и пока не предвидится?",
|
||||||
|
|
@ -3409,26 +3243,6 @@ describe("address decompose stage follow-up carryover", () => {
|
||||||
expect(result?.baseReasons).toContain("address_followup_context_applied");
|
expect(result?.baseReasons).toContain("address_followup_context_applied");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("inherits as_of_date for receivables follow-up without explicit period", () => {
|
|
||||||
const result = runAddressDecomposeStage("\u0430 \u043d\u0430\u043c \u043a\u0442\u043e \u0434\u043e\u043b\u0436\u0435\u043d?.", {
|
|
||||||
previous_intent: "receivables_confirmed_as_of_date",
|
|
||||||
previous_filters: {
|
|
||||||
period_from: "2017-09-01",
|
|
||||||
period_to: "2017-09-30",
|
|
||||||
as_of_date: "2017-09-30"
|
|
||||||
},
|
|
||||||
previous_anchor_type: "unknown",
|
|
||||||
previous_anchor_value: null
|
|
||||||
});
|
|
||||||
expect(result).not.toBeNull();
|
|
||||||
expect(result?.intent.intent).toBe("receivables_confirmed_as_of_date");
|
|
||||||
expect(result?.filters.extracted_filters.period_from).toBe("2017-09-01");
|
|
||||||
expect(result?.filters.extracted_filters.period_to).toBe("2017-09-30");
|
|
||||||
expect(result?.filters.extracted_filters.as_of_date).toBe("2017-09-30");
|
|
||||||
expect(result?.baseReasons).toContain("as_of_date_from_followup_context");
|
|
||||||
expect(result?.baseReasons).toContain("address_followup_context_applied");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps contract scope when follow-up asks for bank operations without explicit anchor", () => {
|
it("keeps contract scope when follow-up asks for bank operations without explicit anchor", () => {
|
||||||
const result = runAddressDecomposeStage("а теперь банковские операции", {
|
const result = runAddressDecomposeStage("а теперь банковские операции", {
|
||||||
previous_intent: "list_documents_by_contract",
|
previous_intent: "list_documents_by_contract",
|
||||||
|
|
@ -3601,27 +3415,6 @@ describe("address decompose stage follow-up carryover", () => {
|
||||||
result?.baseReasons?.includes("intent_from_followup_context")
|
result?.baseReasons?.includes("intent_from_followup_context")
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("promotes short 'а ндс?' follow-up to confirmed VAT intent with inherited as-of date", () => {
|
|
||||||
const result = runAddressDecomposeStage("\u0430 \u043d\u0434\u0441?", {
|
|
||||||
previous_intent: "payables_confirmed_as_of_date",
|
|
||||||
previous_filters: {
|
|
||||||
period_from: "2017-09-01",
|
|
||||||
period_to: "2017-09-30",
|
|
||||||
as_of_date: "2017-09-30"
|
|
||||||
},
|
|
||||||
previous_anchor_type: "unknown",
|
|
||||||
previous_anchor_value: null
|
|
||||||
});
|
|
||||||
expect(result).not.toBeNull();
|
|
||||||
expect(result?.mode.mode).toBe("address_query");
|
|
||||||
expect(result?.intent.intent).toBe("vat_payable_confirmed_as_of_date");
|
|
||||||
expect(result?.filters.extracted_filters.as_of_date).toBe("2017-09-30");
|
|
||||||
expect(result?.filters.extracted_filters.period_from).toBe("2017-09-01");
|
|
||||||
expect(result?.filters.extracted_filters.period_to).toBe("2017-09-30");
|
|
||||||
expect(result?.baseReasons).toContain("intent_adjusted_to_vat_followup_context");
|
|
||||||
expect(result?.baseReasons).toContain("as_of_date_from_followup_context");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("address recipe catalog counterparty filtering", () => {
|
describe("address recipe catalog counterparty filtering", () => {
|
||||||
|
|
|
||||||
|
|
@ -7,36 +7,12 @@ import { evaluateAddressRouteExpectation } from "../src/services/addressRouteExp
|
||||||
import { AddressQueryService } from "../src/services/addressQueryService";
|
import { AddressQueryService } from "../src/services/addressQueryService";
|
||||||
|
|
||||||
describe("receivables confirmed as-of route", () => {
|
describe("receivables confirmed as-of route", () => {
|
||||||
it("routes canonical debtor phrasing into exact receivables intent", () => {
|
|
||||||
const result = resolveAddressIntent("кто является дебитором компании по состоянию на июль 2020 года");
|
|
||||||
expect(result.intent).toBe("receivables_confirmed_as_of_date");
|
|
||||||
expect(result.reasons).toContain("receivables_debt_lifecycle_signal_detected");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps exact receivables route for canonical debtor phrasing in runtime", async () => {
|
|
||||||
const service = new AddressQueryService();
|
|
||||||
const result = await service.tryHandle("кто является дебитором компании по состоянию на июль 2020 года");
|
|
||||||
expect(result?.handled).toBe(true);
|
|
||||||
expect(result?.debug.detected_intent).toBe("receivables_confirmed_as_of_date");
|
|
||||||
expect(result?.debug.selected_recipe).toBe("address_receivables_confirmed_as_of_date_v1");
|
|
||||||
expect(result?.debug.requested_result_mode).toBe("confirmed_balance");
|
|
||||||
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("routes 'кто нам должен' wording into exact receivables intent", () => {
|
it("routes 'кто нам должен' wording into exact receivables intent", () => {
|
||||||
const result = resolveAddressIntent("кто нам должен на июль 2020");
|
const result = resolveAddressIntent("кто нам должен на июль 2020");
|
||||||
expect(result.intent).toBe("receivables_confirmed_as_of_date");
|
expect(result.intent).toBe("receivables_confirmed_as_of_date");
|
||||||
expect(result.reasons).toContain("receivables_debt_lifecycle_signal_detected");
|
expect(result.reasons).toContain("receivables_debt_lifecycle_signal_detected");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("drops low-quality counterparty anchor from as-of debtor phrasing", () => {
|
|
||||||
const extracted = extractAddressFilters(
|
|
||||||
"кто является дебитором компании по состоянию на июль 2020 года",
|
|
||||||
"receivables_confirmed_as_of_date"
|
|
||||||
);
|
|
||||||
expect(extracted.extracted_filters.counterparty).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("selects confirmed receivables recipe and builds balance query", () => {
|
it("selects confirmed receivables recipe and builds balance query", () => {
|
||||||
const filters = extractAddressFilters("кто нам должен на июль 2020", "receivables_confirmed_as_of_date").extracted_filters;
|
const filters = extractAddressFilters("кто нам должен на июль 2020", "receivables_confirmed_as_of_date").extracted_filters;
|
||||||
const selected = selectAddressRecipe("receivables_confirmed_as_of_date", filters);
|
const selected = selectAddressRecipe("receivables_confirmed_as_of_date", filters);
|
||||||
|
|
|
||||||
|
|
@ -34,17 +34,6 @@ describe("address route expectations contract", () => {
|
||||||
expect(audit.reason).toBe("route_expectation_matched");
|
expect(audit.reason).toBe("route_expectation_matched");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("matches expected recipe and result mode for exact VAT payable route", () => {
|
|
||||||
const audit = evaluateAddressRouteExpectation({
|
|
||||||
intent: "vat_payable_confirmed_as_of_date",
|
|
||||||
selectedRecipe: "address_vat_payable_confirmed_as_of_date_v1",
|
|
||||||
requestedResultMode: "confirmed_balance",
|
|
||||||
resultMode: "confirmed_balance"
|
|
||||||
});
|
|
||||||
expect(audit.status).toBe("matched");
|
|
||||||
expect(audit.reason).toBe("route_expectation_matched");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("detects selected recipe mismatch", () => {
|
it("detects selected recipe mismatch", () => {
|
||||||
const audit = evaluateAddressRouteExpectation({
|
const audit = evaluateAddressRouteExpectation({
|
||||||
intent: "payables_confirmed_as_of_date",
|
intent: "payables_confirmed_as_of_date",
|
||||||
|
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import { resolveAddressIntent } from "../src/services/addressIntentResolver";
|
|
||||||
import { extractAddressFilters } from "../src/services/addressFilterExtractor";
|
|
||||||
import { buildAddressRecipePlan, selectAddressRecipe } from "../src/services/addressRecipeCatalog";
|
|
||||||
import { resolveAddressCapabilityRouteDecision } from "../src/services/addressCapabilityPolicy";
|
|
||||||
import { evaluateAddressRouteExpectation } from "../src/services/addressRouteExpectations";
|
|
||||||
import { AddressQueryService } from "../src/services/addressQueryService";
|
|
||||||
|
|
||||||
describe("vat payable confirmed as-of route", () => {
|
|
||||||
it("routes VAT payable question into exact confirmed intent", () => {
|
|
||||||
const result = resolveAddressIntent("сколько НДС к уплате на март 2020");
|
|
||||||
expect(result.intent).toBe("vat_payable_confirmed_as_of_date");
|
|
||||||
expect(result.reasons).toContain("vat_payable_confirmed_signal_detected");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps VAT forecast intent when explicit forecast wording is used", () => {
|
|
||||||
const result = resolveAddressIntent("какой прогноз оплаты ндс на март 2020");
|
|
||||||
expect(result.intent).toBe("vat_payable_forecast");
|
|
||||||
expect(result.reasons).toContain("forecast_tax_signal_detected");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("derives as_of_date for confirmed VAT route from period boundary", () => {
|
|
||||||
const extracted = extractAddressFilters("сколько НДС к уплате на март 2020", "vat_payable_confirmed_as_of_date");
|
|
||||||
expect(extracted.extracted_filters.period_from).toBe("2020-03-01");
|
|
||||||
expect(extracted.extracted_filters.period_to).toBe("2020-03-31");
|
|
||||||
expect(extracted.extracted_filters.as_of_date).toBe("2020-03-31");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("selects confirmed VAT recipe and builds balance query", () => {
|
|
||||||
const filters = extractAddressFilters("сколько НДС к уплате на март 2020", "vat_payable_confirmed_as_of_date").extracted_filters;
|
|
||||||
const selected = selectAddressRecipe("vat_payable_confirmed_as_of_date", filters);
|
|
||||||
expect(selected.selected_recipe?.recipe_id).toBe("address_vat_payable_confirmed_as_of_date_v1");
|
|
||||||
|
|
||||||
const plan = buildAddressRecipePlan(selected.selected_recipe!, filters);
|
|
||||||
expect(plan.query).toContain("РегистрБухгалтерии.Хозрасчетный.Остатки");
|
|
||||||
expect(plan.query).toContain("СуммаРазвернутыйОстатокКт");
|
|
||||||
expect(plan.query).toContain("Остатки.Счет");
|
|
||||||
expect(plan.query).toContain("68.02");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("exposes compute exact capability and route expectation for confirmed VAT route", () => {
|
|
||||||
const capability = resolveAddressCapabilityRouteDecision("vat_payable_confirmed_as_of_date");
|
|
||||||
expect(capability.capability_id).toBe("confirmed_vat_payable_as_of_date");
|
|
||||||
expect(capability.capability_layer).toBe("compute");
|
|
||||||
expect(capability.capability_route_mode).toBe("exact");
|
|
||||||
|
|
||||||
const expectation = evaluateAddressRouteExpectation({
|
|
||||||
intent: "vat_payable_confirmed_as_of_date",
|
|
||||||
selectedRecipe: "address_vat_payable_confirmed_as_of_date_v1",
|
|
||||||
requestedResultMode: "confirmed_balance",
|
|
||||||
resultMode: "confirmed_balance"
|
|
||||||
});
|
|
||||||
expect(expectation.status).toBe("matched");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses exact VAT route in runtime for monthly as-of query", async () => {
|
|
||||||
const service = new AddressQueryService();
|
|
||||||
const result = await service.tryHandle("сколько НДС к уплате на март 2020");
|
|
||||||
expect(result?.handled).toBe(true);
|
|
||||||
expect(result?.debug.detected_intent).toBe("vat_payable_confirmed_as_of_date");
|
|
||||||
expect(result?.debug.selected_recipe).toBe("address_vat_payable_confirmed_as_of_date_v1");
|
|
||||||
expect(result?.debug.requested_result_mode).toBe("confirmed_balance");
|
|
||||||
expect(result?.debug.route_expectation_status).toBe("matched");
|
|
||||||
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1081,340 +1081,6 @@ describe("assistant address follow-up carryover", () => {
|
||||||
expect(String(calls[0].message).toLowerCase()).toContain("свк");
|
expect(String(calls[0].message).toLowerCase()).toContain("свк");
|
||||||
expect(chatClient.chat).toHaveBeenCalledTimes(0);
|
expect(chatClient.chat).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
it("keeps debt lifecycle follow-up context for 'а нам кто должен?.' after payables as-of answer", async () => {
|
|
||||||
const calls: Array<{ message: string; options?: any }> = [];
|
|
||||||
const firstMessage =
|
|
||||||
"\u043a\u043e\u043c\u0443 \u043c\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430 \u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c 2017";
|
|
||||||
const followupMessage =
|
|
||||||
"\u0430 \u043d\u0430\u043c \u043a\u0442\u043e \u0434\u043e\u043b\u0436\u0435\u043d?.";
|
|
||||||
|
|
||||||
const payablesResult = buildAddressLaneResult({
|
|
||||||
reply_text:
|
|
||||||
"\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043d\u044b\u0439 \u0441\u0440\u0435\u0437 \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u0441\u0442\u0432 \u043a \u043e\u043f\u043b\u0430\u0442\u0435 \u043d\u0430 30.09.2017",
|
|
||||||
debug: {
|
|
||||||
...buildAddressLaneResult().debug,
|
|
||||||
query_shape: "UNKNOWN",
|
|
||||||
query_shape_confidence: "low",
|
|
||||||
detected_intent: "payables_confirmed_as_of_date",
|
|
||||||
detected_intent_confidence: "high",
|
|
||||||
extracted_filters: {
|
|
||||||
sort: "period_desc",
|
|
||||||
limit: 20,
|
|
||||||
period_from: "2017-09-01",
|
|
||||||
period_to: "2017-09-30",
|
|
||||||
as_of_date: "2017-09-30"
|
|
||||||
},
|
|
||||||
selected_recipe: "address_payables_confirmed_as_of_date_v1",
|
|
||||||
response_type: "FACTUAL_LIST",
|
|
||||||
requested_result_mode: "confirmed_balance",
|
|
||||||
result_mode: "confirmed_balance",
|
|
||||||
balance_confirmed: true,
|
|
||||||
reasons: ["payables_debt_lifecycle_signal_detected", "confirmed_balance_exact_payables_intent"]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const receivablesResult = buildAddressLaneResult({
|
|
||||||
reply_text:
|
|
||||||
"\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043d\u044b\u0439 \u0441\u0440\u0435\u0437 \u0434\u0435\u0431\u0438\u0442\u043e\u0440\u0441\u043a\u043e\u0439 \u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u0438 \u043d\u0430 30.09.2017",
|
|
||||||
debug: {
|
|
||||||
...buildAddressLaneResult().debug,
|
|
||||||
query_shape: "UNKNOWN",
|
|
||||||
query_shape_confidence: "low",
|
|
||||||
detected_intent: "receivables_confirmed_as_of_date",
|
|
||||||
detected_intent_confidence: "high",
|
|
||||||
extracted_filters: {
|
|
||||||
sort: "period_desc",
|
|
||||||
limit: 20,
|
|
||||||
period_from: "2017-09-01",
|
|
||||||
period_to: "2017-09-30",
|
|
||||||
as_of_date: "2017-09-30"
|
|
||||||
},
|
|
||||||
selected_recipe: "address_receivables_confirmed_as_of_date_v1",
|
|
||||||
response_type: "FACTUAL_LIST",
|
|
||||||
requested_result_mode: "confirmed_balance",
|
|
||||||
result_mode: "confirmed_balance",
|
|
||||||
balance_confirmed: true,
|
|
||||||
reasons: ["receivables_debt_lifecycle_signal_detected", "confirmed_balance_exact_receivables_intent"]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const addressQueryService = {
|
|
||||||
tryHandle: vi.fn(async (message: string, options?: any) => {
|
|
||||||
calls.push({ message, options });
|
|
||||||
if (message === firstMessage) {
|
|
||||||
return payablesResult;
|
|
||||||
}
|
|
||||||
if (message === followupMessage) {
|
|
||||||
if (!options?.followupContext) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return receivablesResult;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
const normalizerService = {
|
|
||||||
normalize: vi.fn(async () => ({
|
|
||||||
assistant_reply: "normalizer_fallback_should_not_be_used",
|
|
||||||
reply_type: "partial_coverage",
|
|
||||||
debug: {}
|
|
||||||
}))
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
const sessions = new AssistantSessionStore();
|
|
||||||
const service = new AssistantService(
|
|
||||||
normalizerService,
|
|
||||||
sessions as any,
|
|
||||||
{} as any,
|
|
||||||
{ persistSession: vi.fn() } as any,
|
|
||||||
addressQueryService
|
|
||||||
);
|
|
||||||
|
|
||||||
const sessionId = `asst-address-followup-debt-${Date.now()}`;
|
|
||||||
const first = await service.handleMessage({
|
|
||||||
session_id: sessionId,
|
|
||||||
user_message: firstMessage,
|
|
||||||
useMock: true
|
|
||||||
} as any);
|
|
||||||
expect(first.ok).toBe(true);
|
|
||||||
expect(first.reply_type).toBe("factual");
|
|
||||||
|
|
||||||
const second = await service.handleMessage({
|
|
||||||
session_id: sessionId,
|
|
||||||
user_message: followupMessage,
|
|
||||||
useMock: true
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
expect(second.ok).toBe(true);
|
|
||||||
expect(second.reply_type).toBe("factual");
|
|
||||||
expect(second.debug?.detected_intent).toBe("receivables_confirmed_as_of_date");
|
|
||||||
expect(second.debug?.selected_recipe).toBe("address_receivables_confirmed_as_of_date_v1");
|
|
||||||
|
|
||||||
expect(calls).toHaveLength(2);
|
|
||||||
expect(calls[1].message).toBe(followupMessage);
|
|
||||||
expect(calls[1].options?.followupContext?.previous_intent).toBe("receivables_confirmed_as_of_date");
|
|
||||||
expect(calls[1].options?.followupContext?.previous_filters?.as_of_date).toBe("2017-09-30");
|
|
||||||
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("mirrors receivables->payables for short follow-up 'a мы кому' and keeps as-of date", async () => {
|
|
||||||
const calls: Array<{ message: string; options?: any }> = [];
|
|
||||||
const firstMessage = "кто нам должен на сентябрь 2017";
|
|
||||||
const followupMessage = "a мы кому";
|
|
||||||
|
|
||||||
const receivablesResult = buildAddressLaneResult({
|
|
||||||
reply_text: "Подтвержденный срез дебиторской задолженности на 30.09.2017",
|
|
||||||
debug: {
|
|
||||||
...buildAddressLaneResult().debug,
|
|
||||||
query_shape: "UNKNOWN",
|
|
||||||
query_shape_confidence: "low",
|
|
||||||
detected_intent: "receivables_confirmed_as_of_date",
|
|
||||||
detected_intent_confidence: "high",
|
|
||||||
extracted_filters: {
|
|
||||||
sort: "period_desc",
|
|
||||||
limit: 20,
|
|
||||||
period_from: "2017-09-01",
|
|
||||||
period_to: "2017-09-30",
|
|
||||||
as_of_date: "2017-09-30"
|
|
||||||
},
|
|
||||||
selected_recipe: "address_receivables_confirmed_as_of_date_v1",
|
|
||||||
response_type: "FACTUAL_LIST",
|
|
||||||
requested_result_mode: "confirmed_balance",
|
|
||||||
result_mode: "confirmed_balance",
|
|
||||||
balance_confirmed: true,
|
|
||||||
reasons: ["receivables_debt_lifecycle_signal_detected", "confirmed_balance_exact_receivables_intent"]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const payablesResult = buildAddressLaneResult({
|
|
||||||
reply_text: "Подтвержденный срез обязательств к оплате на 30.09.2017",
|
|
||||||
debug: {
|
|
||||||
...buildAddressLaneResult().debug,
|
|
||||||
query_shape: "UNKNOWN",
|
|
||||||
query_shape_confidence: "low",
|
|
||||||
detected_intent: "payables_confirmed_as_of_date",
|
|
||||||
detected_intent_confidence: "high",
|
|
||||||
extracted_filters: {
|
|
||||||
sort: "period_desc",
|
|
||||||
limit: 20,
|
|
||||||
period_from: "2017-09-01",
|
|
||||||
period_to: "2017-09-30",
|
|
||||||
as_of_date: "2017-09-30"
|
|
||||||
},
|
|
||||||
selected_recipe: "address_payables_confirmed_as_of_date_v1",
|
|
||||||
response_type: "FACTUAL_LIST",
|
|
||||||
requested_result_mode: "confirmed_balance",
|
|
||||||
result_mode: "confirmed_balance",
|
|
||||||
balance_confirmed: true,
|
|
||||||
reasons: ["payables_debt_lifecycle_signal_detected", "confirmed_balance_exact_payables_intent"]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const addressQueryService = {
|
|
||||||
tryHandle: vi.fn(async (message: string, options?: any) => {
|
|
||||||
calls.push({ message, options });
|
|
||||||
if (message === firstMessage) {
|
|
||||||
return receivablesResult;
|
|
||||||
}
|
|
||||||
if (message === followupMessage) {
|
|
||||||
if (!options?.followupContext) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (options?.followupContext?.previous_intent !== "payables_confirmed_as_of_date") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return payablesResult;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
const normalizerService = {
|
|
||||||
normalize: vi.fn(async () => ({
|
|
||||||
assistant_reply: "normalizer_fallback_should_not_be_used",
|
|
||||||
reply_type: "partial_coverage",
|
|
||||||
debug: {}
|
|
||||||
}))
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
const sessions = new AssistantSessionStore();
|
|
||||||
const service = new AssistantService(
|
|
||||||
normalizerService,
|
|
||||||
sessions as any,
|
|
||||||
{} as any,
|
|
||||||
{ persistSession: vi.fn() } as any,
|
|
||||||
addressQueryService
|
|
||||||
);
|
|
||||||
|
|
||||||
const sessionId = `asst-address-followup-debt-mirror-${Date.now()}`;
|
|
||||||
const first = await service.handleMessage({
|
|
||||||
session_id: sessionId,
|
|
||||||
user_message: firstMessage,
|
|
||||||
useMock: true
|
|
||||||
} as any);
|
|
||||||
expect(first.ok).toBe(true);
|
|
||||||
expect(first.reply_type).toBe("factual");
|
|
||||||
|
|
||||||
const second = await service.handleMessage({
|
|
||||||
session_id: sessionId,
|
|
||||||
user_message: followupMessage,
|
|
||||||
useMock: true
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
expect(second.ok).toBe(true);
|
|
||||||
expect(second.reply_type).toBe("factual");
|
|
||||||
expect(second.debug?.detected_intent).toBe("payables_confirmed_as_of_date");
|
|
||||||
expect(second.debug?.selected_recipe).toBe("address_payables_confirmed_as_of_date_v1");
|
|
||||||
|
|
||||||
expect(calls).toHaveLength(2);
|
|
||||||
expect(calls[1].message).toBe(followupMessage);
|
|
||||||
expect(calls[1].options?.followupContext?.previous_intent).toBe("payables_confirmed_as_of_date");
|
|
||||||
expect(calls[1].options?.followupContext?.previous_filters?.as_of_date).toBe("2017-09-30");
|
|
||||||
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps short VAT follow-up in address lane after debt as-of answer", async () => {
|
|
||||||
const calls: Array<{ message: string; options?: any }> = [];
|
|
||||||
const firstMessage =
|
|
||||||
"\u043a\u043e\u043c\u0443 \u043c\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430 \u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c 2017";
|
|
||||||
const followupMessage = "\u0430 \u043d\u0434\u0441?";
|
|
||||||
|
|
||||||
const payablesResult = buildAddressLaneResult({
|
|
||||||
debug: {
|
|
||||||
...buildAddressLaneResult().debug,
|
|
||||||
detected_intent: "payables_confirmed_as_of_date",
|
|
||||||
extracted_filters: {
|
|
||||||
sort: "period_desc",
|
|
||||||
limit: 20,
|
|
||||||
period_from: "2017-09-01",
|
|
||||||
period_to: "2017-09-30",
|
|
||||||
as_of_date: "2017-09-30"
|
|
||||||
},
|
|
||||||
selected_recipe: "address_payables_confirmed_as_of_date_v1",
|
|
||||||
response_type: "FACTUAL_LIST",
|
|
||||||
requested_result_mode: "confirmed_balance",
|
|
||||||
result_mode: "confirmed_balance",
|
|
||||||
balance_confirmed: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const vatResult = buildAddressLaneResult({
|
|
||||||
debug: {
|
|
||||||
...buildAddressLaneResult().debug,
|
|
||||||
detected_intent: "vat_payable_confirmed_as_of_date",
|
|
||||||
extracted_filters: {
|
|
||||||
sort: "period_desc",
|
|
||||||
limit: 20,
|
|
||||||
period_from: "2017-09-01",
|
|
||||||
period_to: "2017-09-30",
|
|
||||||
as_of_date: "2017-09-30"
|
|
||||||
},
|
|
||||||
selected_recipe: "address_vat_payable_confirmed_as_of_date_v1",
|
|
||||||
response_type: "FACTUAL_LIST",
|
|
||||||
requested_result_mode: "confirmed_balance",
|
|
||||||
result_mode: "confirmed_balance",
|
|
||||||
balance_confirmed: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const addressQueryService = {
|
|
||||||
tryHandle: vi.fn(async (message: string, options?: any) => {
|
|
||||||
calls.push({ message, options });
|
|
||||||
if (message === firstMessage) {
|
|
||||||
return payablesResult;
|
|
||||||
}
|
|
||||||
if (!options?.followupContext) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return vatResult;
|
|
||||||
})
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
const normalizerService = {
|
|
||||||
normalize: vi.fn(async () => ({
|
|
||||||
assistant_reply: "normalizer_fallback_should_not_be_used",
|
|
||||||
reply_type: "partial_coverage",
|
|
||||||
debug: {}
|
|
||||||
}))
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
const sessions = new AssistantSessionStore();
|
|
||||||
const service = new AssistantService(
|
|
||||||
normalizerService,
|
|
||||||
sessions as any,
|
|
||||||
{} as any,
|
|
||||||
{ persistSession: vi.fn() } as any,
|
|
||||||
addressQueryService
|
|
||||||
);
|
|
||||||
|
|
||||||
const sessionId = `asst-address-followup-vat-${Date.now()}`;
|
|
||||||
const first = await service.handleMessage({
|
|
||||||
session_id: sessionId,
|
|
||||||
user_message: firstMessage,
|
|
||||||
useMock: true
|
|
||||||
} as any);
|
|
||||||
expect(first.ok).toBe(true);
|
|
||||||
expect(first.reply_type).toBe("factual");
|
|
||||||
|
|
||||||
const second = await service.handleMessage({
|
|
||||||
session_id: sessionId,
|
|
||||||
user_message: followupMessage,
|
|
||||||
useMock: true
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
expect(second.ok).toBe(true);
|
|
||||||
expect(second.reply_type).toBe("factual");
|
|
||||||
expect(second.debug?.detected_intent).toBe("vat_payable_confirmed_as_of_date");
|
|
||||||
expect(second.debug?.selected_recipe).toBe("address_vat_payable_confirmed_as_of_date_v1");
|
|
||||||
|
|
||||||
expect(calls).toHaveLength(2);
|
|
||||||
expect(typeof calls[1].message).toBe("string");
|
|
||||||
expect(String(calls[1].message).length).toBeGreaterThan(0);
|
|
||||||
expect(calls[1].options?.followupContext?.previous_filters?.as_of_date).toBe("2017-09-30");
|
|
||||||
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("passes active organization scope into address lane follow-up context", async () => {
|
it("passes active organization scope into address lane follow-up context", async () => {
|
||||||
const calls: Array<{ message: string; options?: any }> = [];
|
const calls: Array<{ message: string; options?: any }> = [];
|
||||||
const addressQueryService = {
|
const addressQueryService = {
|
||||||
|
|
|
||||||
|
|
@ -442,105 +442,6 @@ describe("assistant address llm pre-decompose candidate preference", () => {
|
||||||
expect(response.debug?.llm_decomposition_reason).toBe("normalized_fragment_rejected_anchor_substitution");
|
expect(response.debug?.llm_decomposition_reason).toBe("normalized_fragment_rejected_anchor_substitution");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects diagnostic canonical rewrite like 'Неясно...' for debt-intent repair message", async () => {
|
|
||||||
const calls: Array<{ message: string }> = [];
|
|
||||||
const addressQueryService = {
|
|
||||||
tryHandle: vi.fn(async (message: string) => {
|
|
||||||
calls.push({ message });
|
|
||||||
return buildAddressLaneResult(message);
|
|
||||||
})
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
const sourceMessage = "нет вопрос кто нам в целом должен на денег на эту дату";
|
|
||||||
const candidateMessage = "Неясно, кто должен компании деньги на данную дату.";
|
|
||||||
|
|
||||||
const normalizerService = {
|
|
||||||
normalize: vi.fn(async () => ({
|
|
||||||
trace_id: "norm-predecompose-diagnostic-rewrite",
|
|
||||||
ok: true,
|
|
||||||
normalized: {
|
|
||||||
schema_version: "normalized_query_v2_0_2",
|
|
||||||
user_message_raw: sourceMessage,
|
|
||||||
message_in_scope: true,
|
|
||||||
scope_confidence: "medium",
|
|
||||||
contains_multiple_tasks: false,
|
|
||||||
fragments: [
|
|
||||||
{
|
|
||||||
fragment_id: "F1",
|
|
||||||
raw_fragment_text: sourceMessage,
|
|
||||||
normalized_fragment_text: candidateMessage,
|
|
||||||
domain_relevance: "in_scope",
|
|
||||||
business_scope: "company_specific_accounting",
|
|
||||||
entity_hints: [],
|
|
||||||
account_hints: [],
|
|
||||||
document_hints: [],
|
|
||||||
register_hints: [],
|
|
||||||
time_scope: {
|
|
||||||
type: "implicit",
|
|
||||||
value: null,
|
|
||||||
confidence: "low"
|
|
||||||
},
|
|
||||||
flags: {
|
|
||||||
has_multi_entity_scope: false,
|
|
||||||
asks_for_chain_explanation: false,
|
|
||||||
asks_for_ranking_or_top: false,
|
|
||||||
asks_for_period_summary: false,
|
|
||||||
asks_for_rule_check: false,
|
|
||||||
asks_for_anomaly_scan: false,
|
|
||||||
asks_for_exact_object_trace: false,
|
|
||||||
asks_for_evidence: false,
|
|
||||||
mentions_period_close_context: false
|
|
||||||
},
|
|
||||||
candidate_labels: ["simple_factual"],
|
|
||||||
confidence: "medium",
|
|
||||||
execution_readiness: "executable",
|
|
||||||
clarification_reason: null,
|
|
||||||
soft_assumption_used: [],
|
|
||||||
route_status: "routed",
|
|
||||||
no_route_reason: null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
discarded_fragments: [],
|
|
||||||
global_notes: {
|
|
||||||
needs_clarification: false,
|
|
||||||
clarification_reason: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
raw_model_output: null,
|
|
||||||
validation: { passed: true, errors: [] },
|
|
||||||
usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 },
|
|
||||||
latency_ms: 10,
|
|
||||||
prompt_version: "normalizer_v2_0_2",
|
|
||||||
schema_version: "v2_0_2",
|
|
||||||
request_count_for_case: 1
|
|
||||||
}))
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
const sessions = new AssistantSessionStore();
|
|
||||||
const service = new AssistantService(
|
|
||||||
normalizerService,
|
|
||||||
sessions as any,
|
|
||||||
{} as any,
|
|
||||||
{ persistSession: vi.fn() } as any,
|
|
||||||
addressQueryService
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = await service.handleMessage({
|
|
||||||
session_id: `asst-predecompose-diagnostic-rewrite-${Date.now()}`,
|
|
||||||
user_message: sourceMessage,
|
|
||||||
llmProvider: "local",
|
|
||||||
useMock: false
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
expect(response.ok).toBe(true);
|
|
||||||
expect(response.reply_type).toBe("factual");
|
|
||||||
expect(calls).toHaveLength(1);
|
|
||||||
expect(calls[0].message).toBe(sourceMessage);
|
|
||||||
expect(calls[0].message).not.toBe(candidateMessage);
|
|
||||||
expect(response.debug?.llm_decomposition_reason).toBe("normalized_fragment_rejected_diagnostic_rewrite");
|
|
||||||
expect(String(response.debug?.llm_decomposition_effective_message ?? "")).toBe(sourceMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects follow-up intent injection when llm adds documents to same-date account prompt", async () => {
|
it("rejects follow-up intent injection when llm adds documents to same-date account prompt", async () => {
|
||||||
const calls: Array<{ message: string }> = [];
|
const calls: Array<{ message: string }> = [];
|
||||||
const addressQueryService = {
|
const addressQueryService = {
|
||||||
|
|
@ -1153,8 +1054,7 @@ describe("assistant address llm pre-decompose candidate preference", () => {
|
||||||
[
|
[
|
||||||
"llm_predecompose_semantic_guard_rejected",
|
"llm_predecompose_semantic_guard_rejected",
|
||||||
"llm_predecompose_unsupported_mode",
|
"llm_predecompose_unsupported_mode",
|
||||||
"address_signal_unsupported_intent_fallback_to_deep",
|
"address_signal_unsupported_intent_fallback_to_deep"
|
||||||
"non_domain_query_indexed"
|
|
||||||
]
|
]
|
||||||
).toContain(response.debug?.address_tool_gate_reason);
|
).toContain(response.debug?.address_tool_gate_reason);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -315,36 +315,6 @@ describe("assistant orchestration contract", () => {
|
||||||
expect(decision.livingReason).toBe("address_lane_triggered");
|
expect(decision.livingReason).toBe("address_lane_triggered");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps short mirror follow-up 'a мы кому' in address lane instead of non-domain chat", () => {
|
|
||||||
const decision = resolveAssistantOrchestrationDecision({
|
|
||||||
rawUserMessage: "a мы кому",
|
|
||||||
effectiveAddressUserMessage: "a мы кому",
|
|
||||||
followupContext: null,
|
|
||||||
llmPreDecomposeMeta: {
|
|
||||||
applied: false,
|
|
||||||
reason: "normalized_fragment_rejected_semantic_guard",
|
|
||||||
llmCanonicalCandidateDetected: true,
|
|
||||||
predecomposeContract: {
|
|
||||||
mode: "unsupported",
|
|
||||||
mode_confidence: "low",
|
|
||||||
intent: "unknown",
|
|
||||||
intent_confidence: "low"
|
|
||||||
},
|
|
||||||
semanticExtractionContract: {
|
|
||||||
valid: false,
|
|
||||||
apply_canonical_recommended: false,
|
|
||||||
reason_codes: ["unsupported_low_confidence_contract"]
|
|
||||||
}
|
|
||||||
} as any,
|
|
||||||
useMock: false
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(decision.runAddressLane).toBe(true);
|
|
||||||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
|
||||||
expect(decision.livingMode).toBe("address_data");
|
|
||||||
expect(decision.livingReason).toBe("address_lane_triggered");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("routes unsupported turnover-by-organization query to deep analysis", () => {
|
it("routes unsupported turnover-by-organization query to deep analysis", () => {
|
||||||
const decision = resolveAssistantOrchestrationDecision({
|
const decision = resolveAssistantOrchestrationDecision({
|
||||||
rawUserMessage: "\u043a\u0430\u043a\u0438\u0435 \u043e\u0431\u043e\u0440\u043e\u0442\u044b \u043f\u043e \u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435 \u0437\u0430 2020 \u0433\u043e\u0434",
|
rawUserMessage: "\u043a\u0430\u043a\u0438\u0435 \u043e\u0431\u043e\u0440\u043e\u0442\u044b \u043f\u043e \u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435 \u0437\u0430 2020 \u0433\u043e\u0434",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue