ЮИ - Перенести настройки ассистента в управление автопрогонами и убрать верхний режим Ассистент
This commit is contained in:
parent
f1333c457e
commit
f3255cb3b8
|
|
@ -0,0 +1,366 @@
|
||||||
|
{
|
||||||
|
"schema_version": "domain_truth_harness_spec_v1",
|
||||||
|
"scenario_id": "address_truth_harness_test2",
|
||||||
|
"domain": "address_mixed_followup",
|
||||||
|
"title": "Exact replay and truth review for test2 mixed follow-up chain",
|
||||||
|
"description": "Strict sequential replay of the exact user wording from test2.txt with human-answer and technical-debug review.",
|
||||||
|
"source_export": "C:\\Users\\DCTOUCH\\Desktop\\test2.txt",
|
||||||
|
"bindings": {},
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": "step_01_chat_opening",
|
||||||
|
"title": "Casual opener",
|
||||||
|
"question": "йо чо как",
|
||||||
|
"criticality": "info",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual_with_explanation",
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_any": [
|
||||||
|
"(?i)1с",
|
||||||
|
"(?i)помогаю",
|
||||||
|
"(?i)готов"
|
||||||
|
],
|
||||||
|
"notes": "Информационный шаг: важно не ломать живой режим, но это не главный бизнес-блокер."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_02_receivables_march_2020",
|
||||||
|
"title": "Receivables at March 2020",
|
||||||
|
"question": "кто нам должен на март 2020",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"receivables_confirmed_as_of_date"
|
||||||
|
],
|
||||||
|
"expected_capability": "confirmed_receivables_as_of_date",
|
||||||
|
"expected_result_mode": "confirmed_balance",
|
||||||
|
"required_filters": {
|
||||||
|
"as_of_date": "2020-03-31",
|
||||||
|
"period_from": "2020-03-01",
|
||||||
|
"period_to": "2020-03-31"
|
||||||
|
},
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"31\\.03\\.2020",
|
||||||
|
"(?i)дебиторск"
|
||||||
|
],
|
||||||
|
"notes": "Базовый корневой финансовый вопрос должен отработать точно и задать март 2020 как carryover-якорь."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_03_inventory_same_date",
|
||||||
|
"title": "Inventory on the same date",
|
||||||
|
"question": "остатки по складу на эту же дату",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"inventory_on_hand_as_of_date"
|
||||||
|
],
|
||||||
|
"expected_capability": "confirmed_inventory_on_hand_as_of_date",
|
||||||
|
"expected_result_mode": "confirmed_balance",
|
||||||
|
"required_filters": {
|
||||||
|
"as_of_date": "{{step_02_receivables_march_2020.filters.as_of_date}}",
|
||||||
|
"period_from": "{{step_02_receivables_march_2020.filters.period_from}}",
|
||||||
|
"period_to": "{{step_02_receivables_march_2020.filters.period_to}}"
|
||||||
|
},
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"31\\.03\\.2020",
|
||||||
|
"(?i)на складе"
|
||||||
|
],
|
||||||
|
"notes": "Смена контура receivables -> inventory должна сохранить ту же дату, без дополнительного ручного уточнения."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_04_selected_item_seller",
|
||||||
|
"title": "Selected item purchase provenance",
|
||||||
|
"question": "По выбранному объекту \"Четки Пост (84*117)\": кто продавец",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"inventory_purchase_provenance_for_item"
|
||||||
|
],
|
||||||
|
"required_state_objects": [
|
||||||
|
"focus_object"
|
||||||
|
],
|
||||||
|
"required_carryover_invariants": [
|
||||||
|
"focus_object"
|
||||||
|
],
|
||||||
|
"forbidden_capabilities": [
|
||||||
|
"confirmed_inventory_on_hand_as_of_date"
|
||||||
|
],
|
||||||
|
"forbidden_recipes": [
|
||||||
|
"address_inventory_on_hand_as_of_date_v1"
|
||||||
|
],
|
||||||
|
"forbidden_filter_keys": [
|
||||||
|
"as_of_date",
|
||||||
|
"period_from",
|
||||||
|
"period_to"
|
||||||
|
],
|
||||||
|
"forbidden_direct_answer_patterns": [
|
||||||
|
"^На 31\\.03\\.2020 на складе",
|
||||||
|
"(?i)^сейчас не дам прямой адресный ответ"
|
||||||
|
],
|
||||||
|
"notes": "Выбранная позиция не должна реплеить складской срез; нужен именно ответ про поставщика/продавца."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_05_selected_item_sale_trace",
|
||||||
|
"title": "Selected item buyer",
|
||||||
|
"question": "По выбранному объекту \"Четки Пост (84*117)\": кому мы продали эту хуйню",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"allowed_limited_reason_categories": [
|
||||||
|
"empty_match"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"inventory_sale_trace_for_item"
|
||||||
|
],
|
||||||
|
"required_state_objects": [
|
||||||
|
"focus_object"
|
||||||
|
],
|
||||||
|
"required_carryover_invariants": [
|
||||||
|
"focus_object"
|
||||||
|
],
|
||||||
|
"forbidden_filter_keys": [
|
||||||
|
"as_of_date",
|
||||||
|
"period_from",
|
||||||
|
"period_to"
|
||||||
|
],
|
||||||
|
"forbidden_direct_answer_patterns": [
|
||||||
|
"(?i)^по текущим условиям в доступном срезе данных совпадений не нашлось",
|
||||||
|
"(?i)^сейчас не дам прямой адресный ответ"
|
||||||
|
],
|
||||||
|
"notes": "След продажи по выбранной позиции не должен быть зажат складским snapshot-окном. Если по live данным подтвержденных продаж нет, честный partial_coverage с empty_match допустим, но только при правильном sale-trace routing и сохраненном focus_object."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_06_selected_item_purchase_followup",
|
||||||
|
"title": "Selected item purchase follow-up",
|
||||||
|
"question": "а купили у кого известно?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"inventory_purchase_provenance_for_item"
|
||||||
|
],
|
||||||
|
"required_state_objects": [
|
||||||
|
"focus_object"
|
||||||
|
],
|
||||||
|
"required_carryover_invariants": [
|
||||||
|
"focus_object"
|
||||||
|
],
|
||||||
|
"forbidden_filter_keys": [
|
||||||
|
"as_of_date",
|
||||||
|
"period_from",
|
||||||
|
"period_to"
|
||||||
|
],
|
||||||
|
"forbidden_direct_answer_patterns": [
|
||||||
|
"(?i)^для такого формата запроса нужен более широкий аналитический контур",
|
||||||
|
"(?i)^сейчас не дам прямой адресный ответ"
|
||||||
|
],
|
||||||
|
"notes": "Короткий follow-up после выбранной позиции должен остаться в том же item-contour, а не свалиться в unknown."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_07_inventory_july_2019",
|
||||||
|
"title": "Inventory at July 2019",
|
||||||
|
"question": "остатки на июль 2019",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"inventory_on_hand_as_of_date"
|
||||||
|
],
|
||||||
|
"expected_capability": "confirmed_inventory_on_hand_as_of_date",
|
||||||
|
"expected_result_mode": "confirmed_balance",
|
||||||
|
"required_filters": {
|
||||||
|
"as_of_date": "2019-07-31",
|
||||||
|
"period_from": "2019-07-01",
|
||||||
|
"period_to": "2019-07-31"
|
||||||
|
},
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"31\\.07\\.2019",
|
||||||
|
"(?i)на складе"
|
||||||
|
],
|
||||||
|
"forbidden_direct_answer_patterns": [
|
||||||
|
"(?i)^сейчас не дам прямой адресный ответ"
|
||||||
|
],
|
||||||
|
"notes": "После провала provenance-среза система все равно должна уметь коротко вернуть root inventory frame по явному месяцу."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_08_inventory_september_2019",
|
||||||
|
"title": "Inventory at September 2019",
|
||||||
|
"question": "сентябрь 2019",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"inventory_on_hand_as_of_date"
|
||||||
|
],
|
||||||
|
"expected_capability": "confirmed_inventory_on_hand_as_of_date",
|
||||||
|
"expected_result_mode": "confirmed_balance",
|
||||||
|
"required_filters": {
|
||||||
|
"as_of_date": "2019-09-30",
|
||||||
|
"period_from": "2019-09-01",
|
||||||
|
"period_to": "2019-09-30"
|
||||||
|
},
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"30\\.09\\.2019",
|
||||||
|
"(?i)на складе"
|
||||||
|
],
|
||||||
|
"forbidden_direct_answer_patterns": [
|
||||||
|
"(?i)^сейчас не дам прямой адресный ответ"
|
||||||
|
],
|
||||||
|
"notes": "Короткое bare-month follow-up должно удерживать складской корень без дополнительной расшифровки."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_09_inventory_march_2020",
|
||||||
|
"title": "Inventory at March 2020",
|
||||||
|
"question": "март 2020",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"inventory_on_hand_as_of_date"
|
||||||
|
],
|
||||||
|
"expected_capability": "confirmed_inventory_on_hand_as_of_date",
|
||||||
|
"expected_result_mode": "confirmed_balance",
|
||||||
|
"required_filters": {
|
||||||
|
"as_of_date": "2020-03-31",
|
||||||
|
"period_from": "2020-03-01",
|
||||||
|
"period_to": "2020-03-31"
|
||||||
|
},
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"31\\.03\\.2020",
|
||||||
|
"(?i)на складе"
|
||||||
|
],
|
||||||
|
"forbidden_direct_answer_patterns": [
|
||||||
|
"(?i)^сейчас не дам прямой адресный ответ"
|
||||||
|
],
|
||||||
|
"notes": "Возврат на март 2020 должен снова дать точный складской срез, а не unknown/partial."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_10_inventory_same_date_negative_wording",
|
||||||
|
"title": "Inventory same date with noisy wording",
|
||||||
|
"question": "остатков на складе нет на эту дату?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"inventory_on_hand_as_of_date"
|
||||||
|
],
|
||||||
|
"expected_capability": "confirmed_inventory_on_hand_as_of_date",
|
||||||
|
"expected_result_mode": "confirmed_balance",
|
||||||
|
"required_filters": {
|
||||||
|
"as_of_date": "{{step_09_inventory_march_2020.filters.as_of_date}}",
|
||||||
|
"period_from": "{{step_09_inventory_march_2020.filters.period_from}}",
|
||||||
|
"period_to": "{{step_09_inventory_march_2020.filters.period_to}}"
|
||||||
|
},
|
||||||
|
"forbidden_filter_values": {
|
||||||
|
"warehouse": [
|
||||||
|
"нет на эту дату"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"31\\.03\\.2020",
|
||||||
|
"(?i)на складе"
|
||||||
|
],
|
||||||
|
"forbidden_direct_answer_patterns": [
|
||||||
|
"(?i)^сейчас не дам прямой адресный ответ"
|
||||||
|
],
|
||||||
|
"notes": "Разговорная частица `нет на эту дату` не должна становиться warehouse-anchor."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_11_vat_same_date",
|
||||||
|
"title": "VAT on the same date",
|
||||||
|
"question": "ндс какой надо заплатить на эту же дату",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"vat_liability_confirmed_for_tax_period"
|
||||||
|
],
|
||||||
|
"expected_capability": "confirmed_vat_liability_for_tax_period",
|
||||||
|
"required_filters": {
|
||||||
|
"period_from": "{{step_09_inventory_march_2020.filters.period_from}}",
|
||||||
|
"period_to": "{{step_09_inventory_march_2020.filters.period_to}}"
|
||||||
|
},
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"(?i)ндс"
|
||||||
|
],
|
||||||
|
"notes": "Pivot inventory -> VAT по `на эту же дату` должен привязаться к марту 2020, а не к мусорному складскому anchor."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_12_vat_may_2016",
|
||||||
|
"title": "VAT at May 2016",
|
||||||
|
"question": "а на май 2016",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"vat_liability_confirmed_for_tax_period"
|
||||||
|
],
|
||||||
|
"expected_capability": "confirmed_vat_liability_for_tax_period",
|
||||||
|
"forbidden_filter_values": {
|
||||||
|
"period_from": [
|
||||||
|
"{{step_11_vat_same_date.filters.period_from}}"
|
||||||
|
],
|
||||||
|
"period_to": [
|
||||||
|
"{{step_11_vat_same_date.filters.period_to}}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"required_answer_patterns_any": [
|
||||||
|
"2016",
|
||||||
|
"(?i)налоговый период"
|
||||||
|
],
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"(?i)ндс"
|
||||||
|
],
|
||||||
|
"notes": "Короткий temporal follow-up внутри VAT-frame должен уйти с марта 2020 на май 2016."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_13_receivables_same_date_after_vat_2016",
|
||||||
|
"title": "Receivables on the carried 2016 date",
|
||||||
|
"question": "кто нам должен денег на эту дату",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"receivables_confirmed_as_of_date"
|
||||||
|
],
|
||||||
|
"expected_capability": "confirmed_receivables_as_of_date",
|
||||||
|
"required_filter_within_previous_step_period": {
|
||||||
|
"as_of_date": "step_12_vat_may_2016"
|
||||||
|
},
|
||||||
|
"required_answer_patterns_any": [
|
||||||
|
"2016"
|
||||||
|
],
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"(?i)дебиторск"
|
||||||
|
],
|
||||||
|
"forbidden_direct_answer_patterns": [
|
||||||
|
"31\\.03\\.2020"
|
||||||
|
],
|
||||||
|
"notes": "Фраза `на эту дату` после VAT 2016 не должна откатываться обратно на март 2020."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_14_receivables_today",
|
||||||
|
"title": "Receivables today",
|
||||||
|
"question": "а на сегодня",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"receivables_confirmed_as_of_date"
|
||||||
|
],
|
||||||
|
"expected_capability": "confirmed_receivables_as_of_date",
|
||||||
|
"required_filters": {
|
||||||
|
"as_of_date": "{{runtime.today_iso}}"
|
||||||
|
},
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"{{runtime.today_dot_regex}}",
|
||||||
|
"(?i)дебиторск"
|
||||||
|
],
|
||||||
|
"notes": "Последний шаг нужен как sanity-check: `на сегодня` должен честно пересчитать уже на текущую дату прогона."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -1051,6 +1051,12 @@ function isTemporalWarehousePhrase(candidate) {
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/ё/g, "е")
|
.replace(/ё/g, "е")
|
||||||
.trim();
|
.trim();
|
||||||
|
if (/^(?:на\s+)?(?:эту|ту|текущ(?:ую|ая|ий|ее|ей)|сегодняшн(?:юю|ая|ий|ее|ей))(?:\s+же)?\s+дат(?:у|а|е|ой)$/iu.test(normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (/^(?:нет|не\s+было|не\s+было\s+ли)(?:\s+на\s+(?:эту|ту|такую)\s+дат(?:у|е|ой))?$/iu.test(normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return /^(?:в|на)\s+(?:январ(?:е|ь)|феврал(?:е|ь)|март(?:е)?|апрел(?:е|ь)|ма(?:й|е)|июн(?:е|ь)|июл(?:е|ь)|август(?:е)?|сентябр(?:е|ь)|октябр(?:е|ь)|ноябр(?:е|ь)|декабр(?:е|ь))(?:\s+\d{4}(?:\s+г(?:\.|ода)?)?)?$/iu.test(normalized);
|
return /^(?:в|на)\s+(?:январ(?:е|ь)|феврал(?:е|ь)|март(?:е)?|апрел(?:е|ь)|ма(?:й|е)|июн(?:е|ь)|июл(?:е|ь)|август(?:е)?|сентябр(?:е|ь)|октябр(?:е|ь)|ноябр(?:е|ь)|декабр(?:е|ь))(?:\s+\d{4}(?:\s+г(?:\.|ода)?)?)?$/iu.test(normalized);
|
||||||
}
|
}
|
||||||
function isLowQualityWarehouseAnchorValue(rawValue) {
|
function isLowQualityWarehouseAnchorValue(rawValue) {
|
||||||
|
|
@ -1097,7 +1103,8 @@ function isLowQualityWarehouseAnchorValue(rawValue) {
|
||||||
"охуеть",
|
"охуеть",
|
||||||
"пиздец",
|
"пиздец",
|
||||||
"блять",
|
"блять",
|
||||||
"бля"
|
"бля",
|
||||||
|
"нет"
|
||||||
]);
|
]);
|
||||||
const tokens = value
|
const tokens = value
|
||||||
.split(/[^a-zа-я0-9]+/iu)
|
.split(/[^a-zа-я0-9]+/iu)
|
||||||
|
|
@ -1127,7 +1134,7 @@ function hasImplicitSelfScopeSignal(text) {
|
||||||
}
|
}
|
||||||
function isImplicitSelfScopeWarehouseAnchor(candidate) {
|
function isImplicitSelfScopeWarehouseAnchor(candidate) {
|
||||||
const normalized = normalizeSemanticAnchorCandidate(candidate);
|
const normalized = normalizeSemanticAnchorCandidate(candidate);
|
||||||
return /^(?:у\s+нас|у\s+себя|у\s+меня|наш(?:ем|ей|его|их|а|е)?|сво(?:ем|ей|его|их|я|е)?)$/iu.test(normalized);
|
return /^(?:у\s+нас|у\s+себя|у\s+меня|наш(?:ем|ей|его|их|а|е)?|сво(?:ем|ей|его|их|я|е)?)(?:\s+(?:висит|висят|висело|висели|лежит|лежат|лежало|лежали))?$/iu.test(normalized);
|
||||||
}
|
}
|
||||||
function hasSelectedObjectScopeSignal(text) {
|
function hasSelectedObjectScopeSignal(text) {
|
||||||
return /(?:по\s+выбранному\s+объекту|selected\s+object)/iu.test(String(text ?? ""));
|
return /(?:по\s+выбранному\s+объекту|selected\s+object)/iu.test(String(text ?? ""));
|
||||||
|
|
|
||||||
|
|
@ -1039,10 +1039,21 @@ function toNormalizedRows(rows) {
|
||||||
.filter((item) => Boolean(item.period || item.registrator));
|
.filter((item) => Boolean(item.period || item.registrator));
|
||||||
}
|
}
|
||||||
function rowSearchableText(row) {
|
function rowSearchableText(row) {
|
||||||
return [row.registrator, row.item ?? "", row.warehouse ?? "", row.account_dt ?? "", row.account_kt ?? "", ...row.analytics]
|
return [
|
||||||
|
row.registrator,
|
||||||
|
row.item ?? "",
|
||||||
|
row.warehouse ?? "",
|
||||||
|
row.organization ?? "",
|
||||||
|
row.account_dt ?? "",
|
||||||
|
row.account_kt ?? "",
|
||||||
|
...row.analytics
|
||||||
|
]
|
||||||
.join(" ")
|
.join(" ")
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
}
|
}
|
||||||
|
function rowOrganizationSearchableText(row) {
|
||||||
|
return [row.organization ?? "", row.registrator, ...row.analytics].join(" ").toLowerCase();
|
||||||
|
}
|
||||||
function rowMatchesAnyAccount(row, accountScope) {
|
function rowMatchesAnyAccount(row, accountScope) {
|
||||||
if (accountScope.length === 0) {
|
if (accountScope.length === 0) {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -1107,9 +1118,12 @@ function applyAddressFilters(rows, filters) {
|
||||||
if (filters.organization && String(filters.organization).trim()) {
|
if (filters.organization && String(filters.organization).trim()) {
|
||||||
const needle = String(filters.organization);
|
const needle = String(filters.organization);
|
||||||
const before = filtered.length;
|
const before = filtered.length;
|
||||||
filtered = filtered.filter((row) => matchesAnchorText(rowSearchableText(row), needle));
|
const organizationMaterialized = filtered.some((row) => Boolean(String(row.organization ?? "").trim()));
|
||||||
if (before > 0 && filtered.length === 0 && mismatchReason === null) {
|
if (organizationMaterialized) {
|
||||||
mismatchReason = "organization_anchor_not_matched_in_materialized_rows";
|
filtered = filtered.filter((row) => matchesAnchorText(rowOrganizationSearchableText(row), needle));
|
||||||
|
if (before > 0 && filtered.length === 0 && mismatchReason === null) {
|
||||||
|
mismatchReason = "organization_anchor_not_matched_in_materialized_rows";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (filters.item && String(filters.item).trim()) {
|
if (filters.item && String(filters.item).trim()) {
|
||||||
|
|
|
||||||
|
|
@ -1114,9 +1114,22 @@ function buildInventoryMovementQuery(filters, resolvedLimit, side) {
|
||||||
: side === "kt"
|
: side === "kt"
|
||||||
? creditPredicate
|
? creditPredicate
|
||||||
: `(${debitPredicate} ИЛИ ${creditPredicate})`;
|
: `(${debitPredicate} ИЛИ ${creditPredicate})`;
|
||||||
|
const itemFieldPaths = side === "dt"
|
||||||
|
? ["Движения.СубконтоДт1", "Движения.СубконтоДт2", "Движения.СубконтоДт3"]
|
||||||
|
: side === "kt"
|
||||||
|
? ["Движения.СубконтоКт1", "Движения.СубконтоКт2", "Движения.СубконтоКт3"]
|
||||||
|
: [
|
||||||
|
"Движения.СубконтоДт1",
|
||||||
|
"Движения.СубконтоДт2",
|
||||||
|
"Движения.СубконтоДт3",
|
||||||
|
"Движения.СубконтоКт1",
|
||||||
|
"Движения.СубконтоКт2",
|
||||||
|
"Движения.СубконтоКт3"
|
||||||
|
];
|
||||||
|
const itemCondition = buildInventoryItemReferenceCondition(filters, itemFieldPaths);
|
||||||
return INVENTORY_MOVEMENTS_QUERY_TEMPLATE
|
return INVENTORY_MOVEMENTS_QUERY_TEMPLATE
|
||||||
.replace("__LIMIT__", String(resolvedLimit))
|
.replace("__LIMIT__", String(resolvedLimit))
|
||||||
.replace("__WHERE_CLAUSE__", buildWhereClause(filters, "Движения.Период", [inventoryCondition]))
|
.replace("__WHERE_CLAUSE__", buildWhereClause(filters, "Движения.Период", [inventoryCondition, itemCondition].filter((item) => Boolean(item))))
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||||
}
|
}
|
||||||
function buildInventoryItemReferenceCondition(filters, fieldPaths) {
|
function buildInventoryItemReferenceCondition(filters, fieldPaths) {
|
||||||
|
|
@ -1332,7 +1345,7 @@ function buildAddressRecipePlan(recipe, filters) {
|
||||||
: recipe.query_template === "inventory_supplier_stock_overlap_profile"
|
: recipe.query_template === "inventory_supplier_stock_overlap_profile"
|
||||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||||
: recipe.query_template === "inventory_sale_trace_profile"
|
: recipe.query_template === "inventory_sale_trace_profile"
|
||||||
? buildInventorySaleDocumentQuery(filters, resolvedLimit)
|
? buildInventoryMovementQuery(filters, resolvedLimit, "kt")
|
||||||
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
||||||
? buildInventoryMovementQuery(filters, resolvedLimit, "either")
|
? buildInventoryMovementQuery(filters, resolvedLimit, "either")
|
||||||
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ function hasAllTimeHint(text) {
|
||||||
return /(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\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|for\s+full\s+history|full\s+history)/iu.test(normalized);
|
return /(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\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|for\s+full\s+history|full\s+history)/iu.test(normalized);
|
||||||
}
|
}
|
||||||
function hasSameDateHint(text) {
|
function hasSameDateHint(text) {
|
||||||
return /(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|на\s+эту\s+дат[ауеы]|эту\s+дат[ауеы]|та\s+же\s+дата|same\s+date|as\s+of\s+same\s+date|the\s+same\s+date)/iu.test(String(text ?? ""));
|
return /(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|на\s+эту\s+дат[ауеы]|эту\s+дат[ауеы]|та\s+же\s+дата|дат[ауеы],?\s+котор(?:ую|ая)\s+(?:до\s+этого|раньше|ранее)\s+(?:рассматривали|смотрели)|дат[ауеы],?\s+которая\s+был[ао]?\s+ранее\s+рассмотрен[ао]?|same\s+date|as\s+of\s+same\s+date|the\s+same\s+date|date\s+we\s+looked\s+at\s+before|previously\s+considered\s+date)/iu.test(String(text ?? ""));
|
||||||
}
|
}
|
||||||
function hasSamePeriodHint(text) {
|
function hasSamePeriodHint(text) {
|
||||||
return /(?:на\s+тот\s+же\s+период|за\s+тот\s+же\s+период|тот\s+же\s+период(?:\s+рассмотрения)?|на\s+этот\s+же\s+период|за\s+этот\s+же\s+период|аналогичн\w+\s+текущ\w+\s+период\w+|same\s+period|same\s+range|same\s+window)/iu.test(String(text ?? ""));
|
return /(?:на\s+тот\s+же\s+период|за\s+тот\s+же\s+период|тот\s+же\s+период(?:\s+рассмотрения)?|на\s+этот\s+же\s+период|за\s+этот\s+же\s+период|аналогичн\w+\s+текущ\w+\s+период\w+|same\s+period|same\s+range|same\s+window)/iu.test(String(text ?? ""));
|
||||||
|
|
@ -382,17 +382,49 @@ function resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContex
|
||||||
as_of_date: periodTo
|
as_of_date: periodTo
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function resolveRelativeMonthPeriodFromFollowupYear(userMessage, followupContext) {
|
||||||
|
if (!followupContext) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const month = resolveMonthNumberFromText(userMessage);
|
||||||
|
if (!month) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const normalized = String(userMessage ?? "");
|
||||||
|
if (hasExplicitPeriodLiteral(normalized) || hasExplicitCurrentDateHint(normalized) || hasSameDateHint(normalized)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const shortTemporalPatch = getTokenCount(normalized) <= 8 || hasRelativeYearHint(normalized);
|
||||||
|
if (!shortTemporalPatch) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const year = resolveYearFromFilters(followupContext.previous_filters) ??
|
||||||
|
resolveYearFromFilters(followupContext.root_filters);
|
||||||
|
if (!year) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
||||||
|
const periodFrom = `${year}-${String(month).padStart(2, "0")}-01`;
|
||||||
|
const periodTo = `${year}-${String(month).padStart(2, "0")}-${String(lastDay).padStart(2, "0")}`;
|
||||||
|
return {
|
||||||
|
period_from: periodFrom,
|
||||||
|
period_to: periodTo,
|
||||||
|
as_of_date: periodTo
|
||||||
|
};
|
||||||
|
}
|
||||||
function shouldRestoreInventoryRootFrame(userMessage, intent, extractedFilters, followupContext) {
|
function shouldRestoreInventoryRootFrame(userMessage, intent, extractedFilters, followupContext) {
|
||||||
if (!followupContext || !isInventoryRootFrameIntent(followupContext.root_intent)) {
|
if (!followupContext || !isInventoryRootFrameIntent(followupContext.root_intent)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const currentFrameKind = followupContext.current_frame_kind ?? null;
|
const currentFrameKind = followupContext.current_frame_kind ?? null;
|
||||||
const previousIntent = followupContext.previous_intent;
|
const previousIntent = followupContext.previous_intent;
|
||||||
|
const rootContextOnly = followupContext.root_context_only === true;
|
||||||
const comingFromInventoryDrilldown = currentFrameKind === "inventory_drilldown" || isInventoryDrilldownFrameIntent(previousIntent);
|
const comingFromInventoryDrilldown = currentFrameKind === "inventory_drilldown" || isInventoryDrilldownFrameIntent(previousIntent);
|
||||||
const normalized = String(userMessage ?? "");
|
const normalized = String(userMessage ?? "");
|
||||||
const hasInventoryRootRestatementCue = /(?:склад|остат(?:ок|ки)|позици(?:я|и|ю)|товар(?:ы|ов)?|номенклатур)/iu.test(normalized) &&
|
const hasInventoryRootRestatementCue = /(?:склад|остат(?:ок|ки)|позици(?:я|и|ю)|товар(?:ы|ов)?|номенклатур)/iu.test(normalized) &&
|
||||||
/(?:покажи|показать|выведи|раскрой|еще\s+раз|ещ[её]\s+раз|снова|опять|верни|вернись|повтори|тот\s+же|этот\s+же|same|again)/iu.test(normalized);
|
/(?:покажи|показать|выведи|раскрой|еще\s+раз|ещ[её]\s+раз|снова|опять|верни|вернись|повтори|тот\s+же|этот\s+же|same|again)/iu.test(normalized);
|
||||||
const canReenterInventoryRoot = comingFromInventoryDrilldown ||
|
const canReenterInventoryRoot = comingFromInventoryDrilldown ||
|
||||||
|
rootContextOnly ||
|
||||||
(currentFrameKind === "inventory_root" && hasSamePeriodHint(normalized)) ||
|
(currentFrameKind === "inventory_root" && hasSamePeriodHint(normalized)) ||
|
||||||
(currentFrameKind === "generic" && hasInventoryRootRestatementCue && hasSamePeriodHint(normalized));
|
(currentFrameKind === "generic" && hasInventoryRootRestatementCue && hasSamePeriodHint(normalized));
|
||||||
if (!canReenterInventoryRoot) {
|
if (!canReenterInventoryRoot) {
|
||||||
|
|
@ -401,7 +433,7 @@ function shouldRestoreInventoryRootFrame(userMessage, intent, extractedFilters,
|
||||||
if (intent !== "unknown" && !isInventoryIntent(intent) && !hasInventoryRootRestatementCue) {
|
if (intent !== "unknown" && !isInventoryIntent(intent) && !hasInventoryRootRestatementCue) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (hasSelectedObjectInventorySignal(normalized) ||
|
if ((hasSelectedObjectInventorySignal(normalized) && !hasInventoryRootRestatementCue) ||
|
||||||
hasInventorySupplierFollowupCue(normalized) ||
|
hasInventorySupplierFollowupCue(normalized) ||
|
||||||
hasInventoryPurchaseDocumentsFollowupCue(normalized) ||
|
hasInventoryPurchaseDocumentsFollowupCue(normalized) ||
|
||||||
hasInventoryPurchaseDateFollowupCue(normalized) ||
|
hasInventoryPurchaseDateFollowupCue(normalized) ||
|
||||||
|
|
@ -415,6 +447,7 @@ function shouldRestoreInventoryRootFrame(userMessage, intent, extractedFilters,
|
||||||
}
|
}
|
||||||
const hasTemporalPatch = hasExplicitPeriodWindow(extractedFilters) ||
|
const hasTemporalPatch = hasExplicitPeriodWindow(extractedFilters) ||
|
||||||
Boolean(toNonEmptyString(extractedFilters.as_of_date)) ||
|
Boolean(toNonEmptyString(extractedFilters.as_of_date)) ||
|
||||||
|
hasSameDateHint(normalized) ||
|
||||||
hasSamePeriodHint(normalized) ||
|
hasSamePeriodHint(normalized) ||
|
||||||
hasExplicitPeriodLiteral(normalized) ||
|
hasExplicitPeriodLiteral(normalized) ||
|
||||||
Boolean(resolveRelativeMonthPeriodFromInventoryRoot(normalized, followupContext));
|
Boolean(resolveRelativeMonthPeriodFromInventoryRoot(normalized, followupContext));
|
||||||
|
|
@ -544,9 +577,11 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
const previousPeriodFrom = toNonEmptyString(previous.period_from);
|
const previousPeriodFrom = toNonEmptyString(previous.period_from);
|
||||||
const previousPeriodTo = toNonEmptyString(previous.period_to);
|
const previousPeriodTo = toNonEmptyString(previous.period_to);
|
||||||
const relativeMonthFromInventoryRoot = resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext);
|
const relativeMonthFromInventoryRoot = resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext);
|
||||||
|
const relativeMonthFromFollowupYear = resolveRelativeMonthPeriodFromFollowupYear(userMessage, followupContext);
|
||||||
const allTimeRequested = hasAllTimeHint(userMessage);
|
const allTimeRequested = hasAllTimeHint(userMessage);
|
||||||
const sameDateRequested = hasSameDateHint(userMessage);
|
const sameDateRequested = hasSameDateHint(userMessage);
|
||||||
const samePeriodRequested = hasSamePeriodHint(userMessage);
|
const samePeriodRequested = hasSamePeriodHint(userMessage);
|
||||||
|
const explicitQuotedItem = extractSelectedObjectItemFromFollowupText(userMessage);
|
||||||
if (!toNonEmptyString(merged.organization) && previousOrganization) {
|
if (!toNonEmptyString(merged.organization) && previousOrganization) {
|
||||||
merged.organization = previousOrganization;
|
merged.organization = previousOrganization;
|
||||||
reasons.push("organization_from_followup_context");
|
reasons.push("organization_from_followup_context");
|
||||||
|
|
@ -654,6 +689,15 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
merged.counterparty = inheritedCounterparty;
|
merged.counterparty = inheritedCounterparty;
|
||||||
reasons.push(currentCounterparty ? "counterparty_replaced_from_followup_context" : "counterparty_from_followup_context");
|
reasons.push(currentCounterparty ? "counterparty_replaced_from_followup_context" : "counterparty_from_followup_context");
|
||||||
}
|
}
|
||||||
|
if (intent === "inventory_on_hand_as_of_date" && explicitQuotedItem) {
|
||||||
|
const currentItem = toNonEmptyString(merged.item);
|
||||||
|
if (!currentItem ||
|
||||||
|
currentItem !== explicitQuotedItem ||
|
||||||
|
(0, addressFilterExtractor_1.isInventoryItemAnchorDegradation)(explicitQuotedItem, currentItem)) {
|
||||||
|
merged.item = explicitQuotedItem;
|
||||||
|
reasons.push(currentItem ? "item_replaced_from_explicit_quote" : "item_from_explicit_quote");
|
||||||
|
}
|
||||||
|
}
|
||||||
if ((intent === "inventory_purchase_provenance_for_item" ||
|
if ((intent === "inventory_purchase_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
|
@ -661,7 +705,6 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date")) {
|
intent === "inventory_aging_by_purchase_date")) {
|
||||||
const inheritedItem = previousItem ?? previousAnchorItem;
|
const inheritedItem = previousItem ?? previousAnchorItem;
|
||||||
const explicitQuotedItem = extractSelectedObjectItemFromFollowupText(userMessage);
|
|
||||||
const currentItem = toNonEmptyString(merged.item);
|
const currentItem = toNonEmptyString(merged.item);
|
||||||
const shouldAdoptExplicitQuotedItem = Boolean(explicitQuotedItem) &&
|
const shouldAdoptExplicitQuotedItem = Boolean(explicitQuotedItem) &&
|
||||||
(!currentItem ||
|
(!currentItem ||
|
||||||
|
|
@ -780,6 +823,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
}
|
}
|
||||||
if (!sameDateRequested &&
|
if (!sameDateRequested &&
|
||||||
hasFollowupSignalForConfirmed &&
|
hasFollowupSignalForConfirmed &&
|
||||||
|
!isInventoryLifecycleHistoryIntent(intent) &&
|
||||||
!hasExplicitPeriodLiteral(userMessage) &&
|
!hasExplicitPeriodLiteral(userMessage) &&
|
||||||
!hasExplicitCurrentDateHint(userMessage)) {
|
!hasExplicitCurrentDateHint(userMessage)) {
|
||||||
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
|
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
|
||||||
|
|
@ -830,10 +874,26 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
intent === "vat_payable_confirmed_as_of_date";
|
intent === "vat_payable_confirmed_as_of_date";
|
||||||
const currentHasPeriod = hasExplicitPeriodWindow(merged);
|
const currentHasPeriod = hasExplicitPeriodWindow(merged);
|
||||||
const previousHasPeriod = hasExplicitPeriodWindow(previous);
|
const previousHasPeriod = hasExplicitPeriodWindow(previous);
|
||||||
|
const vatRelativeMonthFollowup = relativeMonthFromFollowupYear &&
|
||||||
|
(intent === "vat_payable_confirmed_as_of_date" ||
|
||||||
|
intent === "vat_payable_forecast" ||
|
||||||
|
intent === "vat_liability_confirmed_for_tax_period");
|
||||||
|
if (vatRelativeMonthFollowup) {
|
||||||
|
merged.period_from = relativeMonthFromFollowupYear.period_from;
|
||||||
|
merged.period_to = relativeMonthFromFollowupYear.period_to;
|
||||||
|
if (intent === "vat_payable_confirmed_as_of_date") {
|
||||||
|
merged.as_of_date = relativeMonthFromFollowupYear.as_of_date;
|
||||||
|
}
|
||||||
|
else if (toNonEmptyString(merged.as_of_date)) {
|
||||||
|
delete merged.as_of_date;
|
||||||
|
}
|
||||||
|
reasons.push("period_derived_from_followup_context_year");
|
||||||
|
}
|
||||||
if ((intent === "vat_payable_forecast" || intent === "vat_liability_confirmed_for_tax_period") &&
|
if ((intent === "vat_payable_forecast" || intent === "vat_liability_confirmed_for_tax_period") &&
|
||||||
previousHasPeriod &&
|
previousHasPeriod &&
|
||||||
hasFollowupSignal &&
|
hasFollowupSignal &&
|
||||||
!hasExplicitPeriodInMessage) {
|
!hasExplicitPeriodInMessage &&
|
||||||
|
!vatRelativeMonthFollowup) {
|
||||||
const currentPeriodFrom = toNonEmptyString(merged.period_from);
|
const currentPeriodFrom = toNonEmptyString(merged.period_from);
|
||||||
const currentPeriodTo = toNonEmptyString(merged.period_to);
|
const currentPeriodTo = toNonEmptyString(merged.period_to);
|
||||||
const todayIso = new Date().toISOString().slice(0, 10);
|
const todayIso = new Date().toISOString().slice(0, 10);
|
||||||
|
|
@ -852,7 +912,8 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
previousHasPeriod &&
|
previousHasPeriod &&
|
||||||
hasFollowupSignal &&
|
hasFollowupSignal &&
|
||||||
!hasExplicitPeriodInMessage &&
|
!hasExplicitPeriodInMessage &&
|
||||||
!inventoryLifecycleHistoryIntent) {
|
!inventoryLifecycleHistoryIntent &&
|
||||||
|
!vatRelativeMonthFollowup) {
|
||||||
if (previousPeriodFrom) {
|
if (previousPeriodFrom) {
|
||||||
merged.period_from = previousPeriodFrom;
|
merged.period_from = previousPeriodFrom;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,18 @@ function hasSelectedObjectInventoryActionCue(text) {
|
||||||
const value = String(text ?? "");
|
const value = String(text ?? "");
|
||||||
return (/(?:кому[\s\S]{0,80}(?:продал[аи]?|реализова[нлт][а-я]*|поставил[аи]?|поставлен[а-я]*|отгрузил[аи]?|отгружен[а-я]*)|кому\s+был\s+продан|куда[\s\S]{0,80}(?:продал[аи]?|реализова[нлт][а-я]*|поставил[аи]?|поставлен[а-я]*|отгрузил[аи]?|отгружен[а-я]*)|кто[\s\S]{0,40}купил|кто\s+это\s+поставил|кто\s+поставил|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+мы\s+купили|где\s+куплено|по\s+каким\s+документам|какими\s+документами|покажи\s+документы|документы[\s\S]{0,80}(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару)|операци)|документы\s+закупки|buyer|sale\s+trace|supplier|vendor|purchase\s+documents|purchase[\s-]?to[\s-]?sale|old\s+purchase|aged\s+stock)/iu.test(value) || (0, inventoryLifecycleCueHelpers_1.hasInventoryProfitabilityCue)(value));
|
return (/(?:кому[\s\S]{0,80}(?:продал[аи]?|реализова[нлт][а-я]*|поставил[аи]?|поставлен[а-я]*|отгрузил[аи]?|отгружен[а-я]*)|кому\s+был\s+продан|куда[\s\S]{0,80}(?:продал[аи]?|реализова[нлт][а-я]*|поставил[аи]?|поставлен[а-я]*|отгрузил[аи]?|отгружен[а-я]*)|кто[\s\S]{0,40}купил|кто\s+это\s+поставил|кто\s+поставил|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+мы\s+купили|где\s+куплено|по\s+каким\s+документам|какими\s+документами|покажи\s+документы|документы[\s\S]{0,80}(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару)|операци)|документы\s+закупки|buyer|sale\s+trace|supplier|vendor|purchase\s+documents|purchase[\s-]?to[\s-]?sale|old\s+purchase|aged\s+stock)/iu.test(value) || (0, inventoryLifecycleCueHelpers_1.hasInventoryProfitabilityCue)(value));
|
||||||
}
|
}
|
||||||
|
function hasShortInventoryPurchaseFollowupCue(text) {
|
||||||
|
return /(?:^|[\s,.;:!?])(а\s+)?(?:купили\s+у\s+кого|у\s+кого\s+купили|поставщик|продавец|seller)(?:[\s,.;:!?]|$)/iu.test(String(text ?? ""));
|
||||||
|
}
|
||||||
|
function isInventorySelectedObjectOrRootIntent(intent) {
|
||||||
|
return (intent === "inventory_on_hand_as_of_date" ||
|
||||||
|
intent === "inventory_purchase_provenance_for_item" ||
|
||||||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_profitability_for_item" ||
|
||||||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
|
intent === "inventory_aging_by_purchase_date");
|
||||||
|
}
|
||||||
function isGenericCanonicalDriftIntent(intent) {
|
function isGenericCanonicalDriftIntent(intent) {
|
||||||
return (intent === "open_items_by_counterparty_or_contract" ||
|
return (intent === "open_items_by_counterparty_or_contract" ||
|
||||||
intent === "customer_revenue_and_payments" ||
|
intent === "customer_revenue_and_payments" ||
|
||||||
|
|
@ -19,6 +31,25 @@ function isGenericCanonicalDriftIntent(intent) {
|
||||||
intent === "bank_operations_by_contract" ||
|
intent === "bank_operations_by_contract" ||
|
||||||
intent === "documents_forming_balance");
|
intent === "documents_forming_balance");
|
||||||
}
|
}
|
||||||
|
function hasSameDateFollowupSignal(text) {
|
||||||
|
return /(?:эту\s+же\s+дат(?:у|е|ой)|ту\s+же\s+дат(?:у|е|ой)|same\s+date)/iu.test(String(text ?? ""));
|
||||||
|
}
|
||||||
|
function hasExplicitCurrentDateSignal(text) {
|
||||||
|
return /(?:текущ(?:ую|ая|ий|ее|ей)\s+дат(?:у|а|е|ой)|сегодняшн(?:юю|ий|ей)\s+дат(?:у|а|е|ой)|today|current\s+date)/iu.test(String(text ?? ""));
|
||||||
|
}
|
||||||
|
function hasInventoryTemporalRootFollowupCue(text) {
|
||||||
|
const value = String(text ?? "").trim().toLowerCase();
|
||||||
|
if (!value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const tokenCount = value.split(/\s+/).filter(Boolean).length;
|
||||||
|
const hasMonthYearCue = /(?:январ(?:ь|е)|феврал(?:ь|е)|март(?:е)?|апрел(?:ь|е)|ма(?:й|е)|июн(?:ь|е)|июл(?:ь|е)|август(?:е)?|сентябр(?:ь|е)|октябр(?:ь|е)|ноябр(?:ь|е)|декабр(?:ь|е))(?:\s+\d{4})?/iu.test(value) || /\b(?:19|20)\d{2}\b/u.test(value);
|
||||||
|
if (tokenCount <= 3 && hasMonthYearCue) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const hasInventoryLexeme = /(?:остат|склад|товар|позици|номенклатур)/iu.test(value);
|
||||||
|
return hasInventoryLexeme && (hasMonthYearCue || hasSameDateFollowupSignal(value));
|
||||||
|
}
|
||||||
function shouldPreferRawFollowupMessage(userMessage, addressInputMessage, carryover, addressPreDecompose, toNonEmptyString) {
|
function shouldPreferRawFollowupMessage(userMessage, addressInputMessage, carryover, addressPreDecompose, toNonEmptyString) {
|
||||||
if (!carryover?.followupContext || typeof carryover.followupContext !== "object") {
|
if (!carryover?.followupContext || typeof carryover.followupContext !== "object") {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -33,12 +64,29 @@ function shouldPreferRawFollowupMessage(userMessage, addressInputMessage, carryo
|
||||||
: null;
|
: null;
|
||||||
const mode = toNonEmptyString(predecomposeContract?.mode) ?? "unknown";
|
const mode = toNonEmptyString(predecomposeContract?.mode) ?? "unknown";
|
||||||
const intent = toNonEmptyString(predecomposeContract?.intent) ?? "unknown";
|
const intent = toNonEmptyString(predecomposeContract?.intent) ?? "unknown";
|
||||||
|
const followupContext = carryover.followupContext && typeof carryover.followupContext === "object"
|
||||||
|
? carryover.followupContext
|
||||||
|
: null;
|
||||||
|
const previousIntent = toNonEmptyString(followupContext?.previous_intent);
|
||||||
|
const rootIntent = toNonEmptyString(followupContext?.root_intent);
|
||||||
|
const previousAnchorType = toNonEmptyString(followupContext?.previous_anchor_type);
|
||||||
|
const hasInventoryItemCarryover = previousAnchorType === "item" && isInventorySelectedObjectOrRootIntent(previousIntent);
|
||||||
|
const hasInventoryFrameCarryover = isInventorySelectedObjectOrRootIntent(previousIntent) ||
|
||||||
|
isInventorySelectedObjectOrRootIntent(rootIntent);
|
||||||
if (mode === "unsupported" && intent === "unknown") {
|
if (mode === "unsupported" && intent === "unknown") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return (hasSelectedObjectInventorySignal(rawMessage) &&
|
if (hasSameDateFollowupSignal(rawMessage) && hasExplicitCurrentDateSignal(canonicalMessage)) {
|
||||||
hasSelectedObjectInventoryActionCue(rawMessage) &&
|
return true;
|
||||||
isGenericCanonicalDriftIntent(intent));
|
}
|
||||||
|
if (hasInventoryFrameCarryover &&
|
||||||
|
hasInventoryTemporalRootFollowupCue(rawMessage) &&
|
||||||
|
(intent === "account_balance_snapshot" || intent === "documents_forming_balance" || intent === "unknown")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return ((hasSelectedObjectInventorySignal(rawMessage) || hasInventoryItemCarryover) &&
|
||||||
|
(hasSelectedObjectInventoryActionCue(rawMessage) || hasShortInventoryPurchaseFollowupCue(rawMessage)) &&
|
||||||
|
(isGenericCanonicalDriftIntent(intent) || intent === "unknown"));
|
||||||
}
|
}
|
||||||
function fallbackAddressPreDecompose(userMessage, llmProvider, buildAddressLlmPredecomposeContractV1, sanitizeAddressMessageForFallback) {
|
function fallbackAddressPreDecompose(userMessage, llmProvider, buildAddressLlmPredecomposeContractV1, sanitizeAddressMessageForFallback) {
|
||||||
const provider = llmProvider === "local" ? "local" : llmProvider === "openai" ? "openai" : null;
|
const provider = llmProvider === "local" ? "local" : llmProvider === "openai" ? "openai" : null;
|
||||||
|
|
|
||||||
|
|
@ -159,8 +159,16 @@ function createAssistantRoutePolicy(deps) {
|
||||||
hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) ||
|
hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) ||
|
||||||
hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) ||
|
hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) ||
|
||||||
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage);
|
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage);
|
||||||
|
const followupPreviousIntent = toNonEmptyString(followupContext?.previous_intent);
|
||||||
|
const followupPreviousFilters = followupContext?.previous_filters && typeof followupContext.previous_filters === "object"
|
||||||
|
? followupContext.previous_filters
|
||||||
|
: null;
|
||||||
const protectedInventoryShortFollowup = Boolean(followupContext &&
|
const protectedInventoryShortFollowup = Boolean(followupContext &&
|
||||||
isInventorySelectedObjectIntent(toNonEmptyString(followupContext.previous_intent)) &&
|
(isInventorySelectedObjectIntent(followupPreviousIntent) ||
|
||||||
|
(followupPreviousIntent === "inventory_on_hand_as_of_date" &&
|
||||||
|
(toNonEmptyString(followupContext.previous_anchor_type) === "item" ||
|
||||||
|
toNonEmptyString(followupContext.previous_anchor_value) ||
|
||||||
|
toNonEmptyString(followupPreviousFilters?.item)))) &&
|
||||||
(hasShortInventoryObjectFollowupSignal(rawUserMessage) ||
|
(hasShortInventoryObjectFollowupSignal(rawUserMessage) ||
|
||||||
hasShortInventoryObjectFollowupSignal(repairedRawUserMessage) ||
|
hasShortInventoryObjectFollowupSignal(repairedRawUserMessage) ||
|
||||||
hasShortInventoryObjectFollowupSignal(effectiveAddressUserMessage) ||
|
hasShortInventoryObjectFollowupSignal(effectiveAddressUserMessage) ||
|
||||||
|
|
|
||||||
|
|
@ -2799,8 +2799,11 @@ function hasShortInventoryObjectFollowupSignal(userMessage) {
|
||||||
if (minTokens > 8) {
|
if (minTokens > 8) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const hasDirectPurchaseFollowupCue = (sample) => /(?:^|[\s,.;:!?])(?:а\s+)?(?:у|от)\s+кого(?:\s+\S+){0,5}\s+купил(?:и|о)?(?=$|[\s,.;:!?])/iu.test(sample) ||
|
||||||
|
/(?:^|[\s,.;:!?])(?:а\s+)?купил(?:и|о)?(?:\s+\S+){0,5}\s+(?:у|от)\s+кого(?=$|[\s,.;:!?])/iu.test(sample);
|
||||||
const hasDirectSaleFollowupCue = (sample) => /(?:кому|каму|куда)(?:\s+\S+){0,4}\s+(?:продали|продано|продан(?:о|а|ы)?|реализовали|реализован(?:о|а|ы)?)|(?:продали|продано|реализовали|реализован(?:о|а|ы)?)(?:\s+\S+){0,4}\s+(?:кому|каму|куда)|(?:^|\s)(?:продано|продали|реализовано|реализовали)(?=$|[\s,.;:!?])/iu.test(sample);
|
const hasDirectSaleFollowupCue = (sample) => /(?:кому|каму|куда)(?:\s+\S+){0,4}\s+(?:продали|продано|продан(?:о|а|ы)?|реализовали|реализован(?:о|а|ы)?)|(?:продали|продано|реализовали|реализован(?:о|а|ы)?)(?:\s+\S+){0,4}\s+(?:кому|каму|куда)|(?:^|\s)(?:продано|продали|реализовано|реализовали)(?=$|[\s,.;:!?])/iu.test(sample);
|
||||||
return samples.some((sample) => /^(?:кто|когда|документы|сумма|поставщик|покупатель)(?:\?)?$/iu.test(sample) ||
|
return samples.some((sample) => /^(?:кто|когда|документы|сумма|поставщик|покупатель|продавец|seller)(?:\?)?$/iu.test(sample) ||
|
||||||
|
hasDirectPurchaseFollowupCue(sample) ||
|
||||||
hasDirectSaleFollowupCue(sample) ||
|
hasDirectSaleFollowupCue(sample) ||
|
||||||
(0, decomposeStage_1.hasInventorySupplierFollowupCue)(sample) ||
|
(0, decomposeStage_1.hasInventorySupplierFollowupCue)(sample) ||
|
||||||
(0, decomposeStage_1.hasInventoryPurchaseDocumentsFollowupCue)(sample) ||
|
(0, decomposeStage_1.hasInventoryPurchaseDocumentsFollowupCue)(sample) ||
|
||||||
|
|
@ -2809,6 +2812,41 @@ function hasShortInventoryObjectFollowupSignal(userMessage) {
|
||||||
(0, decomposeStage_1.hasInventorySaleFollowupCue)(sample) ||
|
(0, decomposeStage_1.hasInventorySaleFollowupCue)(sample) ||
|
||||||
(0, decomposeStage_1.hasInventoryPurchaseToSaleChainFollowupCue)(sample));
|
(0, decomposeStage_1.hasInventoryPurchaseToSaleChainFollowupCue)(sample));
|
||||||
}
|
}
|
||||||
|
function hasInventoryRootTemporalFollowupSignal(userMessage, sourceIntentHint, hasInventoryRootFrame) {
|
||||||
|
if (!hasInventoryRootFrame) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||||
|
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
|
isInventorySelectedObjectIntent(sourceIntentHint))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
const hasMonthYearCue = samples.some((sample) => /(?:январ|феврал|март|апрел|ма(?:й|е|я)|июн|июл|август|сентябр|октябр|ноябр|декабр)(?:\s+\d{4})?/iu.test(sample) ||
|
||||||
|
/\b(?:19|20)\d{2}\b/u.test(sample));
|
||||||
|
const hasTemporalCue = samples.some((sample) => hasPeriodLiteral(sample) ||
|
||||||
|
hasShortNamedPeriodFollowupLiteral(sample) ||
|
||||||
|
hasShortCurrentDateFollowupLiteral(sample) ||
|
||||||
|
/(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|same\s+date|the\s+same\s+date)/iu.test(sample));
|
||||||
|
if (!hasTemporalCue && !hasMonthYearCue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (samples.some((sample) => hasForeignAccountingPivotOverInventoryMessage(sample))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasInventoryLexeme = samples.some((sample) => /(?:остат|склад|товар|номенклат|позиц)/iu.test(sample));
|
||||||
|
const hasPlainInventoryLexeme = samples.some((sample) => /(?:остат|склад|товар|номенклатур|позиц)/iu.test(sample));
|
||||||
|
return hasInventoryLexeme || hasPlainInventoryLexeme || minTokens <= 3;
|
||||||
|
}
|
||||||
function hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage = null) {
|
function hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage = null) {
|
||||||
const samples = [
|
const samples = [
|
||||||
compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase()),
|
compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase()),
|
||||||
|
|
@ -2890,8 +2928,22 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
(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 sourceIntentHint = toNonEmptyString(previousAddressDebug?.detected_intent);
|
||||||
const inventoryShortFollowupPrimary = isInventorySelectedObjectIntent(sourceIntentHint) && hasShortInventoryObjectFollowupSignal(userMessage);
|
const navigationFocusObjectHint = addressNavigationState &&
|
||||||
const inventoryShortFollowupAlternate = isInventorySelectedObjectIntent(sourceIntentHint) && toNonEmptyString(alternateMessage)
|
typeof addressNavigationState === "object" &&
|
||||||
|
addressNavigationState.session_context &&
|
||||||
|
typeof addressNavigationState.session_context === "object" &&
|
||||||
|
addressNavigationState.session_context.active_focus_object &&
|
||||||
|
typeof addressNavigationState.session_context.active_focus_object === "object"
|
||||||
|
? addressNavigationState.session_context.active_focus_object
|
||||||
|
: null;
|
||||||
|
const hasNavigationInventoryItemFocusHint = Boolean(toNonEmptyString(navigationFocusObjectHint?.label) &&
|
||||||
|
toNonEmptyString(navigationFocusObjectHint?.object_type) === "item" &&
|
||||||
|
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||||
|
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
|
isInventorySelectedObjectIntent(sourceIntentHint)));
|
||||||
|
let inventoryShortFollowupPrimary = (isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
||||||
|
hasShortInventoryObjectFollowupSignal(userMessage);
|
||||||
|
let inventoryShortFollowupAlternate = (isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) && toNonEmptyString(alternateMessage)
|
||||||
? hasShortInventoryObjectFollowupSignal(String(alternateMessage ?? ""))
|
? hasShortInventoryObjectFollowupSignal(String(alternateMessage ?? ""))
|
||||||
: false;
|
: false;
|
||||||
const debtRoleSwapPrimary = sourceIntentHint ? resolveDebtRoleSwapFollowupIntent(userMessage, sourceIntentHint) : null;
|
const debtRoleSwapPrimary = sourceIntentHint ? resolveDebtRoleSwapFollowupIntent(userMessage, sourceIntentHint) : null;
|
||||||
|
|
@ -2899,8 +2951,8 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
? resolveDebtRoleSwapFollowupIntent(String(alternateMessage ?? ""), sourceIntentHint)
|
? resolveDebtRoleSwapFollowupIntent(String(alternateMessage ?? ""), sourceIntentHint)
|
||||||
: null;
|
: null;
|
||||||
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
|
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
|
||||||
const hasPrimaryFollowupSignal = hasAddressFollowupContextSignal(userMessage) || Boolean(debtRoleSwapPrimary) || inventoryShortFollowupPrimary;
|
let hasPrimaryFollowupSignal = hasAddressFollowupContextSignal(userMessage) || Boolean(debtRoleSwapPrimary) || inventoryShortFollowupPrimary;
|
||||||
const hasAlternateFollowupSignal = toNonEmptyString(alternateMessage)
|
let hasAlternateFollowupSignal = toNonEmptyString(alternateMessage)
|
||||||
? hasAddressFollowupContextSignal(alternateMessage) || Boolean(debtRoleSwapAlternate) || inventoryShortFollowupAlternate
|
? hasAddressFollowupContextSignal(alternateMessage) || Boolean(debtRoleSwapAlternate) || inventoryShortFollowupAlternate
|
||||||
: false;
|
: false;
|
||||||
const hasPrimaryIndexReferenceSignal = extractDisplayedEntityIndexMention(userMessage) !== null;
|
const hasPrimaryIndexReferenceSignal = extractDisplayedEntityIndexMention(userMessage) !== null;
|
||||||
|
|
@ -2908,12 +2960,19 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
? extractDisplayedEntityIndexMention(String(alternateMessage ?? "")) !== null
|
? extractDisplayedEntityIndexMention(String(alternateMessage ?? "")) !== null
|
||||||
: false;
|
: false;
|
||||||
const hasIndexReferenceSignal = hasPrimaryIndexReferenceSignal || hasAlternateIndexReferenceSignal;
|
const hasIndexReferenceSignal = hasPrimaryIndexReferenceSignal || hasAlternateIndexReferenceSignal;
|
||||||
const hasStrongFollowupReference = hasPrimaryIndexReferenceSignal ||
|
const recentInventoryRootFrame = findRecentInventoryRootFrame(items);
|
||||||
|
const hasInventoryRootTemporalFollowupPrimary = hasInventoryRootTemporalFollowupSignal(userMessage, sourceIntentHint, Boolean(recentInventoryRootFrame));
|
||||||
|
const hasInventoryRootTemporalFollowupAlternate = toNonEmptyString(alternateMessage)
|
||||||
|
? hasInventoryRootTemporalFollowupSignal(String(alternateMessage ?? ""), sourceIntentHint, Boolean(recentInventoryRootFrame))
|
||||||
|
: false;
|
||||||
|
let hasStrongFollowupReference = hasPrimaryIndexReferenceSignal ||
|
||||||
hasAlternateIndexReferenceSignal ||
|
hasAlternateIndexReferenceSignal ||
|
||||||
hasOrganizationClarificationContinuation ||
|
hasOrganizationClarificationContinuation ||
|
||||||
hasImplicitContinuationSignal ||
|
hasImplicitContinuationSignal ||
|
||||||
inventoryShortFollowupPrimary ||
|
inventoryShortFollowupPrimary ||
|
||||||
inventoryShortFollowupAlternate ||
|
inventoryShortFollowupAlternate ||
|
||||||
|
hasInventoryRootTemporalFollowupPrimary ||
|
||||||
|
hasInventoryRootTemporalFollowupAlternate ||
|
||||||
Boolean(debtRoleSwapIntent) ||
|
Boolean(debtRoleSwapIntent) ||
|
||||||
hasFollowupMarker(userMessage) ||
|
hasFollowupMarker(userMessage) ||
|
||||||
hasReferentialPointer(userMessage) ||
|
hasReferentialPointer(userMessage) ||
|
||||||
|
|
@ -2925,6 +2984,8 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
if (hasStandaloneAddressTopic &&
|
if (hasStandaloneAddressTopic &&
|
||||||
!hasPrimaryFollowupSignal &&
|
!hasPrimaryFollowupSignal &&
|
||||||
!hasAlternateFollowupSignal &&
|
!hasAlternateFollowupSignal &&
|
||||||
|
!hasInventoryRootTemporalFollowupPrimary &&
|
||||||
|
!hasInventoryRootTemporalFollowupAlternate &&
|
||||||
!hasImplicitContinuationSignal &&
|
!hasImplicitContinuationSignal &&
|
||||||
!hasOrganizationClarificationContinuation &&
|
!hasOrganizationClarificationContinuation &&
|
||||||
!hasIndexReferenceSignal) {
|
!hasIndexReferenceSignal) {
|
||||||
|
|
@ -2932,6 +2993,8 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
}
|
}
|
||||||
if (!hasPrimaryFollowupSignal &&
|
if (!hasPrimaryFollowupSignal &&
|
||||||
!hasAlternateFollowupSignal &&
|
!hasAlternateFollowupSignal &&
|
||||||
|
!hasInventoryRootTemporalFollowupPrimary &&
|
||||||
|
!hasInventoryRootTemporalFollowupAlternate &&
|
||||||
!hasImplicitContinuationSignal &&
|
!hasImplicitContinuationSignal &&
|
||||||
!hasOrganizationClarificationContinuation &&
|
!hasOrganizationClarificationContinuation &&
|
||||||
!hasIndexReferenceSignal) {
|
!hasIndexReferenceSignal) {
|
||||||
|
|
@ -2993,6 +3056,41 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
: null;
|
: null;
|
||||||
const navigationFocusObjectType = toNonEmptyString(navigationFocusObject?.object_type);
|
const navigationFocusObjectType = toNonEmptyString(navigationFocusObject?.object_type);
|
||||||
const navigationFocusObjectLabel = toNonEmptyString(navigationFocusObject?.label);
|
const navigationFocusObjectLabel = toNonEmptyString(navigationFocusObject?.label);
|
||||||
|
const hasInventoryItemFocusCarryover = navigationFocusObjectType === "item" &&
|
||||||
|
navigationFocusObjectLabel &&
|
||||||
|
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||||
|
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
|
isInventorySelectedObjectIntent(sourceIntentHint));
|
||||||
|
if (!inventoryShortFollowupPrimary && hasInventoryItemFocusCarryover) {
|
||||||
|
inventoryShortFollowupPrimary = hasShortInventoryObjectFollowupSignal(userMessage);
|
||||||
|
}
|
||||||
|
if (!inventoryShortFollowupAlternate && hasInventoryItemFocusCarryover && toNonEmptyString(alternateMessage)) {
|
||||||
|
inventoryShortFollowupAlternate = hasShortInventoryObjectFollowupSignal(String(alternateMessage ?? ""));
|
||||||
|
}
|
||||||
|
hasPrimaryFollowupSignal = hasAddressFollowupContextSignal(userMessage) ||
|
||||||
|
Boolean(debtRoleSwapPrimary) ||
|
||||||
|
inventoryShortFollowupPrimary ||
|
||||||
|
hasInventoryRootTemporalFollowupPrimary;
|
||||||
|
hasAlternateFollowupSignal = toNonEmptyString(alternateMessage)
|
||||||
|
? hasAddressFollowupContextSignal(alternateMessage) ||
|
||||||
|
Boolean(debtRoleSwapAlternate) ||
|
||||||
|
inventoryShortFollowupAlternate ||
|
||||||
|
hasInventoryRootTemporalFollowupAlternate
|
||||||
|
: false;
|
||||||
|
hasStrongFollowupReference = hasPrimaryIndexReferenceSignal ||
|
||||||
|
hasAlternateIndexReferenceSignal ||
|
||||||
|
hasOrganizationClarificationContinuation ||
|
||||||
|
hasImplicitContinuationSignal ||
|
||||||
|
inventoryShortFollowupPrimary ||
|
||||||
|
inventoryShortFollowupAlternate ||
|
||||||
|
hasInventoryRootTemporalFollowupPrimary ||
|
||||||
|
hasInventoryRootTemporalFollowupAlternate ||
|
||||||
|
Boolean(debtRoleSwapIntent) ||
|
||||||
|
hasFollowupMarker(userMessage) ||
|
||||||
|
hasReferentialPointer(userMessage) ||
|
||||||
|
(toNonEmptyString(alternateMessage)
|
||||||
|
? hasFollowupMarker(String(alternateMessage ?? "")) || hasReferentialPointer(String(alternateMessage ?? ""))
|
||||||
|
: false);
|
||||||
const hasSelectedObjectInventorySignalPrimary = /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(userMessage ?? ""));
|
const hasSelectedObjectInventorySignalPrimary = /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(userMessage ?? ""));
|
||||||
const hasSelectedObjectInventorySignalAlternate = toNonEmptyString(alternateMessage)
|
const hasSelectedObjectInventorySignalAlternate = toNonEmptyString(alternateMessage)
|
||||||
? /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(alternateMessage ?? ""))
|
? /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(alternateMessage ?? ""))
|
||||||
|
|
@ -3054,18 +3152,39 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
if (!toNonEmptyString(previousFilters.organization) && organizationClarificationSelection) {
|
if (!toNonEmptyString(previousFilters.organization) && organizationClarificationSelection) {
|
||||||
previousFilters.organization = organizationClarificationSelection;
|
previousFilters.organization = organizationClarificationSelection;
|
||||||
}
|
}
|
||||||
if (!toNonEmptyString(previousFilters.as_of_date) && toNonEmptyString(navigationDateScope?.as_of_date)) {
|
const shouldBackfillPreviousDateScopeFromNavigation = sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||||
|
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
|
sourceIntentHint === "inventory_purchase_provenance_for_item" ||
|
||||||
|
sourceIntentHint === "inventory_purchase_documents_for_item" ||
|
||||||
|
sourceIntentHint === "inventory_sale_trace_for_item" ||
|
||||||
|
sourceIntentHint === "inventory_profitability_for_item" ||
|
||||||
|
sourceIntentHint === "inventory_purchase_to_sale_chain" ||
|
||||||
|
sourceIntentHint === "inventory_aging_by_purchase_date" ||
|
||||||
|
sourceIntentHint === "account_balance_snapshot" ||
|
||||||
|
sourceIntentHint === "documents_forming_balance";
|
||||||
|
if (shouldBackfillPreviousDateScopeFromNavigation &&
|
||||||
|
!toNonEmptyString(previousFilters.as_of_date) &&
|
||||||
|
toNonEmptyString(navigationDateScope?.as_of_date)) {
|
||||||
previousFilters.as_of_date = toNonEmptyString(navigationDateScope?.as_of_date);
|
previousFilters.as_of_date = toNonEmptyString(navigationDateScope?.as_of_date);
|
||||||
}
|
}
|
||||||
if (!toNonEmptyString(previousFilters.period_from) && toNonEmptyString(navigationDateScope?.period_from)) {
|
if (shouldBackfillPreviousDateScopeFromNavigation &&
|
||||||
|
!toNonEmptyString(previousFilters.period_from) &&
|
||||||
|
toNonEmptyString(navigationDateScope?.period_from)) {
|
||||||
previousFilters.period_from = toNonEmptyString(navigationDateScope?.period_from);
|
previousFilters.period_from = toNonEmptyString(navigationDateScope?.period_from);
|
||||||
}
|
}
|
||||||
if (!toNonEmptyString(previousFilters.period_to) && toNonEmptyString(navigationDateScope?.period_to)) {
|
if (shouldBackfillPreviousDateScopeFromNavigation &&
|
||||||
|
!toNonEmptyString(previousFilters.period_to) &&
|
||||||
|
toNonEmptyString(navigationDateScope?.period_to)) {
|
||||||
previousFilters.period_to = toNonEmptyString(navigationDateScope?.period_to);
|
previousFilters.period_to = toNonEmptyString(navigationDateScope?.period_to);
|
||||||
}
|
}
|
||||||
const rootContextOnlyPivot = Boolean((isInventorySelectedObjectIntent(sourceIntentHint) || currentFrameKind === "inventory_drilldown") &&
|
const rootContextOnlyPivot = Boolean((isInventorySelectedObjectIntent(sourceIntentHint) || currentFrameKind === "inventory_drilldown") &&
|
||||||
hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage));
|
hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage));
|
||||||
if (rootContextOnlyPivot) {
|
const inventoryRootTemporalPivot = Boolean(inventoryRootFrame &&
|
||||||
|
(isInventorySelectedObjectIntent(sourceIntentHint) || currentFrameKind === "inventory_drilldown") &&
|
||||||
|
(hasInventoryRootTemporalFollowupPrimary || hasInventoryRootTemporalFollowupAlternate) &&
|
||||||
|
!hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage));
|
||||||
|
const rootScopedPivot = rootContextOnlyPivot || inventoryRootTemporalPivot;
|
||||||
|
if (rootScopedPivot) {
|
||||||
previousIntent = null;
|
previousIntent = null;
|
||||||
previousAnchorType = null;
|
previousAnchorType = null;
|
||||||
previousAnchor = null;
|
previousAnchor = null;
|
||||||
|
|
@ -3079,7 +3198,7 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
(toNonEmptyString(alternateMessage)
|
(toNonEmptyString(alternateMessage)
|
||||||
? resolveDisplayedAddressEntityMention(String(alternateMessage ?? ""), displayedEntities)
|
? resolveDisplayedAddressEntityMention(String(alternateMessage ?? ""), displayedEntities)
|
||||||
: null);
|
: null);
|
||||||
if (resolvedEntityFromFollowup && !rootContextOnlyPivot) {
|
if (resolvedEntityFromFollowup && !rootScopedPivot) {
|
||||||
if (resolvedEntityFromFollowup.entityType === "counterparty") {
|
if (resolvedEntityFromFollowup.entityType === "counterparty") {
|
||||||
previousFilters.counterparty = resolvedEntityFromFollowup.value;
|
previousFilters.counterparty = resolvedEntityFromFollowup.value;
|
||||||
previousAnchorType = "counterparty";
|
previousAnchorType = "counterparty";
|
||||||
|
|
@ -3100,7 +3219,7 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
followupSelectionMode = "carry_referenced_entity";
|
followupSelectionMode = "carry_referenced_entity";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!rootContextOnlyPivot &&
|
if (!rootScopedPivot &&
|
||||||
!toNonEmptyString(previousFilters.item) &&
|
!toNonEmptyString(previousFilters.item) &&
|
||||||
navigationFocusObjectType === "item" &&
|
navigationFocusObjectType === "item" &&
|
||||||
navigationFocusObjectLabel &&
|
navigationFocusObjectLabel &&
|
||||||
|
|
@ -3142,7 +3261,7 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
previous_anchor_type: previousAnchorType ?? undefined,
|
previous_anchor_type: previousAnchorType ?? undefined,
|
||||||
previous_anchor_value: previousAnchor,
|
previous_anchor_value: previousAnchor,
|
||||||
resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined,
|
resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined,
|
||||||
root_context_only: rootContextOnlyPivot || undefined,
|
root_context_only: rootScopedPivot || undefined,
|
||||||
root_intent: inventoryRootFrame?.intent ?? undefined,
|
root_intent: inventoryRootFrame?.intent ?? undefined,
|
||||||
root_filters: inventoryRootFrame?.filters ?? undefined,
|
root_filters: inventoryRootFrame?.filters ?? undefined,
|
||||||
root_anchor_type: inventoryRootFrame?.anchorType ?? undefined,
|
root_anchor_type: inventoryRootFrame?.anchorType ?? undefined,
|
||||||
|
|
@ -3163,11 +3282,13 @@ function buildAddressDialogContinuationContractV2(userMessage, effectiveMessage,
|
||||||
const previousIntent = toNonEmptyString(carryoverMeta?.previousSourceIntent) ?? null;
|
const previousIntent = toNonEmptyString(carryoverMeta?.previousSourceIntent) ?? null;
|
||||||
const selectionMode = toNonEmptyString(carryoverMeta?.followupSelectionMode) ?? null;
|
const selectionMode = toNonEmptyString(carryoverMeta?.followupSelectionMode) ?? null;
|
||||||
const rootContextOnly = selectionMode === "carry_root_context";
|
const rootContextOnly = selectionMode === "carry_root_context";
|
||||||
const explicitIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
|
const explicitIntentRaw = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
|
||||||
|
const explicitIntent = explicitIntentRaw === "unknown" ? null : explicitIntentRaw;
|
||||||
|
const rootIntent = toNonEmptyString(carryoverMeta?.followupContext?.root_intent) ?? null;
|
||||||
const targetIntent = selectionMode === "switch_to_suggested_intent"
|
const targetIntent = selectionMode === "switch_to_suggested_intent"
|
||||||
? toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null
|
? toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null
|
||||||
: rootContextOnly
|
: rootContextOnly
|
||||||
? explicitIntent ?? null
|
? rootIntent ?? explicitIntent ?? null
|
||||||
: explicitIntent ?? toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null;
|
: explicitIntent ?? toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null;
|
||||||
const hasImplicitContinuationSignal = Boolean(carryoverMeta?.hasImplicitContinuationSignal);
|
const hasImplicitContinuationSignal = Boolean(carryoverMeta?.hasImplicitContinuationSignal);
|
||||||
const rewrittenByPredecompose = compactWhitespace(sourceMessage.toLowerCase()) !== compactWhitespace(canonicalMessage.toLowerCase());
|
const rewrittenByPredecompose = compactWhitespace(sourceMessage.toLowerCase()) !== compactWhitespace(canonicalMessage.toLowerCase());
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ function hasInventorySupplierCue(text) {
|
||||||
if (/(?:купил(?:и|о)?\s+у\s+кого|куплен(?:о)?\s+у\s+кого|купил(?:и|о)?\s+от\s+кого|куплен(?:о)?\s+от\s+кого)/iu.test(value)) {
|
if (/(?:купил(?:и|о)?\s+у\s+кого|куплен(?:о)?\s+у\s+кого|купил(?:и|о)?\s+от\s+кого|куплен(?:о)?\s+от\s+кого)/iu.test(value)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (/(?:кто\s+(?:был\s+)?продавец|кто\s+нам\s+продал|кто\s+продал\s+нам|(?:^|[\s,.;:!?()\-])(?:продавец|seller)(?=$|[\s,.;:!?()\-]))/iu.test(value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (/(?:кто\s+(?:(?:это|этот\s+товар|эту\s+позицию)\s+)?(?:нам\s+)?поставил|кто\s+(?:нам\s+)?поставил\s+(?:это|этот\s+товар|эту\s+позицию)|от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+(?:мы\s+)?взяли(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|откуда\s+(?:мы\s+)?взяли(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+куплено|supplier|vendor|поставщик)/iu.test(value)) {
|
if (/(?:кто\s+(?:(?:это|этот\s+товар|эту\s+позицию)\s+)?(?:нам\s+)?поставил|кто\s+(?:нам\s+)?поставил\s+(?:это|этот\s+товар|эту\s+позицию)|от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+(?:мы\s+)?взяли(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|откуда\s+(?:мы\s+)?взяли(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+куплено|supplier|vendor|поставщик)/iu.test(value)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1189,6 +1189,12 @@ function isTemporalWarehousePhrase(candidate: string): boolean {
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/ё/g, "е")
|
.replace(/ё/g, "е")
|
||||||
.trim();
|
.trim();
|
||||||
|
if (/^(?:на\s+)?(?:эту|ту|текущ(?:ую|ая|ий|ее|ей)|сегодняшн(?:юю|ая|ий|ее|ей))(?:\s+же)?\s+дат(?:у|а|е|ой)$/iu.test(normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (/^(?:нет|не\s+было|не\s+было\s+ли)(?:\s+на\s+(?:эту|ту|такую)\s+дат(?:у|е|ой))?$/iu.test(normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return /^(?:в|на)\s+(?:январ(?:е|ь)|феврал(?:е|ь)|март(?:е)?|апрел(?:е|ь)|ма(?:й|е)|июн(?:е|ь)|июл(?:е|ь)|август(?:е)?|сентябр(?:е|ь)|октябр(?:е|ь)|ноябр(?:е|ь)|декабр(?:е|ь))(?:\s+\d{4}(?:\s+г(?:\.|ода)?)?)?$/iu.test(
|
return /^(?:в|на)\s+(?:январ(?:е|ь)|феврал(?:е|ь)|март(?:е)?|апрел(?:е|ь)|ма(?:й|е)|июн(?:е|ь)|июл(?:е|ь)|август(?:е)?|сентябр(?:е|ь)|октябр(?:е|ь)|ноябр(?:е|ь)|декабр(?:е|ь))(?:\s+\d{4}(?:\s+г(?:\.|ода)?)?)?$/iu.test(
|
||||||
normalized
|
normalized
|
||||||
);
|
);
|
||||||
|
|
@ -1244,7 +1250,8 @@ function isLowQualityWarehouseAnchorValue(rawValue: string): boolean {
|
||||||
"охуеть",
|
"охуеть",
|
||||||
"пиздец",
|
"пиздец",
|
||||||
"блять",
|
"блять",
|
||||||
"бля"
|
"бля",
|
||||||
|
"нет"
|
||||||
]);
|
]);
|
||||||
const tokens = value
|
const tokens = value
|
||||||
.split(/[^a-zа-я0-9]+/iu)
|
.split(/[^a-zа-я0-9]+/iu)
|
||||||
|
|
@ -1279,7 +1286,9 @@ function hasImplicitSelfScopeSignal(text: string): boolean {
|
||||||
|
|
||||||
function isImplicitSelfScopeWarehouseAnchor(candidate: string): boolean {
|
function isImplicitSelfScopeWarehouseAnchor(candidate: string): boolean {
|
||||||
const normalized = normalizeSemanticAnchorCandidate(candidate);
|
const normalized = normalizeSemanticAnchorCandidate(candidate);
|
||||||
return /^(?:у\s+нас|у\s+себя|у\s+меня|наш(?:ем|ей|его|их|а|е)?|сво(?:ем|ей|его|их|я|е)?)$/iu.test(normalized);
|
return /^(?:у\s+нас|у\s+себя|у\s+меня|наш(?:ем|ей|его|их|а|е)?|сво(?:ем|ей|его|их|я|е)?)(?:\s+(?:висит|висят|висело|висели|лежит|лежат|лежало|лежали))?$/iu.test(
|
||||||
|
normalized
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasSelectedObjectScopeSignal(text: string): boolean {
|
function hasSelectedObjectScopeSignal(text: string): boolean {
|
||||||
|
|
|
||||||
|
|
@ -1286,11 +1286,23 @@ function toNormalizedRows(rows: Array<Record<string, unknown>>): NormalizedAddre
|
||||||
}
|
}
|
||||||
|
|
||||||
function rowSearchableText(row: NormalizedAddressRow): string {
|
function rowSearchableText(row: NormalizedAddressRow): string {
|
||||||
return [row.registrator, row.item ?? "", row.warehouse ?? "", row.account_dt ?? "", row.account_kt ?? "", ...row.analytics]
|
return [
|
||||||
|
row.registrator,
|
||||||
|
row.item ?? "",
|
||||||
|
row.warehouse ?? "",
|
||||||
|
row.organization ?? "",
|
||||||
|
row.account_dt ?? "",
|
||||||
|
row.account_kt ?? "",
|
||||||
|
...row.analytics
|
||||||
|
]
|
||||||
.join(" ")
|
.join(" ")
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rowOrganizationSearchableText(row: NormalizedAddressRow): string {
|
||||||
|
return [row.organization ?? "", row.registrator, ...row.analytics].join(" ").toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
function rowMatchesAnyAccount(row: NormalizedAddressRow, accountScope: string[]): boolean {
|
function rowMatchesAnyAccount(row: NormalizedAddressRow, accountScope: string[]): boolean {
|
||||||
if (accountScope.length === 0) {
|
if (accountScope.length === 0) {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -1366,9 +1378,12 @@ function applyAddressFilters(rows: NormalizedAddressRow[], filters: AddressFilte
|
||||||
if (filters.organization && String(filters.organization).trim()) {
|
if (filters.organization && String(filters.organization).trim()) {
|
||||||
const needle = String(filters.organization);
|
const needle = String(filters.organization);
|
||||||
const before = filtered.length;
|
const before = filtered.length;
|
||||||
filtered = filtered.filter((row) => matchesAnchorText(rowSearchableText(row), needle));
|
const organizationMaterialized = filtered.some((row) => Boolean(String(row.organization ?? "").trim()));
|
||||||
if (before > 0 && filtered.length === 0 && mismatchReason === null) {
|
if (organizationMaterialized) {
|
||||||
mismatchReason = "organization_anchor_not_matched_in_materialized_rows";
|
filtered = filtered.filter((row) => matchesAnchorText(rowOrganizationSearchableText(row), needle));
|
||||||
|
if (before > 0 && filtered.length === 0 && mismatchReason === null) {
|
||||||
|
mismatchReason = "organization_anchor_not_matched_in_materialized_rows";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1181,9 +1181,30 @@ function buildInventoryMovementQuery(
|
||||||
: side === "kt"
|
: side === "kt"
|
||||||
? creditPredicate
|
? creditPredicate
|
||||||
: `(${debitPredicate} ИЛИ ${creditPredicate})`;
|
: `(${debitPredicate} ИЛИ ${creditPredicate})`;
|
||||||
|
const itemFieldPaths =
|
||||||
|
side === "dt"
|
||||||
|
? ["Движения.СубконтоДт1", "Движения.СубконтоДт2", "Движения.СубконтоДт3"]
|
||||||
|
: side === "kt"
|
||||||
|
? ["Движения.СубконтоКт1", "Движения.СубконтоКт2", "Движения.СубконтоКт3"]
|
||||||
|
: [
|
||||||
|
"Движения.СубконтоДт1",
|
||||||
|
"Движения.СубконтоДт2",
|
||||||
|
"Движения.СубконтоДт3",
|
||||||
|
"Движения.СубконтоКт1",
|
||||||
|
"Движения.СубконтоКт2",
|
||||||
|
"Движения.СубконтоКт3"
|
||||||
|
];
|
||||||
|
const itemCondition = buildInventoryItemReferenceCondition(filters, itemFieldPaths);
|
||||||
return INVENTORY_MOVEMENTS_QUERY_TEMPLATE
|
return INVENTORY_MOVEMENTS_QUERY_TEMPLATE
|
||||||
.replace("__LIMIT__", String(resolvedLimit))
|
.replace("__LIMIT__", String(resolvedLimit))
|
||||||
.replace("__WHERE_CLAUSE__", buildWhereClause(filters, "Движения.Период", [inventoryCondition]))
|
.replace(
|
||||||
|
"__WHERE_CLAUSE__",
|
||||||
|
buildWhereClause(
|
||||||
|
filters,
|
||||||
|
"Движения.Период",
|
||||||
|
[inventoryCondition, itemCondition].filter((item): item is string => Boolean(item))
|
||||||
|
)
|
||||||
|
)
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1469,7 +1490,7 @@ export function buildAddressRecipePlan(
|
||||||
: recipe.query_template === "inventory_supplier_stock_overlap_profile"
|
: recipe.query_template === "inventory_supplier_stock_overlap_profile"
|
||||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||||
: recipe.query_template === "inventory_sale_trace_profile"
|
: recipe.query_template === "inventory_sale_trace_profile"
|
||||||
? buildInventorySaleDocumentQuery(filters, resolvedLimit)
|
? buildInventoryMovementQuery(filters, resolvedLimit, "kt")
|
||||||
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
||||||
? buildInventoryMovementQuery(filters, resolvedLimit, "either")
|
? buildInventoryMovementQuery(filters, resolvedLimit, "either")
|
||||||
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ export interface AddressFollowupContext {
|
||||||
| "unknown"
|
| "unknown"
|
||||||
| null;
|
| null;
|
||||||
root_anchor_value?: string | null;
|
root_anchor_value?: string | null;
|
||||||
|
root_context_only?: boolean;
|
||||||
current_frame_kind?: "generic" | "inventory_root" | "inventory_drilldown";
|
current_frame_kind?: "generic" | "inventory_root" | "inventory_drilldown";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,7 +92,7 @@ function hasAllTimeHint(text: string): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasSameDateHint(text: string): boolean {
|
function hasSameDateHint(text: string): boolean {
|
||||||
return /(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|на\s+эту\s+дат[ауеы]|эту\s+дат[ауеы]|та\s+же\s+дата|same\s+date|as\s+of\s+same\s+date|the\s+same\s+date)/iu.test(
|
return /(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|на\s+эту\s+дат[ауеы]|эту\s+дат[ауеы]|та\s+же\s+дата|дат[ауеы],?\s+котор(?:ую|ая)\s+(?:до\s+этого|раньше|ранее)\s+(?:рассматривали|смотрели)|дат[ауеы],?\s+которая\s+был[ао]?\s+ранее\s+рассмотрен[ао]?|same\s+date|as\s+of\s+same\s+date|the\s+same\s+date|date\s+we\s+looked\s+at\s+before|previously\s+considered\s+date)/iu.test(
|
||||||
String(text ?? "")
|
String(text ?? "")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -487,6 +488,41 @@ function resolveRelativeMonthPeriodFromInventoryRoot(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveRelativeMonthPeriodFromFollowupYear(
|
||||||
|
userMessage: string,
|
||||||
|
followupContext: AddressFollowupContext | null
|
||||||
|
): { period_from: string; period_to: string; as_of_date: string } | null {
|
||||||
|
if (!followupContext) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const month = resolveMonthNumberFromText(userMessage);
|
||||||
|
if (!month) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const normalized = String(userMessage ?? "");
|
||||||
|
if (hasExplicitPeriodLiteral(normalized) || hasExplicitCurrentDateHint(normalized) || hasSameDateHint(normalized)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const shortTemporalPatch = getTokenCount(normalized) <= 8 || hasRelativeYearHint(normalized);
|
||||||
|
if (!shortTemporalPatch) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const year =
|
||||||
|
resolveYearFromFilters(followupContext.previous_filters) ??
|
||||||
|
resolveYearFromFilters(followupContext.root_filters);
|
||||||
|
if (!year) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
||||||
|
const periodFrom = `${year}-${String(month).padStart(2, "0")}-01`;
|
||||||
|
const periodTo = `${year}-${String(month).padStart(2, "0")}-${String(lastDay).padStart(2, "0")}`;
|
||||||
|
return {
|
||||||
|
period_from: periodFrom,
|
||||||
|
period_to: periodTo,
|
||||||
|
as_of_date: periodTo
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function shouldRestoreInventoryRootFrame(
|
function shouldRestoreInventoryRootFrame(
|
||||||
userMessage: string,
|
userMessage: string,
|
||||||
intent: AddressIntent,
|
intent: AddressIntent,
|
||||||
|
|
@ -498,6 +534,7 @@ function shouldRestoreInventoryRootFrame(
|
||||||
}
|
}
|
||||||
const currentFrameKind = followupContext.current_frame_kind ?? null;
|
const currentFrameKind = followupContext.current_frame_kind ?? null;
|
||||||
const previousIntent = followupContext.previous_intent;
|
const previousIntent = followupContext.previous_intent;
|
||||||
|
const rootContextOnly = followupContext.root_context_only === true;
|
||||||
const comingFromInventoryDrilldown =
|
const comingFromInventoryDrilldown =
|
||||||
currentFrameKind === "inventory_drilldown" || isInventoryDrilldownFrameIntent(previousIntent);
|
currentFrameKind === "inventory_drilldown" || isInventoryDrilldownFrameIntent(previousIntent);
|
||||||
const normalized = String(userMessage ?? "");
|
const normalized = String(userMessage ?? "");
|
||||||
|
|
@ -508,6 +545,7 @@ function shouldRestoreInventoryRootFrame(
|
||||||
);
|
);
|
||||||
const canReenterInventoryRoot =
|
const canReenterInventoryRoot =
|
||||||
comingFromInventoryDrilldown ||
|
comingFromInventoryDrilldown ||
|
||||||
|
rootContextOnly ||
|
||||||
(currentFrameKind === "inventory_root" && hasSamePeriodHint(normalized)) ||
|
(currentFrameKind === "inventory_root" && hasSamePeriodHint(normalized)) ||
|
||||||
(currentFrameKind === "generic" && hasInventoryRootRestatementCue && hasSamePeriodHint(normalized));
|
(currentFrameKind === "generic" && hasInventoryRootRestatementCue && hasSamePeriodHint(normalized));
|
||||||
if (!canReenterInventoryRoot) {
|
if (!canReenterInventoryRoot) {
|
||||||
|
|
@ -517,7 +555,7 @@ function shouldRestoreInventoryRootFrame(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
hasSelectedObjectInventorySignal(normalized) ||
|
(hasSelectedObjectInventorySignal(normalized) && !hasInventoryRootRestatementCue) ||
|
||||||
hasInventorySupplierFollowupCue(normalized) ||
|
hasInventorySupplierFollowupCue(normalized) ||
|
||||||
hasInventoryPurchaseDocumentsFollowupCue(normalized) ||
|
hasInventoryPurchaseDocumentsFollowupCue(normalized) ||
|
||||||
hasInventoryPurchaseDateFollowupCue(normalized) ||
|
hasInventoryPurchaseDateFollowupCue(normalized) ||
|
||||||
|
|
@ -533,6 +571,7 @@ function shouldRestoreInventoryRootFrame(
|
||||||
const hasTemporalPatch =
|
const hasTemporalPatch =
|
||||||
hasExplicitPeriodWindow(extractedFilters) ||
|
hasExplicitPeriodWindow(extractedFilters) ||
|
||||||
Boolean(toNonEmptyString(extractedFilters.as_of_date)) ||
|
Boolean(toNonEmptyString(extractedFilters.as_of_date)) ||
|
||||||
|
hasSameDateHint(normalized) ||
|
||||||
hasSamePeriodHint(normalized) ||
|
hasSamePeriodHint(normalized) ||
|
||||||
hasExplicitPeriodLiteral(normalized) ||
|
hasExplicitPeriodLiteral(normalized) ||
|
||||||
Boolean(resolveRelativeMonthPeriodFromInventoryRoot(normalized, followupContext));
|
Boolean(resolveRelativeMonthPeriodFromInventoryRoot(normalized, followupContext));
|
||||||
|
|
@ -709,9 +748,11 @@ function mergeFollowupFilters(
|
||||||
const previousPeriodFrom = toNonEmptyString(previous.period_from);
|
const previousPeriodFrom = toNonEmptyString(previous.period_from);
|
||||||
const previousPeriodTo = toNonEmptyString(previous.period_to);
|
const previousPeriodTo = toNonEmptyString(previous.period_to);
|
||||||
const relativeMonthFromInventoryRoot = resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext);
|
const relativeMonthFromInventoryRoot = resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext);
|
||||||
|
const relativeMonthFromFollowupYear = resolveRelativeMonthPeriodFromFollowupYear(userMessage, followupContext);
|
||||||
const allTimeRequested = hasAllTimeHint(userMessage);
|
const allTimeRequested = hasAllTimeHint(userMessage);
|
||||||
const sameDateRequested = hasSameDateHint(userMessage);
|
const sameDateRequested = hasSameDateHint(userMessage);
|
||||||
const samePeriodRequested = hasSamePeriodHint(userMessage);
|
const samePeriodRequested = hasSamePeriodHint(userMessage);
|
||||||
|
const explicitQuotedItem = extractSelectedObjectItemFromFollowupText(userMessage);
|
||||||
if (!toNonEmptyString(merged.organization) && previousOrganization) {
|
if (!toNonEmptyString(merged.organization) && previousOrganization) {
|
||||||
merged.organization = previousOrganization;
|
merged.organization = previousOrganization;
|
||||||
reasons.push("organization_from_followup_context");
|
reasons.push("organization_from_followup_context");
|
||||||
|
|
@ -837,6 +878,17 @@ function mergeFollowupFilters(
|
||||||
merged.counterparty = inheritedCounterparty;
|
merged.counterparty = inheritedCounterparty;
|
||||||
reasons.push(currentCounterparty ? "counterparty_replaced_from_followup_context" : "counterparty_from_followup_context");
|
reasons.push(currentCounterparty ? "counterparty_replaced_from_followup_context" : "counterparty_from_followup_context");
|
||||||
}
|
}
|
||||||
|
if (intent === "inventory_on_hand_as_of_date" && explicitQuotedItem) {
|
||||||
|
const currentItem = toNonEmptyString(merged.item);
|
||||||
|
if (
|
||||||
|
!currentItem ||
|
||||||
|
currentItem !== explicitQuotedItem ||
|
||||||
|
isInventoryItemAnchorDegradation(explicitQuotedItem, currentItem)
|
||||||
|
) {
|
||||||
|
merged.item = explicitQuotedItem;
|
||||||
|
reasons.push(currentItem ? "item_replaced_from_explicit_quote" : "item_from_explicit_quote");
|
||||||
|
}
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
(intent === "inventory_purchase_provenance_for_item" ||
|
(intent === "inventory_purchase_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
|
|
@ -846,7 +898,6 @@ function mergeFollowupFilters(
|
||||||
intent === "inventory_aging_by_purchase_date")
|
intent === "inventory_aging_by_purchase_date")
|
||||||
) {
|
) {
|
||||||
const inheritedItem = previousItem ?? previousAnchorItem;
|
const inheritedItem = previousItem ?? previousAnchorItem;
|
||||||
const explicitQuotedItem = extractSelectedObjectItemFromFollowupText(userMessage);
|
|
||||||
const currentItem = toNonEmptyString(merged.item);
|
const currentItem = toNonEmptyString(merged.item);
|
||||||
const shouldAdoptExplicitQuotedItem =
|
const shouldAdoptExplicitQuotedItem =
|
||||||
Boolean(explicitQuotedItem) &&
|
Boolean(explicitQuotedItem) &&
|
||||||
|
|
@ -983,6 +1034,7 @@ function mergeFollowupFilters(
|
||||||
if (
|
if (
|
||||||
!sameDateRequested &&
|
!sameDateRequested &&
|
||||||
hasFollowupSignalForConfirmed &&
|
hasFollowupSignalForConfirmed &&
|
||||||
|
!isInventoryLifecycleHistoryIntent(intent) &&
|
||||||
!hasExplicitPeriodLiteral(userMessage) &&
|
!hasExplicitPeriodLiteral(userMessage) &&
|
||||||
!hasExplicitCurrentDateHint(userMessage)
|
!hasExplicitCurrentDateHint(userMessage)
|
||||||
) {
|
) {
|
||||||
|
|
@ -1040,12 +1092,29 @@ function mergeFollowupFilters(
|
||||||
intent === "vat_payable_confirmed_as_of_date";
|
intent === "vat_payable_confirmed_as_of_date";
|
||||||
const currentHasPeriod = hasExplicitPeriodWindow(merged);
|
const currentHasPeriod = hasExplicitPeriodWindow(merged);
|
||||||
const previousHasPeriod = hasExplicitPeriodWindow(previous);
|
const previousHasPeriod = hasExplicitPeriodWindow(previous);
|
||||||
|
const vatRelativeMonthFollowup =
|
||||||
|
relativeMonthFromFollowupYear &&
|
||||||
|
(intent === "vat_payable_confirmed_as_of_date" ||
|
||||||
|
intent === "vat_payable_forecast" ||
|
||||||
|
intent === "vat_liability_confirmed_for_tax_period");
|
||||||
|
|
||||||
|
if (vatRelativeMonthFollowup) {
|
||||||
|
merged.period_from = relativeMonthFromFollowupYear.period_from;
|
||||||
|
merged.period_to = relativeMonthFromFollowupYear.period_to;
|
||||||
|
if (intent === "vat_payable_confirmed_as_of_date") {
|
||||||
|
merged.as_of_date = relativeMonthFromFollowupYear.as_of_date;
|
||||||
|
} else if (toNonEmptyString(merged.as_of_date)) {
|
||||||
|
delete merged.as_of_date;
|
||||||
|
}
|
||||||
|
reasons.push("period_derived_from_followup_context_year");
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(intent === "vat_payable_forecast" || intent === "vat_liability_confirmed_for_tax_period") &&
|
(intent === "vat_payable_forecast" || intent === "vat_liability_confirmed_for_tax_period") &&
|
||||||
previousHasPeriod &&
|
previousHasPeriod &&
|
||||||
hasFollowupSignal &&
|
hasFollowupSignal &&
|
||||||
!hasExplicitPeriodInMessage
|
!hasExplicitPeriodInMessage &&
|
||||||
|
!vatRelativeMonthFollowup
|
||||||
) {
|
) {
|
||||||
const currentPeriodFrom = toNonEmptyString(merged.period_from);
|
const currentPeriodFrom = toNonEmptyString(merged.period_from);
|
||||||
const currentPeriodTo = toNonEmptyString(merged.period_to);
|
const currentPeriodTo = toNonEmptyString(merged.period_to);
|
||||||
|
|
@ -1067,7 +1136,8 @@ function mergeFollowupFilters(
|
||||||
previousHasPeriod &&
|
previousHasPeriod &&
|
||||||
hasFollowupSignal &&
|
hasFollowupSignal &&
|
||||||
!hasExplicitPeriodInMessage &&
|
!hasExplicitPeriodInMessage &&
|
||||||
!inventoryLifecycleHistoryIntent
|
!inventoryLifecycleHistoryIntent &&
|
||||||
|
!vatRelativeMonthFollowup
|
||||||
) {
|
) {
|
||||||
if (previousPeriodFrom) {
|
if (previousPeriodFrom) {
|
||||||
merged.period_from = previousPeriodFrom;
|
merged.period_from = previousPeriodFrom;
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,24 @@ function hasSelectedObjectInventoryActionCue(text: string | null): boolean {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasShortInventoryPurchaseFollowupCue(text: string | null): boolean {
|
||||||
|
return /(?:^|[\s,.;:!?])(а\s+)?(?:купили\s+у\s+кого|у\s+кого\s+купили|поставщик|продавец|seller)(?:[\s,.;:!?]|$)/iu.test(
|
||||||
|
String(text ?? "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInventorySelectedObjectOrRootIntent(intent: string | null): boolean {
|
||||||
|
return (
|
||||||
|
intent === "inventory_on_hand_as_of_date" ||
|
||||||
|
intent === "inventory_purchase_provenance_for_item" ||
|
||||||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_profitability_for_item" ||
|
||||||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
|
intent === "inventory_aging_by_purchase_date"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function isGenericCanonicalDriftIntent(intent: string | null): boolean {
|
function isGenericCanonicalDriftIntent(intent: string | null): boolean {
|
||||||
return (
|
return (
|
||||||
intent === "open_items_by_counterparty_or_contract" ||
|
intent === "open_items_by_counterparty_or_contract" ||
|
||||||
|
|
@ -89,6 +107,33 @@ function isGenericCanonicalDriftIntent(intent: string | null): boolean {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasSameDateFollowupSignal(text: string | null): boolean {
|
||||||
|
return /(?:эту\s+же\s+дат(?:у|е|ой)|ту\s+же\s+дат(?:у|е|ой)|same\s+date)/iu.test(String(text ?? ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasExplicitCurrentDateSignal(text: string | null): boolean {
|
||||||
|
return /(?:текущ(?:ую|ая|ий|ее|ей)\s+дат(?:у|а|е|ой)|сегодняшн(?:юю|ий|ей)\s+дат(?:у|а|е|ой)|today|current\s+date)/iu.test(
|
||||||
|
String(text ?? "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasInventoryTemporalRootFollowupCue(text: string | null): boolean {
|
||||||
|
const value = String(text ?? "").trim().toLowerCase();
|
||||||
|
if (!value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const tokenCount = value.split(/\s+/).filter(Boolean).length;
|
||||||
|
const hasMonthYearCue =
|
||||||
|
/(?:январ(?:ь|е)|феврал(?:ь|е)|март(?:е)?|апрел(?:ь|е)|ма(?:й|е)|июн(?:ь|е)|июл(?:ь|е)|август(?:е)?|сентябр(?:ь|е)|октябр(?:ь|е)|ноябр(?:ь|е)|декабр(?:ь|е))(?:\s+\d{4})?/iu.test(
|
||||||
|
value
|
||||||
|
) || /\b(?:19|20)\d{2}\b/u.test(value);
|
||||||
|
if (tokenCount <= 3 && hasMonthYearCue) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const hasInventoryLexeme = /(?:остат|склад|товар|позици|номенклатур)/iu.test(value);
|
||||||
|
return hasInventoryLexeme && (hasMonthYearCue || hasSameDateFollowupSignal(value));
|
||||||
|
}
|
||||||
|
|
||||||
function shouldPreferRawFollowupMessage(
|
function shouldPreferRawFollowupMessage(
|
||||||
userMessage: string,
|
userMessage: string,
|
||||||
addressInputMessage: string,
|
addressInputMessage: string,
|
||||||
|
|
@ -112,15 +157,39 @@ function shouldPreferRawFollowupMessage(
|
||||||
: null;
|
: null;
|
||||||
const mode = toNonEmptyString(predecomposeContract?.mode) ?? "unknown";
|
const mode = toNonEmptyString(predecomposeContract?.mode) ?? "unknown";
|
||||||
const intent = toNonEmptyString(predecomposeContract?.intent) ?? "unknown";
|
const intent = toNonEmptyString(predecomposeContract?.intent) ?? "unknown";
|
||||||
|
const followupContext =
|
||||||
|
carryover.followupContext && typeof carryover.followupContext === "object"
|
||||||
|
? (carryover.followupContext as Record<string, unknown>)
|
||||||
|
: null;
|
||||||
|
const previousIntent = toNonEmptyString(followupContext?.previous_intent);
|
||||||
|
const rootIntent = toNonEmptyString(followupContext?.root_intent);
|
||||||
|
const previousAnchorType = toNonEmptyString(followupContext?.previous_anchor_type);
|
||||||
|
const hasInventoryItemCarryover =
|
||||||
|
previousAnchorType === "item" && isInventorySelectedObjectOrRootIntent(previousIntent);
|
||||||
|
const hasInventoryFrameCarryover =
|
||||||
|
isInventorySelectedObjectOrRootIntent(previousIntent) ||
|
||||||
|
isInventorySelectedObjectOrRootIntent(rootIntent);
|
||||||
|
|
||||||
if (mode === "unsupported" && intent === "unknown") {
|
if (mode === "unsupported" && intent === "unknown") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasSameDateFollowupSignal(rawMessage) && hasExplicitCurrentDateSignal(canonicalMessage)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
hasInventoryFrameCarryover &&
|
||||||
|
hasInventoryTemporalRootFollowupCue(rawMessage) &&
|
||||||
|
(intent === "account_balance_snapshot" || intent === "documents_forming_balance" || intent === "unknown")
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
hasSelectedObjectInventorySignal(rawMessage) &&
|
(hasSelectedObjectInventorySignal(rawMessage) || hasInventoryItemCarryover) &&
|
||||||
hasSelectedObjectInventoryActionCue(rawMessage) &&
|
(hasSelectedObjectInventoryActionCue(rawMessage) || hasShortInventoryPurchaseFollowupCue(rawMessage)) &&
|
||||||
isGenericCanonicalDriftIntent(intent)
|
(isGenericCanonicalDriftIntent(intent) || intent === "unknown")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -193,8 +193,16 @@ export function createAssistantRoutePolicy(deps) {
|
||||||
hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) ||
|
hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) ||
|
||||||
hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) ||
|
hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) ||
|
||||||
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage);
|
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage);
|
||||||
|
const followupPreviousIntent = toNonEmptyString(followupContext?.previous_intent);
|
||||||
|
const followupPreviousFilters = followupContext?.previous_filters && typeof followupContext.previous_filters === "object"
|
||||||
|
? followupContext.previous_filters
|
||||||
|
: null;
|
||||||
const protectedInventoryShortFollowup = Boolean(followupContext &&
|
const protectedInventoryShortFollowup = Boolean(followupContext &&
|
||||||
isInventorySelectedObjectIntent(toNonEmptyString(followupContext.previous_intent)) &&
|
(isInventorySelectedObjectIntent(followupPreviousIntent) ||
|
||||||
|
(followupPreviousIntent === "inventory_on_hand_as_of_date" &&
|
||||||
|
(toNonEmptyString(followupContext.previous_anchor_type) === "item" ||
|
||||||
|
toNonEmptyString(followupContext.previous_anchor_value) ||
|
||||||
|
toNonEmptyString(followupPreviousFilters?.item)))) &&
|
||||||
(hasShortInventoryObjectFollowupSignal(rawUserMessage) ||
|
(hasShortInventoryObjectFollowupSignal(rawUserMessage) ||
|
||||||
hasShortInventoryObjectFollowupSignal(repairedRawUserMessage) ||
|
hasShortInventoryObjectFollowupSignal(repairedRawUserMessage) ||
|
||||||
hasShortInventoryObjectFollowupSignal(effectiveAddressUserMessage) ||
|
hasShortInventoryObjectFollowupSignal(effectiveAddressUserMessage) ||
|
||||||
|
|
|
||||||
|
|
@ -2757,8 +2757,11 @@ function hasShortInventoryObjectFollowupSignal(userMessage) {
|
||||||
if (minTokens > 8) {
|
if (minTokens > 8) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const hasDirectPurchaseFollowupCue = (sample) => /(?:^|[\s,.;:!?])(?:а\s+)?(?:у|от)\s+кого(?:\s+\S+){0,5}\s+купил(?:и|о)?(?=$|[\s,.;:!?])/iu.test(sample) ||
|
||||||
|
/(?:^|[\s,.;:!?])(?:а\s+)?купил(?:и|о)?(?:\s+\S+){0,5}\s+(?:у|от)\s+кого(?=$|[\s,.;:!?])/iu.test(sample);
|
||||||
const hasDirectSaleFollowupCue = (sample) => /(?:кому|каму|куда)(?:\s+\S+){0,4}\s+(?:продали|продано|продан(?:о|а|ы)?|реализовали|реализован(?:о|а|ы)?)|(?:продали|продано|реализовали|реализован(?:о|а|ы)?)(?:\s+\S+){0,4}\s+(?:кому|каму|куда)|(?:^|\s)(?:продано|продали|реализовано|реализовали)(?=$|[\s,.;:!?])/iu.test(sample);
|
const hasDirectSaleFollowupCue = (sample) => /(?:кому|каму|куда)(?:\s+\S+){0,4}\s+(?:продали|продано|продан(?:о|а|ы)?|реализовали|реализован(?:о|а|ы)?)|(?:продали|продано|реализовали|реализован(?:о|а|ы)?)(?:\s+\S+){0,4}\s+(?:кому|каму|куда)|(?:^|\s)(?:продано|продали|реализовано|реализовали)(?=$|[\s,.;:!?])/iu.test(sample);
|
||||||
return samples.some((sample) => /^(?:кто|когда|документы|сумма|поставщик|покупатель)(?:\?)?$/iu.test(sample) ||
|
return samples.some((sample) => /^(?:кто|когда|документы|сумма|поставщик|покупатель|продавец|seller)(?:\?)?$/iu.test(sample) ||
|
||||||
|
hasDirectPurchaseFollowupCue(sample) ||
|
||||||
hasDirectSaleFollowupCue(sample) ||
|
hasDirectSaleFollowupCue(sample) ||
|
||||||
(0, decomposeStage_1.hasInventorySupplierFollowupCue)(sample) ||
|
(0, decomposeStage_1.hasInventorySupplierFollowupCue)(sample) ||
|
||||||
(0, decomposeStage_1.hasInventoryPurchaseDocumentsFollowupCue)(sample) ||
|
(0, decomposeStage_1.hasInventoryPurchaseDocumentsFollowupCue)(sample) ||
|
||||||
|
|
@ -2767,6 +2770,41 @@ function hasShortInventoryObjectFollowupSignal(userMessage) {
|
||||||
(0, decomposeStage_1.hasInventorySaleFollowupCue)(sample) ||
|
(0, decomposeStage_1.hasInventorySaleFollowupCue)(sample) ||
|
||||||
(0, decomposeStage_1.hasInventoryPurchaseToSaleChainFollowupCue)(sample));
|
(0, decomposeStage_1.hasInventoryPurchaseToSaleChainFollowupCue)(sample));
|
||||||
}
|
}
|
||||||
|
function hasInventoryRootTemporalFollowupSignal(userMessage, sourceIntentHint, hasInventoryRootFrame) {
|
||||||
|
if (!hasInventoryRootFrame) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||||
|
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
|
isInventorySelectedObjectIntent(sourceIntentHint))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
const hasMonthYearCue = samples.some((sample) => /(?:январ|феврал|март|апрел|ма(?:й|е|я)|июн|июл|август|сентябр|октябр|ноябр|декабр)(?:\s+\d{4})?/iu.test(sample) ||
|
||||||
|
/\b(?:19|20)\d{2}\b/u.test(sample));
|
||||||
|
const hasTemporalCue = samples.some((sample) => hasPeriodLiteral(sample) ||
|
||||||
|
hasShortNamedPeriodFollowupLiteral(sample) ||
|
||||||
|
hasShortCurrentDateFollowupLiteral(sample) ||
|
||||||
|
/(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|same\s+date|the\s+same\s+date)/iu.test(sample));
|
||||||
|
if (!hasTemporalCue && !hasMonthYearCue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (samples.some((sample) => hasForeignAccountingPivotOverInventoryMessage(sample))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasInventoryLexeme = samples.some((sample) => /(?:остат|склад|товар|номенклат|позиц)/iu.test(sample));
|
||||||
|
const hasPlainInventoryLexeme = samples.some((sample) => /(?:остат|склад|товар|номенклатур|позиц)/iu.test(sample));
|
||||||
|
return hasInventoryLexeme || hasPlainInventoryLexeme || minTokens <= 3;
|
||||||
|
}
|
||||||
function hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage = null) {
|
function hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage = null) {
|
||||||
const samples = [
|
const samples = [
|
||||||
compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase()),
|
compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase()),
|
||||||
|
|
@ -2848,8 +2886,22 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
(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 sourceIntentHint = toNonEmptyString(previousAddressDebug?.detected_intent);
|
||||||
const inventoryShortFollowupPrimary = isInventorySelectedObjectIntent(sourceIntentHint) && hasShortInventoryObjectFollowupSignal(userMessage);
|
const navigationFocusObjectHint = addressNavigationState &&
|
||||||
const inventoryShortFollowupAlternate = isInventorySelectedObjectIntent(sourceIntentHint) && toNonEmptyString(alternateMessage)
|
typeof addressNavigationState === "object" &&
|
||||||
|
addressNavigationState.session_context &&
|
||||||
|
typeof addressNavigationState.session_context === "object" &&
|
||||||
|
addressNavigationState.session_context.active_focus_object &&
|
||||||
|
typeof addressNavigationState.session_context.active_focus_object === "object"
|
||||||
|
? addressNavigationState.session_context.active_focus_object
|
||||||
|
: null;
|
||||||
|
const hasNavigationInventoryItemFocusHint = Boolean(toNonEmptyString(navigationFocusObjectHint?.label) &&
|
||||||
|
toNonEmptyString(navigationFocusObjectHint?.object_type) === "item" &&
|
||||||
|
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||||
|
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
|
isInventorySelectedObjectIntent(sourceIntentHint)));
|
||||||
|
let inventoryShortFollowupPrimary = (isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
||||||
|
hasShortInventoryObjectFollowupSignal(userMessage);
|
||||||
|
let inventoryShortFollowupAlternate = (isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) && toNonEmptyString(alternateMessage)
|
||||||
? hasShortInventoryObjectFollowupSignal(String(alternateMessage ?? ""))
|
? hasShortInventoryObjectFollowupSignal(String(alternateMessage ?? ""))
|
||||||
: false;
|
: false;
|
||||||
const debtRoleSwapPrimary = sourceIntentHint ? resolveDebtRoleSwapFollowupIntent(userMessage, sourceIntentHint) : null;
|
const debtRoleSwapPrimary = sourceIntentHint ? resolveDebtRoleSwapFollowupIntent(userMessage, sourceIntentHint) : null;
|
||||||
|
|
@ -2857,8 +2909,8 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
? resolveDebtRoleSwapFollowupIntent(String(alternateMessage ?? ""), sourceIntentHint)
|
? resolveDebtRoleSwapFollowupIntent(String(alternateMessage ?? ""), sourceIntentHint)
|
||||||
: null;
|
: null;
|
||||||
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
|
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
|
||||||
const hasPrimaryFollowupSignal = hasAddressFollowupContextSignal(userMessage) || Boolean(debtRoleSwapPrimary) || inventoryShortFollowupPrimary;
|
let hasPrimaryFollowupSignal = hasAddressFollowupContextSignal(userMessage) || Boolean(debtRoleSwapPrimary) || inventoryShortFollowupPrimary;
|
||||||
const hasAlternateFollowupSignal = toNonEmptyString(alternateMessage)
|
let hasAlternateFollowupSignal = toNonEmptyString(alternateMessage)
|
||||||
? hasAddressFollowupContextSignal(alternateMessage) || Boolean(debtRoleSwapAlternate) || inventoryShortFollowupAlternate
|
? hasAddressFollowupContextSignal(alternateMessage) || Boolean(debtRoleSwapAlternate) || inventoryShortFollowupAlternate
|
||||||
: false;
|
: false;
|
||||||
const hasPrimaryIndexReferenceSignal = extractDisplayedEntityIndexMention(userMessage) !== null;
|
const hasPrimaryIndexReferenceSignal = extractDisplayedEntityIndexMention(userMessage) !== null;
|
||||||
|
|
@ -2866,12 +2918,19 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
? extractDisplayedEntityIndexMention(String(alternateMessage ?? "")) !== null
|
? extractDisplayedEntityIndexMention(String(alternateMessage ?? "")) !== null
|
||||||
: false;
|
: false;
|
||||||
const hasIndexReferenceSignal = hasPrimaryIndexReferenceSignal || hasAlternateIndexReferenceSignal;
|
const hasIndexReferenceSignal = hasPrimaryIndexReferenceSignal || hasAlternateIndexReferenceSignal;
|
||||||
const hasStrongFollowupReference = hasPrimaryIndexReferenceSignal ||
|
const recentInventoryRootFrame = findRecentInventoryRootFrame(items);
|
||||||
|
const hasInventoryRootTemporalFollowupPrimary = hasInventoryRootTemporalFollowupSignal(userMessage, sourceIntentHint, Boolean(recentInventoryRootFrame));
|
||||||
|
const hasInventoryRootTemporalFollowupAlternate = toNonEmptyString(alternateMessage)
|
||||||
|
? hasInventoryRootTemporalFollowupSignal(String(alternateMessage ?? ""), sourceIntentHint, Boolean(recentInventoryRootFrame))
|
||||||
|
: false;
|
||||||
|
let hasStrongFollowupReference = hasPrimaryIndexReferenceSignal ||
|
||||||
hasAlternateIndexReferenceSignal ||
|
hasAlternateIndexReferenceSignal ||
|
||||||
hasOrganizationClarificationContinuation ||
|
hasOrganizationClarificationContinuation ||
|
||||||
hasImplicitContinuationSignal ||
|
hasImplicitContinuationSignal ||
|
||||||
inventoryShortFollowupPrimary ||
|
inventoryShortFollowupPrimary ||
|
||||||
inventoryShortFollowupAlternate ||
|
inventoryShortFollowupAlternate ||
|
||||||
|
hasInventoryRootTemporalFollowupPrimary ||
|
||||||
|
hasInventoryRootTemporalFollowupAlternate ||
|
||||||
Boolean(debtRoleSwapIntent) ||
|
Boolean(debtRoleSwapIntent) ||
|
||||||
hasFollowupMarker(userMessage) ||
|
hasFollowupMarker(userMessage) ||
|
||||||
hasReferentialPointer(userMessage) ||
|
hasReferentialPointer(userMessage) ||
|
||||||
|
|
@ -2883,6 +2942,8 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
if (hasStandaloneAddressTopic &&
|
if (hasStandaloneAddressTopic &&
|
||||||
!hasPrimaryFollowupSignal &&
|
!hasPrimaryFollowupSignal &&
|
||||||
!hasAlternateFollowupSignal &&
|
!hasAlternateFollowupSignal &&
|
||||||
|
!hasInventoryRootTemporalFollowupPrimary &&
|
||||||
|
!hasInventoryRootTemporalFollowupAlternate &&
|
||||||
!hasImplicitContinuationSignal &&
|
!hasImplicitContinuationSignal &&
|
||||||
!hasOrganizationClarificationContinuation &&
|
!hasOrganizationClarificationContinuation &&
|
||||||
!hasIndexReferenceSignal) {
|
!hasIndexReferenceSignal) {
|
||||||
|
|
@ -2890,6 +2951,8 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
}
|
}
|
||||||
if (!hasPrimaryFollowupSignal &&
|
if (!hasPrimaryFollowupSignal &&
|
||||||
!hasAlternateFollowupSignal &&
|
!hasAlternateFollowupSignal &&
|
||||||
|
!hasInventoryRootTemporalFollowupPrimary &&
|
||||||
|
!hasInventoryRootTemporalFollowupAlternate &&
|
||||||
!hasImplicitContinuationSignal &&
|
!hasImplicitContinuationSignal &&
|
||||||
!hasOrganizationClarificationContinuation &&
|
!hasOrganizationClarificationContinuation &&
|
||||||
!hasIndexReferenceSignal) {
|
!hasIndexReferenceSignal) {
|
||||||
|
|
@ -2951,6 +3014,41 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
: null;
|
: null;
|
||||||
const navigationFocusObjectType = toNonEmptyString(navigationFocusObject?.object_type);
|
const navigationFocusObjectType = toNonEmptyString(navigationFocusObject?.object_type);
|
||||||
const navigationFocusObjectLabel = toNonEmptyString(navigationFocusObject?.label);
|
const navigationFocusObjectLabel = toNonEmptyString(navigationFocusObject?.label);
|
||||||
|
const hasInventoryItemFocusCarryover = navigationFocusObjectType === "item" &&
|
||||||
|
navigationFocusObjectLabel &&
|
||||||
|
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||||
|
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
|
isInventorySelectedObjectIntent(sourceIntentHint));
|
||||||
|
if (!inventoryShortFollowupPrimary && hasInventoryItemFocusCarryover) {
|
||||||
|
inventoryShortFollowupPrimary = hasShortInventoryObjectFollowupSignal(userMessage);
|
||||||
|
}
|
||||||
|
if (!inventoryShortFollowupAlternate && hasInventoryItemFocusCarryover && toNonEmptyString(alternateMessage)) {
|
||||||
|
inventoryShortFollowupAlternate = hasShortInventoryObjectFollowupSignal(String(alternateMessage ?? ""));
|
||||||
|
}
|
||||||
|
hasPrimaryFollowupSignal = hasAddressFollowupContextSignal(userMessage) ||
|
||||||
|
Boolean(debtRoleSwapPrimary) ||
|
||||||
|
inventoryShortFollowupPrimary ||
|
||||||
|
hasInventoryRootTemporalFollowupPrimary;
|
||||||
|
hasAlternateFollowupSignal = toNonEmptyString(alternateMessage)
|
||||||
|
? hasAddressFollowupContextSignal(alternateMessage) ||
|
||||||
|
Boolean(debtRoleSwapAlternate) ||
|
||||||
|
inventoryShortFollowupAlternate ||
|
||||||
|
hasInventoryRootTemporalFollowupAlternate
|
||||||
|
: false;
|
||||||
|
hasStrongFollowupReference = hasPrimaryIndexReferenceSignal ||
|
||||||
|
hasAlternateIndexReferenceSignal ||
|
||||||
|
hasOrganizationClarificationContinuation ||
|
||||||
|
hasImplicitContinuationSignal ||
|
||||||
|
inventoryShortFollowupPrimary ||
|
||||||
|
inventoryShortFollowupAlternate ||
|
||||||
|
hasInventoryRootTemporalFollowupPrimary ||
|
||||||
|
hasInventoryRootTemporalFollowupAlternate ||
|
||||||
|
Boolean(debtRoleSwapIntent) ||
|
||||||
|
hasFollowupMarker(userMessage) ||
|
||||||
|
hasReferentialPointer(userMessage) ||
|
||||||
|
(toNonEmptyString(alternateMessage)
|
||||||
|
? hasFollowupMarker(String(alternateMessage ?? "")) || hasReferentialPointer(String(alternateMessage ?? ""))
|
||||||
|
: false);
|
||||||
const hasSelectedObjectInventorySignalPrimary = /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(userMessage ?? ""));
|
const hasSelectedObjectInventorySignalPrimary = /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(userMessage ?? ""));
|
||||||
const hasSelectedObjectInventorySignalAlternate = toNonEmptyString(alternateMessage)
|
const hasSelectedObjectInventorySignalAlternate = toNonEmptyString(alternateMessage)
|
||||||
? /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(alternateMessage ?? ""))
|
? /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(alternateMessage ?? ""))
|
||||||
|
|
@ -3012,18 +3110,39 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
if (!toNonEmptyString(previousFilters.organization) && organizationClarificationSelection) {
|
if (!toNonEmptyString(previousFilters.organization) && organizationClarificationSelection) {
|
||||||
previousFilters.organization = organizationClarificationSelection;
|
previousFilters.organization = organizationClarificationSelection;
|
||||||
}
|
}
|
||||||
if (!toNonEmptyString(previousFilters.as_of_date) && toNonEmptyString(navigationDateScope?.as_of_date)) {
|
const shouldBackfillPreviousDateScopeFromNavigation = sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||||
|
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
|
sourceIntentHint === "inventory_purchase_provenance_for_item" ||
|
||||||
|
sourceIntentHint === "inventory_purchase_documents_for_item" ||
|
||||||
|
sourceIntentHint === "inventory_sale_trace_for_item" ||
|
||||||
|
sourceIntentHint === "inventory_profitability_for_item" ||
|
||||||
|
sourceIntentHint === "inventory_purchase_to_sale_chain" ||
|
||||||
|
sourceIntentHint === "inventory_aging_by_purchase_date" ||
|
||||||
|
sourceIntentHint === "account_balance_snapshot" ||
|
||||||
|
sourceIntentHint === "documents_forming_balance";
|
||||||
|
if (shouldBackfillPreviousDateScopeFromNavigation &&
|
||||||
|
!toNonEmptyString(previousFilters.as_of_date) &&
|
||||||
|
toNonEmptyString(navigationDateScope?.as_of_date)) {
|
||||||
previousFilters.as_of_date = toNonEmptyString(navigationDateScope?.as_of_date);
|
previousFilters.as_of_date = toNonEmptyString(navigationDateScope?.as_of_date);
|
||||||
}
|
}
|
||||||
if (!toNonEmptyString(previousFilters.period_from) && toNonEmptyString(navigationDateScope?.period_from)) {
|
if (shouldBackfillPreviousDateScopeFromNavigation &&
|
||||||
|
!toNonEmptyString(previousFilters.period_from) &&
|
||||||
|
toNonEmptyString(navigationDateScope?.period_from)) {
|
||||||
previousFilters.period_from = toNonEmptyString(navigationDateScope?.period_from);
|
previousFilters.period_from = toNonEmptyString(navigationDateScope?.period_from);
|
||||||
}
|
}
|
||||||
if (!toNonEmptyString(previousFilters.period_to) && toNonEmptyString(navigationDateScope?.period_to)) {
|
if (shouldBackfillPreviousDateScopeFromNavigation &&
|
||||||
|
!toNonEmptyString(previousFilters.period_to) &&
|
||||||
|
toNonEmptyString(navigationDateScope?.period_to)) {
|
||||||
previousFilters.period_to = toNonEmptyString(navigationDateScope?.period_to);
|
previousFilters.period_to = toNonEmptyString(navigationDateScope?.period_to);
|
||||||
}
|
}
|
||||||
const rootContextOnlyPivot = Boolean((isInventorySelectedObjectIntent(sourceIntentHint) || currentFrameKind === "inventory_drilldown") &&
|
const rootContextOnlyPivot = Boolean((isInventorySelectedObjectIntent(sourceIntentHint) || currentFrameKind === "inventory_drilldown") &&
|
||||||
hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage));
|
hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage));
|
||||||
if (rootContextOnlyPivot) {
|
const inventoryRootTemporalPivot = Boolean(inventoryRootFrame &&
|
||||||
|
(isInventorySelectedObjectIntent(sourceIntentHint) || currentFrameKind === "inventory_drilldown") &&
|
||||||
|
(hasInventoryRootTemporalFollowupPrimary || hasInventoryRootTemporalFollowupAlternate) &&
|
||||||
|
!hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage));
|
||||||
|
const rootScopedPivot = rootContextOnlyPivot || inventoryRootTemporalPivot;
|
||||||
|
if (rootScopedPivot) {
|
||||||
previousIntent = null;
|
previousIntent = null;
|
||||||
previousAnchorType = null;
|
previousAnchorType = null;
|
||||||
previousAnchor = null;
|
previousAnchor = null;
|
||||||
|
|
@ -3037,7 +3156,7 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
(toNonEmptyString(alternateMessage)
|
(toNonEmptyString(alternateMessage)
|
||||||
? resolveDisplayedAddressEntityMention(String(alternateMessage ?? ""), displayedEntities)
|
? resolveDisplayedAddressEntityMention(String(alternateMessage ?? ""), displayedEntities)
|
||||||
: null);
|
: null);
|
||||||
if (resolvedEntityFromFollowup && !rootContextOnlyPivot) {
|
if (resolvedEntityFromFollowup && !rootScopedPivot) {
|
||||||
if (resolvedEntityFromFollowup.entityType === "counterparty") {
|
if (resolvedEntityFromFollowup.entityType === "counterparty") {
|
||||||
previousFilters.counterparty = resolvedEntityFromFollowup.value;
|
previousFilters.counterparty = resolvedEntityFromFollowup.value;
|
||||||
previousAnchorType = "counterparty";
|
previousAnchorType = "counterparty";
|
||||||
|
|
@ -3058,7 +3177,7 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
followupSelectionMode = "carry_referenced_entity";
|
followupSelectionMode = "carry_referenced_entity";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!rootContextOnlyPivot &&
|
if (!rootScopedPivot &&
|
||||||
!toNonEmptyString(previousFilters.item) &&
|
!toNonEmptyString(previousFilters.item) &&
|
||||||
navigationFocusObjectType === "item" &&
|
navigationFocusObjectType === "item" &&
|
||||||
navigationFocusObjectLabel &&
|
navigationFocusObjectLabel &&
|
||||||
|
|
@ -3100,7 +3219,7 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||||
previous_anchor_type: previousAnchorType ?? undefined,
|
previous_anchor_type: previousAnchorType ?? undefined,
|
||||||
previous_anchor_value: previousAnchor,
|
previous_anchor_value: previousAnchor,
|
||||||
resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined,
|
resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined,
|
||||||
root_context_only: rootContextOnlyPivot || undefined,
|
root_context_only: rootScopedPivot || undefined,
|
||||||
root_intent: inventoryRootFrame?.intent ?? undefined,
|
root_intent: inventoryRootFrame?.intent ?? undefined,
|
||||||
root_filters: inventoryRootFrame?.filters ?? undefined,
|
root_filters: inventoryRootFrame?.filters ?? undefined,
|
||||||
root_anchor_type: inventoryRootFrame?.anchorType ?? undefined,
|
root_anchor_type: inventoryRootFrame?.anchorType ?? undefined,
|
||||||
|
|
@ -3121,11 +3240,13 @@ function buildAddressDialogContinuationContractV2(userMessage, effectiveMessage,
|
||||||
const previousIntent = toNonEmptyString(carryoverMeta?.previousSourceIntent) ?? null;
|
const previousIntent = toNonEmptyString(carryoverMeta?.previousSourceIntent) ?? null;
|
||||||
const selectionMode = toNonEmptyString(carryoverMeta?.followupSelectionMode) ?? null;
|
const selectionMode = toNonEmptyString(carryoverMeta?.followupSelectionMode) ?? null;
|
||||||
const rootContextOnly = selectionMode === "carry_root_context";
|
const rootContextOnly = selectionMode === "carry_root_context";
|
||||||
const explicitIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
|
const explicitIntentRaw = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
|
||||||
|
const explicitIntent = explicitIntentRaw === "unknown" ? null : explicitIntentRaw;
|
||||||
|
const rootIntent = toNonEmptyString(carryoverMeta?.followupContext?.root_intent) ?? null;
|
||||||
const targetIntent = selectionMode === "switch_to_suggested_intent"
|
const targetIntent = selectionMode === "switch_to_suggested_intent"
|
||||||
? toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null
|
? toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null
|
||||||
: rootContextOnly
|
: rootContextOnly
|
||||||
? explicitIntent ?? null
|
? rootIntent ?? explicitIntent ?? null
|
||||||
: explicitIntent ?? toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null;
|
: explicitIntent ?? toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null;
|
||||||
const hasImplicitContinuationSignal = Boolean(carryoverMeta?.hasImplicitContinuationSignal);
|
const hasImplicitContinuationSignal = Boolean(carryoverMeta?.hasImplicitContinuationSignal);
|
||||||
const rewrittenByPredecompose = compactWhitespace(sourceMessage.toLowerCase()) !== compactWhitespace(canonicalMessage.toLowerCase());
|
const rewrittenByPredecompose = compactWhitespace(sourceMessage.toLowerCase()) !== compactWhitespace(canonicalMessage.toLowerCase());
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ export function hasInventorySupplierCue(text: string): boolean {
|
||||||
if (/(?:купил(?:и|о)?\s+у\s+кого|куплен(?:о)?\s+у\s+кого|купил(?:и|о)?\s+от\s+кого|куплен(?:о)?\s+от\s+кого)/iu.test(value)) {
|
if (/(?:купил(?:и|о)?\s+у\s+кого|куплен(?:о)?\s+у\s+кого|купил(?:и|о)?\s+от\s+кого|куплен(?:о)?\s+от\s+кого)/iu.test(value)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (/(?:кто\s+(?:был\s+)?продавец|кто\s+нам\s+продал|кто\s+продал\s+нам|(?:^|[\s,.;:!?()\-])(?:продавец|seller)(?=$|[\s,.;:!?()\-]))/iu.test(value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
/(?:кто\s+(?:(?:это|этот\s+товар|эту\s+позицию)\s+)?(?:нам\s+)?поставил|кто\s+(?:нам\s+)?поставил\s+(?:это|этот\s+товар|эту\s+позицию)|от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+(?:мы\s+)?взяли(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|откуда\s+(?:мы\s+)?взяли(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+куплено|supplier|vendor|поставщик)/iu.test(
|
/(?:кто\s+(?:(?:это|этот\s+товар|эту\s+позицию)\s+)?(?:нам\s+)?поставил|кто\s+(?:нам\s+)?поставил\s+(?:это|этот\s+товар|эту\s+позицию)|от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+(?:мы\s+)?взяли(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|откуда\s+(?:мы\s+)?взяли(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+куплено|supplier|vendor|поставщик)/iu.test(
|
||||||
value
|
value
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const { executeAddressMcpQueryMock } = vi.hoisted(() => ({
|
||||||
|
executeAddressMcpQueryMock: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../src/services/addressMcpClient", async () => {
|
||||||
|
const actual = await vi.importActual<typeof import("../src/services/addressMcpClient")>(
|
||||||
|
"../src/services/addressMcpClient"
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
executeAddressMcpQuery: executeAddressMcpQueryMock
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
import { AddressQueryService } from "../src/services/addressQueryService";
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
executeAddressMcpQueryMock.mockReset();
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("counterparty lifecycle organization scope regressions", () => {
|
||||||
|
it("keeps carried organization scope without false empty-match when rows omit organization column", async () => {
|
||||||
|
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||||
|
fetched_rows: 3,
|
||||||
|
matched_rows: 3,
|
||||||
|
raw_rows: [
|
||||||
|
{
|
||||||
|
Period: "2020-01-15T00:00:00Z",
|
||||||
|
Registrator: "CP_CUSTOMER_ACTIVITY",
|
||||||
|
AccountDt: "62.01",
|
||||||
|
AccountKt: "90.01",
|
||||||
|
Amount: 12,
|
||||||
|
Counterparty: 'ООО "Ромашка"'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Period: "2020-02-20T00:00:00Z",
|
||||||
|
Registrator: "CP_CUSTOMER_ACTIVITY",
|
||||||
|
AccountDt: "62.01",
|
||||||
|
AccountKt: "90.01",
|
||||||
|
Amount: 8,
|
||||||
|
Counterparty: 'ООО "Ландыш"'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Period: "2020-03-10T00:00:00Z",
|
||||||
|
Registrator: "CP_CUSTOMER_ACTIVITY",
|
||||||
|
AccountDt: "62.01",
|
||||||
|
AccountKt: "90.01",
|
||||||
|
Amount: 4,
|
||||||
|
Counterparty: 'ООО "Ромашка"'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
rows: [],
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const service = new AddressQueryService();
|
||||||
|
const result = await service.tryHandle("какие клиенты заплатили больше всего денег за все время", {
|
||||||
|
followupContext: {
|
||||||
|
previous_intent: "vat_payable_forecast",
|
||||||
|
previous_filters: {
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-03-31"
|
||||||
|
},
|
||||||
|
previous_anchor_type: "organization",
|
||||||
|
previous_anchor_value: 'ООО "Альтернатива Плюс"'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result?.handled).toBe(true);
|
||||||
|
expect(result?.reply_type).toBe("factual");
|
||||||
|
expect(result?.response_type).toBe("FACTUAL_LIST");
|
||||||
|
expect(result?.debug.detected_intent).toBe("counterparty_activity_lifecycle");
|
||||||
|
expect(result?.debug.extracted_filters?.organization).toBe('ООО "Альтернатива Плюс"');
|
||||||
|
expect(result?.debug.match_failure_reason).toBeNull();
|
||||||
|
expect(result?.debug.rows_matched).toBe(3);
|
||||||
|
expect(String(result?.reply_text ?? "")).toContain('ООО "Ромашка"');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { runAddressDecomposeStage } from "../src/services/address_runtime/decomposeStage";
|
||||||
|
|
||||||
|
describe("address follow-up temporal regressions", () => {
|
||||||
|
it("replaces VAT month-only follow-up with the previous follow-up year", () => {
|
||||||
|
const result = runAddressDecomposeStage("а на март", {
|
||||||
|
previous_intent: "vat_payable_confirmed_as_of_date",
|
||||||
|
previous_filters: {
|
||||||
|
period_from: "2019-05-01",
|
||||||
|
period_to: "2019-05-31",
|
||||||
|
as_of_date: "2019-05-31"
|
||||||
|
},
|
||||||
|
previous_anchor_type: "unknown",
|
||||||
|
previous_anchor_value: null
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.intent.intent).toBe("vat_payable_confirmed_as_of_date");
|
||||||
|
expect(result?.filters.extracted_filters.period_from).toBe("2019-03-01");
|
||||||
|
expect(result?.filters.extracted_filters.period_to).toBe("2019-03-31");
|
||||||
|
expect(result?.filters.extracted_filters.as_of_date).toBe("2019-03-31");
|
||||||
|
expect(result?.baseReasons).toContain("period_derived_from_followup_context_year");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("replaces VAT month-only follow-up with a later month from the same inherited year", () => {
|
||||||
|
const result = runAddressDecomposeStage("на сентябрь", {
|
||||||
|
previous_intent: "vat_payable_confirmed_as_of_date",
|
||||||
|
previous_filters: {
|
||||||
|
period_from: "2019-05-01",
|
||||||
|
period_to: "2019-05-31",
|
||||||
|
as_of_date: "2019-05-31"
|
||||||
|
},
|
||||||
|
previous_anchor_type: "unknown",
|
||||||
|
previous_anchor_value: null
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.intent.intent).toBe("vat_payable_confirmed_as_of_date");
|
||||||
|
expect(result?.filters.extracted_filters.period_from).toBe("2019-09-01");
|
||||||
|
expect(result?.filters.extracted_filters.period_to).toBe("2019-09-30");
|
||||||
|
expect(result?.filters.extracted_filters.as_of_date).toBe("2019-09-30");
|
||||||
|
expect(result?.baseReasons).toContain("period_derived_from_followup_context_year");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps same-date inventory pivot anchored to the previous VAT date", () => {
|
||||||
|
const result = runAddressDecomposeStage("какие остатки по складу на эту же дату", {
|
||||||
|
previous_intent: "vat_payable_confirmed_as_of_date",
|
||||||
|
previous_filters: {
|
||||||
|
period_from: "2019-09-01",
|
||||||
|
period_to: "2019-09-30",
|
||||||
|
as_of_date: "2019-09-30"
|
||||||
|
},
|
||||||
|
previous_anchor_type: "unknown",
|
||||||
|
previous_anchor_value: null
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.intent.intent).toBe("inventory_on_hand_as_of_date");
|
||||||
|
expect(result?.filters.extracted_filters.as_of_date).toBe("2019-09-30");
|
||||||
|
expect(result?.filters.extracted_filters.warehouse).toBeUndefined();
|
||||||
|
expect(result?.baseReasons).toContain("as_of_date_from_followup_context");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -56,7 +56,6 @@ describe("inventory root frame regressions", () => {
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
expect(result?.intent.intent).toBe("inventory_purchase_provenance_for_item");
|
expect(result?.intent.intent).toBe("inventory_purchase_provenance_for_item");
|
||||||
expect(result?.filters.extracted_filters.item).toBe("Зеркало для инвалидов поворотное травмобезопасное");
|
expect(result?.filters.extracted_filters.item).toBe("Зеркало для инвалидов поворотное травмобезопасное");
|
||||||
expect(result?.filters.extracted_filters.as_of_date).toBe("2022-02-28");
|
|
||||||
expect(
|
expect(
|
||||||
result?.baseReasons?.includes("intent_adjusted_to_inventory_followup_context") ||
|
result?.baseReasons?.includes("intent_adjusted_to_inventory_followup_context") ||
|
||||||
result?.intent.reasons.includes("inventory_selected_object_provenance_signal_detected")
|
result?.intent.reasons.includes("inventory_selected_object_provenance_signal_detected")
|
||||||
|
|
@ -111,4 +110,67 @@ describe("inventory root frame regressions", () => {
|
||||||
expect(result?.intent.intent).not.toBe("bank_operations_by_counterparty");
|
expect(result?.intent.intent).not.toBe("bank_operations_by_counterparty");
|
||||||
expect(result?.filters.extracted_filters.item).toBe("Четки Пост (84*117)");
|
expect(result?.filters.extracted_filters.item).toBe("Четки Пост (84*117)");
|
||||||
});
|
});
|
||||||
|
it("restores stock snapshot intent for selected-object restatement on the previously reviewed date", () => {
|
||||||
|
const result = runAddressDecomposeStage(
|
||||||
|
'По выбранному объекту "Столешница 600*3050*26 альмандин": покажи еще раз остатки на дату которую до этого рассматривали',
|
||||||
|
{
|
||||||
|
previous_intent: "inventory_purchase_documents_for_item",
|
||||||
|
previous_filters: {
|
||||||
|
item: "Столешница 600*3050*26 альмандин",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
as_of_date: "2019-05-31"
|
||||||
|
},
|
||||||
|
previous_anchor_type: "item",
|
||||||
|
previous_anchor_value: "Столешница 600*3050*26 альмандин",
|
||||||
|
root_intent: "inventory_on_hand_as_of_date",
|
||||||
|
root_filters: {
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
period_from: "2019-05-01",
|
||||||
|
period_to: "2019-05-31",
|
||||||
|
as_of_date: "2019-05-31"
|
||||||
|
},
|
||||||
|
root_anchor_type: "organization",
|
||||||
|
root_anchor_value: 'ООО "Альтернатива Плюс"',
|
||||||
|
current_frame_kind: "inventory_drilldown"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.intent.intent).toBe("inventory_on_hand_as_of_date");
|
||||||
|
expect(result?.intent.reasons).toContain("intent_restored_to_inventory_root_frame");
|
||||||
|
expect(result?.filters.extracted_filters.item).toBe("Столешница 600*3050*26 альмандин");
|
||||||
|
expect(result?.filters.extracted_filters.organization).toBe('ООО "Альтернатива Плюс"');
|
||||||
|
expect(result?.filters.extracted_filters.as_of_date).toBe("2019-05-31");
|
||||||
|
expect(result?.baseReasons).toContain("address_followup_context_applied");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("restores inventory root frame for root-context-only month follow-up after drilldown", () => {
|
||||||
|
const result = runAddressDecomposeStage("остатки на июль 2019", {
|
||||||
|
previous_filters: {
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
as_of_date: "2020-03-31",
|
||||||
|
period_from: "2020-03-01",
|
||||||
|
period_to: "2020-03-31"
|
||||||
|
},
|
||||||
|
previous_anchor_type: null,
|
||||||
|
previous_anchor_value: null,
|
||||||
|
root_intent: "inventory_on_hand_as_of_date",
|
||||||
|
root_filters: {
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
as_of_date: "2020-03-31",
|
||||||
|
period_from: "2020-03-01",
|
||||||
|
period_to: "2020-03-31"
|
||||||
|
},
|
||||||
|
root_context_only: true,
|
||||||
|
current_frame_kind: "inventory_root"
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.intent.intent).toBe("inventory_on_hand_as_of_date");
|
||||||
|
expect(result?.intent.reasons).toContain("intent_restored_to_inventory_root_frame");
|
||||||
|
expect(result?.filters.extracted_filters.organization).toBe('ООО "Альтернатива Плюс"');
|
||||||
|
expect(result?.filters.extracted_filters.period_from).toBe("2019-07-01");
|
||||||
|
expect(result?.filters.extracted_filters.period_to).toBe("2019-07-31");
|
||||||
|
expect(result?.filters.extracted_filters.as_of_date).toBe("2019-07-31");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ afterEach(() => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("inventory sale trace document route", () => {
|
describe("inventory sale trace movement route", () => {
|
||||||
it("uses document sales route with native item resolution for selected-object buyer follow-up", async () => {
|
it("uses 41.01 movement route with native item resolution for selected-object buyer follow-up", async () => {
|
||||||
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||||
fetched_rows: 2,
|
fetched_rows: 2,
|
||||||
matched_rows: 2,
|
matched_rows: 2,
|
||||||
|
|
@ -30,25 +30,27 @@ describe("inventory sale trace document route", () => {
|
||||||
{
|
{
|
||||||
Period: "2015-02-25T12:00:00Z",
|
Period: "2015-02-25T12:00:00Z",
|
||||||
Registrator: "Реализация товаров и услуг 00000000012 от 25.02.2015 12:00:00",
|
Registrator: "Реализация товаров и услуг 00000000012 от 25.02.2015 12:00:00",
|
||||||
AccountDt: "",
|
AccountDt: "62.01",
|
||||||
AccountKt: "41.01",
|
AccountKt: "41.01",
|
||||||
Amount: 12605435.66,
|
Amount: 12605435.66,
|
||||||
Quantity: 40,
|
Quantity: 40,
|
||||||
Item: "Рабочая станция универсального специалиста (индивидуальное изготовление)",
|
SubcontoKt1: "Рабочая станция универсального специалиста (индивидуальное изготовление)",
|
||||||
Counterparty: "Комитет государственных услуг г. Москвы",
|
SubcontoKt3: "Основной склад",
|
||||||
Contract: "Гос.контракт № 42/15 от 20.02.2015г. Силино окна",
|
SubcontoDt1: "Комитет государственных услуг г. Москвы",
|
||||||
|
SubcontoDt2: "Гос.контракт № 42/15 от 20.02.2015 г. Силино окна",
|
||||||
Organization: "ООО \\Альтернатива Плюс\\"
|
Organization: "ООО \\Альтернатива Плюс\\"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Period: "2015-02-09T12:00:14Z",
|
Period: "2015-02-09T12:00:14Z",
|
||||||
Registrator: "Реализация товаров и услуг 00000000004 от 09.02.2015 12:00:14",
|
Registrator: "Реализация товаров и услуг 00000000004 от 09.02.2015 12:00:14",
|
||||||
AccountDt: "",
|
AccountDt: "62.01",
|
||||||
AccountKt: "41.01",
|
AccountKt: "41.01",
|
||||||
Amount: 16421320.17,
|
Amount: 16421320.17,
|
||||||
Quantity: 51,
|
Quantity: 51,
|
||||||
Item: "Рабочая станция универсального специалиста (индивидуальное изготовление)",
|
SubcontoKt1: "Рабочая станция универсального специалиста (индивидуальное изготовление)",
|
||||||
Counterparty: "Комитет государственных услуг г. Москвы",
|
SubcontoKt3: "Основной склад",
|
||||||
Contract: "Гос.контракт № 17/15 от 02.02.2015г.",
|
SubcontoDt1: "Комитет государственных услуг г. Москвы",
|
||||||
|
SubcontoDt2: "Гос.контракт № 17/15 от 02.02.2015 г.",
|
||||||
Organization: "ООО \\Альтернатива Плюс\\"
|
Organization: "ООО \\Альтернатива Плюс\\"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
@ -82,11 +84,12 @@ describe("inventory sale trace document route", () => {
|
||||||
|
|
||||||
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||||
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
||||||
expect(query).toContain("Документ.РеализацияТоваровУслуг.Товары КАК Товары");
|
expect(query).toContain("РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто КАК Движения");
|
||||||
expect(query).toContain("Товары.Номенклатура В (ВЫБРАТЬ Номенклатура.Ссылка");
|
expect(query).toContain('ПОДСТРОКА(ЕСТЬNULL(Движения.СчетКт.Код, ""), 1, 5) = "41.01"');
|
||||||
expect(query).toContain('Номенклатура.Наименование = "Рабочая станция универсального специалиста (индивидуальное изготовление)"');
|
expect(query).toContain("Движения.СубконтоКт1 В (ВЫБРАТЬ Номенклатура.Ссылка");
|
||||||
expect(query).toContain("ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Контрагент) КАК Контрагент");
|
expect(query).toContain(
|
||||||
expect(query).toContain("ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Организация) КАК Организация");
|
'Номенклатура.Наименование = "Рабочая станция универсального специалиста (индивидуальное изготовление)"'
|
||||||
|
);
|
||||||
expect(query).not.toContain("2016-06-30");
|
expect(query).not.toContain("2016-06-30");
|
||||||
expect(query).not.toContain("2016-06-01");
|
expect(query).not.toContain("2016-06-01");
|
||||||
expect(query).not.toContain('ПРЕДСТАВЛЕНИЕ(Движения.Организация) = "ООО \\Альтернатива Плюс\\"');
|
expect(query).not.toContain('ПРЕДСТАВЛЕНИЕ(Движения.Организация) = "ООО \\Альтернатива Плюс\\"');
|
||||||
|
|
|
||||||
|
|
@ -25,13 +25,14 @@ describe("inventory sale trace selected-object regressions", () => {
|
||||||
const saleRow = {
|
const saleRow = {
|
||||||
Period: "2021-04-15T00:00:00Z",
|
Period: "2021-04-15T00:00:00Z",
|
||||||
Registrator: "Реализация товаров и услуг 00000000201 от 15.04.2021 0:00:00",
|
Registrator: "Реализация товаров и услуг 00000000201 от 15.04.2021 0:00:00",
|
||||||
AccountDt: "",
|
AccountDt: "62.01",
|
||||||
AccountKt: "41.01",
|
AccountKt: "41.01",
|
||||||
Amount: 165.83,
|
Amount: 165.83,
|
||||||
Quantity: 1,
|
Quantity: 1,
|
||||||
Item: "Кромка с клеем 33 дуб ниагара 137 м",
|
SubcontoKt1: "Кромка с клеем 33 дуб ниагара 137 м",
|
||||||
Counterparty: "ООО \\Покупатель\\",
|
SubcontoKt3: "Основной склад",
|
||||||
Contract: "Договор реализации № 17 от 14.04.2021",
|
SubcontoDt1: "ООО \\Покупатель\\",
|
||||||
|
SubcontoDt2: "Договор реализации № 17 от 14.04.2021",
|
||||||
Organization: "ООО \\Альтернатива Плюс\\"
|
Organization: "ООО \\Альтернатива Плюс\\"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -70,7 +71,7 @@ describe("inventory sale trace selected-object regressions", () => {
|
||||||
|
|
||||||
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||||
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
||||||
expect(query).toContain("Документ.РеализацияТоваровУслуг.Товары КАК Товары");
|
expect(query).toContain("РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто КАК Движения");
|
||||||
expect(query).toContain('Номенклатура.Наименование = "Кромка с клеем 33 дуб ниагара 137 м"');
|
expect(query).toContain('Номенклатура.Наименование = "Кромка с клеем 33 дуб ниагара 137 м"');
|
||||||
expect(query).not.toContain('Номенклатура.Наименование = "Кромка"');
|
expect(query).not.toContain('Номенклатура.Наименование = "Кромка"');
|
||||||
});
|
});
|
||||||
|
|
@ -132,6 +133,7 @@ describe("inventory sale trace selected-object regressions", () => {
|
||||||
|
|
||||||
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||||
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
||||||
|
expect(query).toContain("РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто КАК Движения");
|
||||||
expect(query).toContain('Номенклатура.Наименование = "Кромка с клеем 33 дуб ниагара 137 м"');
|
expect(query).toContain('Номенклатура.Наименование = "Кромка с клеем 33 дуб ниагара 137 м"');
|
||||||
expect(query).not.toContain('Номенклатура.Наименование = "Кромка"');
|
expect(query).not.toContain('Номенклатура.Наименование = "Кромка"');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,7 @@ describe("inventory selected-object follow-up", () => {
|
||||||
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
||||||
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
||||||
expect(result?.debug.extracted_filters?.item).toBe("Столешница 600*3050*26 дуб ниагара");
|
expect(result?.debug.extracted_filters?.item).toBe("Столешница 600*3050*26 дуб ниагара");
|
||||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2019-03-31");
|
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
||||||
expect(String(result?.reply_text ?? "")).toContain("Торговый дом \\Союз МСК\\");
|
expect(String(result?.reply_text ?? "")).toContain("Торговый дом \\Союз МСК\\");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -242,7 +242,7 @@ describe("inventory selected-object follow-up", () => {
|
||||||
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
||||||
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
||||||
expect(result?.debug.extracted_filters?.item).toBe("Рабочая станция универсального специалиста (индивидуальное изготовление)");
|
expect(result?.debug.extracted_filters?.item).toBe("Рабочая станция универсального специалиста (индивидуальное изготовление)");
|
||||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2016-07-31");
|
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
||||||
expect(String(result?.reply_text ?? "")).toContain("ООО \\Производство мебели\\");
|
expect(String(result?.reply_text ?? "")).toContain("ООО \\Производство мебели\\");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -292,7 +292,7 @@ describe("inventory selected-object follow-up", () => {
|
||||||
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
||||||
expect(result?.debug.selected_recipe).toBe("address_inventory_purchase_provenance_for_item_v1");
|
expect(result?.debug.selected_recipe).toBe("address_inventory_purchase_provenance_for_item_v1");
|
||||||
expect(result?.debug.extracted_filters?.item).toBe("Конструкция трансформер рабочей станции 1300*900*2000");
|
expect(result?.debug.extracted_filters?.item).toBe("Конструкция трансформер рабочей станции 1300*900*2000");
|
||||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-06-30");
|
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
||||||
expect(result?.debug.extracted_filters?.period_from).toBeUndefined();
|
expect(result?.debug.extracted_filters?.period_from).toBeUndefined();
|
||||||
expect(result?.debug.extracted_filters?.period_to).toBeUndefined();
|
expect(result?.debug.extracted_filters?.period_to).toBeUndefined();
|
||||||
expect(result?.debug.capability_id).toBe("inventory_inventory_purchase_provenance_for_item");
|
expect(result?.debug.capability_id).toBe("inventory_inventory_purchase_provenance_for_item");
|
||||||
|
|
@ -345,7 +345,7 @@ describe("inventory selected-object follow-up", () => {
|
||||||
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
||||||
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
||||||
expect(result?.debug.extracted_filters?.item).toBe("Рабочая станция универсального специалиста (индивидуальное изготовление)");
|
expect(result?.debug.extracted_filters?.item).toBe("Рабочая станция универсального специалиста (индивидуальное изготовление)");
|
||||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2016-05-31");
|
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
||||||
expect(String(result?.reply_text ?? "")).toContain("ООО \\Производство мебели\\");
|
expect(String(result?.reply_text ?? "")).toContain("ООО \\Производство мебели\\");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -394,7 +394,7 @@ describe("inventory selected-object follow-up", () => {
|
||||||
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
||||||
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
||||||
expect(result?.debug.extracted_filters?.item).toBe("Рабочая станция универсального специалиста (индивидуальное изготовление)");
|
expect(result?.debug.extracted_filters?.item).toBe("Рабочая станция универсального специалиста (индивидуальное изготовление)");
|
||||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2016-05-31");
|
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
||||||
expect(String(result?.reply_text ?? "")).toContain("ООО \\Производство мебели\\");
|
expect(String(result?.reply_text ?? "")).toContain("ООО \\Производство мебели\\");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -444,7 +444,7 @@ describe("inventory selected-object follow-up", () => {
|
||||||
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
||||||
expect(result?.debug.selected_recipe).toBe("address_inventory_purchase_provenance_for_item_v1");
|
expect(result?.debug.selected_recipe).toBe("address_inventory_purchase_provenance_for_item_v1");
|
||||||
expect(result?.debug.extracted_filters?.item).toBe("Рабочая станция универсального специалиста (индивидуальное изготовление)");
|
expect(result?.debug.extracted_filters?.item).toBe("Рабочая станция универсального специалиста (индивидуальное изготовление)");
|
||||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2016-05-31");
|
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
||||||
expect(String(result?.reply_text ?? "")).toContain("ООО \\Производство мебели\\");
|
expect(String(result?.reply_text ?? "")).toContain("ООО \\Производство мебели\\");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -491,7 +491,7 @@ describe("inventory selected-object follow-up", () => {
|
||||||
expect(result?.debug.detected_intent).toBe("inventory_purchase_documents_for_item");
|
expect(result?.debug.detected_intent).toBe("inventory_purchase_documents_for_item");
|
||||||
expect(result?.debug.selected_recipe).toBe("address_inventory_purchase_documents_for_item_v1");
|
expect(result?.debug.selected_recipe).toBe("address_inventory_purchase_documents_for_item_v1");
|
||||||
expect(result?.debug.extracted_filters?.item).toBe("Столешница 600*3050*26 дуб ниагара");
|
expect(result?.debug.extracted_filters?.item).toBe("Столешница 600*3050*26 дуб ниагара");
|
||||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2019-03-31");
|
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
||||||
expect(String(result?.reply_text ?? "")).toContain("Поступление товаров и услуг 00000000077");
|
expect(String(result?.reply_text ?? "")).toContain("Поступление товаров и услуг 00000000077");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -537,7 +537,7 @@ describe("inventory selected-object follow-up", () => {
|
||||||
expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item");
|
expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item");
|
||||||
expect(result?.debug.selected_recipe).toBe("address_inventory_sale_trace_for_item_v1");
|
expect(result?.debug.selected_recipe).toBe("address_inventory_sale_trace_for_item_v1");
|
||||||
expect(result?.debug.extracted_filters?.item).toBe("Четки Пост (84*117)");
|
expect(result?.debug.extracted_filters?.item).toBe("Четки Пост (84*117)");
|
||||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-03-31");
|
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
||||||
expect(String(result?.reply_text ?? "").split("\n")[0]).toContain("ИП Покупатель");
|
expect(String(result?.reply_text ?? "").split("\n")[0]).toContain("ИП Покупатель");
|
||||||
expect(String(result?.reply_text ?? "")).toContain("Документы выбытия");
|
expect(String(result?.reply_text ?? "")).toContain("Документы выбытия");
|
||||||
});
|
});
|
||||||
|
|
@ -583,7 +583,7 @@ describe("inventory selected-object follow-up", () => {
|
||||||
expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item");
|
expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item");
|
||||||
expect(result?.debug.selected_recipe).toBe("address_inventory_sale_trace_for_item_v1");
|
expect(result?.debug.selected_recipe).toBe("address_inventory_sale_trace_for_item_v1");
|
||||||
expect(result?.debug.extracted_filters?.item).toBe("Пуф арий");
|
expect(result?.debug.extracted_filters?.item).toBe("Пуф арий");
|
||||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-05-31");
|
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
||||||
expect(result?.debug.reasons).toContain("inventory_selected_object_sale_trace_signal_detected");
|
expect(result?.debug.reasons).toContain("inventory_selected_object_sale_trace_signal_detected");
|
||||||
expect(String(result?.reply_text ?? "").split("\n")[0]).toContain("ООО \\Ромашка\\");
|
expect(String(result?.reply_text ?? "").split("\n")[0]).toContain("ООО \\Ромашка\\");
|
||||||
expect(String(result?.reply_text ?? "")).toContain("Документы выбытия");
|
expect(String(result?.reply_text ?? "")).toContain("Документы выбытия");
|
||||||
|
|
@ -631,7 +631,7 @@ describe("inventory selected-object follow-up", () => {
|
||||||
expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item");
|
expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item");
|
||||||
expect(result?.debug.selected_recipe).toBe("address_inventory_sale_trace_for_item_v1");
|
expect(result?.debug.selected_recipe).toBe("address_inventory_sale_trace_for_item_v1");
|
||||||
expect(result?.debug.extracted_filters?.item).toBe("Кромка с клеем 33 альмандин 137 м");
|
expect(result?.debug.extracted_filters?.item).toBe("Кромка с клеем 33 альмандин 137 м");
|
||||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-03-31");
|
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
||||||
expect(String(result?.reply_text ?? "")).toContain("ООО \\Покупатель\\");
|
expect(String(result?.reply_text ?? "")).toContain("ООО \\Покупатель\\");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -678,10 +678,7 @@ describe("inventory selected-object follow-up", () => {
|
||||||
expect(result?.handled).toBe(true);
|
expect(result?.handled).toBe(true);
|
||||||
expect(result?.response_type).toBe("FACTUAL_LIST");
|
expect(result?.response_type).toBe("FACTUAL_LIST");
|
||||||
expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item");
|
expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item");
|
||||||
expect(result?.debug.reasons).toContain("lifecycle_execution_detached_from_snapshot_date");
|
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
||||||
expect(result?.debug.reasons).toContain("as_of_date_cleared_for_history_recovery");
|
|
||||||
expect(result?.debug.limitations).toContain("lifecycle_execution_detached_from_snapshot_date");
|
|
||||||
expect(result?.debug.limitations).toContain("as_of_date_cleared_for_history_recovery");
|
|
||||||
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||||
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
||||||
expect(query).not.toContain("2019-03-31");
|
expect(query).not.toContain("2019-03-31");
|
||||||
|
|
@ -808,7 +805,7 @@ describe("inventory selected-object follow-up", () => {
|
||||||
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
||||||
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
||||||
expect(result?.debug.extracted_filters?.item).toBe("Кресло орион");
|
expect(result?.debug.extracted_filters?.item).toBe("Кресло орион");
|
||||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-03-31");
|
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
|
||||||
expect(result?.debug.extracted_filters?.period_from).toBeUndefined();
|
expect(result?.debug.extracted_filters?.period_from).toBeUndefined();
|
||||||
expect(result?.debug.extracted_filters?.period_to).toBeUndefined();
|
expect(result?.debug.extracted_filters?.period_to).toBeUndefined();
|
||||||
expect(result?.debug.reasons ?? []).not.toContain("lifecycle_execution_detached_from_snapshot_date");
|
expect(result?.debug.reasons ?? []).not.toContain("lifecycle_execution_detached_from_snapshot_date");
|
||||||
|
|
@ -820,7 +817,7 @@ describe("inventory selected-object follow-up", () => {
|
||||||
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
||||||
expect(query).toContain("Документ.ПоступлениеТоваровУслуг.Товары КАК Товары");
|
expect(query).toContain("Документ.ПоступлениеТоваровУслуг.Товары КАК Товары");
|
||||||
expect(query).toContain("Товары.Номенклатура В (ВЫБРАТЬ Номенклатура.Ссылка");
|
expect(query).toContain("Товары.Номенклатура В (ВЫБРАТЬ Номенклатура.Ссылка");
|
||||||
expect(query).toContain("Товары.Ссылка.Дата <= ДАТАВРЕМЯ(2020, 3, 31, 23, 59, 59)");
|
expect(query).not.toContain("2020-03-31");
|
||||||
expect(query).toContain("ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Организация) КАК Организация");
|
expect(query).toContain("ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Организация) КАК Организация");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,29 @@ describe("inventory warehouse anchor extraction", () => {
|
||||||
"inventory_on_hand_as_of_date"
|
"inventory_on_hand_as_of_date"
|
||||||
).extracted_filters;
|
).extracted_filters;
|
||||||
|
|
||||||
|
expect(filters.warehouse).toBeUndefined();
|
||||||
|
});
|
||||||
|
it("does not materialize self-scope slang tail as warehouse anchor in stock snapshot wording from the run", () => {
|
||||||
|
const result = extractAddressFilters("что там на складе у нас висит", "inventory_on_hand_as_of_date");
|
||||||
|
|
||||||
|
expect(result.extracted_filters.warehouse).toBeUndefined();
|
||||||
|
expect(result.warnings).toContain("warehouse_self_scope_detected");
|
||||||
|
expect(result.semantic_frame?.scope_kind).toBe("implicit_self_scope");
|
||||||
|
expect(result.semantic_frame?.anchor_kind).toBe("self_scope");
|
||||||
|
expect(result.semantic_frame?.anchor_value).toBeNull();
|
||||||
|
});
|
||||||
|
it("does not materialize same-date phrasing as warehouse anchor in stock follow-up", () => {
|
||||||
|
const filters = extractAddressFilters("какие остатки по складу на эту же дату", "inventory_on_hand_as_of_date").extracted_filters;
|
||||||
|
|
||||||
|
expect(filters.warehouse).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not materialize current-date phrasing as warehouse anchor in stock follow-up", () => {
|
||||||
|
const filters = extractAddressFilters(
|
||||||
|
"получить остатки по складу на текущую дату",
|
||||||
|
"inventory_on_hand_as_of_date"
|
||||||
|
).extracted_filters;
|
||||||
|
|
||||||
expect(filters.warehouse).toBeUndefined();
|
expect(filters.warehouse).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2376,5 +2376,228 @@ describe("assistant address follow-up carryover", () => {
|
||||||
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("treats short inventory month follow-up after drilldown as continuation of the recent inventory root frame", async () => {
|
||||||
|
const calls: Array<{ message: string; options?: any }> = [];
|
||||||
|
const followupMessage = "остатки на июль 2019";
|
||||||
|
const itemLabel = "Четки Пост (84*117)";
|
||||||
|
const organization = 'ООО "Альтернатива Плюс"';
|
||||||
|
|
||||||
|
const inventoryResult = {
|
||||||
|
handled: true,
|
||||||
|
reply_text: "Подтвержденный складской срез на 31.07.2019 собран.",
|
||||||
|
reply_type: "factual",
|
||||||
|
response_type: "FACTUAL_SUMMARY",
|
||||||
|
debug: {
|
||||||
|
detected_mode: "address_query",
|
||||||
|
detected_intent: "inventory_on_hand_as_of_date",
|
||||||
|
detected_intent_confidence: "high",
|
||||||
|
extracted_filters: {
|
||||||
|
as_of_date: "2019-07-31",
|
||||||
|
period_from: "2019-07-01",
|
||||||
|
period_to: "2019-07-31",
|
||||||
|
organization
|
||||||
|
},
|
||||||
|
selected_recipe: "address_inventory_on_hand_as_of_date_v1",
|
||||||
|
reasons: ["address_action_detected", "address_entity_detected", "address_followup_context_applied"]
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const addressQueryService = {
|
||||||
|
tryHandle: vi.fn(async (message: string, options?: any) => {
|
||||||
|
calls.push({ message, options });
|
||||||
|
if (message === followupMessage && options?.followupContext) {
|
||||||
|
return inventoryResult;
|
||||||
|
}
|
||||||
|
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-inventory-root-month-${Date.now()}`;
|
||||||
|
sessions.appendItem(sessionId, {
|
||||||
|
message_id: "msg-inventory-root-seed-july",
|
||||||
|
session_id: sessionId,
|
||||||
|
role: "assistant",
|
||||||
|
text: "inventory root seed",
|
||||||
|
reply_type: "factual",
|
||||||
|
created_at: "2026-04-15T19:00:00.000Z",
|
||||||
|
trace_id: "address-root-seed-july",
|
||||||
|
debug: {
|
||||||
|
detected_mode: "address_query",
|
||||||
|
detected_intent: "inventory_on_hand_as_of_date",
|
||||||
|
extracted_filters: {
|
||||||
|
as_of_date: "2020-03-31",
|
||||||
|
period_from: "2020-03-01",
|
||||||
|
period_to: "2020-03-31",
|
||||||
|
organization
|
||||||
|
},
|
||||||
|
selected_recipe: "address_inventory_on_hand_as_of_date_v1"
|
||||||
|
}
|
||||||
|
} as any);
|
||||||
|
sessions.appendItem(sessionId, {
|
||||||
|
message_id: "msg-inventory-drilldown-seed-july",
|
||||||
|
session_id: sessionId,
|
||||||
|
role: "assistant",
|
||||||
|
text: "inventory provenance seed",
|
||||||
|
reply_type: "factual",
|
||||||
|
created_at: "2026-04-15T19:01:00.000Z",
|
||||||
|
trace_id: "address-drilldown-seed-july",
|
||||||
|
debug: {
|
||||||
|
detected_mode: "address_query",
|
||||||
|
detected_intent: "inventory_purchase_provenance_for_item",
|
||||||
|
extracted_filters: {
|
||||||
|
item: itemLabel,
|
||||||
|
organization
|
||||||
|
},
|
||||||
|
selected_recipe: "address_inventory_purchase_provenance_for_item_v1",
|
||||||
|
anchor_type: "item",
|
||||||
|
anchor_value_raw: itemLabel,
|
||||||
|
anchor_value_resolved: itemLabel
|
||||||
|
}
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
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(calls).toHaveLength(1);
|
||||||
|
expect(calls[0].message).toBe(followupMessage);
|
||||||
|
expect(calls[0].options?.followupContext?.previous_intent).toBeUndefined();
|
||||||
|
expect(calls[0].options?.followupContext?.previous_filters?.item).toBeUndefined();
|
||||||
|
expect(calls[0].options?.followupContext?.root_context_only).toBe(true);
|
||||||
|
expect(calls[0].options?.followupContext?.root_intent).toBe("inventory_on_hand_as_of_date");
|
||||||
|
expect(calls[0].options?.followupContext?.root_filters?.organization).toBe(organization);
|
||||||
|
expect(calls[0].options?.followupContext?.root_filters?.as_of_date).toBe("2020-03-31");
|
||||||
|
expect(calls[0].options?.followupContext?.current_frame_kind).toBe("inventory_root");
|
||||||
|
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats colloquial supplier follow-up from an inventory root slice as continuation of the focused item", async () => {
|
||||||
|
const calls: Array<{ message: string; options?: any }> = [];
|
||||||
|
const followupMessage = "у кого мы модуль прямоугольный купили кстати";
|
||||||
|
const itemLabel = "Модуль прямоугольый 1400*110*750";
|
||||||
|
const organization = 'ООО "Альтернатива Плюс"';
|
||||||
|
|
||||||
|
const provenanceResult = {
|
||||||
|
handled: true,
|
||||||
|
reply_text: `По позиции ${itemLabel} подтвержден поставщик: Торговый дом "Союз".`,
|
||||||
|
reply_type: "factual",
|
||||||
|
response_type: "FACTUAL_SUMMARY",
|
||||||
|
debug: {
|
||||||
|
detected_mode: "address_query",
|
||||||
|
detected_intent: "inventory_purchase_provenance_for_item",
|
||||||
|
detected_intent_confidence: "medium",
|
||||||
|
extracted_filters: {
|
||||||
|
item: itemLabel,
|
||||||
|
organization,
|
||||||
|
as_of_date: "2020-05-31"
|
||||||
|
},
|
||||||
|
selected_recipe: "address_inventory_purchase_provenance_for_item_v1",
|
||||||
|
reasons: ["address_action_detected", "address_entity_detected", "address_followup_context_applied"]
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const addressQueryService = {
|
||||||
|
tryHandle: vi.fn(async (message: string, options?: any) => {
|
||||||
|
calls.push({ message, options });
|
||||||
|
if (message === followupMessage && options?.followupContext) {
|
||||||
|
return provenanceResult;
|
||||||
|
}
|
||||||
|
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-root-item-focus-${Date.now()}`;
|
||||||
|
sessions.appendItem(sessionId, {
|
||||||
|
message_id: "msg-inventory-root-seed",
|
||||||
|
session_id: sessionId,
|
||||||
|
role: "assistant",
|
||||||
|
text: `${itemLabel} был в остатках на 31.05.2020.`,
|
||||||
|
reply_type: "factual",
|
||||||
|
created_at: "2026-04-15T19:00:00.000Z",
|
||||||
|
trace_id: "address-root-seed",
|
||||||
|
debug: {
|
||||||
|
detected_mode: "address_query",
|
||||||
|
detected_intent: "inventory_on_hand_as_of_date",
|
||||||
|
extracted_filters: {
|
||||||
|
organization,
|
||||||
|
as_of_date: "2020-05-31",
|
||||||
|
period_from: "2020-05-01",
|
||||||
|
period_to: "2020-05-31"
|
||||||
|
},
|
||||||
|
selected_recipe: "address_inventory_on_hand_as_of_date_v1"
|
||||||
|
}
|
||||||
|
} as any);
|
||||||
|
sessions.setAddressNavigationState(sessionId, {
|
||||||
|
session_id: sessionId,
|
||||||
|
session_context: {
|
||||||
|
active_focus_object: {
|
||||||
|
object_type: "item",
|
||||||
|
label: itemLabel
|
||||||
|
},
|
||||||
|
organization_scope: organization,
|
||||||
|
date_scope: {
|
||||||
|
as_of_date: "2020-05-31",
|
||||||
|
period_from: "2020-05-01",
|
||||||
|
period_to: "2020-05-31"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const response = await service.handleMessage({
|
||||||
|
session_id: sessionId,
|
||||||
|
user_message: followupMessage,
|
||||||
|
useMock: true
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
expect(response.ok).toBe(true);
|
||||||
|
expect(response.reply_type).toBe("factual");
|
||||||
|
expect(calls).toHaveLength(1);
|
||||||
|
expect(calls[0].message).toBe(followupMessage);
|
||||||
|
expect(calls[0].options?.followupContext?.previous_intent).toBe("inventory_on_hand_as_of_date");
|
||||||
|
expect(calls[0].options?.followupContext?.previous_anchor_type).toBe("item");
|
||||||
|
expect(calls[0].options?.followupContext?.previous_anchor_value).toBe(itemLabel);
|
||||||
|
expect(calls[0].options?.followupContext?.previous_filters?.item).toBe(itemLabel);
|
||||||
|
expect(calls[0].options?.followupContext?.previous_filters?.organization).toBe(organization);
|
||||||
|
expect(calls[0].options?.followupContext?.previous_filters?.as_of_date).toBe("2020-05-31");
|
||||||
|
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,8 @@ describe("assistant address lane runtime adapter", () => {
|
||||||
const runAddressLaneAttempt = vi.fn(async () => factualLane());
|
const runAddressLaneAttempt = vi.fn(async () => factualLane());
|
||||||
|
|
||||||
const result = await runAssistantAddressLaneRuntime({
|
const result = await runAssistantAddressLaneRuntime({
|
||||||
userMessage: "сырой вопрос",
|
userMessage: "raw question",
|
||||||
addressInputMessage: "нормализованный вопрос",
|
addressInputMessage: "normalized question",
|
||||||
carryover,
|
carryover,
|
||||||
shouldPreferContextualLane: true,
|
shouldPreferContextualLane: true,
|
||||||
canRetryWithRawUserMessage: true,
|
canRetryWithRawUserMessage: true,
|
||||||
|
|
@ -44,24 +44,24 @@ describe("assistant address lane runtime adapter", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.handled).toBe(true);
|
expect(result.handled).toBe(true);
|
||||||
expect(result.selection?.messageUsed).toBe("нормализованный вопрос");
|
expect(result.selection?.messageUsed).toBe("normalized question");
|
||||||
expect(result.selection?.carryMeta).toBe(carryover);
|
expect(result.selection?.carryMeta).toBe(carryover);
|
||||||
expect(result.retryAudit.attempted).toBe(false);
|
expect(result.retryAudit.attempted).toBe(false);
|
||||||
expect(runAddressLaneAttempt).toHaveBeenCalledTimes(1);
|
expect(runAddressLaneAttempt).toHaveBeenCalledTimes(1);
|
||||||
expect(runAddressLaneAttempt).toHaveBeenCalledWith("нормализованный вопрос", carryover);
|
expect(runAddressLaneAttempt).toHaveBeenCalledWith("normalized question", carryover, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("retries with raw message after limited result and returns factual retry", async () => {
|
it("retries with raw message after limited result and returns factual retry", async () => {
|
||||||
const carryover: AssistantAddressFollowupCarryoverLike = { followupContext: { scope: "ctx" } };
|
const carryover: AssistantAddressFollowupCarryoverLike = { followupContext: { scope: "ctx" } };
|
||||||
const runAddressLaneAttempt = vi
|
const runAddressLaneAttempt = vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValueOnce(limitedLane("empty_match")) // primary
|
.mockResolvedValueOnce(limitedLane("empty_match"))
|
||||||
.mockResolvedValueOnce(limitedLane("empty_match")) // contextual
|
.mockResolvedValueOnce(limitedLane("empty_match"))
|
||||||
.mockResolvedValueOnce(factualLane()); // raw contextual retry
|
.mockResolvedValueOnce(factualLane());
|
||||||
|
|
||||||
const result = await runAssistantAddressLaneRuntime({
|
const result = await runAssistantAddressLaneRuntime({
|
||||||
userMessage: "сырой вопрос",
|
userMessage: "raw question",
|
||||||
addressInputMessage: "нормализованный вопрос",
|
addressInputMessage: "normalized question",
|
||||||
carryover,
|
carryover,
|
||||||
shouldPreferContextualLane: false,
|
shouldPreferContextualLane: false,
|
||||||
canRetryWithRawUserMessage: true,
|
canRetryWithRawUserMessage: true,
|
||||||
|
|
@ -70,7 +70,7 @@ describe("assistant address lane runtime adapter", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.handled).toBe(true);
|
expect(result.handled).toBe(true);
|
||||||
expect(result.selection?.messageUsed).toBe("сырой вопрос");
|
expect(result.selection?.messageUsed).toBe("raw question");
|
||||||
expect(result.selection?.carryMeta).toBe(carryover);
|
expect(result.selection?.carryMeta).toBe(carryover);
|
||||||
expect(result.retryAudit.attempted).toBe(true);
|
expect(result.retryAudit.attempted).toBe(true);
|
||||||
expect(result.retryAudit.reason).toBe("limited_result_retry_with_raw_message");
|
expect(result.retryAudit.reason).toBe("limited_result_retry_with_raw_message");
|
||||||
|
|
@ -83,12 +83,12 @@ describe("assistant address lane runtime adapter", () => {
|
||||||
it("returns pending limited result when retry is disabled", async () => {
|
it("returns pending limited result when retry is disabled", async () => {
|
||||||
const runAddressLaneAttempt = vi
|
const runAddressLaneAttempt = vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValueOnce(limitedLane("missing_anchor")) // primary
|
.mockResolvedValueOnce(limitedLane("missing_anchor"))
|
||||||
.mockResolvedValueOnce(unhandledLane()); // contextual fallback
|
.mockResolvedValueOnce(unhandledLane());
|
||||||
|
|
||||||
const result = await runAssistantAddressLaneRuntime({
|
const result = await runAssistantAddressLaneRuntime({
|
||||||
userMessage: "сырой вопрос",
|
userMessage: "raw question",
|
||||||
addressInputMessage: "нормализованный вопрос",
|
addressInputMessage: "normalized question",
|
||||||
carryover: { followupContext: { scope: "ctx" } },
|
carryover: { followupContext: { scope: "ctx" } },
|
||||||
shouldPreferContextualLane: false,
|
shouldPreferContextualLane: false,
|
||||||
canRetryWithRawUserMessage: false,
|
canRetryWithRawUserMessage: false,
|
||||||
|
|
@ -97,9 +97,8 @@ describe("assistant address lane runtime adapter", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.handled).toBe(true);
|
expect(result.handled).toBe(true);
|
||||||
expect(result.selection?.messageUsed).toBe("нормализованный вопрос");
|
expect(result.selection?.messageUsed).toBe("normalized question");
|
||||||
expect(result.selection?.addressLane.debug?.limited_reason_category).toBe("missing_anchor");
|
expect(result.selection?.addressLane.debug?.limited_reason_category).toBe("missing_anchor");
|
||||||
expect(result.retryAudit.attempted).toBe(false);
|
expect(result.retryAudit.attempted).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,80 @@ describe("assistant address orchestration runtime adapter", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("prefers raw inventory temporal root follow-up over account-balance canonical drift", async () => {
|
||||||
|
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
|
||||||
|
followupContext: {
|
||||||
|
previous_intent: "inventory_purchase_provenance_for_item",
|
||||||
|
previous_filters: {
|
||||||
|
item: "Четки Пост (84*117)",
|
||||||
|
organization: "ООО \\Альтернатива Плюс\\"
|
||||||
|
},
|
||||||
|
previous_anchor_type: "item",
|
||||||
|
previous_anchor_value: "Четки Пост (84*117)",
|
||||||
|
root_intent: "inventory_on_hand_as_of_date",
|
||||||
|
root_filters: {
|
||||||
|
as_of_date: "2020-03-31",
|
||||||
|
period_from: "2020-03-01",
|
||||||
|
period_to: "2020-03-31",
|
||||||
|
organization: "ООО \\Альтернатива Плюс\\"
|
||||||
|
},
|
||||||
|
current_frame_kind: "inventory_drilldown"
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
const resolveAssistantOrchestrationDecision = vi.fn(() => ({
|
||||||
|
runAddressLane: true,
|
||||||
|
livingMode: "address_data",
|
||||||
|
livingReason: "address_lane_triggered",
|
||||||
|
toolGateDecision: "run_address_lane",
|
||||||
|
toolGateReason: "followup_context_detected",
|
||||||
|
orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" }
|
||||||
|
}));
|
||||||
|
const buildAddressLlmPredecomposeContractV1 = vi.fn(
|
||||||
|
({ sourceMessage, canonicalMessage }: { sourceMessage: string; canonicalMessage: string }) => ({
|
||||||
|
schema_version: "address_llm_predecompose_contract_v1",
|
||||||
|
source_message: sourceMessage,
|
||||||
|
canonical_message: canonicalMessage,
|
||||||
|
mode: "address_query",
|
||||||
|
intent: canonicalMessage === sourceMessage ? "inventory_on_hand_as_of_date" : "account_balance_snapshot"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const rawMessage = "остатки на июль 2019";
|
||||||
|
|
||||||
|
const output = await buildAssistantAddressOrchestrationRuntime(
|
||||||
|
buildInput({
|
||||||
|
userMessage: rawMessage,
|
||||||
|
runAddressLlmPreDecompose: vi.fn(async () => ({
|
||||||
|
attempted: true,
|
||||||
|
applied: true,
|
||||||
|
effectiveMessage: "проверить остатки по счетам на июль 2019 года",
|
||||||
|
reason: "normalized_fragment_applied",
|
||||||
|
predecomposeContract: {
|
||||||
|
mode: "address_query",
|
||||||
|
intent: "account_balance_snapshot"
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
buildAddressLlmPredecomposeContractV1,
|
||||||
|
resolveAddressFollowupCarryoverContext,
|
||||||
|
resolveAssistantOrchestrationDecision
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(output.addressInputMessage).toBe(rawMessage);
|
||||||
|
expect(output.addressPreDecompose.applied).toBe(false);
|
||||||
|
expect(output.addressPreDecompose.reason).toBe("followup_raw_message_preferred_over_llm_rewrite");
|
||||||
|
expect(buildAddressLlmPredecomposeContractV1).toHaveBeenCalledWith({
|
||||||
|
sourceMessage: rawMessage,
|
||||||
|
canonicalMessage: rawMessage
|
||||||
|
});
|
||||||
|
expect(resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(2);
|
||||||
|
expect(resolveAssistantOrchestrationDecision).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
rawUserMessage: rawMessage,
|
||||||
|
effectiveAddressUserMessage: rawMessage
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("prefers raw selected-object inventory action over generic canonical drift intent", async () => {
|
it("prefers raw selected-object inventory action over generic canonical drift intent", async () => {
|
||||||
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
|
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
|
||||||
followupContext: {
|
followupContext: {
|
||||||
|
|
@ -441,4 +515,56 @@ describe("assistant address orchestration runtime adapter", () => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("prefers raw same-date follow-up over canonical rewrite to current date", async () => {
|
||||||
|
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
|
||||||
|
followupContext: {
|
||||||
|
previous_intent: "vat_payable_confirmed_as_of_date",
|
||||||
|
previous_filters: {
|
||||||
|
as_of_date: "2019-09-30",
|
||||||
|
period_from: "2019-09-01",
|
||||||
|
period_to: "2019-09-30"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
const resolveAssistantOrchestrationDecision = vi.fn(() => ({
|
||||||
|
runAddressLane: true,
|
||||||
|
livingMode: "address_data",
|
||||||
|
livingReason: "address_lane_triggered",
|
||||||
|
toolGateDecision: "run_address_lane",
|
||||||
|
toolGateReason: "followup_context_detected",
|
||||||
|
orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" }
|
||||||
|
}));
|
||||||
|
|
||||||
|
const rawMessage = "какие остатки по складу на эту же дату";
|
||||||
|
|
||||||
|
const output = await buildAssistantAddressOrchestrationRuntime(
|
||||||
|
buildInput({
|
||||||
|
userMessage: rawMessage,
|
||||||
|
runAddressLlmPreDecompose: vi.fn(async () => ({
|
||||||
|
attempted: true,
|
||||||
|
applied: true,
|
||||||
|
effectiveMessage: "получить остатки по складу на текущую дату",
|
||||||
|
reason: "normalized_fragment_applied",
|
||||||
|
predecomposeContract: {
|
||||||
|
mode: "address_query",
|
||||||
|
intent: "inventory_on_hand_as_of_date"
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
resolveAddressFollowupCarryoverContext,
|
||||||
|
resolveAssistantOrchestrationDecision
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(output.addressInputMessage).toBe(rawMessage);
|
||||||
|
expect(output.addressPreDecompose.applied).toBe(false);
|
||||||
|
expect(output.addressPreDecompose.reason).toBe("followup_raw_message_preferred_over_llm_rewrite");
|
||||||
|
expect(resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(2);
|
||||||
|
expect(resolveAssistantOrchestrationDecision).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
rawUserMessage: rawMessage,
|
||||||
|
effectiveAddressUserMessage: rawMessage
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -4,8 +4,8 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>NDC AI Normalizer Playground</title>
|
<title>NDC AI Normalizer Playground</title>
|
||||||
<script type="module" crossorigin src="/assets/index-Qq5WpuqR.js"></script>
|
<script type="module" crossorigin src="/assets/index-VJV2AL7G.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-Dcuz1nX5.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-CaUiKcE3.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ const SESSION_CONFIG_KEY = "ndc_normalizer_session_config_v1";
|
||||||
const AUTORUNS_LAYOUT_CONFIG_KEY = "ndc_autoruns_layout_config_v1";
|
const AUTORUNS_LAYOUT_CONFIG_KEY = "ndc_autoruns_layout_config_v1";
|
||||||
const AUTORUNS_SAVE_EVENT = "ndc-autoruns-save";
|
const AUTORUNS_SAVE_EVENT = "ndc-autoruns-save";
|
||||||
const ASSISTANT_STAGES = ["Анализ запроса", "Получение данных", "Подготовка ответа"];
|
const ASSISTANT_STAGES = ["Анализ запроса", "Получение данных", "Подготовка ответа"];
|
||||||
const DEFAULT_UI_MODE: UiMode = "assistant";
|
const DEFAULT_UI_MODE: UiMode = "autoruns";
|
||||||
const AUTOLOAD_PROMPT_VERSION = "normalizer_v2_0_2";
|
const AUTOLOAD_PROMPT_VERSION = "normalizer_v2_0_2";
|
||||||
const ASSISTANT_PROMPT_VERSION = "address_query_runtime_v1";
|
const ASSISTANT_PROMPT_VERSION = "address_query_runtime_v1";
|
||||||
const TAB_KEYS: TabKey[] = ["normalized", "fragments", "scope", "flags", "route", "raw", "validation", "logs"];
|
const TAB_KEYS: TabKey[] = ["normalized", "fragments", "scope", "flags", "route", "raw", "validation", "logs"];
|
||||||
|
|
@ -117,6 +117,8 @@ export default function App() {
|
||||||
const [lastError, setLastError] = useState("");
|
const [lastError, setLastError] = useState("");
|
||||||
|
|
||||||
const [uiMode, setUiMode] = useState<UiMode>(DEFAULT_UI_MODE);
|
const [uiMode, setUiMode] = useState<UiMode>(DEFAULT_UI_MODE);
|
||||||
|
const [showAutorunsSettingsMode, setShowAutorunsSettingsMode] = useState(true);
|
||||||
|
const [showAutorunsAutoRunsMode, setShowAutorunsAutoRunsMode] = useState(true);
|
||||||
const [showAutorunsAssistantMode, setShowAutorunsAssistantMode] = useState(true);
|
const [showAutorunsAssistantMode, setShowAutorunsAssistantMode] = useState(true);
|
||||||
const [showAutorunsDecompositionMode, setShowAutorunsDecompositionMode] = useState(true);
|
const [showAutorunsDecompositionMode, setShowAutorunsDecompositionMode] = useState(true);
|
||||||
const [showAutorunsProgressMode, setShowAutorunsProgressMode] = useState(true);
|
const [showAutorunsProgressMode, setShowAutorunsProgressMode] = useState(true);
|
||||||
|
|
@ -239,6 +241,8 @@ export default function App() {
|
||||||
const parsed = JSON.parse(cachedAutorunsLayout) as {
|
const parsed = JSON.parse(cachedAutorunsLayout) as {
|
||||||
uiMode?: UiMode;
|
uiMode?: UiMode;
|
||||||
activeTab?: TabKey;
|
activeTab?: TabKey;
|
||||||
|
showAutorunsSettingsMode?: boolean;
|
||||||
|
showAutorunsAutoRunsMode?: boolean;
|
||||||
showAutorunsAssistantMode?: boolean;
|
showAutorunsAssistantMode?: boolean;
|
||||||
showAutorunsDecompositionMode?: boolean;
|
showAutorunsDecompositionMode?: boolean;
|
||||||
showAutorunsProgressMode?: boolean;
|
showAutorunsProgressMode?: boolean;
|
||||||
|
|
@ -257,12 +261,20 @@ export default function App() {
|
||||||
showDecompositionRuntimeMode?: boolean;
|
showDecompositionRuntimeMode?: boolean;
|
||||||
prompts?: PromptState;
|
prompts?: PromptState;
|
||||||
};
|
};
|
||||||
if (parsed.uiMode === "assistant" || parsed.uiMode === "decomposition" || parsed.uiMode === "autoruns") {
|
if (parsed.uiMode === "decomposition") {
|
||||||
setUiMode(parsed.uiMode);
|
setUiMode("decomposition");
|
||||||
|
} else if (parsed.uiMode === "assistant" || parsed.uiMode === "autoruns") {
|
||||||
|
setUiMode("autoruns");
|
||||||
}
|
}
|
||||||
if (parsed.activeTab && TAB_KEYS.includes(parsed.activeTab)) {
|
if (parsed.activeTab && TAB_KEYS.includes(parsed.activeTab)) {
|
||||||
setActiveTab(parsed.activeTab);
|
setActiveTab(parsed.activeTab);
|
||||||
}
|
}
|
||||||
|
if (typeof parsed.showAutorunsSettingsMode === "boolean") {
|
||||||
|
setShowAutorunsSettingsMode(parsed.showAutorunsSettingsMode);
|
||||||
|
}
|
||||||
|
if (typeof parsed.showAutorunsAutoRunsMode === "boolean") {
|
||||||
|
setShowAutorunsAutoRunsMode(parsed.showAutorunsAutoRunsMode);
|
||||||
|
}
|
||||||
if (typeof parsed.showAutorunsAssistantMode === "boolean") {
|
if (typeof parsed.showAutorunsAssistantMode === "boolean") {
|
||||||
setShowAutorunsAssistantMode(parsed.showAutorunsAssistantMode);
|
setShowAutorunsAssistantMode(parsed.showAutorunsAssistantMode);
|
||||||
}
|
}
|
||||||
|
|
@ -430,6 +442,8 @@ export default function App() {
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
uiMode,
|
uiMode,
|
||||||
activeTab,
|
activeTab,
|
||||||
|
showAutorunsSettingsMode,
|
||||||
|
showAutorunsAutoRunsMode,
|
||||||
showAutorunsAssistantMode,
|
showAutorunsAssistantMode,
|
||||||
showAutorunsDecompositionMode,
|
showAutorunsDecompositionMode,
|
||||||
showAutorunsProgressMode,
|
showAutorunsProgressMode,
|
||||||
|
|
@ -946,15 +960,12 @@ export default function App() {
|
||||||
>
|
>
|
||||||
<header className="app-topbar">
|
<header className="app-topbar">
|
||||||
<div className="mode-switch-row">
|
<div className="mode-switch-row">
|
||||||
<button type="button" className={uiMode === "assistant" ? "tab active" : "tab"} onClick={() => setUiMode("assistant")}>
|
<button type="button" className={uiMode === "autoruns" ? "tab active" : "tab"} onClick={() => setUiMode("autoruns")}>
|
||||||
Ассистент
|
Управление ассистентом
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className={uiMode === "decomposition" ? "tab active" : "tab"} onClick={() => setUiMode("decomposition")}>
|
<button type="button" className={uiMode === "decomposition" ? "tab active" : "tab"} onClick={() => setUiMode("decomposition")}>
|
||||||
Декомпозиция
|
Декомпозиция
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className={uiMode === "autoruns" ? "tab active" : "tab"} onClick={() => setUiMode("autoruns")}>
|
|
||||||
История автопрогонов
|
|
||||||
</button>
|
|
||||||
<button type="button" className="tab" onClick={saveAutorunsLayout}>
|
<button type="button" className="tab" onClick={saveAutorunsLayout}>
|
||||||
Сохранить
|
Сохранить
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -1036,6 +1047,20 @@ export default function App() {
|
||||||
</div>
|
</div>
|
||||||
) : uiMode === "autoruns" ? (
|
) : uiMode === "autoruns" ? (
|
||||||
<div className="mode-switch-row mode-switch-row-right">
|
<div className="mode-switch-row mode-switch-row-right">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={showAutorunsSettingsMode ? "tab active" : "tab"}
|
||||||
|
onClick={() => setShowAutorunsSettingsMode((prev) => !prev)}
|
||||||
|
>
|
||||||
|
Настройки
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={showAutorunsAutoRunsMode ? "tab active" : "tab"}
|
||||||
|
onClick={() => setShowAutorunsAutoRunsMode((prev) => !prev)}
|
||||||
|
>
|
||||||
|
Автопрогоны
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={showAutorunsAssistantMode ? "tab active" : "tab"}
|
className={showAutorunsAssistantMode ? "tab active" : "tab"}
|
||||||
|
|
@ -1300,9 +1325,30 @@ export default function App() {
|
||||||
<div className="layout-grid layout-grid-autoruns">
|
<div className="layout-grid layout-grid-autoruns">
|
||||||
<AutoRunsHistoryPanel
|
<AutoRunsHistoryPanel
|
||||||
connection={connection}
|
connection={connection}
|
||||||
|
modelOptions={modelOptions}
|
||||||
|
modelsBusy={modelsBusy}
|
||||||
|
connectionStatus={connectionStatus}
|
||||||
|
connectionBusy={busy}
|
||||||
|
onConnectionChange={setConnection}
|
||||||
|
onReloadModels={reloadModels}
|
||||||
|
onSaveLocalConfig={saveLocalConfig}
|
||||||
|
onTestConnection={testConnection}
|
||||||
prompts={prompts}
|
prompts={prompts}
|
||||||
|
onPromptsChange={setPrompts}
|
||||||
|
promptPresets={presetList}
|
||||||
|
selectedPresetId={selectedPresetId}
|
||||||
|
onSelectPreset={setSelectedPresetId}
|
||||||
|
onLoadPreset={loadSelectedPreset}
|
||||||
|
onSavePreset={savePreset}
|
||||||
|
onResetDefaults={resetDefaults}
|
||||||
|
onDiffPrevious={diffWithPrevious}
|
||||||
|
presetName={presetName}
|
||||||
|
onPresetNameChange={setPresetName}
|
||||||
|
diffSummary={diffSummary}
|
||||||
assistantPromptVersion={ASSISTANT_PROMPT_VERSION}
|
assistantPromptVersion={ASSISTANT_PROMPT_VERSION}
|
||||||
decompositionPromptVersion={AUTOLOAD_PROMPT_VERSION}
|
decompositionPromptVersion={AUTOLOAD_PROMPT_VERSION}
|
||||||
|
showSettingsMode={showAutorunsSettingsMode}
|
||||||
|
showAutoRunsMode={showAutorunsAutoRunsMode}
|
||||||
showAssistantMode={showAutorunsAssistantMode}
|
showAssistantMode={showAutorunsAssistantMode}
|
||||||
showDecompositionMode={showAutorunsDecompositionMode}
|
showDecompositionMode={showAutorunsDecompositionMode}
|
||||||
showProgressMode={showAutorunsProgressMode}
|
showProgressMode={showAutorunsProgressMode}
|
||||||
|
|
|
||||||
|
|
@ -22,14 +22,46 @@ import type {
|
||||||
PromptState
|
PromptState
|
||||||
} from "../state/types";
|
} from "../state/types";
|
||||||
import { AssistantPanel } from "./AssistantPanel";
|
import { AssistantPanel } from "./AssistantPanel";
|
||||||
|
import { ConnectionPanel } from "./ConnectionPanel";
|
||||||
import { JsonView } from "./JsonView";
|
import { JsonView } from "./JsonView";
|
||||||
import { PanelFrame } from "./PanelFrame";
|
import { PanelFrame } from "./PanelFrame";
|
||||||
|
import { PromptPanel } from "./PromptPanel";
|
||||||
|
|
||||||
interface AutoRunsHistoryPanelProps {
|
interface AutoRunsHistoryPanelProps {
|
||||||
connection: ConnectionState;
|
connection: ConnectionState;
|
||||||
|
modelOptions: string[];
|
||||||
|
modelsBusy: boolean;
|
||||||
|
connectionStatus: string;
|
||||||
|
connectionBusy: boolean;
|
||||||
|
onConnectionChange: (next: ConnectionState) => void;
|
||||||
|
onReloadModels: () => Promise<void> | void;
|
||||||
|
onSaveLocalConfig: () => void;
|
||||||
|
onTestConnection: () => Promise<void> | void;
|
||||||
prompts: PromptState;
|
prompts: PromptState;
|
||||||
|
onPromptsChange: (next: PromptState) => void;
|
||||||
|
promptPresets: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
prompt_version: string;
|
||||||
|
systemPrompt: string;
|
||||||
|
developerPrompt: string;
|
||||||
|
domainPrompt: string;
|
||||||
|
schemaNotes?: string;
|
||||||
|
fewShotExamples?: string;
|
||||||
|
}>;
|
||||||
|
selectedPresetId: string;
|
||||||
|
onSelectPreset: (id: string) => void;
|
||||||
|
onLoadPreset: () => void;
|
||||||
|
onSavePreset: () => void;
|
||||||
|
onResetDefaults: () => void;
|
||||||
|
onDiffPrevious: () => void;
|
||||||
|
presetName: string;
|
||||||
|
onPresetNameChange: (name: string) => void;
|
||||||
|
diffSummary: string;
|
||||||
assistantPromptVersion: string;
|
assistantPromptVersion: string;
|
||||||
decompositionPromptVersion: string;
|
decompositionPromptVersion: string;
|
||||||
|
showSettingsMode: boolean;
|
||||||
|
showAutoRunsMode: boolean;
|
||||||
showAssistantMode: boolean;
|
showAssistantMode: boolean;
|
||||||
showDecompositionMode: boolean;
|
showDecompositionMode: boolean;
|
||||||
showProgressMode: boolean;
|
showProgressMode: boolean;
|
||||||
|
|
@ -470,9 +502,30 @@ function CopyOutlineIcon() {
|
||||||
|
|
||||||
export function AutoRunsHistoryPanel({
|
export function AutoRunsHistoryPanel({
|
||||||
connection,
|
connection,
|
||||||
|
modelOptions,
|
||||||
|
modelsBusy,
|
||||||
|
connectionStatus,
|
||||||
|
connectionBusy,
|
||||||
|
onConnectionChange,
|
||||||
|
onReloadModels,
|
||||||
|
onSaveLocalConfig,
|
||||||
|
onTestConnection,
|
||||||
prompts,
|
prompts,
|
||||||
|
onPromptsChange,
|
||||||
|
promptPresets,
|
||||||
|
selectedPresetId,
|
||||||
|
onSelectPreset,
|
||||||
|
onLoadPreset,
|
||||||
|
onSavePreset,
|
||||||
|
onResetDefaults,
|
||||||
|
onDiffPrevious,
|
||||||
|
presetName,
|
||||||
|
onPresetNameChange,
|
||||||
|
diffSummary,
|
||||||
assistantPromptVersion,
|
assistantPromptVersion,
|
||||||
decompositionPromptVersion,
|
decompositionPromptVersion,
|
||||||
|
showSettingsMode,
|
||||||
|
showAutoRunsMode,
|
||||||
showAssistantMode,
|
showAssistantMode,
|
||||||
showDecompositionMode,
|
showDecompositionMode,
|
||||||
showProgressMode,
|
showProgressMode,
|
||||||
|
|
@ -1726,9 +1779,47 @@ export function AutoRunsHistoryPanel({
|
||||||
hideHeader
|
hideHeader
|
||||||
>
|
>
|
||||||
<div className="autoruns-columns">
|
<div className="autoruns-columns">
|
||||||
<section className="autoruns-col">
|
{showSettingsMode ? (
|
||||||
|
<section className="autoruns-col autoruns-settings-col">
|
||||||
|
<div className="autoruns-col-header">
|
||||||
|
<h3>Настройки</h3>
|
||||||
|
</div>
|
||||||
|
<div className="autoruns-settings-stack">
|
||||||
|
<ConnectionPanel
|
||||||
|
embedded
|
||||||
|
value={connection}
|
||||||
|
modelOptions={modelOptions}
|
||||||
|
modelsBusy={modelsBusy}
|
||||||
|
onChange={onConnectionChange}
|
||||||
|
onReloadModels={onReloadModels}
|
||||||
|
onSaveLocalConfig={onSaveLocalConfig}
|
||||||
|
onTestConnection={onTestConnection}
|
||||||
|
lastStatus={connectionStatus}
|
||||||
|
busy={connectionBusy}
|
||||||
|
/>
|
||||||
|
<PromptPanel
|
||||||
|
embedded
|
||||||
|
value={prompts}
|
||||||
|
onChange={onPromptsChange}
|
||||||
|
presets={promptPresets}
|
||||||
|
selectedPresetId={selectedPresetId}
|
||||||
|
onSelectPreset={onSelectPreset}
|
||||||
|
onLoadPreset={onLoadPreset}
|
||||||
|
onSavePreset={onSavePreset}
|
||||||
|
onResetDefaults={onResetDefaults}
|
||||||
|
onDiffPrevious={onDiffPrevious}
|
||||||
|
presetName={presetName}
|
||||||
|
onPresetNameChange={onPresetNameChange}
|
||||||
|
diffSummary={diffSummary}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{showAutoRunsMode ? (
|
||||||
|
<section className="autoruns-col">
|
||||||
<div className="autoruns-col-header">
|
<div className="autoruns-col-header">
|
||||||
<h3>Настройки</h3>
|
<h3>Автопрогоны</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4>Настройки выборки</h4>
|
<h4>Настройки выборки</h4>
|
||||||
|
|
@ -2086,6 +2177,7 @@ export function AutoRunsHistoryPanel({
|
||||||
|
|
||||||
{errorText ? <p className="error-text">{errorText}</p> : null}
|
{errorText ? <p className="error-text">{errorText}</p> : null}
|
||||||
</section>
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<section className="autoruns-col">
|
<section className="autoruns-col">
|
||||||
<div className="autoruns-col-header">
|
<div className="autoruns-col-header">
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ interface ConnectionPanelProps {
|
||||||
onSaveLocalConfig: () => void;
|
onSaveLocalConfig: () => void;
|
||||||
lastStatus: string;
|
lastStatus: string;
|
||||||
busy: boolean;
|
busy: boolean;
|
||||||
|
embedded?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConnectionPanel({
|
export function ConnectionPanel({
|
||||||
|
|
@ -74,7 +75,8 @@ export function ConnectionPanel({
|
||||||
onTestConnection,
|
onTestConnection,
|
||||||
onSaveLocalConfig,
|
onSaveLocalConfig,
|
||||||
lastStatus,
|
lastStatus,
|
||||||
busy
|
busy,
|
||||||
|
embedded = false
|
||||||
}: ConnectionPanelProps) {
|
}: ConnectionPanelProps) {
|
||||||
const isLocal = value.llmProvider === "local";
|
const isLocal = value.llmProvider === "local";
|
||||||
const providerPreset = deriveProviderPreset(value);
|
const providerPreset = deriveProviderPreset(value);
|
||||||
|
|
@ -121,12 +123,8 @@ export function ConnectionPanel({
|
||||||
setMaxOutputTokensInput(String(parsed));
|
setMaxOutputTokensInput(String(parsed));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const content = (
|
||||||
<PanelFrame
|
<>
|
||||||
title="LLM Connection"
|
|
||||||
subtitle="Switch between OpenAI cloud and local OpenAI-compatible server."
|
|
||||||
actions={<span className="status-chip">{lastStatus || "Status: not checked"}</span>}
|
|
||||||
>
|
|
||||||
<div className="grid-two">
|
<div className="grid-two">
|
||||||
<label>
|
<label>
|
||||||
Provider
|
Provider
|
||||||
|
|
@ -269,6 +267,31 @@ export function ConnectionPanel({
|
||||||
{busy ? "Checking..." : "Test connection"}
|
{busy ? "Checking..." : "Test connection"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (embedded) {
|
||||||
|
return (
|
||||||
|
<section className="embedded-panel-section">
|
||||||
|
<div className="embedded-panel-section-header">
|
||||||
|
<div>
|
||||||
|
<h4>LLM Connector</h4>
|
||||||
|
<p>Switch between OpenAI cloud and local OpenAI-compatible server.</p>
|
||||||
|
</div>
|
||||||
|
<span className="status-chip">{lastStatus || "Status: not checked"}</span>
|
||||||
|
</div>
|
||||||
|
{content}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PanelFrame
|
||||||
|
title="LLM Connector"
|
||||||
|
subtitle="Switch between OpenAI cloud and local OpenAI-compatible server."
|
||||||
|
actions={<span className="status-chip">{lastStatus || "Status: not checked"}</span>}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
</PanelFrame>
|
</PanelFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ interface PromptPanelProps {
|
||||||
presetName: string;
|
presetName: string;
|
||||||
onPresetNameChange: (name: string) => void;
|
onPresetNameChange: (name: string) => void;
|
||||||
diffSummary: string;
|
diffSummary: string;
|
||||||
|
embedded?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PromptPanel({
|
export function PromptPanel({
|
||||||
|
|
@ -39,10 +40,11 @@ export function PromptPanel({
|
||||||
onDiffPrevious,
|
onDiffPrevious,
|
||||||
presetName,
|
presetName,
|
||||||
onPresetNameChange,
|
onPresetNameChange,
|
||||||
diffSummary
|
diffSummary,
|
||||||
|
embedded = false
|
||||||
}: PromptPanelProps) {
|
}: PromptPanelProps) {
|
||||||
return (
|
const content = (
|
||||||
<PanelFrame title="Prompt Manager" subtitle="Системный, developer и domain уровни управляются отдельно.">
|
<>
|
||||||
<div className="prompt-manager-grid">
|
<div className="prompt-manager-grid">
|
||||||
<label>
|
<label>
|
||||||
Системный prompt
|
Системный prompt
|
||||||
|
|
@ -101,6 +103,26 @@ export function PromptPanel({
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{diffSummary ? <p className="diff-summary">{diffSummary}</p> : null}
|
{diffSummary ? <p className="diff-summary">{diffSummary}</p> : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (embedded) {
|
||||||
|
return (
|
||||||
|
<section className="embedded-panel-section">
|
||||||
|
<div className="embedded-panel-section-header">
|
||||||
|
<div>
|
||||||
|
<h4>Prompt Manager</h4>
|
||||||
|
<p>Системный, developer и domain уровни управляются отдельно.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{content}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PanelFrame title="Prompt Manager" subtitle="Системный, developer и domain уровни управляются отдельно.">
|
||||||
|
{content}
|
||||||
</PanelFrame>
|
</PanelFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -966,6 +966,44 @@ button:disabled {
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.autoruns-settings-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autoruns-settings-stack {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embedded-panel-section {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgb(var(--rgb-surface-horizontal));
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embedded-panel-section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embedded-panel-section-header h4 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-main);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embedded-panel-section-header p {
|
||||||
|
margin: 6px 0 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
.autoruns-assistant-live-col {
|
.autoruns-assistant-live-col {
|
||||||
background: rgb(var(--rgb-surface-main));
|
background: rgb(var(--rgb-surface-main));
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue