Compare commits
10 Commits
36f7b4df03
...
3bf7611b53
| Author | SHA1 | Date |
|---|---|---|
|
|
3bf7611b53 | |
|
|
a14eee4976 | |
|
|
43ae3237ce | |
|
|
e3e17ac7db | |
|
|
92473c894f | |
|
|
baa8aa31e7 | |
|
|
0eb583f7dc | |
|
|
af4b01d954 | |
|
|
d1536ed84b | |
|
|
c577ac6b00 |
|
|
@ -0,0 +1,138 @@
|
||||||
|
{
|
||||||
|
"schema_version": "domain_truth_harness_spec_v1",
|
||||||
|
"scenario_id": "assistant_stage1_hm_vat_period_oldstock_regression_20260525",
|
||||||
|
"domain": "assistant_stage1_hm_vat_period_oldstock_regression",
|
||||||
|
"title": "GUI Hm VAT explicit period and factual old-stock replay",
|
||||||
|
"description": "Focused replay for assistant-stage1-HmCqi80Vkb: explicit VAT month questions after an inventory purchase-date bridge must not reuse the old purchase date, and old-stock aging must be grounded in factual positive stock rather than earliest historical purchases.",
|
||||||
|
"bindings": {
|
||||||
|
"organization": "ООО \"Альтернатива Плюс\"",
|
||||||
|
"focus_item": "Рабочая станция универсального специалиста (индивидуальное изготовление)"
|
||||||
|
},
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": "s01_current_stock_for_org",
|
||||||
|
"title": "Current inventory snapshot for organization",
|
||||||
|
"question": "Какие остатки на складе у ООО \"Альтернатива Плюс\"?",
|
||||||
|
"allowed_reply_types": ["factual"],
|
||||||
|
"expected_intents": ["inventory_on_hand_as_of_date"],
|
||||||
|
"expected_recipe": "address_inventory_on_hand_as_of_date_v1",
|
||||||
|
"required_direct_answer_patterns_any": ["остат", "склад"],
|
||||||
|
"forbidden_answer_patterns": ["(?m)^Коротко:", "(?i)capability_id", "(?i)mcp_discovery"],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["inventory", "organization_scope"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s02_historical_stock_march_2016",
|
||||||
|
"title": "Historical inventory snapshot from Hm run",
|
||||||
|
"question": "март 2016",
|
||||||
|
"allowed_reply_types": ["factual"],
|
||||||
|
"expected_intents": ["inventory_on_hand_as_of_date"],
|
||||||
|
"expected_recipe": "address_inventory_on_hand_as_of_date_v1",
|
||||||
|
"required_filters": {
|
||||||
|
"period_from": "2016-03-01",
|
||||||
|
"period_to": "2016-03-31",
|
||||||
|
"as_of_date": "2016-03-31"
|
||||||
|
},
|
||||||
|
"required_direct_answer_patterns_any": ["31\\.03\\.2016", "Рабочая станция"],
|
||||||
|
"forbidden_answer_patterns": ["(?m)^Коротко:", "(?i)capability_id", "(?i)mcp_discovery"],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["inventory", "historical_snapshot", "date_carryover"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s03_selected_item_purchase_provenance",
|
||||||
|
"title": "Selected workstation purchase provenance",
|
||||||
|
"question": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?",
|
||||||
|
"allowed_reply_types": ["factual", "partial_coverage"],
|
||||||
|
"expected_intents": ["inventory_purchase_provenance_for_item"],
|
||||||
|
"expected_recipe": "address_inventory_purchase_provenance_for_item_v1",
|
||||||
|
"required_filters": {
|
||||||
|
"item": "Рабочая станция универсального специалиста (индивидуальное изготовление)",
|
||||||
|
"as_of_date": "2016-03-31"
|
||||||
|
},
|
||||||
|
"required_direct_answer_patterns_any": ["Рабочая станция", "поставщик|закуп"],
|
||||||
|
"forbidden_answer_patterns": ["(?m)^Коротко:", "(?i)capability_id", "(?i)mcp_discovery", "контрагент:\\s*Рабочая станция универсального специалиста"],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["inventory", "selected_object", "purchase_provenance"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s04_selected_item_sale_trace",
|
||||||
|
"title": "Selected workstation sale trace follow-up",
|
||||||
|
"question": "а кому продали?",
|
||||||
|
"allowed_reply_types": ["factual", "partial_coverage"],
|
||||||
|
"expected_intents": ["inventory_sale_trace_for_item"],
|
||||||
|
"expected_recipe": "address_inventory_sale_trace_for_item_v1",
|
||||||
|
"required_direct_answer_patterns_any": ["Комитет государственных услуг", "без партионного учета"],
|
||||||
|
"forbidden_answer_patterns": ["(?m)^Коротко:", "(?i)capability_id", "(?i)mcp_discovery", "контрагент:\\s*Рабочая станция универсального специалиста"],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["inventory", "selected_object", "sale_trace"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s05_item_field_truth_meta",
|
||||||
|
"title": "Item versus counterparty field-truth meta check",
|
||||||
|
"question": "у тебя написано кто контрагент: рабочая станция - это ошибка?",
|
||||||
|
"allowed_reply_types": ["chat", "factual", "factual_with_explanation", "partial_coverage"],
|
||||||
|
"required_direct_answer_patterns_any": ["товар", "не контрагент"],
|
||||||
|
"forbidden_answer_patterns": ["(?m)^Коротко:", "(?i)capability_id", "(?i)mcp_discovery"],
|
||||||
|
"criticality": "warning",
|
||||||
|
"semantic_tags": ["field_truth", "item_vs_counterparty"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s06_vat_purchase_date_bridge",
|
||||||
|
"title": "VAT by selected item purchase date bridge",
|
||||||
|
"question": "ндс можешь прикинуть на дату покупки рабочей станции?",
|
||||||
|
"allowed_reply_types": ["factual", "partial_coverage"],
|
||||||
|
"expected_intents": ["vat_liability_confirmed_for_tax_period"],
|
||||||
|
"expected_recipe": "address_vat_liability_confirmed_tax_period_v1",
|
||||||
|
"required_direct_answer_patterns_any": ["НДС", "05\\.02\\.2015", "дата закупки|дата покупки"],
|
||||||
|
"forbidden_answer_patterns": ["(?m)^Коротко:", "(?i)capability_id", "(?i)mcp_discovery"],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["vat", "purchase_date_bridge", "temporal_honesty"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s07_vat_explicit_march_2020",
|
||||||
|
"title": "Explicit March 2020 VAT must override purchase-date bridge",
|
||||||
|
"question": "а какой ндс мы должны сгрузить на март 2020?",
|
||||||
|
"allowed_reply_types": ["factual", "partial_coverage"],
|
||||||
|
"expected_intents": ["vat_liability_confirmed_for_tax_period"],
|
||||||
|
"expected_recipe": "address_vat_liability_confirmed_tax_period_v1",
|
||||||
|
"required_direct_answer_patterns_any": ["НДС", "2020"],
|
||||||
|
"forbidden_answer_patterns": ["(?m)^Коротко:", "05\\.02\\.2015", "дата закупки", "дата покупки", "(?i)capability_id", "(?i)mcp_discovery"],
|
||||||
|
"forbidden_filter_values": {
|
||||||
|
"period_from": ["2015-01-01", "2015-02-01"],
|
||||||
|
"period_to": ["2015-03-31", "2015-02-28"],
|
||||||
|
"purchase_date_bridge_selected": ["2015-02-05"]
|
||||||
|
},
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["vat", "explicit_period_override", "temporal_honesty"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s08_vat_explicit_february_2017",
|
||||||
|
"title": "Explicit February 2017 VAT must override purchase-date bridge",
|
||||||
|
"question": "прикинь какой ндс нам надо заплатить на февраль 2017",
|
||||||
|
"allowed_reply_types": ["factual", "partial_coverage"],
|
||||||
|
"expected_intents": ["vat_liability_confirmed_for_tax_period"],
|
||||||
|
"expected_recipe": "address_vat_liability_confirmed_tax_period_v1",
|
||||||
|
"required_direct_answer_patterns_any": ["НДС", "2017"],
|
||||||
|
"forbidden_answer_patterns": ["(?m)^Коротко:", "05\\.02\\.2015", "дата закупки", "дата покупки", "(?i)capability_id", "(?i)mcp_discovery"],
|
||||||
|
"forbidden_filter_values": {
|
||||||
|
"period_from": ["2015-01-01", "2015-02-01"],
|
||||||
|
"period_to": ["2015-03-31", "2015-02-28"],
|
||||||
|
"purchase_date_bridge_selected": ["2015-02-05"]
|
||||||
|
},
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["vat", "explicit_period_override", "temporal_honesty"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s09_old_stock_factual_positive_stock",
|
||||||
|
"title": "Old stock aging must be based on factual positive stock",
|
||||||
|
"question": "Есть ли у ООО \"Альтернатива Плюс\" остатки товара, которые закупались очень давно",
|
||||||
|
"allowed_reply_types": ["factual", "partial_coverage"],
|
||||||
|
"expected_intents": ["inventory_aging_by_purchase_date"],
|
||||||
|
"expected_recipe": "address_inventory_aging_by_purchase_date_v1",
|
||||||
|
"required_direct_answer_patterns_any": ["положительн", "остат", "давно закуп"],
|
||||||
|
"forbidden_answer_patterns": ["(?m)^Коротко:", "Позиций в выборке:\\s*390", "(?i)capability_id", "(?i)mcp_discovery"],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["inventory", "old_stock", "positive_stock_basis"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
{
|
||||||
|
"schema_version": "domain_scenario_pack_v1",
|
||||||
|
"pack_id": "assistant_stage1_y9_inventory_selected_object_regression_20260525",
|
||||||
|
"domain": "inventory_selected_object_reliability",
|
||||||
|
"title": "GUI Y9 inventory selected-object regression proof",
|
||||||
|
"description": "Focused replay for assistant-stage1-Y9qmtnz66r: inventory snapshot, selected item provenance, sale trace, field-truth meta question, VAT carryover from ambiguous purchase dates, and old-stock aging.",
|
||||||
|
"analysis_context": {
|
||||||
|
"as_of_date": "2026-05-25",
|
||||||
|
"source": "assistant-stage1-Y9qmtnz66r"
|
||||||
|
},
|
||||||
|
"bindings": {
|
||||||
|
"organization_alias": "АЛЬТЕРНАТИВА",
|
||||||
|
"organization": "ООО \"Альтернатива Плюс\"",
|
||||||
|
"historical_month": "март 2016",
|
||||||
|
"historical_as_of_date": "2016-03-31",
|
||||||
|
"focus_item": "Рабочая станция универсального специалиста (индивидуальное изготовление)",
|
||||||
|
"known_buyer": "Комитет государственных услуг г. Москвы"
|
||||||
|
},
|
||||||
|
"scenarios": [
|
||||||
|
{
|
||||||
|
"scenario_id": "inventory_selected_item_provenance_chain",
|
||||||
|
"title": "Inventory selected item provenance chain from GUI run Y9",
|
||||||
|
"description": "Validates that the assistant keeps organization/date/item carryover from a historical stock snapshot and answers selected-object provenance without confusing item, counterparty, lot trace, and purchase-date VAT semantics.",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": "s01_stock_now_for_org",
|
||||||
|
"title": "Current stock for explicit organization",
|
||||||
|
"node_role": "root",
|
||||||
|
"question": "Какие остатки на складе у {{bindings.organization}}?",
|
||||||
|
"semantic_tags": ["inventory", "organization_scope"],
|
||||||
|
"required_answer_shape": "direct_answer_first",
|
||||||
|
"required_answer_patterns_all": ["остат", "Альтернатива"],
|
||||||
|
"forbidden_answer_patterns": ["(?i)capability_id", "(?i)runtime_", "(?i)mcp_discovery"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s02_historical_stock_march_2016",
|
||||||
|
"title": "Historical stock snapshot",
|
||||||
|
"question": "остатки на {{bindings.historical_month}}",
|
||||||
|
"depends_on": ["s01_stock_now_for_org"],
|
||||||
|
"semantic_tags": ["inventory", "date_carryover", "historical_snapshot"],
|
||||||
|
"required_answer_shape": "direct_answer_first",
|
||||||
|
"required_answer_patterns_all": ["31.03.2016", "Рабочая станция"],
|
||||||
|
"forbidden_answer_patterns": ["(?i)capability_id", "(?i)runtime_", "(?i)mcp_discovery"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s03_selected_item_purchase_provenance",
|
||||||
|
"title": "Selected item purchase provenance",
|
||||||
|
"question": "По выбранному объекту \"{{bindings.focus_item}}\": где взяли это?",
|
||||||
|
"depends_on": ["s02_historical_stock_march_2016"],
|
||||||
|
"semantic_tags": ["inventory", "selected_object", "purchase_provenance", "field_truth"],
|
||||||
|
"required_answer_shape": "direct_answer_first",
|
||||||
|
"required_answer_patterns_all": ["Рабочая станция", "Поставщики", "05.02.2015"],
|
||||||
|
"forbidden_answer_patterns": ["(?i)capability_id", "(?i)runtime_", "(?i)mcp_discovery", "контрагент:\\s*{{bindings.focus_item}}"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s04_selected_item_sale_trace",
|
||||||
|
"title": "Selected item sale trace follow-up",
|
||||||
|
"question": "а кому продали?",
|
||||||
|
"depends_on": ["s03_selected_item_purchase_provenance"],
|
||||||
|
"semantic_tags": ["inventory", "selected_object", "sale_trace", "pronoun_followup"],
|
||||||
|
"required_answer_shape": "direct_answer_first",
|
||||||
|
"required_answer_patterns_all": ["Рабочая станция", "Комитет государственных услуг", "без партионного учета"],
|
||||||
|
"forbidden_answer_patterns": ["(?i)capability_id", "(?i)runtime_", "(?i)mcp_discovery", "контрагент:\\s*{{bindings.focus_item}}"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s05_item_counterparty_field_truth_meta",
|
||||||
|
"title": "Field-truth correction for item versus counterparty",
|
||||||
|
"question": "у тебя написано кто контрагент: рабочая станция - это ошибка?",
|
||||||
|
"depends_on": ["s04_selected_item_sale_trace"],
|
||||||
|
"semantic_tags": ["field_truth", "item_vs_counterparty", "answer_inspection"],
|
||||||
|
"required_answer_shape": "direct_answer_first",
|
||||||
|
"required_answer_patterns_all": ["товар", "не контрагент"],
|
||||||
|
"forbidden_answer_patterns": ["(?i)capability_id", "(?i)runtime_", "(?i)mcp_discovery"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s06_vat_by_ambiguous_purchase_date",
|
||||||
|
"title": "VAT period from ambiguous purchase date must disclose basis",
|
||||||
|
"question": "ндс можешь прикинуть на дату покупки рабочей станции?",
|
||||||
|
"depends_on": ["s03_selected_item_purchase_provenance", "s05_item_counterparty_field_truth_meta"],
|
||||||
|
"semantic_tags": ["vat", "date_carryover", "purchase_date_carryover", "field_truth", "temporal_honesty"],
|
||||||
|
"required_answer_shape": "direct_answer_first",
|
||||||
|
"required_answer_patterns_all": ["НДС", "первую подтвержденную дату закупки", "05.02.2015", "несколько подтвержденных дат закупки"],
|
||||||
|
"forbidden_answer_patterns": ["(?i)capability_id", "(?i)runtime_", "(?i)mcp_discovery", "это единственная возможная дата"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s07_vat_first_confirmed_purchase",
|
||||||
|
"title": "VAT recalculation by first confirmed purchase date",
|
||||||
|
"question": "если у нее несколько закупок, посчитай по первой подтвержденной покупке",
|
||||||
|
"depends_on": ["s06_vat_by_ambiguous_purchase_date"],
|
||||||
|
"semantic_tags": ["vat", "purchase_date_carryover", "first_purchase"],
|
||||||
|
"required_answer_shape": "direct_answer_first",
|
||||||
|
"required_answer_patterns_all": ["НДС", "первую подтвержденную дату закупки", "05.02.2015", "01.02.2015", "28.02.2015"],
|
||||||
|
"forbidden_answer_patterns": ["(?i)capability_id", "(?i)runtime_", "(?i)mcp_discovery"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s08_vat_last_confirmed_purchase",
|
||||||
|
"title": "VAT recalculation by last confirmed purchase date",
|
||||||
|
"question": "а теперь по последней подтвержденной покупке",
|
||||||
|
"depends_on": ["s07_vat_first_confirmed_purchase"],
|
||||||
|
"semantic_tags": ["vat", "purchase_date_carryover", "last_purchase"],
|
||||||
|
"required_answer_shape": "direct_answer_first",
|
||||||
|
"required_answer_patterns_all": ["НДС", "последнюю подтвержденную дату закупки", "22.07.2015", "01.07.2015", "31.07.2015"],
|
||||||
|
"forbidden_answer_patterns": ["(?i)capability_id", "(?i)runtime_", "(?i)mcp_discovery"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scenario_id": "inventory_old_stock_aging_from_y9",
|
||||||
|
"title": "Old stock aging question from GUI run Y9",
|
||||||
|
"description": "Validates old-stock answer shape, date basis, and supplier/item field truth for broad aging question.",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": "s01_old_stock_aging_for_org",
|
||||||
|
"title": "Old stock aging answer for explicit organization",
|
||||||
|
"node_role": "root",
|
||||||
|
"question": "Есть ли у {{bindings.organization}} остатки товара, которые закупались очень давно",
|
||||||
|
"semantic_tags": ["inventory", "aging", "supplier_provenance", "field_truth"],
|
||||||
|
"required_answer_shape": "direct_answer_first",
|
||||||
|
"required_answer_patterns_all": ["самым старым закупкам", "05.11.2014", "Альтернатива"],
|
||||||
|
"forbidden_answer_patterns": ["(?i)capability_id", "(?i)runtime_", "(?i)mcp_discovery", "контрагент:\\s*Стенд информационный"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s02_old_stock_same_date_followup",
|
||||||
|
"title": "Old stock same-date follow-up should keep inventory date basis",
|
||||||
|
"question": "какие остатки по товарам на эту дату относятся к старым закупкам?",
|
||||||
|
"depends_on": ["s01_old_stock_aging_for_org"],
|
||||||
|
"semantic_tags": ["inventory", "aging", "followup_date_carryover"],
|
||||||
|
"required_answer_shape": "direct_answer_first",
|
||||||
|
"required_answer_patterns_all": ["самым старым закупкам", "05.11.2014", "Альтернатива"],
|
||||||
|
"forbidden_answer_patterns": ["(?i)capability_id", "(?i)runtime_", "(?i)mcp_discovery"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
{
|
||||||
|
"schema_version": "domain_truth_harness_spec_v1",
|
||||||
|
"scenario_id": "hm_business_overview_cashflow_limits_20260525",
|
||||||
|
"domain": "assistant_stage1_business_overview",
|
||||||
|
"title": "Hm business overview cashflow limit honesty",
|
||||||
|
"description": "Focused live replay for the Hm business overview chain: all-time earnings wording must not present limited cashflow probes as confirmed all-time accounting profit.",
|
||||||
|
"bindings": {},
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": "s01_top_year",
|
||||||
|
"title": "Top revenue year",
|
||||||
|
"question": "\u043a\u0430\u043a\u043e\u0439 \u0443 \u041e\u041e\u041e \"\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441\" \u0441\u0430\u043c\u044b\u0439 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0439 \u0433\u043e\u0434",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"2015",
|
||||||
|
"\u0432\u0445\u043e\u0434\u044f\u0449"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"\u041a\u043e\u0440\u043e\u0442\u043a\u043e:",
|
||||||
|
"capability_id",
|
||||||
|
"mcp_discovery"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"business_overview",
|
||||||
|
"yearly_ranking"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s02_earnings_2017",
|
||||||
|
"title": "2017 accounting earnings",
|
||||||
|
"question": "\u0430 \u0437\u0430 2017 \u043c\u044b \u0441\u043a\u043e\u043a \u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual",
|
||||||
|
"factual_with_explanation"
|
||||||
|
],
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"\u0443\u0447\u0435\u0442\u043d\u0430\u044f \u043f\u0440\u0438\u0431\u044b\u043b\u044c",
|
||||||
|
"116 938,23"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"\u041a\u043e\u0440\u043e\u0442\u043a\u043e:",
|
||||||
|
"capability_id",
|
||||||
|
"mcp_discovery"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"business_overview",
|
||||||
|
"accounting_result"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s03_all_time_earnings_limit",
|
||||||
|
"title": "All-time earnings must disclose limited cashflow coverage",
|
||||||
|
"question": "\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0432\u043e\u043e\u0431\u0449\u0435 \u0434\u0435\u043d\u0435\u0433 \u043c\u044b \u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438 \u0437\u0430 \u0432\u0441\u0435 \u0432\u0440\u0435\u043c\u044f?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual",
|
||||||
|
"factual_with_explanation"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"\u0442\u043e\u0447\u043d\u0443\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u043f\u0440\u0438\u0431\u044b\u043b\u044c",
|
||||||
|
"\u043d\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u044e",
|
||||||
|
"\u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u043d",
|
||||||
|
"\u043d\u0435 \u0441\u0447\u0438\u0442\u0430\u044e \u043f\u043e\u043b\u043d\u044b\u043c \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u043c \u043e\u0431\u043e\u0440\u043e\u0442\u043e\u043c"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"\u041a\u043e\u0440\u043e\u0442\u043a\u043e:",
|
||||||
|
"capability_id",
|
||||||
|
"mcp_discovery"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"business_overview",
|
||||||
|
"coverage_honesty",
|
||||||
|
"all_time_cashflow"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,186 @@
|
||||||
|
{
|
||||||
|
"schema_version": "domain_truth_harness_spec_v1",
|
||||||
|
"scenario_id": "hm_business_profile_dependency_mini_pack_20260526",
|
||||||
|
"domain": "assistant_stage1_business_overview",
|
||||||
|
"title": "Hm business profile dependency mini pack",
|
||||||
|
"description": "Mini semantic replay for broad HM business profile: company evaluation, profit boundary, customer/supplier dependency signals, and next checks must stay business-useful, evidence-bounded, and free of internal MCP mechanics.",
|
||||||
|
"bindings": {},
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": "s01_establish_company_top_year",
|
||||||
|
"title": "Establish company context with a top-year question",
|
||||||
|
"question": "какой у ООО \"Альтернатива Плюс\" самый доходный год",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"2015",
|
||||||
|
"136 723 459,73",
|
||||||
|
"не полный бухгалтерский рейтинг"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Коротко:",
|
||||||
|
"capability_id",
|
||||||
|
"mcp_discovery",
|
||||||
|
"query_movements"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"business_overview",
|
||||||
|
"company_context",
|
||||||
|
"yearly_ranking"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s02_broad_company_evaluation",
|
||||||
|
"title": "Broad company evaluation keeps the evidence limit in the first layer",
|
||||||
|
"question": "Как ты оценишь деятельность компании?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual_with_explanation",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"ограниченн",
|
||||||
|
"не аудиторское",
|
||||||
|
"не подтвержден",
|
||||||
|
"чист",
|
||||||
|
"прибыл",
|
||||||
|
"Комитет государственных услуг",
|
||||||
|
"зависим"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Коротко:",
|
||||||
|
"capability_id",
|
||||||
|
"mcp_discovery",
|
||||||
|
"query_movements",
|
||||||
|
"business_overview_route_template_v1"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"business_overview",
|
||||||
|
"broad_business_evaluation",
|
||||||
|
"coverage_honesty",
|
||||||
|
"profit_boundary"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s03_profitability_boundary",
|
||||||
|
"title": "Profitability follow-up does not convert cashflow net into profit",
|
||||||
|
"question": "то есть бизнес прибыльный или это только обороты?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual_with_explanation",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"не подтверждаю",
|
||||||
|
"чист",
|
||||||
|
"прибыл",
|
||||||
|
"денежн",
|
||||||
|
"нетто",
|
||||||
|
"не полный отчет"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Коротко:",
|
||||||
|
"capability_id",
|
||||||
|
"mcp_discovery",
|
||||||
|
"это прибыль"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"business_overview",
|
||||||
|
"profitability_boundary",
|
||||||
|
"cashflow_not_profit"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s04_customer_dependency_signal",
|
||||||
|
"title": "Customer dependency signal stays bounded to incoming money evidence",
|
||||||
|
"question": "от кого мы больше всего зависим по входящим деньгам?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"Комитет государственных услуг",
|
||||||
|
"133.839.880,82",
|
||||||
|
"концентрац",
|
||||||
|
"входящ",
|
||||||
|
"не чистая прибыль"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Коротко:",
|
||||||
|
"capability_id",
|
||||||
|
"mcp_discovery",
|
||||||
|
"query_movements"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"business_overview",
|
||||||
|
"customer_dependency",
|
||||||
|
"incoming_counterparty_concentration"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s05_supplier_dependency_signal",
|
||||||
|
"title": "Supplier dependency signal stays bounded to outgoing money evidence",
|
||||||
|
"question": "а по поставщикам и исходящим платежам от кого зависимость?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"ГТК-Интер",
|
||||||
|
"44 943 460",
|
||||||
|
"32.58",
|
||||||
|
"исходящ",
|
||||||
|
"не аудит"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Коротко:",
|
||||||
|
"capability_id",
|
||||||
|
"mcp_discovery",
|
||||||
|
"query_movements"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"business_overview",
|
||||||
|
"supplier_dependency",
|
||||||
|
"outgoing_counterparty_concentration"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s06_next_checks",
|
||||||
|
"title": "Next-check answer gives concrete analyst path instead of vague advice",
|
||||||
|
"question": "что дальше проверять в первую очередь?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual_with_explanation",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"чист",
|
||||||
|
"прибыл",
|
||||||
|
"90/91/99",
|
||||||
|
"топ-3",
|
||||||
|
"склад",
|
||||||
|
"долг"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Коротко:",
|
||||||
|
"capability_id",
|
||||||
|
"mcp_discovery",
|
||||||
|
"query_movements",
|
||||||
|
"business_overview_route_template_v1"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"business_overview",
|
||||||
|
"next_checks",
|
||||||
|
"analyst_action_plan"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
{
|
||||||
|
"schema_version": "domain_truth_harness_spec_v1",
|
||||||
|
"scenario_id": "hm_business_profile_evaluation_limits_20260525",
|
||||||
|
"domain": "assistant_stage1_business_overview",
|
||||||
|
"title": "Hm business profile evaluation coverage honesty",
|
||||||
|
"description": "Focused live replay for broad company evaluation: the assistant must give a useful business reading while clearly presenting cashflow probes as a limited checked 1C slice, not a full audit, clean profit, or complete accounting turnover.",
|
||||||
|
"bindings": {},
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": "s01_top_year_establish_company",
|
||||||
|
"title": "Establish company context through top-year question",
|
||||||
|
"question": "какой у ООО \"Альтернатива Плюс\" самый доходный год",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"2015",
|
||||||
|
"входящ"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Коротко:",
|
||||||
|
"capability_id",
|
||||||
|
"mcp_discovery"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"business_overview",
|
||||||
|
"company_context",
|
||||||
|
"yearly_ranking"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s02_broad_company_evaluation",
|
||||||
|
"title": "Broad company evaluation must lead with limited-evidence framing",
|
||||||
|
"question": "Как ты оценишь деятельность компании?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual_with_explanation",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"ограниченн",
|
||||||
|
"денежн",
|
||||||
|
"не аудиторское",
|
||||||
|
"не подтвержден",
|
||||||
|
"чист",
|
||||||
|
"прибыл",
|
||||||
|
"Комитет государственных услуг",
|
||||||
|
"зависим"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Коротко:",
|
||||||
|
"capability_id",
|
||||||
|
"mcp_discovery",
|
||||||
|
"query_movements",
|
||||||
|
"business_overview_route_template_v1"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"business_overview",
|
||||||
|
"broad_business_evaluation",
|
||||||
|
"coverage_honesty",
|
||||||
|
"profit_boundary"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "s03_profitability_boundary",
|
||||||
|
"title": "Profitability follow-up must not convert net cashflow into profit",
|
||||||
|
"question": "то есть бизнес прибыльный или это только обороты?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual_with_explanation",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"не подтверждаю",
|
||||||
|
"прибыл",
|
||||||
|
"денежн",
|
||||||
|
"нетто",
|
||||||
|
"не полн"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"Коротко:",
|
||||||
|
"capability_id",
|
||||||
|
"mcp_discovery",
|
||||||
|
"это прибыль"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"business_overview",
|
||||||
|
"profitability_boundary",
|
||||||
|
"cashflow_not_profit"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -2302,91 +2302,7 @@ function buildAutoRunsRouter(services, openaiClient = new openaiResponsesClient_
|
||||||
});
|
});
|
||||||
router.post("/api/autoruns/autogen/generate", async (req, res, next) => {
|
router.post("/api/autoruns/autogen/generate", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const body = toRecord(req.body);
|
throw new http_1.ApiError("AUTOGEN_QUESTION_GENERATION_DISABLED", "Автогенерация вопросов отключена: используйте сохранение живых пользовательских сессий.", 410);
|
||||||
if (!body) {
|
|
||||||
throw new http_1.ApiError("INVALID_AUTOGEN_PAYLOAD", "JSON body is required", 400);
|
|
||||||
}
|
|
||||||
const mode = parseAutoGenMode(body.mode);
|
|
||||||
const count = parseAutogenCount(body.count);
|
|
||||||
const domain = parseAutogenDomain(body.domain);
|
|
||||||
const persistCaseSet = toBooleanSafe(body.persist_to_eval_cases) ?? true;
|
|
||||||
const generatedBy = parseAnnotationAuthor(body.generated_by);
|
|
||||||
const context = toRecord(body.context);
|
|
||||||
const llmConfig = parseAutogenLlmRuntimeConfig(body, context);
|
|
||||||
const personalityPrompt = toStringSafe(context?.autogen_personality_prompt);
|
|
||||||
if (mode === "saved_user_sessions") {
|
|
||||||
throw new http_1.ApiError("AUTOGEN_MODE_NOT_SUPPORTED", "Use `/api/autoruns/autogen/save-assistant-session` to save user sessions.", 400);
|
|
||||||
}
|
|
||||||
let questions = [];
|
|
||||||
if (mode === "qwen_seed") {
|
|
||||||
if (!llmConfig) {
|
|
||||||
throw new http_1.ApiError("AUTOGEN_LLM_CONFIG_REQUIRED", "Для режима qwen_seed нужен активный LLM-контур (provider/model/baseUrl) из настроек подключения.", 400);
|
|
||||||
}
|
|
||||||
questions = await generateQwenSeedQuestionsLive({
|
|
||||||
count,
|
|
||||||
domain,
|
|
||||||
personalityPrompt,
|
|
||||||
llmConfig,
|
|
||||||
client: openaiClient
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
questions = generateCodexCreativeQuestions(count, domain);
|
|
||||||
}
|
|
||||||
questions = Array.from(new Set(questions.map((item) => sanitizeGeneratedQuestion(item)).filter((item) => item.length > 0))).slice(0, count);
|
|
||||||
const generationId = generateAutogenId();
|
|
||||||
let savedCaseSetFile = null;
|
|
||||||
if (persistCaseSet) {
|
|
||||||
if (!fs_1.default.existsSync(config_1.EVAL_CASES_DIR)) {
|
|
||||||
fs_1.default.mkdirSync(config_1.EVAL_CASES_DIR, { recursive: true });
|
|
||||||
}
|
|
||||||
const fileName = buildAutogenCaseSetFileName(mode, generationId);
|
|
||||||
const filePath = path_1.default.resolve(config_1.EVAL_CASES_DIR, fileName);
|
|
||||||
const payload = buildAutogenCaseSetPayload({
|
|
||||||
generationId,
|
|
||||||
mode,
|
|
||||||
domain,
|
|
||||||
questions
|
|
||||||
});
|
|
||||||
fs_1.default.writeFileSync(filePath, JSON.stringify(payload, null, 2), "utf-8");
|
|
||||||
savedCaseSetFile = fileName;
|
|
||||||
}
|
|
||||||
const record = {
|
|
||||||
generation_id: generationId,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
mode,
|
|
||||||
title: null,
|
|
||||||
count: questions.length,
|
|
||||||
domain,
|
|
||||||
questions,
|
|
||||||
generated_by: generatedBy,
|
|
||||||
saved_case_set_file: savedCaseSetFile,
|
|
||||||
context: context
|
|
||||||
? {
|
|
||||||
llm_provider: toStringSafe(context.llm_provider),
|
|
||||||
model: toStringSafe(context.model),
|
|
||||||
assistant_prompt_version: toStringSafe(context.assistant_prompt_version),
|
|
||||||
decomposition_prompt_version: toStringSafe(context.decomposition_prompt_version),
|
|
||||||
prompt_fingerprint: toStringSafe(context.prompt_fingerprint)
|
|
||||||
? repairAutogenMojibake(String(context.prompt_fingerprint))
|
|
||||||
: null,
|
|
||||||
autogen_personality_id: toStringSafe(context.autogen_personality_id),
|
|
||||||
autogen_personality_prompt: toStringSafe(context.autogen_personality_prompt)
|
|
||||||
? repairAutogenMojibake(String(context.autogen_personality_prompt))
|
|
||||||
: null,
|
|
||||||
source_session_id: null,
|
|
||||||
saved_session_file: null,
|
|
||||||
saved_case_set_kind: "single_turn_list"
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
};
|
|
||||||
const history = readAutoGenHistory();
|
|
||||||
history.unshift(record);
|
|
||||||
writeAutoGenHistory(history.slice(0, 500));
|
|
||||||
(0, http_1.ok)(res, {
|
|
||||||
ok: true,
|
|
||||||
generation: record
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
|
|
|
||||||
|
|
@ -1869,6 +1869,10 @@ function resolveUnicodeAddressIntentBridge(text) {
|
||||||
/(?:хвост|задержк|проблем|систематическ)/iu.test(normalized)) {
|
/(?:хвост|задержк|проблем|систематическ)/iu.test(normalized)) {
|
||||||
return unicodeBridgeResolution("list_payables_counterparties", "high", "supplier_tail_risk_signal_detected");
|
return unicodeBridgeResolution("list_payables_counterparties", "high", "supplier_tail_risk_signal_detected");
|
||||||
}
|
}
|
||||||
|
if (/(?:поставщик|закуп|исходящ|плат[её]ж|supplier|vendor|procurement|outgoing|payout)/iu.test(normalized) &&
|
||||||
|
/(?:зависим|завязан|концентрац|ключев|основн|критич|dependency|concentration|key|critical)/iu.test(normalized)) {
|
||||||
|
return unicodeBridgeResolution("unknown", "high", "unicode_business_overview_supplier_quality_deferred_to_discovery");
|
||||||
|
}
|
||||||
if (hasDocumentCue && (hasCounterpartyCue || hasLooseCounterpartyByAnchor) && !hasContractCue) {
|
if (hasDocumentCue && (hasCounterpartyCue || hasLooseCounterpartyByAnchor) && !hasContractCue) {
|
||||||
return unicodeBridgeResolution("list_documents_by_counterparty", "high", "unicode_documents_by_counterparty_bridge_signal_detected");
|
return unicodeBridgeResolution("list_documents_by_counterparty", "high", "unicode_documents_by_counterparty_bridge_signal_detected");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -211,6 +211,22 @@ function deriveTaxQuarterWindowForDate(value) {
|
||||||
period_to: `${year}-${String(quarterEndMonth).padStart(2, "0")}-${String(quarterEndDay).padStart(2, "0")}`
|
period_to: `${year}-${String(quarterEndMonth).padStart(2, "0")}-${String(quarterEndDay).padStart(2, "0")}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function isFullCalendarMonthWindow(periodFrom, periodTo) {
|
||||||
|
const fromIso = normalizeIsoDateForQuery(periodFrom);
|
||||||
|
const toIso = normalizeIsoDateForQuery(periodTo);
|
||||||
|
if (!fromIso || !toIso) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const fromMatch = fromIso.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
const toMatch = toIso.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
if (!fromMatch || !toMatch || fromMatch[1] !== toMatch[1] || fromMatch[2] !== toMatch[2] || fromMatch[3] !== "01") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const year = Number(fromMatch[1]);
|
||||||
|
const month = Number(fromMatch[2]);
|
||||||
|
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
||||||
|
return toMatch[3] === String(lastDay).padStart(2, "0");
|
||||||
|
}
|
||||||
function deriveMonthWindowForDate(value) {
|
function deriveMonthWindowForDate(value) {
|
||||||
const isoDate = normalizeIsoDateForQuery(value);
|
const isoDate = normalizeIsoDateForQuery(value);
|
||||||
if (!isoDate) {
|
if (!isoDate) {
|
||||||
|
|
@ -3003,8 +3019,18 @@ class AddressQueryService {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const vatTaxPeriodFromCarriedMonth = intent.intent === "vat_liability_confirmed_for_tax_period" &&
|
||||||
|
(filters.warnings.includes("period_from_from_followup_context") ||
|
||||||
|
filters.warnings.includes("period_to_from_followup_context")) &&
|
||||||
|
isFullCalendarMonthWindow(filters.extracted_filters.period_from, filters.extracted_filters.period_to);
|
||||||
|
const vatTaxPeriodFromPurchaseBridgeMonth = intent.intent === "vat_liability_confirmed_for_tax_period" &&
|
||||||
|
(filters.warnings.includes("period_from_purchase_date_bridge_followup_context") ||
|
||||||
|
filters.warnings.includes("period_to_purchase_date_bridge_followup_context")) &&
|
||||||
|
isFullCalendarMonthWindow(filters.extracted_filters.period_from, filters.extracted_filters.period_to);
|
||||||
if (intent.intent === "vat_liability_confirmed_for_tax_period" &&
|
if (intent.intent === "vat_liability_confirmed_for_tax_period" &&
|
||||||
filters.warnings.includes("period_derived_from_month_phrase")) {
|
(filters.warnings.includes("period_derived_from_month_phrase") ||
|
||||||
|
vatTaxPeriodFromCarriedMonth ||
|
||||||
|
vatTaxPeriodFromPurchaseBridgeMonth)) {
|
||||||
const taxQuarterWindow = deriveTaxQuarterWindowForDate(filters.extracted_filters.period_to);
|
const taxQuarterWindow = deriveTaxQuarterWindowForDate(filters.extracted_filters.period_to);
|
||||||
if (taxQuarterWindow) {
|
if (taxQuarterWindow) {
|
||||||
filters.extracted_filters = {
|
filters.extracted_filters = {
|
||||||
|
|
@ -3176,8 +3202,14 @@ class AddressQueryService {
|
||||||
shadowRouteAudit
|
shadowRouteAudit
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const rawUserMessageForReply = typeof options.rawUserMessage === "string" && options.rawUserMessage.trim().length > 0
|
||||||
|
? options.rawUserMessage
|
||||||
|
: undefined;
|
||||||
const composeOptionsFromFilters = (filterSet, options = {}) => ({
|
const composeOptionsFromFilters = (filterSet, options = {}) => ({
|
||||||
userMessage,
|
userMessage,
|
||||||
|
rawUserMessage: typeof options.rawUserMessage === "string" && options.rawUserMessage.trim().length > 0
|
||||||
|
? options.rawUserMessage
|
||||||
|
: rawUserMessageForReply,
|
||||||
itemHint: typeof filterSet.item === "string" ? filterSet.item : undefined,
|
itemHint: typeof filterSet.item === "string" ? filterSet.item : undefined,
|
||||||
counterpartyHint: typeof options.counterpartyHint === "string"
|
counterpartyHint: typeof options.counterpartyHint === "string"
|
||||||
? options.counterpartyHint
|
? options.counterpartyHint
|
||||||
|
|
@ -3195,6 +3227,17 @@ class AddressQueryService {
|
||||||
asOfDate: typeof filterSet.as_of_date === "string" ? filterSet.as_of_date : undefined,
|
asOfDate: typeof filterSet.as_of_date === "string" ? filterSet.as_of_date : undefined,
|
||||||
requestedResultMode,
|
requestedResultMode,
|
||||||
vatDirectSourceProbe: options.vatDirectSourceProbe ?? undefined,
|
vatDirectSourceProbe: options.vatDirectSourceProbe ?? undefined,
|
||||||
|
purchaseDateBridge: typeof filterSet.purchase_date_bridge_selected === "string" ||
|
||||||
|
typeof filterSet.purchase_date_bridge_first === "string" ||
|
||||||
|
typeof filterSet.purchase_date_bridge_last === "string"
|
||||||
|
? {
|
||||||
|
selectedPurchaseDate: filterSet.purchase_date_bridge_selected,
|
||||||
|
firstPurchaseDate: filterSet.purchase_date_bridge_first,
|
||||||
|
lastPurchaseDate: filterSet.purchase_date_bridge_last,
|
||||||
|
basis: filterSet.purchase_date_bridge_basis,
|
||||||
|
hasMultiplePurchaseDates: filterSet.purchase_date_bridge_has_multiple === true
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
emphasizeNumbers: options.emphasizeNumbers ?? undefined,
|
emphasizeNumbers: options.emphasizeNumbers ?? undefined,
|
||||||
useRubCurrency: options.useRubCurrency ?? undefined
|
useRubCurrency: options.useRubCurrency ?? undefined
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1413,6 +1413,85 @@ function buildInventoryMovementQuery(filters, resolvedLimit, side) {
|
||||||
.replace("__WHERE_CLAUSE__", buildWhereClause(filters, "Движения.Период", [inventoryCondition, itemCondition].filter((item) => Boolean(item))))
|
.replace("__WHERE_CLAUSE__", buildWhereClause(filters, "Движения.Период", [inventoryCondition, itemCondition].filter((item) => Boolean(item))))
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||||
}
|
}
|
||||||
|
function buildStaticWhereClause(conditions, indent = " ") {
|
||||||
|
const cleaned = conditions.map((condition) => String(condition ?? "").trim()).filter((condition) => condition.length > 0);
|
||||||
|
return cleaned.length > 0 ? `ГДЕ\n${indent}${cleaned.join(`\n${indent}И `)}` : "";
|
||||||
|
}
|
||||||
|
function resolveInventoryAgingAsOfExpr(filters) {
|
||||||
|
const now = new Date();
|
||||||
|
return ((typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||||
|
? toDateTimeExpr(filters.as_of_date, true)
|
||||||
|
: null) ??
|
||||||
|
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||||
|
? toDateTimeExpr(filters.period_to, true)
|
||||||
|
: null) ??
|
||||||
|
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||||
|
? toDateTimeExpr(filters.period_from, true)
|
||||||
|
: null) ??
|
||||||
|
`ДАТАВРЕМЯ(${now.getFullYear()}, ${now.getMonth() + 1}, ${now.getDate()}, 23, 59, 59)`);
|
||||||
|
}
|
||||||
|
function buildInventoryAgingByPurchaseDateQuery(filters, resolvedLimit) {
|
||||||
|
const asOfExpr = resolveInventoryAgingAsOfExpr(filters);
|
||||||
|
const inventoryAccountPredicate = buildAccountPrefixPredicate("Остатки.Счет", ["41.01"]);
|
||||||
|
const onHandScopeConditions = [
|
||||||
|
"Остатки.КоличествоРазвернутыйОстатокДт > 0",
|
||||||
|
inventoryAccountPredicate,
|
||||||
|
buildOrganizationReferenceCondition(filters, ["Остатки.Организация"]),
|
||||||
|
buildWarehouseReferenceCondition(filters, ["Остатки.Субконто3"]),
|
||||||
|
buildInventoryItemReferenceCondition(filters, ["Остатки.Субконто1"])
|
||||||
|
];
|
||||||
|
const onHandWhereClause = buildStaticWhereClause(onHandScopeConditions);
|
||||||
|
const currentStockItemSubquery = [
|
||||||
|
"(ВЫБРАТЬ РАЗЛИЧНЫЕ",
|
||||||
|
" Остатки.Субконто1",
|
||||||
|
" ИЗ",
|
||||||
|
` РегистрБухгалтерии.Хозрасчетный.Остатки(${asOfExpr}, , , ) КАК Остатки`,
|
||||||
|
buildStaticWhereClause(onHandScopeConditions, " "),
|
||||||
|
")"
|
||||||
|
].join("\n");
|
||||||
|
const purchaseWhereClause = buildStaticWhereClause([
|
||||||
|
"Товары.Ссылка.Проведен = ИСТИНА",
|
||||||
|
`Товары.Ссылка.Дата <= ${asOfExpr}`,
|
||||||
|
buildOrganizationReferenceCondition(filters, ["Товары.Ссылка.Организация"]),
|
||||||
|
buildInventoryItemReferenceCondition(filters, ["Товары.Номенклатура"]),
|
||||||
|
`Товары.Номенклатура В ${currentStockItemSubquery}`
|
||||||
|
]);
|
||||||
|
return `
|
||||||
|
ВЫБРАТЬ ПЕРВЫЕ ${resolvedLimit}
|
||||||
|
${asOfExpr} КАК Период,
|
||||||
|
"Остатки на дату" КАК Регистратор,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетДт,
|
||||||
|
"" КАК СчетКт,
|
||||||
|
Остатки.СуммаРазвернутыйОстатокДт КАК Сумма,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК Номенклатура,
|
||||||
|
"" КАК Контрагент,
|
||||||
|
"" КАК Договор,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК Склад,
|
||||||
|
Остатки.КоличествоРазвернутыйОстатокДт КАК Количество
|
||||||
|
ИЗ
|
||||||
|
РегистрБухгалтерии.Хозрасчетный.Остатки(${asOfExpr}, , , ) КАК Остатки
|
||||||
|
${onHandWhereClause}
|
||||||
|
ОБЪЕДИНИТЬ ВСЕ
|
||||||
|
ВЫБРАТЬ ПЕРВЫЕ ${resolvedLimit}
|
||||||
|
Товары.Ссылка.Дата КАК Период,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка) КАК Регистратор,
|
||||||
|
"41.01" КАК СчетДт,
|
||||||
|
"" КАК СчетКт,
|
||||||
|
Товары.Сумма КАК Сумма,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Товары.Номенклатура) КАК Номенклатура,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Контрагент) КАК Контрагент,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.ДоговорКонтрагента) КАК Договор,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Организация) КАК Организация,
|
||||||
|
"" КАК Склад,
|
||||||
|
Товары.Количество КАК Количество
|
||||||
|
ИЗ
|
||||||
|
Документ.ПоступлениеТоваровУслуг.Товары КАК Товары
|
||||||
|
${purchaseWhereClause}
|
||||||
|
УПОРЯДОЧИТЬ ПО
|
||||||
|
Период ВОЗР
|
||||||
|
`.trim();
|
||||||
|
}
|
||||||
function buildWarehouseReferenceCondition(filters, fieldPaths) {
|
function buildWarehouseReferenceCondition(filters, fieldPaths) {
|
||||||
const warehouse = typeof filters.warehouse === "string" ? filters.warehouse.trim() : "";
|
const warehouse = typeof filters.warehouse === "string" ? filters.warehouse.trim() : "";
|
||||||
if (!warehouse) {
|
if (!warehouse) {
|
||||||
|
|
@ -1922,7 +2001,7 @@ function buildAddressRecipePlan(recipe, filters) {
|
||||||
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
||||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
||||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
? buildInventoryAgingByPurchaseDateQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "inventory_quality_events_profile"
|
: recipe.query_template === "inventory_quality_events_profile"
|
||||||
? buildInventoryQualityEventsQuery(filters, resolvedLimit)
|
? buildInventoryQualityEventsQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "contracts_by_counterparty_profile"
|
: recipe.query_template === "contracts_by_counterparty_profile"
|
||||||
|
|
|
||||||
|
|
@ -549,9 +549,33 @@ function needsVatPurchaseDateAnchorDisclosure(userMessage) {
|
||||||
}
|
}
|
||||||
return /(?:дата|дату|дате|момент)\s+(?:покуп|закуп)|(?:покуп|закуп)\S*\s+(?:дат|момент)|purchase\s+date|date\s+of\s+purchase/iu.test(text);
|
return /(?:дата|дату|дате|момент)\s+(?:покуп|закуп)|(?:покуп|закуп)\S*\s+(?:дат|момент)|purchase\s+date|date\s+of\s+purchase/iu.test(text);
|
||||||
}
|
}
|
||||||
function buildVatPurchaseDateAnchorDisclosureLine(options, periodWindowLabel) {
|
function vatPurchaseDateBasisLabel(basis, hasMultiplePurchaseDates) {
|
||||||
if (!periodWindowLabel || !needsVatPurchaseDateAnchorDisclosure(options.userMessage)) {
|
if (!hasMultiplePurchaseDates) {
|
||||||
return null;
|
return "подтвержденную дату закупки";
|
||||||
|
}
|
||||||
|
if (basis === "last_confirmed_purchase") {
|
||||||
|
return "последнюю подтвержденную дату закупки";
|
||||||
|
}
|
||||||
|
return "первую подтвержденную дату закупки";
|
||||||
|
}
|
||||||
|
function buildVatPurchaseDateAnchorDisclosureLines(options, periodWindowLabel) {
|
||||||
|
const bridge = options.purchaseDateBridge ?? null;
|
||||||
|
const selectedPurchaseDate = normalizeIsoDateOnly(bridge?.selectedPurchaseDate);
|
||||||
|
const firstPurchaseDate = normalizeIsoDateOnly(bridge?.firstPurchaseDate);
|
||||||
|
const lastPurchaseDate = normalizeIsoDateOnly(bridge?.lastPurchaseDate);
|
||||||
|
const hasMultiplePurchaseDates = Boolean(bridge?.hasMultiplePurchaseDates || (firstPurchaseDate && lastPurchaseDate && firstPurchaseDate !== lastPurchaseDate));
|
||||||
|
if (!periodWindowLabel || (!selectedPurchaseDate && !needsVatPurchaseDateAnchorDisclosure(options.userMessage))) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (selectedPurchaseDate) {
|
||||||
|
const basisLabel = vatPurchaseDateBasisLabel(bridge?.basis, hasMultiplePurchaseDates);
|
||||||
|
const lines = [
|
||||||
|
`- Якорь периода: для расчета я использую ${basisLabel} ${formatDateRu(selectedPurchaseDate)}; налоговый период ${periodWindowLabel}; сам расчет ниже взят из книг продаж/покупок за это окно.`
|
||||||
|
];
|
||||||
|
if (hasMultiplePurchaseDates && firstPurchaseDate && lastPurchaseDate) {
|
||||||
|
lines.push(`- Важно: у позиции несколько подтвержденных дат закупки (${formatDateRu(firstPurchaseDate)}..${formatDateRu(lastPurchaseDate)}); это расчет по выбранному якорю, а не единственная возможная дата.`);
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
}
|
}
|
||||||
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
||||||
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
||||||
|
|
@ -565,9 +589,13 @@ function buildVatPurchaseDateAnchorDisclosureLine(options, periodWindowLabel) {
|
||||||
toTs !== null &&
|
toTs !== null &&
|
||||||
asOfTs >= fromTs &&
|
asOfTs >= fromTs &&
|
||||||
asOfTs <= toTs) {
|
asOfTs <= toTs) {
|
||||||
return `- Якорь периода: дата покупки ${formatDateRu(asOfDate)} попадает в налоговый период ${periodWindowLabel}; поэтому расчет ниже взят из книг продаж/покупок за это окно.`;
|
return [
|
||||||
|
`- Якорь периода: дата покупки ${formatDateRu(asOfDate)} попадает в налоговый период ${periodWindowLabel}; поэтому расчет ниже взят из книг продаж/покупок за это окно.`
|
||||||
|
];
|
||||||
}
|
}
|
||||||
return `- Якорь периода: дата покупки из вопроса/контекста использована для выбора налогового периода ${periodWindowLabel}; сам расчет ниже взят из книг продаж/покупок за это окно.`;
|
return [
|
||||||
|
`- Якорь периода: дата покупки из вопроса/контекста использована для выбора налогового периода ${periodWindowLabel}; сам расчет ниже взят из книг продаж/покупок за это окно.`
|
||||||
|
];
|
||||||
}
|
}
|
||||||
function detectRankingLimit(userMessage, fallback = 20) {
|
function detectRankingLimit(userMessage, fallback = 20) {
|
||||||
const text = normalizeQuestionText(userMessage);
|
const text = normalizeQuestionText(userMessage);
|
||||||
|
|
@ -996,12 +1024,72 @@ function hasInventoryAccountPrefix(value, prefix) {
|
||||||
.replace(",", ".");
|
.replace(",", ".");
|
||||||
return normalized === prefix || normalized.startsWith(`${prefix}.`) || normalized.startsWith(prefix);
|
return normalized === prefix || normalized.startsWith(`${prefix}.`) || normalized.startsWith(prefix);
|
||||||
}
|
}
|
||||||
|
function isInventoryOnHandSnapshotRow(row) {
|
||||||
|
const registrator = String(row.registrator ?? "").trim().toLowerCase();
|
||||||
|
return /(?:остатки\s+на\s+дату|stock\s*on\s*hand|balance\s+as\s+of)/iu.test(registrator);
|
||||||
|
}
|
||||||
function isInventoryPurchaseMovement(row) {
|
function isInventoryPurchaseMovement(row) {
|
||||||
return hasInventoryAccountPrefix(row.account_dt, "41.01");
|
return hasInventoryAccountPrefix(row.account_dt, "41.01") && !isInventoryOnHandSnapshotRow(row);
|
||||||
}
|
}
|
||||||
function isInventorySaleMovement(row) {
|
function isInventorySaleMovement(row) {
|
||||||
return hasInventoryAccountPrefix(row.account_kt, "41.01");
|
return hasInventoryAccountPrefix(row.account_kt, "41.01");
|
||||||
}
|
}
|
||||||
|
function inventoryAgingKeyPart(value) {
|
||||||
|
return normalizeEntityToken(value) ?? "";
|
||||||
|
}
|
||||||
|
function buildInventoryOnHandAgingScope(rows) {
|
||||||
|
const scope = {
|
||||||
|
itemKeys: new Set(),
|
||||||
|
itemOrgKeys: new Set(),
|
||||||
|
fullKeys: new Set(),
|
||||||
|
orgKeys: new Set()
|
||||||
|
};
|
||||||
|
for (const row of rows) {
|
||||||
|
if (!isInventoryOnHandSnapshotRow(row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const quantity = extractInventoryQuantity(row);
|
||||||
|
if (quantity === null || quantity <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const itemKey = inventoryAgingKeyPart(extractInventoryItemName(row));
|
||||||
|
if (!itemKey) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const organizationKey = inventoryAgingKeyPart(extractInventoryOrganizationName(row));
|
||||||
|
const warehouseKey = inventoryAgingKeyPart(extractInventoryWarehouseName(row));
|
||||||
|
scope.itemKeys.add(itemKey);
|
||||||
|
if (organizationKey) {
|
||||||
|
scope.orgKeys.add(organizationKey);
|
||||||
|
scope.itemOrgKeys.add(`${itemKey}|${organizationKey}`);
|
||||||
|
}
|
||||||
|
if (organizationKey || warehouseKey) {
|
||||||
|
scope.fullKeys.add(`${itemKey}|${warehouseKey}|${organizationKey}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return scope;
|
||||||
|
}
|
||||||
|
function inventoryPurchaseMatchesOnHandAgingScope(row, scope) {
|
||||||
|
if (scope.itemKeys.size === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const itemKey = inventoryAgingKeyPart(extractInventoryItemName(row));
|
||||||
|
if (!itemKey || !scope.itemKeys.has(itemKey)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const organizationKey = inventoryAgingKeyPart(extractInventoryOrganizationName(row));
|
||||||
|
const warehouseKey = inventoryAgingKeyPart(extractInventoryWarehouseName(row));
|
||||||
|
if (scope.fullKeys.has(`${itemKey}|${warehouseKey}|${organizationKey}`)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (organizationKey && scope.itemOrgKeys.has(`${itemKey}|${organizationKey}`)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (organizationKey && scope.orgKeys.size > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
function looksLikeInventoryTraceDocumentToken(value) {
|
function looksLikeInventoryTraceDocumentToken(value) {
|
||||||
const normalized = String(value ?? "").trim();
|
const normalized = String(value ?? "").trim();
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
|
|
@ -1162,7 +1250,11 @@ function formatCounterpartyItemFlowRows(rows, limit = 12) {
|
||||||
function buildInventoryAgingByItemAggregate(rows, asOfDate) {
|
function buildInventoryAgingByItemAggregate(rows, asOfDate) {
|
||||||
const byItem = new Map();
|
const byItem = new Map();
|
||||||
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
|
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
|
||||||
|
const onHandScope = buildInventoryOnHandAgingScope(rows);
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
|
if (!isInventoryPurchaseMovement(row) || !inventoryPurchaseMatchesOnHandAgingScope(row, onHandScope)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const item = extractInventoryItemName(row);
|
const item = extractInventoryItemName(row);
|
||||||
if (!item) {
|
if (!item) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -1245,19 +1337,11 @@ function formatInventoryAgingRows(items, asOfDate, limit = 10) {
|
||||||
const parts = [
|
const parts = [
|
||||||
`${index + 1}. ${item.item}`,
|
`${index + 1}. ${item.item}`,
|
||||||
`первая закупка: ${inventoryTraceDateLabel(item.firstPurchasePeriod)}`,
|
`первая закупка: ${inventoryTraceDateLabel(item.firstPurchasePeriod)}`,
|
||||||
`последняя закупка: ${inventoryTraceDateLabel(item.lastPurchasePeriod)}`,
|
`последняя закупка: ${inventoryTraceDateLabel(item.lastPurchasePeriod)}`
|
||||||
`документов: ${formatNumberWithDots(item.documentCount)}`,
|
|
||||||
`операций: ${formatNumberWithDots(item.operations)}`
|
|
||||||
];
|
];
|
||||||
if (item.ageDays !== null) {
|
if (item.ageDays !== null) {
|
||||||
parts.push(`возраст следа на ${formatDateRu(asOfDate)}: ${formatNumberWithDots(item.ageDays)} дн.`);
|
parts.push(`возраст следа на ${formatDateRu(asOfDate)}: ${formatNumberWithDots(item.ageDays)} дн.`);
|
||||||
}
|
}
|
||||||
if (item.warehouse) {
|
|
||||||
parts.push(`склад: ${item.warehouse}`);
|
|
||||||
}
|
|
||||||
if (item.organization) {
|
|
||||||
parts.push(`организация: ${item.organization}`);
|
|
||||||
}
|
|
||||||
if (item.counterparties.length > 0) {
|
if (item.counterparties.length > 0) {
|
||||||
parts.push(`поставщики: ${item.counterparties.slice(0, 3).join("; ")}`);
|
parts.push(`поставщики: ${item.counterparties.slice(0, 3).join("; ")}`);
|
||||||
}
|
}
|
||||||
|
|
@ -3060,15 +3144,27 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||||
const formatConfirmedMoney = (value) => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
const formatConfirmedMoney = (value) => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
||||||
const organizationLabel = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.organizationHint);
|
const organizationLabel = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.organizationHint);
|
||||||
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
|
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
|
||||||
const purchaseDateAnchorLine = buildVatPurchaseDateAnchorDisclosureLine(options, periodWindowLabel);
|
const purchaseDateAnchorLines = buildVatPurchaseDateAnchorDisclosureLines(options, periodWindowLabel);
|
||||||
|
const selectedPurchaseDate = normalizeIsoDateOnly(options.purchaseDateBridge?.selectedPurchaseDate);
|
||||||
|
const hasMultiplePurchaseDates = Boolean(options.purchaseDateBridge?.hasMultiplePurchaseDates ||
|
||||||
|
(normalizeIsoDateOnly(options.purchaseDateBridge?.firstPurchaseDate) &&
|
||||||
|
normalizeIsoDateOnly(options.purchaseDateBridge?.lastPurchaseDate) &&
|
||||||
|
normalizeIsoDateOnly(options.purchaseDateBridge?.firstPurchaseDate) !==
|
||||||
|
normalizeIsoDateOnly(options.purchaseDateBridge?.lastPurchaseDate)));
|
||||||
|
const purchaseBasisLabel = vatPurchaseDateBasisLabel(options.purchaseDateBridge?.basis, hasMultiplePurchaseDates);
|
||||||
|
const directVatLine = selectedPurchaseDate
|
||||||
|
? hasMultiplePurchaseDates
|
||||||
|
? `Коротко: если брать ${purchaseBasisLabel} ${formatDateRu(selectedPurchaseDate)}, подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`
|
||||||
|
: `Коротко: по дате покупки ${formatDateRu(selectedPurchaseDate)} подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`
|
||||||
|
: `Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`;
|
||||||
const lines = [
|
const lines = [
|
||||||
`Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`,
|
directVatLine,
|
||||||
"Расчет сделан по книгам продаж и покупок.",
|
"Расчет сделан по книгам продаж и покупок.",
|
||||||
"",
|
"",
|
||||||
"Что вошло в расчет:",
|
"Что вошло в расчет:",
|
||||||
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
|
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
|
||||||
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
|
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
|
||||||
...(purchaseDateAnchorLine ? [purchaseDateAnchorLine] : []),
|
...purchaseDateAnchorLines,
|
||||||
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
|
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
|
||||||
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
|
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
|
||||||
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`
|
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`
|
||||||
|
|
|
||||||
|
|
@ -406,7 +406,11 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||||
const focus = deps.detectValueRankingFocus(options.userMessage);
|
const focus = deps.detectValueRankingFocus(options.userMessage);
|
||||||
const limit = deps.detectRankingLimit(options.userMessage, 20);
|
const limit = deps.detectRankingLimit(options.userMessage, 20);
|
||||||
const minOpsForAvgCheck = deps.detectMinOpsForAvgCheck(options.userMessage);
|
const minOpsForAvgCheck = deps.detectMinOpsForAvgCheck(options.userMessage);
|
||||||
const normalizedQuestion = deps.normalizeQuestionText(options.userMessage);
|
const questionSignalText = [options.rawUserMessage, options.userMessage]
|
||||||
|
.map((value) => String(value ?? "").trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
const normalizedQuestion = deps.normalizeQuestionText(questionSignalText || options.userMessage);
|
||||||
const asksSingleBestCounterparty = focus === "top_by_total" &&
|
const asksSingleBestCounterparty = focus === "top_by_total" &&
|
||||||
/(?:какой|кто|which|who|какой|кто)/iu.test(normalizedQuestion) &&
|
/(?:какой|кто|which|who|какой|кто)/iu.test(normalizedQuestion) &&
|
||||||
/(?:больше\s+всего|сам(?:ый|ая|ое|ые)|наибольш|прин[её]с|highest|most|больше\s+всего|сам(?:ый|ая|РѕРµ|ые)|наибол|РїСЂРёРЅ[её]СЃ)/iu.test(normalizedQuestion) &&
|
/(?:больше\s+всего|сам(?:ый|ая|ое|ые)|наибольш|прин[её]с|highest|most|больше\s+всего|сам(?:ый|ая|РѕРµ|ые)|наибол|РїСЂРёРЅ[её]СЃ)/iu.test(normalizedQuestion) &&
|
||||||
|
|
@ -418,6 +422,7 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||||
const effectiveLimit = asksSingleBestCounterparty || semanticSingleBestCounterparty ? 1 : limit;
|
const effectiveLimit = asksSingleBestCounterparty || semanticSingleBestCounterparty ? 1 : limit;
|
||||||
const cashflowBoundaryLine = buildCashflowBoundaryLine(isSupplier);
|
const cashflowBoundaryLine = buildCashflowBoundaryLine(isSupplier);
|
||||||
const cashflowNextStepLine = buildCashflowNextStepLine(isSupplier, normalizedQuestion);
|
const cashflowNextStepLine = buildCashflowNextStepLine(isSupplier, normalizedQuestion);
|
||||||
|
const dependencyCue = /(?:зависим|завязан|концентрац|ключев|основн|критич|dependency|concentration|key|critical)/iu.test(normalizedQuestion);
|
||||||
const byCounterparty = new Map();
|
const byCounterparty = new Map();
|
||||||
const byYear = new Map();
|
const byYear = new Map();
|
||||||
const deals = [];
|
const deals = [];
|
||||||
|
|
@ -653,13 +658,20 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||||
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
||||||
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
||||||
: isSupplier
|
: isSupplier
|
||||||
? `Крупнейший поставщик по подтвержденным выплатам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
? dependencyCue
|
||||||
: `Самый доходный клиент ${rankingPeriodLabel} по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`;
|
? `Главный сигнал зависимости по исходящим платежам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||||
|
: `Крупнейший поставщик по подтвержденным выплатам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||||
|
: dependencyCue
|
||||||
|
? `Главный сигнал зависимости по входящим деньгам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||||
|
: `Самый доходный клиент ${rankingPeriodLabel} по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`;
|
||||||
lines.unshift(directAnswerLine);
|
lines.unshift(directAnswerLine);
|
||||||
|
if (dependencyCue) {
|
||||||
|
lines.splice(1, 0, "Это сигнал концентрации по найденным денежным строкам 1С, а не подтверждение договорной зависимости, надежности контрагента или чистой прибыли.");
|
||||||
|
}
|
||||||
if (!isSupplier) {
|
if (!isSupplier) {
|
||||||
lines.splice(1, 0, cashflowBoundaryLine);
|
lines.splice(dependencyCue ? 2 : 1, 0, cashflowBoundaryLine);
|
||||||
if (cashflowNextStepLine) {
|
if (cashflowNextStepLine) {
|
||||||
lines.splice(2, 0, cashflowNextStepLine);
|
lines.splice(dependencyCue ? 3 : 2, 0, cashflowNextStepLine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -460,6 +460,23 @@ function resolveYearFromFilters(filters) {
|
||||||
function hasRelativeYearHint(text) {
|
function hasRelativeYearHint(text) {
|
||||||
return /(?:эт(?:от|ого)(?:\s+же)?\s+год|этого\s+же\s+года|того\s+же\s+года|this\s+year|same\s+year|that\s+year)/iu.test(String(text ?? ""));
|
return /(?:эт(?:от|ого)(?:\s+же)?\s+год|этого\s+же\s+года|того\s+же\s+года|this\s+year|same\s+year|that\s+year)/iu.test(String(text ?? ""));
|
||||||
}
|
}
|
||||||
|
function resolveMonthPeriodFromIsoDate(isoDate) {
|
||||||
|
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const year = Number(match[1]);
|
||||||
|
const month = Number(match[2]);
|
||||||
|
if (!Number.isFinite(year) || !Number.isFinite(month) || month < 1 || month > 12) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
||||||
|
return {
|
||||||
|
period_from: `${match[1]}-${match[2]}-01`,
|
||||||
|
period_to: `${match[1]}-${match[2]}-${String(lastDay).padStart(2, "0")}`,
|
||||||
|
as_of_date: `${match[1]}-${match[2]}-${String(lastDay).padStart(2, "0")}`
|
||||||
|
};
|
||||||
|
}
|
||||||
function resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext) {
|
function resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext) {
|
||||||
if (!followupContext || !isInventoryRootFrameIntent(followupContext.root_intent)) {
|
if (!followupContext || !isInventoryRootFrameIntent(followupContext.root_intent)) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -629,6 +646,34 @@ function hasInventoryPurchaseDateVatBridgeCue(text) {
|
||||||
return (/(?:ндс|vat)/iu.test(normalized) &&
|
return (/(?:ндс|vat)/iu.test(normalized) &&
|
||||||
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/iu.test(normalized));
|
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/iu.test(normalized));
|
||||||
}
|
}
|
||||||
|
function hasInventoryPurchaseDateVatBridgeContinuationCue(text) {
|
||||||
|
const normalized = textWithRepairedVariant(String(text ?? ""))
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ё/g, "е");
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const mentionsPurchase = /(?:покупк|закупк|purchase)/iu.test(normalized);
|
||||||
|
const mentionsFirstOrLast = /(?:перв|последн|earliest|oldest|latest|last)/iu.test(normalized);
|
||||||
|
return mentionsPurchase && mentionsFirstOrLast;
|
||||||
|
}
|
||||||
|
function purchaseDateBridgeBasisFromMessage(userMessage) {
|
||||||
|
const normalized = textWithRepairedVariant(String(userMessage ?? ""))
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ё/g, "е");
|
||||||
|
if (!normalized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (/(?:последн|latest|last)/iu.test(normalized) && /(?:покупк|закупк|purchase)/iu.test(normalized)) {
|
||||||
|
return "last_confirmed_purchase";
|
||||||
|
}
|
||||||
|
if (/(?:перв|earliest|oldest)/iu.test(normalized) && /(?:покупк|закупк|purchase)/iu.test(normalized)) {
|
||||||
|
return "first_confirmed_purchase";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
function hasInventoryMarginRankingFollowupCue(text) {
|
function hasInventoryMarginRankingFollowupCue(text) {
|
||||||
const normalized = textWithRepairedVariant(String(text ?? ""))
|
const normalized = textWithRepairedVariant(String(text ?? ""))
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
|
@ -745,6 +790,31 @@ function isShortDebtRoleMirrorFollowup(intent, text, followupContext) {
|
||||||
function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
const merged = { ...current };
|
const merged = { ...current };
|
||||||
const reasons = [];
|
const reasons = [];
|
||||||
|
const clearInventoryAgingOrganizationAliasItem = (organizationHint) => {
|
||||||
|
if (intent !== "inventory_aging_by_purchase_date") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const explicitItemMention = /(?:^|[\s,.;:!?()\-\u2014])(?:товар(?:у|а|ом)?|позици(?:и|я|ю)|item|row|line)(?=$|[\s,.;:!?()\-\u2014])/iu.test(String(userMessage ?? ""));
|
||||||
|
const agingItem = toNonEmptyString(merged.item);
|
||||||
|
const agingOrganization = toNonEmptyString(merged.organization) ?? organizationHint;
|
||||||
|
const normalizedAgingItem = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeSearchText)(agingItem ?? "");
|
||||||
|
const normalizedAgingOrganization = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeSearchText)(agingOrganization ?? "");
|
||||||
|
const itemLooksLikeOrganizationScope = Boolean(agingItem &&
|
||||||
|
agingOrganization &&
|
||||||
|
((0, assistantOrganizationMatcher_1.organizationsLikelySameEntity)(agingItem, agingOrganization) ||
|
||||||
|
(normalizedAgingItem &&
|
||||||
|
normalizedAgingOrganization &&
|
||||||
|
(normalizedAgingOrganization.includes(normalizedAgingItem) ||
|
||||||
|
normalizedAgingItem.includes(normalizedAgingOrganization)))));
|
||||||
|
if (agingItem && itemLooksLikeOrganizationScope) {
|
||||||
|
delete merged.item;
|
||||||
|
reasons.push("item_cleared_as_organization_scope_alias_for_stock_aging");
|
||||||
|
}
|
||||||
|
else if (agingItem && !explicitItemMention) {
|
||||||
|
delete merged.item;
|
||||||
|
reasons.push("item_cleared_for_stock_slice_aging");
|
||||||
|
}
|
||||||
|
};
|
||||||
if (!followupContext) {
|
if (!followupContext) {
|
||||||
if ((intent === "list_open_contracts" ||
|
if ((intent === "list_open_contracts" ||
|
||||||
intent === "open_contracts_confirmed_as_of_date" ||
|
intent === "open_contracts_confirmed_as_of_date" ||
|
||||||
|
|
@ -760,6 +830,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
: "as_of_date_derived_from_period_for_open_contracts");
|
: "as_of_date_derived_from_period_for_open_contracts");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
clearInventoryAgingOrganizationAliasItem(null);
|
||||||
return { filters: merged, reasons };
|
return { filters: merged, reasons };
|
||||||
}
|
}
|
||||||
const previous = followupContext.previous_filters ?? {};
|
const previous = followupContext.previous_filters ?? {};
|
||||||
|
|
@ -773,6 +844,12 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
const previousAsOfDate = toNonEmptyString(previous.as_of_date);
|
const previousAsOfDate = toNonEmptyString(previous.as_of_date);
|
||||||
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 previousPurchaseDateBridgeSelected = toNonEmptyString(previous.purchase_date_bridge_selected);
|
||||||
|
const previousPurchaseDateBridgeFirst = toNonEmptyString(previous.purchase_date_bridge_first);
|
||||||
|
const previousPurchaseDateBridgeLast = toNonEmptyString(previous.purchase_date_bridge_last);
|
||||||
|
const previousPurchaseDateBridgeBasis = toNonEmptyString(previous.purchase_date_bridge_basis);
|
||||||
|
const previousPurchaseDateBridgeHasMultiple = previous.purchase_date_bridge_has_multiple === true ||
|
||||||
|
String(previous.purchase_date_bridge_has_multiple ?? "").trim().toLowerCase() === "true";
|
||||||
const relativeMonthFromInventoryRoot = resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext);
|
const relativeMonthFromInventoryRoot = resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext);
|
||||||
const relativeMonthFromFollowupYear = resolveRelativeMonthPeriodFromFollowupYear(userMessage, followupContext);
|
const relativeMonthFromFollowupYear = resolveRelativeMonthPeriodFromFollowupYear(userMessage, followupContext);
|
||||||
const allTimeRequested = hasAllTimeHint(userMessage);
|
const allTimeRequested = hasAllTimeHint(userMessage);
|
||||||
|
|
@ -790,6 +867,53 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
merged.organization = previousOrganization;
|
merged.organization = previousOrganization;
|
||||||
reasons.push("organization_from_followup_context");
|
reasons.push("organization_from_followup_context");
|
||||||
}
|
}
|
||||||
|
if (intent === "vat_liability_confirmed_for_tax_period" && previousPurchaseDateBridgeSelected) {
|
||||||
|
const requestedBridgeBasis = purchaseDateBridgeBasisFromMessage(userMessage);
|
||||||
|
const shouldCarryPurchaseDateBridge = !currentHasExplicitTemporalScope &&
|
||||||
|
(Boolean(requestedBridgeBasis) ||
|
||||||
|
hasInventoryPurchaseDateVatBridgeCue(userMessage) ||
|
||||||
|
hasInventoryPurchaseDateVatBridgeContinuationCue(userMessage));
|
||||||
|
if (!shouldCarryPurchaseDateBridge) {
|
||||||
|
delete merged.purchase_date_bridge_selected;
|
||||||
|
delete merged.purchase_date_bridge_first;
|
||||||
|
delete merged.purchase_date_bridge_last;
|
||||||
|
delete merged.purchase_date_bridge_basis;
|
||||||
|
delete merged.purchase_date_bridge_has_multiple;
|
||||||
|
reasons.push(currentHasExplicitTemporalScope
|
||||||
|
? "purchase_date_bridge_suppressed_by_explicit_temporal_scope"
|
||||||
|
: "purchase_date_bridge_not_reused_without_bridge_cue");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const selectedBridgeDate = requestedBridgeBasis === "last_confirmed_purchase"
|
||||||
|
? previousPurchaseDateBridgeLast ?? previousPurchaseDateBridgeSelected
|
||||||
|
: requestedBridgeBasis === "first_confirmed_purchase"
|
||||||
|
? previousPurchaseDateBridgeFirst ?? previousPurchaseDateBridgeSelected
|
||||||
|
: previousPurchaseDateBridgeSelected;
|
||||||
|
merged.purchase_date_bridge_selected = selectedBridgeDate;
|
||||||
|
const selectedBridgeWindow = resolveMonthPeriodFromIsoDate(selectedBridgeDate);
|
||||||
|
if (selectedBridgeWindow) {
|
||||||
|
merged.period_from = selectedBridgeWindow.period_from;
|
||||||
|
merged.period_to = selectedBridgeWindow.period_to;
|
||||||
|
merged.as_of_date = selectedBridgeWindow.as_of_date;
|
||||||
|
reasons.push("period_from_purchase_date_bridge_followup_context");
|
||||||
|
reasons.push("period_to_purchase_date_bridge_followup_context");
|
||||||
|
}
|
||||||
|
if (previousPurchaseDateBridgeFirst) {
|
||||||
|
merged.purchase_date_bridge_first = previousPurchaseDateBridgeFirst;
|
||||||
|
}
|
||||||
|
if (previousPurchaseDateBridgeLast) {
|
||||||
|
merged.purchase_date_bridge_last = previousPurchaseDateBridgeLast;
|
||||||
|
}
|
||||||
|
const effectiveBridgeBasis = requestedBridgeBasis ?? previousPurchaseDateBridgeBasis;
|
||||||
|
if (effectiveBridgeBasis) {
|
||||||
|
merged.purchase_date_bridge_basis = effectiveBridgeBasis;
|
||||||
|
}
|
||||||
|
if (previousPurchaseDateBridgeHasMultiple) {
|
||||||
|
merged.purchase_date_bridge_has_multiple = true;
|
||||||
|
}
|
||||||
|
reasons.push("purchase_date_bridge_from_followup_context");
|
||||||
|
}
|
||||||
|
}
|
||||||
if (intent === "inventory_on_hand_as_of_date" &&
|
if (intent === "inventory_on_hand_as_of_date" &&
|
||||||
followupContext.previous_intent === "inventory_on_hand_as_of_date" &&
|
followupContext.previous_intent === "inventory_on_hand_as_of_date" &&
|
||||||
followupContext.target_intent === "inventory_on_hand_as_of_date" &&
|
followupContext.target_intent === "inventory_on_hand_as_of_date" &&
|
||||||
|
|
@ -1099,11 +1223,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
reasons.push("period_derived_from_inventory_root_frame_year");
|
reasons.push("period_derived_from_inventory_root_frame_year");
|
||||||
}
|
}
|
||||||
if (intent === "inventory_aging_by_purchase_date") {
|
if (intent === "inventory_aging_by_purchase_date") {
|
||||||
const explicitItemMention = /(?:^|[\s,.;:!?()\-\u2014])(?:товар(?:у|а|ом)?|позици(?:и|я|ю)|item|row|line)(?=$|[\s,.;:!?()\-\u2014])/iu.test(String(userMessage ?? ""));
|
clearInventoryAgingOrganizationAliasItem(previousOrganization);
|
||||||
if (toNonEmptyString(merged.item) && !explicitItemMention) {
|
|
||||||
delete merged.item;
|
|
||||||
reasons.push("item_cleared_for_stock_slice_aging");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!sameDateRequested &&
|
if (!sameDateRequested &&
|
||||||
hasFollowupSignalForConfirmed &&
|
hasFollowupSignalForConfirmed &&
|
||||||
|
|
@ -1317,7 +1437,12 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||||
return detectedIntent;
|
return detectedIntent;
|
||||||
}
|
}
|
||||||
const normalizedMessage = String(userMessage ?? "");
|
const normalizedMessage = String(userMessage ?? "");
|
||||||
const hasFollowupSignal = hasAddressFollowupContextSignal(normalizedMessage);
|
const genericFollowupSignal = hasAddressFollowupContextSignal(normalizedMessage);
|
||||||
|
const previousFilters = followupContext.previous_filters ?? {};
|
||||||
|
const purchaseBridgeContinuationSignal = followupContext.previous_intent === "vat_liability_confirmed_for_tax_period" &&
|
||||||
|
Boolean(toNonEmptyString(previousFilters.purchase_date_bridge_selected)) &&
|
||||||
|
hasInventoryPurchaseDateVatBridgeContinuationCue(normalizedMessage);
|
||||||
|
const hasFollowupSignal = genericFollowupSignal || purchaseBridgeContinuationSignal;
|
||||||
if (!hasFollowupSignal) {
|
if (!hasFollowupSignal) {
|
||||||
return detectedIntent;
|
return detectedIntent;
|
||||||
}
|
}
|
||||||
|
|
@ -1326,7 +1451,6 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||||
if (!sourceIntent && !fallbackIntent) {
|
if (!sourceIntent && !fallbackIntent) {
|
||||||
return detectedIntent;
|
return detectedIntent;
|
||||||
}
|
}
|
||||||
const previousFilters = followupContext.previous_filters ?? {};
|
|
||||||
const previousPeriodFrom = toNonEmptyString(previousFilters.period_from);
|
const previousPeriodFrom = toNonEmptyString(previousFilters.period_from);
|
||||||
const previousPeriodTo = toNonEmptyString(previousFilters.period_to);
|
const previousPeriodTo = toNonEmptyString(previousFilters.period_to);
|
||||||
const previousContract = toNonEmptyString(previousFilters.contract);
|
const previousContract = toNonEmptyString(previousFilters.contract);
|
||||||
|
|
@ -1340,6 +1464,7 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||||
const samePeriodVatFollowup = isVatFollowup &&
|
const samePeriodVatFollowup = isVatFollowup &&
|
||||||
hasSamePeriodHint(normalizedMessage) &&
|
hasSamePeriodHint(normalizedMessage) &&
|
||||||
Boolean(previousPeriodFrom || previousPeriodTo);
|
Boolean(previousPeriodFrom || previousPeriodTo);
|
||||||
|
const previousPurchaseDateBridgeSelected = toNonEmptyString(previousFilters.purchase_date_bridge_selected);
|
||||||
const previousIsInventoryFamily = isInventoryIntent(sourceIntent ?? undefined);
|
const previousIsInventoryFamily = isInventoryIntent(sourceIntent ?? undefined);
|
||||||
const rootIsInventoryFamily = isInventoryIntent(followupContext.root_intent ?? undefined);
|
const rootIsInventoryFamily = isInventoryIntent(followupContext.root_intent ?? undefined);
|
||||||
const inventoryLineageActive = previousIsInventoryFamily ||
|
const inventoryLineageActive = previousIsInventoryFamily ||
|
||||||
|
|
@ -1361,7 +1486,11 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||||
sourceIntent === "open_items_by_counterparty_or_contract");
|
sourceIntent === "open_items_by_counterparty_or_contract");
|
||||||
const hasExplicitInventoryItemReference = /(?:товар|номенклатур|позици|склад|остат|sku|item|product|товар|номенклатур|позици|склад|остат)/iu.test(normalizedMessage) || hasSelectedObjectInlineSnapshotMetadata(normalizedMessage);
|
const hasExplicitInventoryItemReference = /(?:товар|номенклатур|позици|склад|остат|sku|item|product|товар|номенклатур|позици|склад|остат)/iu.test(normalizedMessage) || hasSelectedObjectInlineSnapshotMetadata(normalizedMessage);
|
||||||
const staleInventoryLineageCanYieldToCounterparty = previousCounterpartyLaneActive && !hasExplicitInventoryItemReference;
|
const staleInventoryLineageCanYieldToCounterparty = previousCounterpartyLaneActive && !hasExplicitInventoryItemReference;
|
||||||
const inventoryPurchaseDateVatBridge = inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage);
|
const vatPurchaseDateBridgeContinuation = sourceIntent === "vat_liability_confirmed_for_tax_period" &&
|
||||||
|
Boolean(previousPurchaseDateBridgeSelected) &&
|
||||||
|
hasInventoryPurchaseDateVatBridgeContinuationCue(normalizedMessage);
|
||||||
|
const inventoryPurchaseDateVatBridge = (inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage)) ||
|
||||||
|
vatPurchaseDateBridgeContinuation;
|
||||||
const marginRankingLineageActive = sourceIntent === "inventory_margin_ranking_for_nomenclature" ||
|
const marginRankingLineageActive = sourceIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
fallbackIntent === "inventory_margin_ranking_for_nomenclature" ||
|
fallbackIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
followupContext.root_intent === "inventory_margin_ranking_for_nomenclature";
|
followupContext.root_intent === "inventory_margin_ranking_for_nomenclature";
|
||||||
|
|
@ -1382,6 +1511,7 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||||
if (inventoryPurchaseDateVatBridge &&
|
if (inventoryPurchaseDateVatBridge &&
|
||||||
(detectedIntent.intent === "unknown" ||
|
(detectedIntent.intent === "unknown" ||
|
||||||
detectedIntent.intent === sourceIntent ||
|
detectedIntent.intent === sourceIntent ||
|
||||||
|
detectedIntent.intent === "account_balance_snapshot" ||
|
||||||
detectedIntent.intent === "vat_payable_confirmed_as_of_date" ||
|
detectedIntent.intent === "vat_payable_confirmed_as_of_date" ||
|
||||||
detectedIntent.intent === "vat_payable_forecast")) {
|
detectedIntent.intent === "vat_payable_forecast")) {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,16 @@ function inventoryRequestedPartyMatches(requested, actualParties) {
|
||||||
function inventoryPartyListOrUnknown(parties) {
|
function inventoryPartyListOrUnknown(parties) {
|
||||||
return parties.length > 0 ? parties.slice(0, 4).join("; ") : "не выделен отдельным полем";
|
return parties.length > 0 ? parties.slice(0, 4).join("; ") : "не выделен отдельным полем";
|
||||||
}
|
}
|
||||||
|
function normalizeInventoryReplyEntityToken(value) {
|
||||||
|
const normalized = String(value ?? "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ё/gu, "е")
|
||||||
|
.replace(/[^a-zа-я0-9]+/giu, " ")
|
||||||
|
.replace(/\s+/gu, " ")
|
||||||
|
.trim();
|
||||||
|
return normalized || null;
|
||||||
|
}
|
||||||
function sumInventoryRowAmount(rows) {
|
function sumInventoryRowAmount(rows) {
|
||||||
return rows.reduce((sum, row) => sum + (typeof row.amount === "number" && Number.isFinite(row.amount) ? row.amount : 0), 0);
|
return rows.reduce((sum, row) => sum + (typeof row.amount === "number" && Number.isFinite(row.amount) ? row.amount : 0), 0);
|
||||||
}
|
}
|
||||||
|
|
@ -380,29 +390,47 @@ function composeInventoryReply(intent, rows, options, deps) {
|
||||||
}
|
}
|
||||||
if (intent === "inventory_aging_by_purchase_date") {
|
if (intent === "inventory_aging_by_purchase_date") {
|
||||||
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
||||||
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
const agingItems = deps.buildInventoryAgingByItemAggregate(rows, asOfDate);
|
||||||
|
const agingItemTokens = new Set(agingItems
|
||||||
|
.map((item) => normalizeInventoryReplyEntityToken(item.item))
|
||||||
|
.filter((item) => Boolean(item)));
|
||||||
|
const purchaseRows = rows.filter((row) => {
|
||||||
|
if (!deps.isInventoryPurchaseMovement(row)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (agingItemTokens.size === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const item = deps.summarizeInventoryTraceRows([row]).item;
|
||||||
|
const itemToken = normalizeInventoryReplyEntityToken(item);
|
||||||
|
return Boolean(itemToken && agingItemTokens.has(itemToken));
|
||||||
|
});
|
||||||
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
|
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
|
||||||
const agingItems = deps.buildInventoryAgingByItemAggregate(purchaseRows, asOfDate);
|
|
||||||
const oldestPurchaseDate = agingItems[0]?.firstPurchasePeriod ?? summary.firstPeriod;
|
const oldestPurchaseDate = agingItems[0]?.firstPurchasePeriod ?? summary.firstPeriod;
|
||||||
const oldestPurchaseAgeDays = agingItems[0]?.ageDays ?? null;
|
const oldestPurchaseAgeDays = agingItems[0]?.ageDays ?? null;
|
||||||
|
const organizationLabel = agingItems.find((item) => item.organization)?.organization ?? null;
|
||||||
const oldestAnswerPreview = agingItems
|
const oldestAnswerPreview = agingItems
|
||||||
.slice(0, 3)
|
.slice(0, 3)
|
||||||
.map((item) => `${item.item} (${deps.inventoryTraceDateLabel(item.firstPurchasePeriod)})`)
|
.map((item) => `${item.item} (${deps.inventoryTraceDateLabel(item.firstPurchasePeriod)})`)
|
||||||
.join("; ");
|
.join("; ");
|
||||||
const directAnswerLine = agingItems.length > 0
|
const directAnswerLine = agingItems.length > 0
|
||||||
? `К самым старым закупкам в текущем подтвержденном срезе относятся позиции с самой ранней первой закупкой: ${oldestAnswerPreview}.`
|
? `Среди фактических положительных остатков есть давно закупавшиеся позиции: ${oldestAnswerPreview}.`
|
||||||
: "По доступному закупочному следу позиции со старыми закупками не материализованы.";
|
: "В фактическом положительном остатке не найдено позиций с подтвержденным старым закупочным следом.";
|
||||||
const lines = [directAnswerLine];
|
const lines = [directAnswerLine];
|
||||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Сводка:", [
|
const summaryLines = [
|
||||||
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||||
`Самая ранняя первая закупка среди позиций: ${deps.inventoryTraceDateLabel(oldestPurchaseDate)}.`,
|
`Самая ранняя первая закупка среди позиций: ${deps.inventoryTraceDateLabel(oldestPurchaseDate)}.`,
|
||||||
`Самая поздняя найденная закупка: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
`Самая поздняя найденная закупка: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
||||||
`Позиций в выборке: ${deps.formatNumberWithDots(agingItems.length)}.`,
|
`Позиций в выборке: ${deps.formatNumberWithDots(agingItems.length)}.`,
|
||||||
`Закупочных документов: ${deps.formatNumberWithDots(summary.documents.length)}.`,
|
`Закупочных документов: ${deps.formatNumberWithDots(summary.documents.length)}.`,
|
||||||
`Закупочных операций: ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
`Закупочных операций: ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
||||||
]);
|
];
|
||||||
|
if (organizationLabel) {
|
||||||
|
summaryLines.splice(1, 0, `Организация: ${organizationLabel}.`);
|
||||||
|
}
|
||||||
|
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Сводка:", summaryLines);
|
||||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Ограничения:", [
|
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Ограничения:", [
|
||||||
"Без партионного учета этот ответ показывает возраст закупочного следа по товарной позиции, а не возраст конкретного лота."
|
"Берем только позиции с положительным остатком на дату среза; без партионного учета это возраст закупочного следа по номенклатуре, а не доказанный возраст конкретной партии."
|
||||||
]);
|
]);
|
||||||
if (oldestPurchaseAgeDays !== null) {
|
if (oldestPurchaseAgeDays !== null) {
|
||||||
lines.push(`- Между самой ранней первой закупкой и датой среза прошло ${deps.formatNumberWithDots(oldestPurchaseAgeDays)} дн.`);
|
lines.push(`- Между самой ранней первой закупкой и датой среза прошло ${deps.formatNumberWithDots(oldestPurchaseAgeDays)} дн.`);
|
||||||
|
|
@ -411,11 +439,11 @@ function composeInventoryReply(intent, rows, options, deps) {
|
||||||
lines.push(`- Поставщики, встречающиеся в наблюдаемом закупочном следе: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
lines.push(`- Поставщики, встречающиеся в наблюдаемом закупочном следе: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||||
}
|
}
|
||||||
if (agingItems.length > 0) {
|
if (agingItems.length > 0) {
|
||||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Позиции от самых старых закупок:", deps.formatInventoryAgingRows(agingItems, asOfDate, 12));
|
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Позиции от самых старых закупок:", deps.formatInventoryAgingRows(agingItems, asOfDate, 5));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Позиции от самых старых закупок:", [
|
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Позиции от самых старых закупок:", [
|
||||||
"- В доступных данных не найдено закупочных движений для выбранного среза."
|
"- В доступных данных не найдено закупочных движений по позициям, которые есть в положительном остатке на дату среза."
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(agingItems.length > 0 ? "strong" : "medium", agingItems.length > 0));
|
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(agingItems.length > 0 ? "strong" : "medium", agingItems.length > 0));
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,7 @@ async function runAssistantAddressAttemptRuntime(input) {
|
||||||
};
|
};
|
||||||
const runAddressLaneAttempt = async (messageUsed, carryMeta, analysisDateHint, llmSemanticHints = null) => runAddressLaneAttemptRuntimeSafe((0, assistantAddressLaneAttemptInputBuilder_1.buildAssistantAddressLaneAttemptRuntimeInput)({
|
const runAddressLaneAttempt = async (messageUsed, carryMeta, analysisDateHint, llmSemanticHints = null) => runAddressLaneAttemptRuntimeSafe((0, assistantAddressLaneAttemptInputBuilder_1.buildAssistantAddressLaneAttemptRuntimeInput)({
|
||||||
messageUsed,
|
messageUsed,
|
||||||
|
rawUserMessage: input.userMessage,
|
||||||
carryMeta,
|
carryMeta,
|
||||||
analysisDateHint,
|
analysisDateHint,
|
||||||
llmSemanticHints,
|
llmSemanticHints,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ exports.buildAssistantAddressLaneAttemptRuntimeInput = buildAssistantAddressLane
|
||||||
function buildAssistantAddressLaneAttemptRuntimeInput(input) {
|
function buildAssistantAddressLaneAttemptRuntimeInput(input) {
|
||||||
return {
|
return {
|
||||||
messageUsed: input.messageUsed,
|
messageUsed: input.messageUsed,
|
||||||
|
rawUserMessage: input.rawUserMessage,
|
||||||
carryMeta: input.carryMeta,
|
carryMeta: input.carryMeta,
|
||||||
analysisDateHint: input.analysisDateHint,
|
analysisDateHint: input.analysisDateHint,
|
||||||
llmSemanticHints: input.llmSemanticHints,
|
llmSemanticHints: input.llmSemanticHints,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ function resolveAssistantAddressLaneAttemptFollowupContext(carryMeta) {
|
||||||
}
|
}
|
||||||
function buildAssistantAddressLaneAttemptQueryOptions(input) {
|
function buildAssistantAddressLaneAttemptQueryOptions(input) {
|
||||||
const base = {
|
const base = {
|
||||||
analysisDateHint: input.analysisDateHint
|
analysisDateHint: input.analysisDateHint,
|
||||||
|
rawUserMessage: input.rawUserMessage ?? undefined
|
||||||
};
|
};
|
||||||
if (input.scopedFollowupContext) {
|
if (input.scopedFollowupContext) {
|
||||||
base.followupContext = input.scopedFollowupContext;
|
base.followupContext = input.scopedFollowupContext;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ async function runAssistantAddressLaneAttemptRuntime(input) {
|
||||||
scopedFollowupContext,
|
scopedFollowupContext,
|
||||||
llmSemanticHints: input.llmSemanticHints ?? null,
|
llmSemanticHints: input.llmSemanticHints ?? null,
|
||||||
activeOrganization: input.activeOrganization,
|
activeOrganization: input.activeOrganization,
|
||||||
knownOrganizations: input.knownOrganizations
|
knownOrganizations: input.knownOrganizations,
|
||||||
|
rawUserMessage: input.rawUserMessage ?? input.messageUsed
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,17 @@ function cleanComparisonScopeCompanyLine(line, organization) {
|
||||||
}
|
}
|
||||||
return clean.trim();
|
return clean.trim();
|
||||||
}
|
}
|
||||||
|
function hasComparisonScopeProofCue(value) {
|
||||||
|
const text = toNullableString(value)?.toLocaleLowerCase("ru-RU").replace(/\s+/gu, " ").trim();
|
||||||
|
if (!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:\bсравни\b|собери\s+коротк\p{L}*\s+итог|коротк\p{L}*\s+сравн|что\s+.*подтверд\p{L}*.*(?:компан|контрагент|свк)|что\s+.*отдельно\s+по\s+(?:группа\s+свк|выбранн\p{L}*\s+контрагент)|какие\s+выводы\s+можно\s+делать\s+и\s+какие\s+нельзя)/iu.test(text);
|
||||||
|
}
|
||||||
function buildComparisonScopeProofReply(input) {
|
function buildComparisonScopeProofReply(input) {
|
||||||
|
if (!hasComparisonScopeProofCue(input.userMessage)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const entryPoint = toRecordObject(input.debug.assistant_mcp_discovery_entry_point_v1);
|
const entryPoint = toRecordObject(input.debug.assistant_mcp_discovery_entry_point_v1);
|
||||||
const turnInput = toRecordObject(entryPoint?.turn_input);
|
const turnInput = toRecordObject(entryPoint?.turn_input);
|
||||||
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
|
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ function hasCompactCashflowFollowupSignal(text) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return /(?:\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0440\u0435\u0437\p{L}*|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433|\u043f\u0440\u0438\u0448\p{L}*[\s\S]{0,80}\u0443\u0448\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u043f\u043e\u043b\u0443\u0447\p{L}*[\s\S]{0,80}\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*[\s\S]{0,80}\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b)/iu.test(value);
|
return /(?:\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0440\u0435\u0437\p{L}*|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433|\u043f\u0440\u0438\u0448\p{L}*[\s\S]{0,80}\u0443\u0448\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u043f\u043e\u043b\u0443\u0447\p{L}*[\s\S]{0,80}\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*[\s\S]{0,80}\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b|кто\s+больше\s+всего\s+зан[её]с|кому\s+больше\s+всего\s+ушл\p{L}*|банк\s+не\s+называй\s+обычн\p{L}*\s+клиент\p{L}*|главн\p{L}*\s+клиент|главн\p{L}*\s+поставщик)/iu.test(value);
|
||||||
}
|
}
|
||||||
function dateScopeToFilterWindow(dateScope) {
|
function dateScopeToFilterWindow(dateScope) {
|
||||||
if (!dateScope) {
|
if (!dateScope) {
|
||||||
|
|
@ -79,6 +79,35 @@ function dateScopeToFilterWindow(dateScope) {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
function filterWindowFromExtractedFilters(filters) {
|
||||||
|
if (!filters) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const asOfDate = typeof filters.as_of_date === "string" ? filters.as_of_date.trim() : "";
|
||||||
|
if (/^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(asOfDate)) {
|
||||||
|
return { as_of_date: asOfDate };
|
||||||
|
}
|
||||||
|
const periodFrom = typeof filters.period_from === "string" ? filters.period_from.trim() : "";
|
||||||
|
const periodTo = typeof filters.period_to === "string" ? filters.period_to.trim() : "";
|
||||||
|
if (/^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(periodFrom) && /^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(periodTo)) {
|
||||||
|
return {
|
||||||
|
period_from: periodFrom,
|
||||||
|
period_to: periodTo
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function hasBoundedFilterWindow(filters) {
|
||||||
|
if (!filters) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (typeof filters.as_of_date === "string" && /^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(filters.as_of_date.trim())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const periodFrom = typeof filters.period_from === "string" ? filters.period_from.trim() : "";
|
||||||
|
const periodTo = typeof filters.period_to === "string" ? filters.period_to.trim() : "";
|
||||||
|
return /^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(periodFrom) && /^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(periodTo);
|
||||||
|
}
|
||||||
function looksLikeReliableOrganizationScope(value) {
|
function looksLikeReliableOrganizationScope(value) {
|
||||||
const text = compactLower(value);
|
const text = compactLower(value);
|
||||||
if (!text) {
|
if (!text) {
|
||||||
|
|
@ -103,12 +132,15 @@ function inferBusinessOverviewDiscoveryContextFromSessionItems(sessionItems, toN
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const turnMeaningRef = toRecordObject(turnInput?.turn_meaning_ref);
|
const turnMeaningRef = toRecordObject(turnInput?.turn_meaning_ref);
|
||||||
const filterWindow = dateScopeToFilterWindow(toNonEmptyString(turnMeaningRef?.explicit_date_scope));
|
const debugFilters = toRecordObject(debug?.extracted_filters);
|
||||||
|
const filterWindow = dateScopeToFilterWindow(toNonEmptyString(turnMeaningRef?.explicit_date_scope)) ??
|
||||||
|
filterWindowFromExtractedFilters(debugFilters);
|
||||||
if (!filterWindow) {
|
if (!filterWindow) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const previousFilters = { ...filterWindow };
|
const previousFilters = { ...filterWindow };
|
||||||
const organization = toNonEmptyString(turnMeaningRef?.explicit_organization_scope);
|
const organization = toNonEmptyString(turnMeaningRef?.explicit_organization_scope) ??
|
||||||
|
toNonEmptyString(debugFilters?.organization);
|
||||||
if (looksLikeReliableOrganizationScope(organization)) {
|
if (looksLikeReliableOrganizationScope(organization)) {
|
||||||
previousFilters.organization = organization;
|
previousFilters.organization = organization;
|
||||||
}
|
}
|
||||||
|
|
@ -129,9 +161,7 @@ function mergeBusinessOverviewDateContextForCompactCashflow(input) {
|
||||||
return input.followupContext;
|
return input.followupContext;
|
||||||
}
|
}
|
||||||
const existingFilters = toRecordObject(input.followupContext?.previous_filters);
|
const existingFilters = toRecordObject(input.followupContext?.previous_filters);
|
||||||
if (input.toNonEmptyString(existingFilters?.period_from) ||
|
if (hasBoundedFilterWindow(existingFilters)) {
|
||||||
input.toNonEmptyString(existingFilters?.period_to) ||
|
|
||||||
input.toNonEmptyString(existingFilters?.as_of_date)) {
|
|
||||||
return input.followupContext;
|
return input.followupContext;
|
||||||
}
|
}
|
||||||
const inferred = inferBusinessOverviewDiscoveryContextFromSessionItems(input.sessionItems, input.toNonEmptyString);
|
const inferred = inferBusinessOverviewDiscoveryContextFromSessionItems(input.sessionItems, input.toNonEmptyString);
|
||||||
|
|
@ -142,12 +172,12 @@ function mergeBusinessOverviewDateContextForCompactCashflow(input) {
|
||||||
...inferred,
|
...inferred,
|
||||||
...(input.followupContext ?? {}),
|
...(input.followupContext ?? {}),
|
||||||
previous_filters: {
|
previous_filters: {
|
||||||
...(toRecordObject(inferred.previous_filters) ?? {}),
|
...(toRecordObject(input.followupContext?.previous_filters) ?? {}),
|
||||||
...(toRecordObject(input.followupContext?.previous_filters) ?? {})
|
...(toRecordObject(inferred.previous_filters) ?? {})
|
||||||
},
|
},
|
||||||
root_filters: {
|
root_filters: {
|
||||||
...(toRecordObject(inferred.root_filters) ?? {}),
|
...(toRecordObject(input.followupContext?.root_filters) ?? {}),
|
||||||
...(toRecordObject(input.followupContext?.root_filters) ?? {})
|
...(toRecordObject(inferred.root_filters) ?? {})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -172,6 +202,13 @@ function sameEntityHint(expected, actual) {
|
||||||
}
|
}
|
||||||
return left === right || left.includes(right) || right.includes(left);
|
return left === right || left.includes(right) || right.includes(left);
|
||||||
}
|
}
|
||||||
|
function hasComparisonScopeProofCue(text) {
|
||||||
|
const value = compactLower(text);
|
||||||
|
if (!value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:\bсравни\b|собери\s+коротк\p{L}*\s+итог|коротк\p{L}*\s+сравн|что\s+.*подтверд\p{L}*.*(?:компан|контрагент|свк)|что\s+.*отдельно\s+по\s+(?:группа\s+свк|выбранн\p{L}*\s+контрагент)|какие\s+выводы\s+можно\s+делать\s+и\s+какие\s+нельзя)/iu.test(value);
|
||||||
|
}
|
||||||
function isBusinessOverviewDiscoveryFollowup(followupContext, toNonEmptyString) {
|
function isBusinessOverviewDiscoveryFollowup(followupContext, toNonEmptyString) {
|
||||||
if (!followupContext) {
|
if (!followupContext) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -344,6 +381,9 @@ function mergeBusinessOverviewProofBundlesFromNavigationState(input) {
|
||||||
const entities = toRecordObject(input.predecomposeContract?.entities);
|
const entities = toRecordObject(input.predecomposeContract?.entities);
|
||||||
const hasCurrentOrganizationSelection = Boolean(input.toNonEmptyString(entities?.organization));
|
const hasCurrentOrganizationSelection = Boolean(input.toNonEmptyString(entities?.organization));
|
||||||
const businessOverviewFollowup = isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString);
|
const businessOverviewFollowup = isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString);
|
||||||
|
if (!hasComparisonScopeProofCue(input.userMessage)) {
|
||||||
|
return input.followupContext;
|
||||||
|
}
|
||||||
if (!businessOverviewFollowup && !hasCurrentOrganizationSelection) {
|
if (!businessOverviewFollowup && !hasCurrentOrganizationSelection) {
|
||||||
return input.followupContext;
|
return input.followupContext;
|
||||||
}
|
}
|
||||||
|
|
@ -376,6 +416,9 @@ function mergeBusinessOverviewProofBundlesFromNavigationState(input) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function mergeBusinessOverviewProofBundlesFromSessionItems(input) {
|
function mergeBusinessOverviewProofBundlesFromSessionItems(input) {
|
||||||
|
if (!hasComparisonScopeProofCue(input.userMessage)) {
|
||||||
|
return input.followupContext;
|
||||||
|
}
|
||||||
if (!isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString)) {
|
if (!isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString)) {
|
||||||
return input.followupContext;
|
return input.followupContext;
|
||||||
}
|
}
|
||||||
|
|
@ -576,6 +619,7 @@ async function buildAssistantAddressOrchestrationRuntime(input) {
|
||||||
});
|
});
|
||||||
const discoveryFollowupContextWithProofBundles = mergeBusinessOverviewProofBundlesFromSessionItems({
|
const discoveryFollowupContextWithProofBundles = mergeBusinessOverviewProofBundlesFromSessionItems({
|
||||||
followupContext: discoveryFollowupContext,
|
followupContext: discoveryFollowupContext,
|
||||||
|
userMessage: input.userMessage,
|
||||||
sessionItems: input.sessionItems,
|
sessionItems: input.sessionItems,
|
||||||
toNonEmptyString: input.toNonEmptyString
|
toNonEmptyString: input.toNonEmptyString
|
||||||
});
|
});
|
||||||
|
|
@ -583,6 +627,7 @@ async function buildAssistantAddressOrchestrationRuntime(input) {
|
||||||
followupContext: discoveryFollowupContextWithProofBundles,
|
followupContext: discoveryFollowupContextWithProofBundles,
|
||||||
sessionAddressNavigationState: input.sessionAddressNavigationState,
|
sessionAddressNavigationState: input.sessionAddressNavigationState,
|
||||||
predecomposeContract,
|
predecomposeContract,
|
||||||
|
userMessage: input.userMessage,
|
||||||
toNonEmptyString: input.toNonEmptyString
|
toNonEmptyString: input.toNonEmptyString
|
||||||
});
|
});
|
||||||
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(input.userMessage, addressInputMessage, carryover, addressPreDecompose);
|
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(input.userMessage, addressInputMessage, carryover, addressPreDecompose);
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,15 @@ function normalizeAddressRuntimeMetaForDeep(value) {
|
||||||
mcpDiscoveryRuntimeEntryPoint: source.mcpDiscoveryRuntimeEntryPoint ?? null
|
mcpDiscoveryRuntimeEntryPoint: source.mcpDiscoveryRuntimeEntryPoint ?? null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function stripRoutineShortAnswerPrefix(value) {
|
||||||
|
return String(value ?? "")
|
||||||
|
.split(/\r?\n/g)
|
||||||
|
.map((line) => line
|
||||||
|
.replace(/^((?:\s|\uFEFF)*(?:[-*]\s*)?)(?:\u041a\u043e\u0440\u043e\u0442\u043a\u043e:\s*)+/u, "$1")
|
||||||
|
.replace(/(:\s*)\u041a\u043e\u0440\u043e\u0442\u043a\u043e:\s*/u, "$1"))
|
||||||
|
.join("\n")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
function runAssistantDeepTurnResponseRuntime(input) {
|
function runAssistantDeepTurnResponseRuntime(input) {
|
||||||
const runPackagingRuntimeSafe = input.runPackagingRuntime ?? assistantDeepTurnPackagingRuntimeAdapter_1.runAssistantDeepTurnPackagingRuntime;
|
const runPackagingRuntimeSafe = input.runPackagingRuntime ?? assistantDeepTurnPackagingRuntimeAdapter_1.runAssistantDeepTurnPackagingRuntime;
|
||||||
const runFinalizeDeepTurnSafe = input.runFinalizeDeepTurn ?? assistantDeepTurnFinalizeRuntimeAdapter_1.finalizeAssistantDeepTurn;
|
const runFinalizeDeepTurnSafe = input.runFinalizeDeepTurn ?? assistantDeepTurnFinalizeRuntimeAdapter_1.finalizeAssistantDeepTurn;
|
||||||
|
|
@ -125,9 +134,9 @@ function runAssistantDeepTurnResponseRuntime(input) {
|
||||||
currentReplySource: "deep_analysis",
|
currentReplySource: "deep_analysis",
|
||||||
addressRuntimeMeta: addressRuntimeMetaForDeep
|
addressRuntimeMeta: addressRuntimeMetaForDeep
|
||||||
});
|
});
|
||||||
const assistantReply = mcpDiscoveryResponsePolicy.applied
|
const assistantReply = stripRoutineShortAnswerPrefix(mcpDiscoveryResponsePolicy.applied
|
||||||
? mcpDiscoveryResponsePolicy.reply_text
|
? input.sanitizeReply(mcpDiscoveryResponsePolicy.reply_text, packagingRuntime.safeAssistantReply)
|
||||||
: packagingRuntime.safeAssistantReply;
|
: packagingRuntime.safeAssistantReply);
|
||||||
const replyType = mcpDiscoveryResponsePolicy.applied ? "partial_coverage" : input.composition.reply_type;
|
const replyType = mcpDiscoveryResponsePolicy.applied ? "partial_coverage" : input.composition.reply_type;
|
||||||
const debug = {
|
const debug = {
|
||||||
...packagingRuntime.debug,
|
...packagingRuntime.debug,
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,10 @@ function requestsCounterpartyLeaderSurface(turnMeaning, graph) {
|
||||||
}
|
}
|
||||||
return /(?:(?:кто|как\p{L}*|покаж\p{L}*|назов\p{L}*|раскро\p{L}*)[\s\S]{0,100}(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*|топ)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:топ[-\s]*(?:клиент|заказчик|поставщик|получател))|(?:клиент|поставщик)[\s\S]{0,80}(?:главн|крупнейш|основн|ведущ|топ))/iu.test(text);
|
return /(?:(?:кто|как\p{L}*|покаж\p{L}*|назов\p{L}*|раскро\p{L}*)[\s\S]{0,100}(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*|топ)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:топ[-\s]*(?:клиент|заказчик|поставщик|получател))|(?:клиент|поставщик)[\s\S]{0,80}(?:главн|крупнейш|основн|ведущ|топ))/iu.test(text);
|
||||||
}
|
}
|
||||||
|
function requestsBusinessOverviewNextChecks(turnMeaning, graph) {
|
||||||
|
const text = normalizedTurnAndGraphText(turnMeaning, graph);
|
||||||
|
return /(?:что\s+(?:дальше\s+)?провер\w*|что\s+провер\w*\s+дальше|следующ\w*\s+шаг|в\s+первую\s+очередь\s+провер\w*|next\s+checks?|next\s+steps?)/iu.test(text);
|
||||||
|
}
|
||||||
function requestsCompactCashflowAnswer(turnMeaning, graph) {
|
function requestsCompactCashflowAnswer(turnMeaning, graph) {
|
||||||
const text = normalizeQuestionText([
|
const text = normalizeQuestionText([
|
||||||
turnMeaning?.raw_message,
|
turnMeaning?.raw_message,
|
||||||
|
|
@ -429,6 +433,13 @@ function businessOverviewPeriodText(overview) {
|
||||||
const period = toNonEmptyString(overview.period_scope);
|
const period = toNonEmptyString(overview.period_scope);
|
||||||
return period ? `за ${period}` : "за все доступное проверенное окно";
|
return period ? `за ${period}` : "за все доступное проверенное окно";
|
||||||
}
|
}
|
||||||
|
function businessOverviewPeriodPhrase(value) {
|
||||||
|
const normalized = toNonEmptyString(value);
|
||||||
|
if (!normalized) {
|
||||||
|
return "за все доступное проверенное окно";
|
||||||
|
}
|
||||||
|
return /^за\s+/iu.test(normalized) ? normalized : `за ${normalized}`;
|
||||||
|
}
|
||||||
function strongestIncomingYear(overview) {
|
function strongestIncomingYear(overview) {
|
||||||
const years = Array.isArray(overview.yearly_breakdown) ? overview.yearly_breakdown : [];
|
const years = Array.isArray(overview.yearly_breakdown) ? overview.yearly_breakdown : [];
|
||||||
const sorted = years
|
const sorted = years
|
||||||
|
|
@ -857,7 +868,10 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||||
const compactCashflowRequested = directMoneyOnlyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
|
const compactCashflowRequested = directMoneyOnlyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
|
||||||
const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph);
|
const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph);
|
||||||
const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph);
|
const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph);
|
||||||
|
const nextChecksRequested = requestsBusinessOverviewNextChecks(turnMeaning, graph);
|
||||||
const rawMessage = toNonEmptyString(turnMeaning?.raw_message) ?? toNonEmptyString(turnMeaning?.effective_message);
|
const rawMessage = toNonEmptyString(turnMeaning?.raw_message) ?? toNonEmptyString(turnMeaning?.effective_message);
|
||||||
|
const earningsAmountRequested = /(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0434\u043e\u0445\u043e\u0434\p{L}*)/iu.test(normalizeQuestionText(rawMessage ?? ""));
|
||||||
|
const accountingFinancialResultForEarnings = toRecordObject(overview.accounting_financial_result);
|
||||||
const rawMessageComparable = compactComparable(rawMessage);
|
const rawMessageComparable = compactComparable(rawMessage);
|
||||||
const organizationScopeComparable = compactComparable(organizationScope);
|
const organizationScopeComparable = compactComparable(organizationScope);
|
||||||
const plainOrganizationClarificationSelection = Boolean(separateSubject &&
|
const plainOrganizationClarificationSelection = Boolean(separateSubject &&
|
||||||
|
|
@ -868,15 +882,51 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||||
rawMessageComparable.includes(organizationScopeComparable) &&
|
rawMessageComparable.includes(organizationScopeComparable) &&
|
||||||
rawMessage.length <= 90 &&
|
rawMessage.length <= 90 &&
|
||||||
!/(?:сравн|подтвержд|деньг|сколько|что\s+|покаж|дай|вывод|нельзя|клиент|поставщик|\?)/iu.test(rawMessage));
|
!/(?:сравн|подтвержд|деньг|сколько|что\s+|покаж|дай|вывод|нельзя|клиент|поставщик|\?)/iu.test(rawMessage));
|
||||||
|
if (compactCashflowRequested &&
|
||||||
|
!rankingNeed &&
|
||||||
|
!profitMarginBoundary &&
|
||||||
|
!cashflowPolarityRequested &&
|
||||||
|
earningsAmountRequested &&
|
||||||
|
accountingFinancialResultForEarnings) {
|
||||||
|
const direction = toNonEmptyString(accountingFinancialResultForEarnings.final_result_direction);
|
||||||
|
const amount = moneyText(accountingFinancialResultForEarnings.final_result_amount_human_ru);
|
||||||
|
const periodScope = toNonEmptyString(accountingFinancialResultForEarnings.period_scope) ?? period;
|
||||||
|
const marginPct = typeof accountingFinancialResultForEarnings.net_margin_to_revenue_pct === "number" &&
|
||||||
|
Number.isFinite(accountingFinancialResultForEarnings.net_margin_to_revenue_pct)
|
||||||
|
? `${accountingFinancialResultForEarnings.net_margin_to_revenue_pct}%`
|
||||||
|
: null;
|
||||||
|
const directionText = direction === "profit"
|
||||||
|
? "\u0443\u0447\u0435\u0442\u043d\u0430\u044f \u043f\u0440\u0438\u0431\u044b\u043b\u044c"
|
||||||
|
: direction === "loss"
|
||||||
|
? "\u0443\u0447\u0435\u0442\u043d\u044b\u0439 \u0443\u0431\u044b\u0442\u043e\u043a"
|
||||||
|
: "\u043d\u0443\u043b\u0435\u0432\u043e\u0439 \u0443\u0447\u0435\u0442\u043d\u044b\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442";
|
||||||
|
const amountText = amount
|
||||||
|
? direction === "loss"
|
||||||
|
? `\u043c\u0438\u043d\u0443\u0441 ${amount}`
|
||||||
|
: amount
|
||||||
|
: "\u0441\u0443\u043c\u043c\u0430 \u043d\u0435 \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u043d\u0430";
|
||||||
|
lines.push(`${organizationPrefix}\u0437\u0430 ${periodScope} \u043f\u043e \u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044e \u0441\u0447\u0435\u0442\u043e\u0432 90/91/99 \u0432 1\u0421 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0430 ${directionText}: ${amountText}${marginPct ? `; \u043c\u0430\u0440\u0436\u0430 \u043a \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043d\u043e\u0439 \u0432\u044b\u0440\u0443\u0447\u043a\u0435 ${marginPct}` : ""}.`);
|
||||||
|
if (incomingAmount || outgoingAmount || netAmount) {
|
||||||
|
lines.push(`\u0414\u043b\u044f \u0441\u0432\u0435\u0440\u043a\u0438 \u043f\u043e \u0434\u0435\u043d\u044c\u0433\u0430\u043c: \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 ${incomingAmount ?? "0 \u0440\u0443\u0431."}; \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438/\u0441\u043f\u0438\u0441\u0430\u043b\u0438 ${outgoingAmount ?? "0 \u0440\u0443\u0431."}; \u0434\u0435\u043d\u0435\u0436\u043d\u043e\u0435 \u043d\u0435\u0442\u0442\u043e ${sentenceAmount(netAmount) ?? netAmount ?? "0 \u0440\u0443\u0431."}.`);
|
||||||
|
}
|
||||||
|
lines.push("\u0412\u0430\u0436\u043d\u043e: \u0443\u0447\u0435\u0442\u043d\u044b\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u0438 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435 \u0434\u0435\u043d\u0435\u0433 - \u0440\u0430\u0437\u043d\u044b\u0435 \u043c\u0435\u0442\u0440\u0438\u043a\u0438; \u043d\u0435 \u0441\u043c\u0435\u0448\u0438\u0432\u0430\u044e \u0438\u0445 \u0432 \u043e\u0434\u043d\u0443 \"\u043f\u0440\u0438\u0431\u044b\u043b\u044c\".");
|
||||||
|
return joinBusinessReplyLines(lines);
|
||||||
|
}
|
||||||
if (compactCashflowRequested && !rankingNeed && (incomingAmount || outgoingAmount || netAmount)) {
|
if (compactCashflowRequested && !rankingNeed && (incomingAmount || outgoingAmount || netAmount)) {
|
||||||
const netDisplay = sentenceAmount(netAmount) ?? netAmount ?? "0 \u0440\u0443\u0431.";
|
const netDisplay = sentenceAmount(netAmount) ?? netAmount ?? "0 \u0440\u0443\u0431.";
|
||||||
const signedNetDisplay = cashflowPolarityRequested && netDisplay && !String(netDisplay).trim().startsWith("-")
|
const signedNetDisplay = cashflowPolarityRequested && netDisplay && !String(netDisplay).trim().startsWith("-")
|
||||||
? `+${netDisplay}`
|
? `+${netDisplay}`
|
||||||
: netDisplay;
|
: netDisplay;
|
||||||
const polarityLead = cashflowPolarityRequested
|
const limitedEarningsAnswer = Boolean(earningsAmountRequested && !accountingFinancialResultForEarnings && limitLine);
|
||||||
? `\u041a\u043e\u0440\u043e\u0442\u043a\u043e: \u043f\u043e \u0434\u0435\u043d\u044c\u0433\u0430\u043c \u043f\u043b\u044e\u0441. `
|
const polarityLead = limitedEarningsAnswer
|
||||||
: "\u041a\u043e\u0440\u043e\u0442\u043a\u043e: ";
|
? "\u041a\u043e\u0440\u043e\u0442\u043a\u043e: \u0442\u043e\u0447\u043d\u0443\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0437\u0430 \u044d\u0442\u043e \u043e\u043a\u043d\u043e \u043d\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u044e; \u0432 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u043d\u043e\u043c \u0434\u0435\u043d\u0435\u0436\u043d\u043e\u043c \u0441\u0440\u0435\u0437\u0435 "
|
||||||
|
: cashflowPolarityRequested
|
||||||
|
? `\u041a\u043e\u0440\u043e\u0442\u043a\u043e: \u043f\u043e \u0434\u0435\u043d\u044c\u0433\u0430\u043c \u043f\u043b\u044e\u0441. `
|
||||||
|
: "\u041a\u043e\u0440\u043e\u0442\u043a\u043e: ";
|
||||||
lines.push(`${polarityLead}${organizationPrefix}${period} \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 ${incomingAmount ?? "0 \u0440\u0443\u0431."}; \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438/\u0441\u043f\u0438\u0441\u0430\u043b\u0438 ${outgoingAmount ?? "0 \u0440\u0443\u0431."}; \u0434\u0435\u043d\u0435\u0436\u043d\u043e\u0435 \u043d\u0435\u0442\u0442\u043e ${signedNetDisplay}.`);
|
lines.push(`${polarityLead}${organizationPrefix}${period} \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 ${incomingAmount ?? "0 \u0440\u0443\u0431."}; \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438/\u0441\u043f\u0438\u0441\u0430\u043b\u0438 ${outgoingAmount ?? "0 \u0440\u0443\u0431."}; \u0434\u0435\u043d\u0435\u0436\u043d\u043e\u0435 \u043d\u0435\u0442\u0442\u043e ${signedNetDisplay}.`);
|
||||||
|
if (limitLine) {
|
||||||
|
lines.push(limitLine);
|
||||||
|
}
|
||||||
lines.push(cashflowPolarityRequested
|
lines.push(cashflowPolarityRequested
|
||||||
? "\u042d\u0442\u043e \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u043f\u043e\u0442\u043e\u043a \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c 1\u0421, \u043d\u0435 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442."
|
? "\u042d\u0442\u043e \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u043f\u043e\u0442\u043e\u043a \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c 1\u0421, \u043d\u0435 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442."
|
||||||
: "\u042d\u0442\u043e \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u043f\u043e\u0442\u043e\u043a \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c 1\u0421, \u043d\u0435 \u0447\u0438\u0441\u0442\u0430\u044f \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0438 \u043d\u0435 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442.");
|
: "\u042d\u0442\u043e \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u043f\u043e\u0442\u043e\u043a \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c 1\u0421, \u043d\u0435 \u0447\u0438\u0441\u0442\u0430\u044f \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0438 \u043d\u0435 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442.");
|
||||||
|
|
@ -921,11 +971,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||||
: "Это учетный финрезультат по найденным строкам закрытия периода в 1С, а не внешний аудит и не юридически подтвержденная отчетность.");
|
: "Это учетный финрезультат по найденным строкам закрытия периода в 1С, а не внешний аудит и не юридически подтвержденная отчетность.");
|
||||||
return joinBusinessReplyLines(lines);
|
return joinBusinessReplyLines(lines);
|
||||||
}
|
}
|
||||||
const headline = toNonEmptyString(draft.headline);
|
lines.push("Коротко: чистую прибыль и маржу не подтверждаю по текущему срезу 1С; видны только ограниченные денежные обороты и расчетное нетто/операционный сигнал, а не полный отчет о прибыли и не бухгалтерский финансовый результат.");
|
||||||
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
|
|
||||||
lines.push(cleanHeadline
|
|
||||||
? `Коротко: ${localizeLine(cleanHeadline)}`
|
|
||||||
: "Коротко: нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только ограниченный операционный денежный/товарный сигнал, а не полный отчет о прибыли и не бухгалтерский финансовый результат.");
|
|
||||||
const boundaryLines = userFacingLines([
|
const boundaryLines = userFacingLines([
|
||||||
...toStringList(draft.confirmed_lines),
|
...toStringList(draft.confirmed_lines),
|
||||||
...toStringList(draft.inference_lines),
|
...toStringList(draft.inference_lines),
|
||||||
|
|
@ -1005,9 +1051,12 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||||
? `${vendorProcurementQuality.top_non_financial_supplier_share_pct}%`
|
? `${vendorProcurementQuality.top_non_financial_supplier_share_pct}%`
|
||||||
: null;
|
: null;
|
||||||
const periodScope = toNonEmptyString(vendorProcurementQuality.period_scope) ?? period;
|
const periodScope = toNonEmptyString(vendorProcurementQuality.period_scope) ?? period;
|
||||||
const totalText = totalOutgoing ? `; всего исходящих платежей в проверенном срезе ${totalOutgoing}` : "";
|
const periodPhrase = businessOverviewPeriodPhrase(periodScope);
|
||||||
|
const totalText = totalOutgoing
|
||||||
|
? `; всего исходящих платежей в проверенном срезе ${sentenceAmount(totalOutgoing)}`
|
||||||
|
: "";
|
||||||
if (status === "financial_institution_leads_outgoing_cash") {
|
if (status === "financial_institution_leads_outgoing_cash") {
|
||||||
lines.push(`Коротко: проверка концентрации закупок/исходящих платежей за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`);
|
lines.push(`Коротко: проверка концентрации закупок/исходящих платежей ${periodPhrase} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`);
|
||||||
const financialHintText = financialFlowHintTextRuFromRecord(topOutgoingRecord);
|
const financialHintText = financialFlowHintTextRuFromRecord(topOutgoingRecord);
|
||||||
if (financialHintText) {
|
if (financialHintText) {
|
||||||
lines.push(financialHintText);
|
lines.push(financialHintText);
|
||||||
|
|
@ -1017,17 +1066,17 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (status === "reviewed_procurement_concentration") {
|
else if (status === "reviewed_procurement_concentration") {
|
||||||
lines.push(`Коротко: точный риск зависимости от одного поставщика не подтвержден полностью; проверка концентрации закупок/исходящих платежей за ${periodScope} нашла крупнейшего получателя исходящего потока: ${topOutgoingName ?? nonFinancialName ?? "получатель не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : nonFinancialShare ? ` держит около ${nonFinancialShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}${totalText}.`);
|
lines.push(`Коротко: точный риск зависимости от одного поставщика не подтвержден полностью; проверка концентрации закупок/исходящих платежей ${periodPhrase} нашла крупнейшего получателя исходящего потока: ${topOutgoingName ?? nonFinancialName ?? "получатель не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : nonFinancialShare ? ` держит около ${nonFinancialShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}${totalText}.`);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
lines.push(`Коротко: проверка концентрации закупок/исходящих платежей за ${periodScope} выполнена, но надежной небанковской концентрации поставщика по найденным исходящим платежам не хватает${totalText}.`);
|
lines.push(`Коротко: проверка концентрации закупок/исходящих платежей ${periodPhrase} выполнена, но надежной небанковской концентрации поставщика по найденным исходящим платежам не хватает${totalText}.`);
|
||||||
}
|
}
|
||||||
const contractText = typeof vendorProcurementQuality.used_contracts === "number" && Number.isFinite(vendorProcurementQuality.used_contracts)
|
const contractText = typeof vendorProcurementQuality.used_contracts === "number" && Number.isFinite(vendorProcurementQuality.used_contracts)
|
||||||
? typeof vendorProcurementQuality.total_contracts === "number" && Number.isFinite(vendorProcurementQuality.total_contracts)
|
? typeof vendorProcurementQuality.total_contracts === "number" && Number.isFinite(vendorProcurementQuality.total_contracts)
|
||||||
? ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts}/${vendorProcurementQuality.total_contracts} договоров${typeof vendorProcurementQuality.used_contract_share_pct === "number" && Number.isFinite(vendorProcurementQuality.used_contract_share_pct) ? ` (${vendorProcurementQuality.used_contract_share_pct}%)` : ""}.`
|
? ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts}/${vendorProcurementQuality.total_contracts} договоров${typeof vendorProcurementQuality.used_contract_share_pct === "number" && Number.isFinite(vendorProcurementQuality.used_contract_share_pct) ? ` (${vendorProcurementQuality.used_contract_share_pct}%)` : ""}.`
|
||||||
: ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts} договоров.`
|
: ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts} договоров.`
|
||||||
: "";
|
: "";
|
||||||
lines.push(`Что не доказано этим срезом: надежность поставщика, качество поставок, договорные условия, назначение каждого платежа и полная структура всех расходов.${contractText}`);
|
lines.push(`Это не аудит поставщиков: срез не доказывает надежность поставщика, качество поставок, договорные условия, назначение каждого платежа и полную структуру всех расходов.${contractText}`);
|
||||||
return joinBusinessReplyLines(lines);
|
return joinBusinessReplyLines(lines);
|
||||||
}
|
}
|
||||||
const supplierBasis = topSupplier
|
const supplierBasis = topSupplier
|
||||||
|
|
@ -1068,6 +1117,19 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||||
lines.push("Проверить нужно отдельно: складской срез на дату, учетную политику резервов, списания и ликвидационную стоимость; косвенные признаки нельзя выдавать за доказанный факт резерва.");
|
lines.push("Проверить нужно отдельно: складской срез на дату, учетную политику резервов, списания и ликвидационную стоимость; косвенные признаки нельзя выдавать за доказанный факт резерва.");
|
||||||
return joinBusinessReplyLines(lines);
|
return joinBusinessReplyLines(lines);
|
||||||
}
|
}
|
||||||
|
if (!separateSubject &&
|
||||||
|
!crossScopeExecutiveSummary &&
|
||||||
|
!counterpartyLeaderSurfaceRequested &&
|
||||||
|
!rankingNeed &&
|
||||||
|
nextChecksRequested) {
|
||||||
|
const subject = organizationScope ? `по компании ${organizationScope}` : "по компании";
|
||||||
|
lines.push(`В первую очередь ${subject} надо проверить чистую прибыль через закрытие 90/91/99, зависимость от топ-3 клиентов и поставщиков, долги/зависшие расчеты, складские остатки и НДС-позицию.`);
|
||||||
|
lines.push("Смысл порядка: сначала отделить бухгалтерский финрезультат от денежного нетто, затем проверить концентрацию потоков и только после этого углубляться в склад, долги и налоговую позицию.");
|
||||||
|
if (limitLine) {
|
||||||
|
lines.push(limitLine);
|
||||||
|
}
|
||||||
|
return joinBusinessReplyLines(lines);
|
||||||
|
}
|
||||||
if (!separateSubject &&
|
if (!separateSubject &&
|
||||||
!crossScopeExecutiveSummary &&
|
!crossScopeExecutiveSummary &&
|
||||||
!counterpartyLeaderSurfaceRequested &&
|
!counterpartyLeaderSurfaceRequested &&
|
||||||
|
|
@ -1075,8 +1137,8 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||||
(actionFamily === "broad_evaluation" || unsupportedFamily === "broad_business_evaluation")) {
|
(actionFamily === "broad_evaluation" || unsupportedFamily === "broad_business_evaluation")) {
|
||||||
const subject = organizationScope ?? "компания";
|
const subject = organizationScope ?? "компания";
|
||||||
const periodWithoutPrefix = period.replace(/^за\s+/iu, "");
|
const periodWithoutPrefix = period.replace(/^за\s+/iu, "");
|
||||||
lines.push(`Коротко: по доступным данным ${subject} выглядит как бизнес с крупными контрактными денежными потоками и заметной зависимостью от нескольких крупных контрагентов, а не как равномерный поток мелких продаж.`);
|
lines.push(`Коротко: по ограниченному проверенному 1С-срезу ${subject} выглядит как бизнес с крупными контрактными денежными потоками и заметной зависимостью от нескольких крупных контрагентов, а не как равномерный поток мелких продаж; это не аудиторское заключение и не подтвержденная чистая прибыль.`);
|
||||||
lines.push("Что видно:");
|
lines.push("Что видно в ограниченном денежном срезе:");
|
||||||
if (incomingAmount) {
|
if (incomingAmount) {
|
||||||
lines.push(`- входящие деньги за ${periodWithoutPrefix}: ${incomingAmount};`);
|
lines.push(`- входящие деньги за ${periodWithoutPrefix}: ${incomingAmount};`);
|
||||||
}
|
}
|
||||||
|
|
@ -1108,7 +1170,9 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||||
if (debtLine) {
|
if (debtLine) {
|
||||||
lines.push(`- ${localizeLine(debtLine)}`);
|
lines.push(`- ${localizeLine(debtLine)}`);
|
||||||
}
|
}
|
||||||
lines.push("Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль.");
|
if (!limitLine) {
|
||||||
|
lines.push("Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль.");
|
||||||
|
}
|
||||||
const missingOverviewFamilies = [];
|
const missingOverviewFamilies = [];
|
||||||
if (!taxLine) {
|
if (!taxLine) {
|
||||||
missingOverviewFamilies.push("НДС/налоговая позиция без отдельного точного расчета");
|
missingOverviewFamilies.push("НДС/налоговая позиция без отдельного точного расчета");
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,15 @@ function hasInternalMechanics(value) {
|
||||||
text.includes("catalog_") ||
|
text.includes("catalog_") ||
|
||||||
text.includes("select "));
|
text.includes("select "));
|
||||||
}
|
}
|
||||||
|
function stripRoutineShortAnswerPrefix(value) {
|
||||||
|
return String(value ?? "")
|
||||||
|
.split(/\r?\n/g)
|
||||||
|
.map((line) => line
|
||||||
|
.replace(/^((?:\s|\uFEFF)*(?:[-*]\s*)?)(?:\u041a\u043e\u0440\u043e\u0442\u043a\u043e:\s*)+/u, "$1")
|
||||||
|
.replace(/(:\s*)\u041a\u043e\u0440\u043e\u0442\u043a\u043e:\s*/u, "$1"))
|
||||||
|
.join("\n")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
function isMcpDiscoveryEntryPointContract(value) {
|
function isMcpDiscoveryEntryPointContract(value) {
|
||||||
const record = toRecordObject(value);
|
const record = toRecordObject(value);
|
||||||
return (record?.schema_version === "assistant_mcp_discovery_runtime_entry_point_v1" &&
|
return (record?.schema_version === "assistant_mcp_discovery_runtime_entry_point_v1" &&
|
||||||
|
|
@ -797,12 +806,13 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_applied");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_applied");
|
||||||
|
const replyText = stripRoutineShortAnswerPrefix(String(candidate.reply_text));
|
||||||
return {
|
return {
|
||||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION,
|
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION,
|
||||||
policy_owner: "assistantMcpDiscoveryResponsePolicy",
|
policy_owner: "assistantMcpDiscoveryResponsePolicy",
|
||||||
decision: "apply_candidate",
|
decision: "apply_candidate",
|
||||||
applied: true,
|
applied: true,
|
||||||
reply_text: String(candidate.reply_text),
|
reply_text: replyText || String(candidate.reply_text),
|
||||||
reply_source: "mcp_discovery_response_candidate_guarded",
|
reply_source: "mcp_discovery_response_candidate_guarded",
|
||||||
candidate,
|
candidate,
|
||||||
reason_codes: reasonCodes
|
reason_codes: reasonCodes
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,7 @@ function isGarbageSemanticAnchorCandidate(value) {
|
||||||
}
|
}
|
||||||
if (/^(?:по\s+)?(?:этим|этими)\s+данн\p{L}*$/iu.test(text) ||
|
if (/^(?:по\s+)?(?:этим|этими)\s+данн\p{L}*$/iu.test(text) ||
|
||||||
/^(?:и\s+)?кто\s+(?:главн\p{L}*|основн\p{L}*|крупн\p{L}*)\s+(?:клиент|покупател|поставщик|контрагент)(?:\s+в)?$/iu.test(text) ||
|
/^(?:и\s+)?кто\s+(?:главн\p{L}*|основн\p{L}*|крупн\p{L}*)\s+(?:клиент|покупател|поставщик|контрагент)(?:\s+в)?$/iu.test(text) ||
|
||||||
|
/^(?:и\s+)?(?:главн\p{L}*|основн\p{L}*|крупн\p{L}*)\s+(?:клиент|покупател|поставщик|контрагент)(?:\s+в)?$/iu.test(text) ||
|
||||||
/^(?:или\s+)?(?:обычн\p{L}*\s+)?(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент)(?:\s+или\s+(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент))?$/iu.test(text) ||
|
/^(?:или\s+)?(?:обычн\p{L}*\s+)?(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент)(?:\s+или\s+(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент))?$/iu.test(text) ||
|
||||||
/^(?:что|чего)\s+(?:подтвержден\p{L}*|не\s+хватает)/iu.test(text) ||
|
/^(?:что|чего)\s+(?:подтвержден\p{L}*|не\s+хватает)/iu.test(text) ||
|
||||||
/^(?:можно\s+ли|если\s+нет|дай\s+proxy|дай\s+прокси)/iu.test(text)) {
|
/^(?:можно\s+ли|если\s+нет|дай\s+proxy|дай\s+прокси)/iu.test(text)) {
|
||||||
|
|
@ -675,6 +676,15 @@ function hasDebtDueDateBoundaryFollowupSignal(text) {
|
||||||
const hasFollowupShape = /(?:\u0442\u043e\s+\u0435\u0441\u0442\u044c|\u043f\u043e\u0447\u0435\u043c\u0443|\u043a\u043e\u0440\u043e\u0442\w*|\u043d\u0435\u043b\u044c\u0437\u044f|\u043c\u043e\u0436\u043d\u043e\s+\u043b\u0438|\u0437\u043d\u0430\u0447\u0438\u0442|\u0432\u044b\u0445\u043e\u0434\u0438\u0442|why|short|brief|so)/iu.test(text);
|
const hasFollowupShape = /(?:\u0442\u043e\s+\u0435\u0441\u0442\u044c|\u043f\u043e\u0447\u0435\u043c\u0443|\u043a\u043e\u0440\u043e\u0442\w*|\u043d\u0435\u043b\u044c\u0437\u044f|\u043c\u043e\u0436\u043d\u043e\s+\u043b\u0438|\u0437\u043d\u0430\u0447\u0438\u0442|\u0432\u044b\u0445\u043e\u0434\u0438\u0442|why|short|brief|so)/iu.test(text);
|
||||||
return hasDueDateCue && hasFollowupShape;
|
return hasDueDateCue && hasFollowupShape;
|
||||||
}
|
}
|
||||||
|
function hasSupplierDependencyFollowupSignal(text) {
|
||||||
|
if (!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const normalized = compactLower(text);
|
||||||
|
const hasSupplierOrOutgoingCue = /(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0437\u0430\u043a\u0443\u043f|\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u043f\u043b\u0430\u0442[\u0435\u0451]\u0436|\u0437\u0430\u043f\u043b\u0430\u0442|supplier|vendor|procurement|outgoing|payout)/iu.test(normalized);
|
||||||
|
const hasDependencyCue = /(?:\u0437\u0430\u0432\u0438\u0441\u0438\u043c|\u0437\u0430\u0432\u044f\u0437\u0430\u043d|\u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446|\u043a\u043b\u044e\u0447\u0435\u0432|\u043e\u0441\u043d\u043e\u0432\u043d|\u043a\u0440\u0438\u0442\u0438\u0447|dependency|concentration|key|critical)/iu.test(normalized);
|
||||||
|
return hasSupplierOrOutgoingCue && hasDependencyCue;
|
||||||
|
}
|
||||||
function hasOrganizationLevelDebtDueDateOverviewSignal(text) {
|
function hasOrganizationLevelDebtDueDateOverviewSignal(text) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -770,7 +780,7 @@ function hasBusinessOverviewContinuationSignal(text) {
|
||||||
}
|
}
|
||||||
const normalized = compactLower(text);
|
const normalized = compactLower(text);
|
||||||
const hasEvidenceContinuationCue = /(?:по\s+этим\s+данн|на\s+базе\s+этих\s+данн|из\s+этого|по\s+итогам|что\s+подтвержден|чего\s+не\s+хватает|что\s+пока\s+нельзя|что\s+можно\s+сказать|какой\s+вывод|итогов|резюм|вывод)/iu.test(normalized);
|
const hasEvidenceContinuationCue = /(?:по\s+этим\s+данн|на\s+базе\s+этих\s+данн|из\s+этого|по\s+итогам|что\s+подтвержден|чего\s+не\s+хватает|что\s+пока\s+нельзя|что\s+можно\s+сказать|какой\s+вывод|итогов|резюм|вывод)/iu.test(normalized);
|
||||||
const hasAnalystContinuationCue = /(?:можно\s+ли|если\s+нет|proxy|прокси|аудит|оцен|что\s+думаешь|нормальн\p{L}*\s+прибыл|прибыл|марж|рентаб|ликвидн|просроч|качество\s+долг|риск|налогов\p{L}*\s+вывод)/iu.test(normalized);
|
const hasAnalystContinuationCue = /(?:можно\s+ли|если\s+нет|proxy|прокси|аудит|оцен|что\s+думаешь|нормальн\p{L}*\s+прибыл|прибыл|марж|рентаб|ликвидн|просроч|качество\s+долг|риск|зависим|завязан|концентрац|налогов\p{L}*\s+вывод)/iu.test(normalized);
|
||||||
const hasTaxContinuationCue = /(?:ндс|vat)[\s\S]{0,120}(?:позици|период|основан|не\s+хватает|налогов\p{L}*\s+вывод)|(?:позици|налогов\p{L}*\s+вывод)[\s\S]{0,80}(?:ндс|vat)/iu.test(normalized);
|
const hasTaxContinuationCue = /(?:ндс|vat)[\s\S]{0,120}(?:позици|период|основан|не\s+хватает|налогов\p{L}*\s+вывод)|(?:позици|налогов\p{L}*\s+вывод)[\s\S]{0,80}(?:ндс|vat)/iu.test(normalized);
|
||||||
const hasFinalSummaryCue = /(?:\u0447\u0442\u043e\s+\u043c\u044b\s+\u0437\u043d\u0430\u0435\u043c|\u0447\u0442\u043e\s+\u043f\u043e\u043d\u044f\u0442\u043d\u043e|\u0447\u0442\u043e\s+\u043f\u0440\u043e\u0432\u0435\u0440\w*\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u0441\u043b\u0435\u0434\u0443\u044e\u0449\w*\s+\u0448\u0430\u0433|\u0438\u0442\u043e\u0433\w*\s+\u0432\u044b\u0432\u043e\u0434|\u043a\u0430\u043a\u043e\u0439\s+\u0432\u044b\u0432\u043e\u0434|\u0447\u0442\u043e\s+\u0441\s+\u044d\u0442\u0438\u043c\s+\u0434\u0435\u043b\u0430\u0442\u044c|what\s+do\s+we\s+know|what\s+is\s+missing|next\s+step|final\s+summary)/iu.test(normalized);
|
const hasFinalSummaryCue = /(?:\u0447\u0442\u043e\s+\u043c\u044b\s+\u0437\u043d\u0430\u0435\u043c|\u0447\u0442\u043e\s+\u043f\u043e\u043d\u044f\u0442\u043d\u043e|\u0447\u0442\u043e\s+\u043f\u0440\u043e\u0432\u0435\u0440\w*\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u0441\u043b\u0435\u0434\u0443\u044e\u0449\w*\s+\u0448\u0430\u0433|\u0438\u0442\u043e\u0433\w*\s+\u0432\u044b\u0432\u043e\u0434|\u043a\u0430\u043a\u043e\u0439\s+\u0432\u044b\u0432\u043e\u0434|\u0447\u0442\u043e\s+\u0441\s+\u044d\u0442\u0438\u043c\s+\u0434\u0435\u043b\u0430\u0442\u044c|what\s+do\s+we\s+know|what\s+is\s+missing|next\s+step|final\s+summary)/iu.test(normalized);
|
||||||
const hasMoneyBreakdownCue = /(?:\u0440\u0430\u0441\u043a\u0440\u043e\p{L}*\s+\u0434\u0435\u043d\p{L}*|\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+\u0432\u0441\u0435\u0433\u043e\s+\u043f\u043e\u043b\u0443\u0447|\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:\u0432\u0441\u0435\u0433\u043e\s+)?\u0437\u0430\u043f\u043b\u0430\u0442|\u0447\u0438\u0441\u0442\p{L}*\s+\u0434\u0435\u043d\u0435\u0436\u043d\p{L}*\s+\u043f\u043e\u0442\u043e\u043a|\u0433\u043b\u0430\u0432\u043d\p{L}*\s+(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a)|top\s+(?:customer|supplier)|cash\s+breakdown)/iu.test(normalized) &&
|
const hasMoneyBreakdownCue = /(?:\u0440\u0430\u0441\u043a\u0440\u043e\p{L}*\s+\u0434\u0435\u043d\p{L}*|\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+\u0432\u0441\u0435\u0433\u043e\s+\u043f\u043e\u043b\u0443\u0447|\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:\u0432\u0441\u0435\u0433\u043e\s+)?\u0437\u0430\u043f\u043b\u0430\u0442|\u0447\u0438\u0441\u0442\p{L}*\s+\u0434\u0435\u043d\u0435\u0436\u043d\p{L}*\s+\u043f\u043e\u0442\u043e\u043a|\u0433\u043b\u0430\u0432\u043d\p{L}*\s+(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a)|top\s+(?:customer|supplier)|cash\s+breakdown)/iu.test(normalized) &&
|
||||||
|
|
@ -1338,7 +1348,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
const debtDueDateBusinessOverviewSignal = ((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
const debtDueDateBusinessOverviewSignal = ((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
||||||
hasOrganizationLevelDebtDueDateOverviewSignal(rawText)) ||
|
hasOrganizationLevelDebtDueDateOverviewSignal(rawText)) ||
|
||||||
debtDueDateFollowupBusinessOverviewSignal;
|
debtDueDateFollowupBusinessOverviewSignal;
|
||||||
const supplierQualityBusinessOverviewSignal = (rawBusinessOverviewSignal || seededBusinessOverviewSignal) && hasOrganizationLevelSupplierQualityOverviewSignal(rawText);
|
const supplierQualityBusinessOverviewSignal = ((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
||||||
|
(hasOrganizationLevelSupplierQualityOverviewSignal(rawText) || hasSupplierDependencyFollowupSignal(rawText))) ||
|
||||||
|
(businessOverviewContinuationSignal && hasSupplierDependencyFollowupSignal(rawText));
|
||||||
const profitMarginFollowupBusinessOverviewSignal = businessOverviewContinuationSignal && hasProfitMarginBoundaryFollowupSignal(rawText);
|
const profitMarginFollowupBusinessOverviewSignal = businessOverviewContinuationSignal && hasProfitMarginBoundaryFollowupSignal(rawText);
|
||||||
const profitMarginBusinessOverviewSignal = ((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
const profitMarginBusinessOverviewSignal = ((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
||||||
hasOrganizationLevelProfitMarginBoundaryOverviewSignal(rawText)) ||
|
hasOrganizationLevelProfitMarginBoundaryOverviewSignal(rawText)) ||
|
||||||
|
|
@ -1823,11 +1835,13 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
? null
|
? null
|
||||||
: explicitCurrentCounterpartyOverridesFollowupEntity
|
: explicitCurrentCounterpartyOverridesFollowupEntity
|
||||||
? null
|
? null
|
||||||
: rawMetadataScopeHint ??
|
: businessOverviewSuppressesFollowupCounterparty
|
||||||
followupSeed.metadataScopeHint ??
|
? rawMetadataScopeHint
|
||||||
followupSeed.discoveryEntity ??
|
: rawMetadataScopeHint ??
|
||||||
followupSeed.metadataSelectedEntitySet ??
|
followupSeed.metadataScopeHint ??
|
||||||
null;
|
followupSeed.discoveryEntity ??
|
||||||
|
followupSeed.metadataSelectedEntitySet ??
|
||||||
|
null;
|
||||||
const metadataScopedLaneWithoutSubject = Boolean((metadataGroundedMovementLaneApplicable ||
|
const metadataScopedLaneWithoutSubject = Boolean((metadataGroundedMovementLaneApplicable ||
|
||||||
metadataGroundedDocumentLaneApplicable ||
|
metadataGroundedDocumentLaneApplicable ||
|
||||||
explicitVatMovementEvidenceSignal) &&
|
explicitVatMovementEvidenceSignal) &&
|
||||||
|
|
@ -2013,7 +2027,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
/^\d{4}$/.test(rawDateScope) &&
|
/^\d{4}$/.test(rawDateScope) &&
|
||||||
normalizedPredecomposeDateScope &&
|
normalizedPredecomposeDateScope &&
|
||||||
normalizedPredecomposeDateScope.startsWith(`${rawDateScope}-`));
|
normalizedPredecomposeDateScope.startsWith(`${rawDateScope}-`));
|
||||||
const followupAllTimeScopeApplied = normalizedFollowupDateScope === "all_time_scope";
|
const followupAllTimeScopeApplied = normalizedFollowupDateScope === "all_time_scope" && !currentTurnCarriesExplicitPeriod;
|
||||||
const explicitDateScope = rawAllTimeScopeSignal || followupAllTimeScopeApplied
|
const explicitDateScope = rawAllTimeScopeSignal || followupAllTimeScopeApplied
|
||||||
? null
|
? null
|
||||||
: normalizedAssistantTurnMeaningDateScope ??
|
: normalizedAssistantTurnMeaningDateScope ??
|
||||||
|
|
@ -2386,6 +2400,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
if (debtDueDateFollowupBusinessOverviewSignal) {
|
if (debtDueDateFollowupBusinessOverviewSignal) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_business_overview_debt_due_date_followup_boundary");
|
pushReason(reasonCodes, "mcp_discovery_business_overview_debt_due_date_followup_boundary");
|
||||||
}
|
}
|
||||||
|
if (supplierQualityBusinessOverviewSignal && hasSupplierDependencyFollowupSignal(rawText)) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_business_overview_supplier_dependency_followup_boundary");
|
||||||
|
}
|
||||||
if (explicitVatSuppressesBusinessOverviewContinuation) {
|
if (explicitVatSuppressesBusinessOverviewContinuation) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_suppressed_by_explicit_vat_question");
|
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_suppressed_by_explicit_vat_question");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2025,7 +2025,11 @@ function repairAddressMojibake(value) {
|
||||||
}
|
}
|
||||||
function sanitizeOutgoingAssistantText(value, fallback = "Не смог сформировать читаемый ответ. Уточните запрос.") {
|
function sanitizeOutgoingAssistantText(value, fallback = "Не смог сформировать читаемый ответ. Уточните запрос.") {
|
||||||
const repaired = repairAddressMojibake(String(value ?? ""));
|
const repaired = repairAddressMojibake(String(value ?? ""));
|
||||||
const sanitized = String((0, answerComposer_1.sanitizeAssistantReplyForUserFacing)(repaired) ?? "").trim();
|
const sanitized = String((0, answerComposer_1.sanitizeAssistantReplyForUserFacing)(repaired) ?? "")
|
||||||
|
.split(/\r?\n/g)
|
||||||
|
.map((line) => line.replace(/^Коротко:\s*/u, "").replace(/(:\s*)Коротко:\s*/u, "$1"))
|
||||||
|
.join("\n")
|
||||||
|
.trim();
|
||||||
if (sanitized) {
|
if (sanitized) {
|
||||||
return sanitized;
|
return sanitized;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -182,7 +182,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
}
|
}
|
||||||
return `${match[3]}-${match[2]}-${match[1]}`;
|
return `${match[3]}-${match[2]}-${match[1]}`;
|
||||||
}
|
}
|
||||||
function computeMonthWindowFromIso(isoDate) {
|
function computeMonthWindowFromIso(isoDate, bridgeFacts = {}) {
|
||||||
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -199,10 +199,15 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
purchase_date: isoDate,
|
purchase_date: isoDate,
|
||||||
period_from: `${year}-${mm}-01`,
|
period_from: `${year}-${mm}-01`,
|
||||||
period_to: `${year}-${mm}-${dd}`,
|
period_to: `${year}-${mm}-${dd}`,
|
||||||
as_of_date: `${year}-${mm}-${dd}`
|
as_of_date: `${year}-${mm}-${dd}`,
|
||||||
|
purchase_date_bridge_selected: isoDate,
|
||||||
|
...bridgeFacts
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function extractEarliestDmyDateFromEntityRefs(entityRefs) {
|
function uniqueSortedIsoDates(dates) {
|
||||||
|
return Array.from(new Set(dates.filter(Boolean))).sort();
|
||||||
|
}
|
||||||
|
function extractDmyDatesFromEntityRefs(entityRefs) {
|
||||||
const dates = [];
|
const dates = [];
|
||||||
for (const entityRef of Array.isArray(entityRefs) ? entityRefs : []) {
|
for (const entityRef of Array.isArray(entityRefs) ? entityRefs : []) {
|
||||||
const value = deps.toNonEmptyString(entityRef?.value);
|
const value = deps.toNonEmptyString(entityRef?.value);
|
||||||
|
|
@ -220,32 +225,80 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (dates.length === 0) {
|
return uniqueSortedIsoDates(dates);
|
||||||
|
}
|
||||||
|
function detectPurchaseDateBridgeBasis(userMessage, alternateMessage) {
|
||||||
|
const text = [userMessage, alternateMessage]
|
||||||
|
.map((item) => deps.compactWhitespace(deps.repairAddressMojibake(String(item ?? "")).toLowerCase()).replace(/ё/g, "е"))
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
if (/(?:последн|latest|last|сам[а-я\s-]*поздн)/iu.test(text)) {
|
||||||
|
return "last_confirmed_purchase";
|
||||||
|
}
|
||||||
|
if (/(?:перв|earliest|oldest|ранн|сам[а-я\s-]*стар)/iu.test(text)) {
|
||||||
|
return "first_confirmed_purchase";
|
||||||
|
}
|
||||||
|
return "first_confirmed_purchase_default";
|
||||||
|
}
|
||||||
|
function extractPurchaseDateBridgeFacts(previousAddressItem, addressNavigationState, userMessage, alternateMessage) {
|
||||||
|
const previousFilters = (0, assistantContinuityPolicy_1.readAddressDebugFilters)(previousAddressItem?.debug ?? null);
|
||||||
|
let firstIsoDate = deps.toNonEmptyString(previousFilters?.purchase_date_bridge_first);
|
||||||
|
let lastIsoDate = deps.toNonEmptyString(previousFilters?.purchase_date_bridge_last);
|
||||||
|
const selectedIsoDateFromFilters = deps.toNonEmptyString(previousFilters?.purchase_date_bridge_selected);
|
||||||
|
if (!firstIsoDate && selectedIsoDateFromFilters) {
|
||||||
|
firstIsoDate = selectedIsoDateFromFilters;
|
||||||
|
}
|
||||||
|
if (!lastIsoDate && selectedIsoDateFromFilters) {
|
||||||
|
lastIsoDate = selectedIsoDateFromFilters;
|
||||||
|
}
|
||||||
|
const replyText = deps.toNonEmptyString(previousAddressItem?.text);
|
||||||
|
if (replyText && (!firstIsoDate || !lastIsoDate)) {
|
||||||
|
const repairedText = deps.repairAddressMojibake(replyText);
|
||||||
|
const explicitFirstDateMatch = repairedText.match(/первая\s+(?:найденная|подтвержденная)\s+дата\s+закупки:\s*(\d{2}\.\d{2}\.\d{4})/iu);
|
||||||
|
const explicitLastDateMatch = repairedText.match(/последняя\s+(?:найденная|подтвержденная)\s+дата\s+закупки:\s*(\d{2}\.\d{2}\.\d{4})/iu);
|
||||||
|
firstIsoDate = explicitFirstDateMatch ? parseDmyDateToIso(explicitFirstDateMatch[1]) : null;
|
||||||
|
lastIsoDate = explicitLastDateMatch ? parseDmyDateToIso(explicitLastDateMatch[1]) : null;
|
||||||
|
}
|
||||||
|
if (!firstIsoDate || !lastIsoDate) {
|
||||||
|
const navigationSessionState = (0, assistantContinuityPolicy_1.resolveNavigationSessionContextState)(addressNavigationState, deps.toNonEmptyString, deps.normalizeOrganizationScopeValue);
|
||||||
|
const focusObject = navigationSessionState.focusObject;
|
||||||
|
const preferredResultSetId = deps.toNonEmptyString(focusObject?.provenanceResultSetId) ?? navigationSessionState.activeResultSetId;
|
||||||
|
const resultSets = Array.isArray(addressNavigationState?.result_sets) ? addressNavigationState.result_sets : [];
|
||||||
|
const preferredResultSet = (preferredResultSetId
|
||||||
|
? resultSets.find((item) => deps.toNonEmptyString(item?.result_set_id) === preferredResultSetId) ?? null
|
||||||
|
: null) ??
|
||||||
|
resultSets.find((item) => deps.toNonEmptyString(item?.intent) === "inventory_purchase_provenance_for_item") ??
|
||||||
|
null;
|
||||||
|
const datesFromResultSet = extractDmyDatesFromEntityRefs(preferredResultSet?.entity_refs);
|
||||||
|
firstIsoDate = firstIsoDate ?? datesFromResultSet[0] ?? null;
|
||||||
|
lastIsoDate = lastIsoDate ?? datesFromResultSet[datesFromResultSet.length - 1] ?? null;
|
||||||
|
}
|
||||||
|
if (!firstIsoDate && !lastIsoDate) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return dates.sort()[0] ?? null;
|
const normalizedFirst = firstIsoDate ?? lastIsoDate;
|
||||||
|
const normalizedLast = lastIsoDate ?? firstIsoDate;
|
||||||
|
const basis = detectPurchaseDateBridgeBasis(userMessage, alternateMessage);
|
||||||
|
const selectedIsoDate = basis === "last_confirmed_purchase" ? normalizedLast : normalizedFirst;
|
||||||
|
return {
|
||||||
|
selectedIsoDate,
|
||||||
|
firstIsoDate: normalizedFirst,
|
||||||
|
lastIsoDate: normalizedLast,
|
||||||
|
basis,
|
||||||
|
hasMultiplePurchaseDates: Boolean(normalizedFirst && normalizedLast && normalizedFirst !== normalizedLast)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
function extractPurchaseDateBridgeWindow(previousAddressItem, addressNavigationState) {
|
function extractPurchaseDateBridgeWindow(previousAddressItem, addressNavigationState, userMessage, alternateMessage) {
|
||||||
const replyText = deps.toNonEmptyString(previousAddressItem?.text);
|
const facts = extractPurchaseDateBridgeFacts(previousAddressItem, addressNavigationState, userMessage, alternateMessage);
|
||||||
if (replyText) {
|
if (!facts?.selectedIsoDate) {
|
||||||
const repairedText = deps.repairAddressMojibake(replyText);
|
return null;
|
||||||
const explicitFirstDateMatch = repairedText.match(/первая\s+найденная\s+дата\s+закупки:\s*(\d{2}\.\d{2}\.\d{4})/iu);
|
|
||||||
const explicitFirstDateIso = explicitFirstDateMatch ? parseDmyDateToIso(explicitFirstDateMatch[1]) : null;
|
|
||||||
if (explicitFirstDateIso) {
|
|
||||||
return computeMonthWindowFromIso(explicitFirstDateIso);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const navigationSessionState = (0, assistantContinuityPolicy_1.resolveNavigationSessionContextState)(addressNavigationState, deps.toNonEmptyString, deps.normalizeOrganizationScopeValue);
|
return computeMonthWindowFromIso(facts.selectedIsoDate, {
|
||||||
const focusObject = navigationSessionState.focusObject;
|
purchase_date_bridge_first: facts.firstIsoDate,
|
||||||
const preferredResultSetId = deps.toNonEmptyString(focusObject?.provenanceResultSetId) ?? navigationSessionState.activeResultSetId;
|
purchase_date_bridge_last: facts.lastIsoDate,
|
||||||
const resultSets = Array.isArray(addressNavigationState?.result_sets) ? addressNavigationState.result_sets : [];
|
purchase_date_bridge_basis: facts.basis,
|
||||||
const preferredResultSet = (preferredResultSetId
|
purchase_date_bridge_has_multiple: facts.hasMultiplePurchaseDates
|
||||||
? resultSets.find((item) => deps.toNonEmptyString(item?.result_set_id) === preferredResultSetId) ?? null
|
});
|
||||||
: null) ??
|
|
||||||
resultSets.find((item) => deps.toNonEmptyString(item?.intent) === "inventory_purchase_provenance_for_item") ??
|
|
||||||
null;
|
|
||||||
const earliestIsoDate = extractEarliestDmyDateFromEntityRefs(preferredResultSet?.entity_refs);
|
|
||||||
return earliestIsoDate ? computeMonthWindowFromIso(earliestIsoDate) : null;
|
|
||||||
}
|
}
|
||||||
function isUsableFollowupSourceDebug(debug) {
|
function isUsableFollowupSourceDebug(debug) {
|
||||||
if (!debug || typeof debug !== "object") {
|
if (!debug || typeof debug !== "object") {
|
||||||
|
|
@ -460,10 +513,12 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
function hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasInventoryItemFocusHint) {
|
function hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasInventoryItemFocusHint, sourceHasPurchaseDateBridgeHint = false) {
|
||||||
|
const vatPurchaseBridgeContinuation = sourceIntentHint === "vat_liability_confirmed_for_tax_period" && sourceHasPurchaseDateBridgeHint;
|
||||||
if (sourceIntentHint !== "inventory_purchase_provenance_for_item" &&
|
if (sourceIntentHint !== "inventory_purchase_provenance_for_item" &&
|
||||||
!hasInventoryItemFocusHint &&
|
!hasInventoryItemFocusHint &&
|
||||||
!deps.isInventorySelectedObjectIntent(sourceIntentHint)) {
|
!deps.isInventorySelectedObjectIntent(sourceIntentHint) &&
|
||||||
|
!vatPurchaseBridgeContinuation) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const samples = [userMessage, alternateMessage]
|
const samples = [userMessage, alternateMessage]
|
||||||
|
|
@ -472,8 +527,11 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
if (samples.length === 0) {
|
if (samples.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return samples.some((sample) => /(?:ндс|vat)/iu.test(sample) &&
|
return samples.some((sample) => ((/(?:ндс|vat)/iu.test(sample) &&
|
||||||
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/iu.test(sample));
|
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/iu.test(sample)) ||
|
||||||
|
((vatPurchaseBridgeContinuation || sourceIntentHint === "vat_liability_confirmed_for_tax_period") &&
|
||||||
|
/(?:перв|последн|earliest|oldest|latest|last)/iu.test(sample) &&
|
||||||
|
/(?:покупк|закупк|purchase)/iu.test(sample))));
|
||||||
}
|
}
|
||||||
function hasInventoryRootRestatementLikeSignal(userMessage, sourceIntentHint, hasInventoryRootFrame) {
|
function hasInventoryRootRestatementLikeSignal(userMessage, sourceIntentHint, hasInventoryRootFrame) {
|
||||||
if (!hasInventoryRootFrame) {
|
if (!hasInventoryRootFrame) {
|
||||||
|
|
@ -701,12 +759,17 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
const hasBusinessOverviewCarryoverSourceHint = sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
|
const hasBusinessOverviewCarryoverSourceHint = sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
|
||||||
const navigationSessionState = earlyNavigationSessionState;
|
const navigationSessionState = earlyNavigationSessionState;
|
||||||
const navigationFocusObjectHint = navigationSessionState.focusObject;
|
const navigationFocusObjectHint = navigationSessionState.focusObject;
|
||||||
|
const sourceFiltersHint = (0, assistantContinuityPolicy_1.readAddressDebugFilters)(carryoverSourceDebug);
|
||||||
|
const sourceHasPurchaseDateBridgeHint = Boolean(deps.toNonEmptyString(sourceFiltersHint?.purchase_date_bridge_selected));
|
||||||
const hasNavigationInventoryItemFocusHint = Boolean(deps.toNonEmptyString(navigationFocusObjectHint?.label) &&
|
const hasNavigationInventoryItemFocusHint = Boolean(deps.toNonEmptyString(navigationFocusObjectHint?.label) &&
|
||||||
deps.toNonEmptyString(navigationFocusObjectHint?.objectType) === "item" &&
|
deps.toNonEmptyString(navigationFocusObjectHint?.objectType) === "item" &&
|
||||||
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||||
|
sourceIntentHint === "vat_liability_confirmed_for_tax_period" ||
|
||||||
|
sourceIntentHint === "vat_payable_forecast" ||
|
||||||
|
sourceIntentHint === "vat_payable_confirmed_as_of_date" ||
|
||||||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
deps.isInventorySelectedObjectIntent(sourceIntentHint)));
|
deps.isInventorySelectedObjectIntent(sourceIntentHint)));
|
||||||
const inventoryPurchaseDateVatBridge = hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasNavigationInventoryItemFocusHint);
|
const inventoryPurchaseDateVatBridge = hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasNavigationInventoryItemFocusHint, sourceHasPurchaseDateBridgeHint);
|
||||||
const inventoryMarginRankingFollowup = hasInventoryMarginRankingFollowupSignal(userMessage, alternateMessage, sourceIntentHint);
|
const inventoryMarginRankingFollowup = hasInventoryMarginRankingFollowupSignal(userMessage, alternateMessage, sourceIntentHint);
|
||||||
let inventoryShortFollowupPrimary = (deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
let inventoryShortFollowupPrimary = (deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
||||||
deps.hasShortInventoryObjectFollowupSignal(userMessage);
|
deps.hasShortInventoryObjectFollowupSignal(userMessage);
|
||||||
|
|
@ -1092,10 +1155,16 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
: findRecentInventoryPurchaseProvenanceItem(items, deps.toNonEmptyString(navigationFocusObjectLabel) ??
|
: findRecentInventoryPurchaseProvenanceItem(items, deps.toNonEmptyString(navigationFocusObjectLabel) ??
|
||||||
(0, assistantContinuityPolicy_1.readAddressDebugItem)(carryoverSourceDebug, deps.toNonEmptyString) ??
|
(0, assistantContinuityPolicy_1.readAddressDebugItem)(carryoverSourceDebug, deps.toNonEmptyString) ??
|
||||||
deps.toNonEmptyString(previousFilters.item)) ?? previousAddressItem;
|
deps.toNonEmptyString(previousFilters.item)) ?? previousAddressItem;
|
||||||
const purchaseBridgeWindow = extractPurchaseDateBridgeWindow(purchaseBridgeItem, addressNavigationState);
|
const purchaseBridgeWindow = extractPurchaseDateBridgeWindow(purchaseBridgeItem, addressNavigationState, userMessage, alternateMessage);
|
||||||
if (purchaseBridgeWindow) {
|
if (purchaseBridgeWindow) {
|
||||||
previousFilters.period_from = purchaseBridgeWindow.period_from;
|
previousFilters.period_from = purchaseBridgeWindow.period_from;
|
||||||
previousFilters.period_to = purchaseBridgeWindow.period_to;
|
previousFilters.period_to = purchaseBridgeWindow.period_to;
|
||||||
|
previousFilters.as_of_date = purchaseBridgeWindow.as_of_date;
|
||||||
|
previousFilters.purchase_date_bridge_selected = purchaseBridgeWindow.purchase_date_bridge_selected;
|
||||||
|
previousFilters.purchase_date_bridge_first = purchaseBridgeWindow.purchase_date_bridge_first;
|
||||||
|
previousFilters.purchase_date_bridge_last = purchaseBridgeWindow.purchase_date_bridge_last;
|
||||||
|
previousFilters.purchase_date_bridge_basis = purchaseBridgeWindow.purchase_date_bridge_basis;
|
||||||
|
previousFilters.purchase_date_bridge_has_multiple = purchaseBridgeWindow.purchase_date_bridge_has_multiple;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
previousFilters = (0, assistantContinuityPolicy_1.applyTemporalCarryoverFilters)(previousFilters, navigationDateScope, continuityTemporalScope, sourceIntentHint, deps.toNonEmptyString);
|
previousFilters = (0, assistantContinuityPolicy_1.applyTemporalCarryoverFilters)(previousFilters, navigationDateScope, continuityTemporalScope, sourceIntentHint, deps.toNonEmptyString);
|
||||||
|
|
|
||||||
|
|
@ -309,6 +309,17 @@ function detectBroadBusinessEvaluation(text) {
|
||||||
family: "broad_business_evaluation"
|
family: "broad_business_evaluation"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (/(?:что\s+(?:дальше\s+)?провер\w*|что\s+провер\w*\s+дальше|следующ\w*\s+шаг|в\s+первую\s+очередь\s+провер\w*|next\s+checks?|next\s+steps?)/iu.test(normalized)) {
|
||||||
|
return {
|
||||||
|
family: "broad_business_evaluation"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (/(?:поставщик|закуп|исходящ|плат[её]ж|supplier|vendor|procurement|outgoing|payout)/iu.test(normalized) &&
|
||||||
|
/(?:зависим|завязан|концентрац|ключев|основн|критич|dependency|concentration|key|critical)/iu.test(normalized)) {
|
||||||
|
return {
|
||||||
|
family: "broad_business_evaluation"
|
||||||
|
};
|
||||||
|
}
|
||||||
if (hasOrganizationLevelEarningsOverviewSignal(normalized)) {
|
if (hasOrganizationLevelEarningsOverviewSignal(normalized)) {
|
||||||
return {
|
return {
|
||||||
family: "broad_business_evaluation"
|
family: "broad_business_evaluation"
|
||||||
|
|
|
||||||
|
|
@ -2777,106 +2777,11 @@ export function buildAutoRunsRouter(services: AppServices, openaiClient = new Op
|
||||||
|
|
||||||
router.post("/api/autoruns/autogen/generate", async (req, res, next) => {
|
router.post("/api/autoruns/autogen/generate", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const body = toRecord(req.body);
|
throw new ApiError(
|
||||||
if (!body) {
|
"AUTOGEN_QUESTION_GENERATION_DISABLED",
|
||||||
throw new ApiError("INVALID_AUTOGEN_PAYLOAD", "JSON body is required", 400);
|
"Автогенерация вопросов отключена: используйте сохранение живых пользовательских сессий.",
|
||||||
}
|
410
|
||||||
const mode = parseAutoGenMode(body.mode);
|
|
||||||
const count = parseAutogenCount(body.count);
|
|
||||||
const domain = parseAutogenDomain(body.domain);
|
|
||||||
const persistCaseSet = toBooleanSafe(body.persist_to_eval_cases) ?? true;
|
|
||||||
const generatedBy = parseAnnotationAuthor(body.generated_by);
|
|
||||||
const context = toRecord(body.context);
|
|
||||||
const llmConfig = parseAutogenLlmRuntimeConfig(body, context);
|
|
||||||
const personalityPrompt = toStringSafe(context?.autogen_personality_prompt);
|
|
||||||
|
|
||||||
if (mode === "saved_user_sessions") {
|
|
||||||
throw new ApiError(
|
|
||||||
"AUTOGEN_MODE_NOT_SUPPORTED",
|
|
||||||
"Use `/api/autoruns/autogen/save-assistant-session` to save user sessions.",
|
|
||||||
400
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let questions: string[] = [];
|
|
||||||
if (mode === "qwen_seed") {
|
|
||||||
if (!llmConfig) {
|
|
||||||
throw new ApiError(
|
|
||||||
"AUTOGEN_LLM_CONFIG_REQUIRED",
|
|
||||||
"Для режима qwen_seed нужен активный LLM-контур (provider/model/baseUrl) из настроек подключения.",
|
|
||||||
400
|
|
||||||
);
|
|
||||||
}
|
|
||||||
questions = await generateQwenSeedQuestionsLive({
|
|
||||||
count,
|
|
||||||
domain,
|
|
||||||
personalityPrompt,
|
|
||||||
llmConfig,
|
|
||||||
client: openaiClient
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
questions = generateCodexCreativeQuestions(count, domain);
|
|
||||||
}
|
|
||||||
questions = Array.from(new Set(questions.map((item) => sanitizeGeneratedQuestion(item)).filter((item) => item.length > 0))).slice(
|
|
||||||
0,
|
|
||||||
count
|
|
||||||
);
|
);
|
||||||
const generationId = generateAutogenId();
|
|
||||||
|
|
||||||
let savedCaseSetFile: string | null = null;
|
|
||||||
if (persistCaseSet) {
|
|
||||||
if (!fs.existsSync(EVAL_CASES_DIR)) {
|
|
||||||
fs.mkdirSync(EVAL_CASES_DIR, { recursive: true });
|
|
||||||
}
|
|
||||||
const fileName = buildAutogenCaseSetFileName(mode, generationId);
|
|
||||||
const filePath = path.resolve(EVAL_CASES_DIR, fileName);
|
|
||||||
const payload = buildAutogenCaseSetPayload({
|
|
||||||
generationId,
|
|
||||||
mode,
|
|
||||||
domain,
|
|
||||||
questions
|
|
||||||
});
|
|
||||||
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), "utf-8");
|
|
||||||
savedCaseSetFile = fileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
const record: AutoGenHistoryRecord = {
|
|
||||||
generation_id: generationId,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
mode,
|
|
||||||
title: null,
|
|
||||||
count: questions.length,
|
|
||||||
domain,
|
|
||||||
questions,
|
|
||||||
generated_by: generatedBy,
|
|
||||||
saved_case_set_file: savedCaseSetFile,
|
|
||||||
context: context
|
|
||||||
? {
|
|
||||||
llm_provider: toStringSafe(context.llm_provider),
|
|
||||||
model: toStringSafe(context.model),
|
|
||||||
assistant_prompt_version: toStringSafe(context.assistant_prompt_version),
|
|
||||||
decomposition_prompt_version: toStringSafe(context.decomposition_prompt_version),
|
|
||||||
prompt_fingerprint: toStringSafe(context.prompt_fingerprint)
|
|
||||||
? repairAutogenMojibake(String(context.prompt_fingerprint))
|
|
||||||
: null,
|
|
||||||
autogen_personality_id: toStringSafe(context.autogen_personality_id),
|
|
||||||
autogen_personality_prompt: toStringSafe(context.autogen_personality_prompt)
|
|
||||||
? repairAutogenMojibake(String(context.autogen_personality_prompt))
|
|
||||||
: null,
|
|
||||||
source_session_id: null,
|
|
||||||
saved_session_file: null,
|
|
||||||
saved_case_set_kind: "single_turn_list"
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
};
|
|
||||||
const history = readAutoGenHistory();
|
|
||||||
history.unshift(record);
|
|
||||||
writeAutoGenHistory(history.slice(0, 500));
|
|
||||||
|
|
||||||
ok(res, {
|
|
||||||
ok: true,
|
|
||||||
generation: record
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2558,6 +2558,17 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
/(?:поставщик|закуп|исходящ|плат[её]ж|supplier|vendor|procurement|outgoing|payout)/iu.test(normalized) &&
|
||||||
|
/(?:зависим|завязан|концентрац|ключев|основн|критич|dependency|concentration|key|critical)/iu.test(normalized)
|
||||||
|
) {
|
||||||
|
return unicodeBridgeResolution(
|
||||||
|
"unknown",
|
||||||
|
"high",
|
||||||
|
"unicode_business_overview_supplier_quality_deferred_to_discovery"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (hasDocumentCue && (hasCounterpartyCue || hasLooseCounterpartyByAnchor) && !hasContractCue) {
|
if (hasDocumentCue && (hasCounterpartyCue || hasLooseCounterpartyByAnchor) && !hasContractCue) {
|
||||||
return unicodeBridgeResolution(
|
return unicodeBridgeResolution(
|
||||||
"list_documents_by_counterparty",
|
"list_documents_by_counterparty",
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,7 @@ interface AddressTryHandleOptions {
|
||||||
llmSemanticHints?: AddressLlmSemanticHints | null;
|
llmSemanticHints?: AddressLlmSemanticHints | null;
|
||||||
activeOrganization?: string | null;
|
activeOrganization?: string | null;
|
||||||
knownOrganizations?: string[];
|
knownOrganizations?: string[];
|
||||||
|
rawUserMessage?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AddressCapabilityAudit {
|
interface AddressCapabilityAudit {
|
||||||
|
|
@ -355,6 +356,23 @@ function deriveTaxQuarterWindowForDate(value: unknown): { period_from: string; p
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isFullCalendarMonthWindow(periodFrom: unknown, periodTo: unknown): boolean {
|
||||||
|
const fromIso = normalizeIsoDateForQuery(periodFrom);
|
||||||
|
const toIso = normalizeIsoDateForQuery(periodTo);
|
||||||
|
if (!fromIso || !toIso) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const fromMatch = fromIso.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
const toMatch = toIso.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
if (!fromMatch || !toMatch || fromMatch[1] !== toMatch[1] || fromMatch[2] !== toMatch[2] || fromMatch[3] !== "01") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const year = Number(fromMatch[1]);
|
||||||
|
const month = Number(fromMatch[2]);
|
||||||
|
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
||||||
|
return toMatch[3] === String(lastDay).padStart(2, "0");
|
||||||
|
}
|
||||||
|
|
||||||
function deriveMonthWindowForDate(value: unknown): { period_from: string; period_to: string } | null {
|
function deriveMonthWindowForDate(value: unknown): { period_from: string; period_to: string } | null {
|
||||||
const isoDate = normalizeIsoDateForQuery(value);
|
const isoDate = normalizeIsoDateForQuery(value);
|
||||||
if (!isoDate) {
|
if (!isoDate) {
|
||||||
|
|
@ -3716,9 +3734,21 @@ export class AddressQueryService {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const vatTaxPeriodFromCarriedMonth =
|
||||||
|
intent.intent === "vat_liability_confirmed_for_tax_period" &&
|
||||||
|
(filters.warnings.includes("period_from_from_followup_context") ||
|
||||||
|
filters.warnings.includes("period_to_from_followup_context")) &&
|
||||||
|
isFullCalendarMonthWindow(filters.extracted_filters.period_from, filters.extracted_filters.period_to);
|
||||||
|
const vatTaxPeriodFromPurchaseBridgeMonth =
|
||||||
|
intent.intent === "vat_liability_confirmed_for_tax_period" &&
|
||||||
|
(filters.warnings.includes("period_from_purchase_date_bridge_followup_context") ||
|
||||||
|
filters.warnings.includes("period_to_purchase_date_bridge_followup_context")) &&
|
||||||
|
isFullCalendarMonthWindow(filters.extracted_filters.period_from, filters.extracted_filters.period_to);
|
||||||
if (
|
if (
|
||||||
intent.intent === "vat_liability_confirmed_for_tax_period" &&
|
intent.intent === "vat_liability_confirmed_for_tax_period" &&
|
||||||
filters.warnings.includes("period_derived_from_month_phrase")
|
(filters.warnings.includes("period_derived_from_month_phrase") ||
|
||||||
|
vatTaxPeriodFromCarriedMonth ||
|
||||||
|
vatTaxPeriodFromPurchaseBridgeMonth)
|
||||||
) {
|
) {
|
||||||
const taxQuarterWindow = deriveTaxQuarterWindowForDate(filters.extracted_filters.period_to);
|
const taxQuarterWindow = deriveTaxQuarterWindowForDate(filters.extracted_filters.period_to);
|
||||||
if (taxQuarterWindow) {
|
if (taxQuarterWindow) {
|
||||||
|
|
@ -3914,6 +3944,10 @@ export class AddressQueryService {
|
||||||
shadowRouteAudit
|
shadowRouteAudit
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const rawUserMessageForReply =
|
||||||
|
typeof options.rawUserMessage === "string" && options.rawUserMessage.trim().length > 0
|
||||||
|
? options.rawUserMessage
|
||||||
|
: undefined;
|
||||||
const composeOptionsFromFilters = (
|
const composeOptionsFromFilters = (
|
||||||
filterSet: AddressFilterSet,
|
filterSet: AddressFilterSet,
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -3922,9 +3956,14 @@ export class AddressQueryService {
|
||||||
useRubCurrency?: boolean;
|
useRubCurrency?: boolean;
|
||||||
counterpartyHint?: string;
|
counterpartyHint?: string;
|
||||||
accountHint?: string;
|
accountHint?: string;
|
||||||
|
rawUserMessage?: string | null;
|
||||||
} = {}
|
} = {}
|
||||||
) => ({
|
) => ({
|
||||||
userMessage,
|
userMessage,
|
||||||
|
rawUserMessage:
|
||||||
|
typeof options.rawUserMessage === "string" && options.rawUserMessage.trim().length > 0
|
||||||
|
? options.rawUserMessage
|
||||||
|
: rawUserMessageForReply,
|
||||||
itemHint: typeof filterSet.item === "string" ? filterSet.item : undefined,
|
itemHint: typeof filterSet.item === "string" ? filterSet.item : undefined,
|
||||||
counterpartyHint:
|
counterpartyHint:
|
||||||
typeof options.counterpartyHint === "string"
|
typeof options.counterpartyHint === "string"
|
||||||
|
|
@ -3944,6 +3983,18 @@ export class AddressQueryService {
|
||||||
asOfDate: typeof filterSet.as_of_date === "string" ? filterSet.as_of_date : undefined,
|
asOfDate: typeof filterSet.as_of_date === "string" ? filterSet.as_of_date : undefined,
|
||||||
requestedResultMode,
|
requestedResultMode,
|
||||||
vatDirectSourceProbe: options.vatDirectSourceProbe ?? undefined,
|
vatDirectSourceProbe: options.vatDirectSourceProbe ?? undefined,
|
||||||
|
purchaseDateBridge:
|
||||||
|
typeof filterSet.purchase_date_bridge_selected === "string" ||
|
||||||
|
typeof filterSet.purchase_date_bridge_first === "string" ||
|
||||||
|
typeof filterSet.purchase_date_bridge_last === "string"
|
||||||
|
? {
|
||||||
|
selectedPurchaseDate: filterSet.purchase_date_bridge_selected,
|
||||||
|
firstPurchaseDate: filterSet.purchase_date_bridge_first,
|
||||||
|
lastPurchaseDate: filterSet.purchase_date_bridge_last,
|
||||||
|
basis: filterSet.purchase_date_bridge_basis,
|
||||||
|
hasMultiplePurchaseDates: filterSet.purchase_date_bridge_has_multiple === true
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
emphasizeNumbers: options.emphasizeNumbers ?? undefined,
|
emphasizeNumbers: options.emphasizeNumbers ?? undefined,
|
||||||
useRubCurrency: options.useRubCurrency ?? undefined
|
useRubCurrency: options.useRubCurrency ?? undefined
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1520,6 +1520,91 @@ function buildInventoryMovementQuery(
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildStaticWhereClause(conditions: Array<string | null | undefined>, indent = " "): string {
|
||||||
|
const cleaned = conditions.map((condition) => String(condition ?? "").trim()).filter((condition) => condition.length > 0);
|
||||||
|
return cleaned.length > 0 ? `ГДЕ\n${indent}${cleaned.join(`\n${indent}И `)}` : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveInventoryAgingAsOfExpr(filters: AddressFilterSet): string {
|
||||||
|
const now = new Date();
|
||||||
|
return (
|
||||||
|
(typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||||
|
? toDateTimeExpr(filters.as_of_date, true)
|
||||||
|
: null) ??
|
||||||
|
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||||
|
? toDateTimeExpr(filters.period_to, true)
|
||||||
|
: null) ??
|
||||||
|
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||||
|
? toDateTimeExpr(filters.period_from, true)
|
||||||
|
: null) ??
|
||||||
|
`ДАТАВРЕМЯ(${now.getFullYear()}, ${now.getMonth() + 1}, ${now.getDate()}, 23, 59, 59)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInventoryAgingByPurchaseDateQuery(filters: AddressFilterSet, resolvedLimit: number): string {
|
||||||
|
const asOfExpr = resolveInventoryAgingAsOfExpr(filters);
|
||||||
|
const inventoryAccountPredicate = buildAccountPrefixPredicate("Остатки.Счет", ["41.01"]);
|
||||||
|
const onHandScopeConditions = [
|
||||||
|
"Остатки.КоличествоРазвернутыйОстатокДт > 0",
|
||||||
|
inventoryAccountPredicate,
|
||||||
|
buildOrganizationReferenceCondition(filters, ["Остатки.Организация"]),
|
||||||
|
buildWarehouseReferenceCondition(filters, ["Остатки.Субконто3"]),
|
||||||
|
buildInventoryItemReferenceCondition(filters, ["Остатки.Субконто1"])
|
||||||
|
];
|
||||||
|
const onHandWhereClause = buildStaticWhereClause(onHandScopeConditions);
|
||||||
|
const currentStockItemSubquery = [
|
||||||
|
"(ВЫБРАТЬ РАЗЛИЧНЫЕ",
|
||||||
|
" Остатки.Субконто1",
|
||||||
|
" ИЗ",
|
||||||
|
` РегистрБухгалтерии.Хозрасчетный.Остатки(${asOfExpr}, , , ) КАК Остатки`,
|
||||||
|
buildStaticWhereClause(onHandScopeConditions, " "),
|
||||||
|
")"
|
||||||
|
].join("\n");
|
||||||
|
const purchaseWhereClause = buildStaticWhereClause([
|
||||||
|
"Товары.Ссылка.Проведен = ИСТИНА",
|
||||||
|
`Товары.Ссылка.Дата <= ${asOfExpr}`,
|
||||||
|
buildOrganizationReferenceCondition(filters, ["Товары.Ссылка.Организация"]),
|
||||||
|
buildInventoryItemReferenceCondition(filters, ["Товары.Номенклатура"]),
|
||||||
|
`Товары.Номенклатура В ${currentStockItemSubquery}`
|
||||||
|
]);
|
||||||
|
|
||||||
|
return `
|
||||||
|
ВЫБРАТЬ ПЕРВЫЕ ${resolvedLimit}
|
||||||
|
${asOfExpr} КАК Период,
|
||||||
|
"Остатки на дату" КАК Регистратор,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетДт,
|
||||||
|
"" КАК СчетКт,
|
||||||
|
Остатки.СуммаРазвернутыйОстатокДт КАК Сумма,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК Номенклатура,
|
||||||
|
"" КАК Контрагент,
|
||||||
|
"" КАК Договор,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК Склад,
|
||||||
|
Остатки.КоличествоРазвернутыйОстатокДт КАК Количество
|
||||||
|
ИЗ
|
||||||
|
РегистрБухгалтерии.Хозрасчетный.Остатки(${asOfExpr}, , , ) КАК Остатки
|
||||||
|
${onHandWhereClause}
|
||||||
|
ОБЪЕДИНИТЬ ВСЕ
|
||||||
|
ВЫБРАТЬ ПЕРВЫЕ ${resolvedLimit}
|
||||||
|
Товары.Ссылка.Дата КАК Период,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка) КАК Регистратор,
|
||||||
|
"41.01" КАК СчетДт,
|
||||||
|
"" КАК СчетКт,
|
||||||
|
Товары.Сумма КАК Сумма,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Товары.Номенклатура) КАК Номенклатура,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Контрагент) КАК Контрагент,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.ДоговорКонтрагента) КАК Договор,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Организация) КАК Организация,
|
||||||
|
"" КАК Склад,
|
||||||
|
Товары.Количество КАК Количество
|
||||||
|
ИЗ
|
||||||
|
Документ.ПоступлениеТоваровУслуг.Товары КАК Товары
|
||||||
|
${purchaseWhereClause}
|
||||||
|
УПОРЯДОЧИТЬ ПО
|
||||||
|
Период ВОЗР
|
||||||
|
`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
function buildWarehouseReferenceCondition(filters: AddressFilterSet, fieldPaths: string[]): string | null {
|
function buildWarehouseReferenceCondition(filters: AddressFilterSet, fieldPaths: string[]): string | null {
|
||||||
const warehouse = typeof filters.warehouse === "string" ? filters.warehouse.trim() : "";
|
const warehouse = typeof filters.warehouse === "string" ? filters.warehouse.trim() : "";
|
||||||
if (!warehouse) {
|
if (!warehouse) {
|
||||||
|
|
@ -2199,7 +2284,7 @@ export function buildAddressRecipePlan(
|
||||||
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
||||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
||||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
? buildInventoryAgingByPurchaseDateQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "inventory_quality_events_profile"
|
: recipe.query_template === "inventory_quality_events_profile"
|
||||||
? buildInventoryQualityEventsQuery(filters, resolvedLimit)
|
? buildInventoryQualityEventsQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "contracts_by_counterparty_profile"
|
: recipe.query_template === "contracts_by_counterparty_profile"
|
||||||
|
|
|
||||||
|
|
@ -743,12 +743,42 @@ function needsVatPurchaseDateAnchorDisclosure(userMessage: string | null | undef
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildVatPurchaseDateAnchorDisclosureLine(
|
function vatPurchaseDateBasisLabel(basis: string | null | undefined, hasMultiplePurchaseDates: boolean): string {
|
||||||
|
if (!hasMultiplePurchaseDates) {
|
||||||
|
return "подтвержденную дату закупки";
|
||||||
|
}
|
||||||
|
if (basis === "last_confirmed_purchase") {
|
||||||
|
return "последнюю подтвержденную дату закупки";
|
||||||
|
}
|
||||||
|
return "первую подтвержденную дату закупки";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildVatPurchaseDateAnchorDisclosureLines(
|
||||||
options: ComposeFactualReplyOptions,
|
options: ComposeFactualReplyOptions,
|
||||||
periodWindowLabel: string | null
|
periodWindowLabel: string | null
|
||||||
): string | null {
|
): string[] {
|
||||||
if (!periodWindowLabel || !needsVatPurchaseDateAnchorDisclosure(options.userMessage)) {
|
const bridge = options.purchaseDateBridge ?? null;
|
||||||
return null;
|
const selectedPurchaseDate = normalizeIsoDateOnly(bridge?.selectedPurchaseDate);
|
||||||
|
const firstPurchaseDate = normalizeIsoDateOnly(bridge?.firstPurchaseDate);
|
||||||
|
const lastPurchaseDate = normalizeIsoDateOnly(bridge?.lastPurchaseDate);
|
||||||
|
const hasMultiplePurchaseDates = Boolean(
|
||||||
|
bridge?.hasMultiplePurchaseDates || (firstPurchaseDate && lastPurchaseDate && firstPurchaseDate !== lastPurchaseDate)
|
||||||
|
);
|
||||||
|
if (!periodWindowLabel || (!selectedPurchaseDate && !needsVatPurchaseDateAnchorDisclosure(options.userMessage))) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedPurchaseDate) {
|
||||||
|
const basisLabel = vatPurchaseDateBasisLabel(bridge?.basis, hasMultiplePurchaseDates);
|
||||||
|
const lines = [
|
||||||
|
`- Якорь периода: для расчета я использую ${basisLabel} ${formatDateRu(selectedPurchaseDate)}; налоговый период ${periodWindowLabel}; сам расчет ниже взят из книг продаж/покупок за это окно.`
|
||||||
|
];
|
||||||
|
if (hasMultiplePurchaseDates && firstPurchaseDate && lastPurchaseDate) {
|
||||||
|
lines.push(
|
||||||
|
`- Важно: у позиции несколько подтвержденных дат закупки (${formatDateRu(firstPurchaseDate)}..${formatDateRu(lastPurchaseDate)}); это расчет по выбранному якорю, а не единственная возможная дата.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
||||||
|
|
@ -765,10 +795,14 @@ function buildVatPurchaseDateAnchorDisclosureLine(
|
||||||
asOfTs >= fromTs &&
|
asOfTs >= fromTs &&
|
||||||
asOfTs <= toTs
|
asOfTs <= toTs
|
||||||
) {
|
) {
|
||||||
return `- Якорь периода: дата покупки ${formatDateRu(asOfDate)} попадает в налоговый период ${periodWindowLabel}; поэтому расчет ниже взят из книг продаж/покупок за это окно.`;
|
return [
|
||||||
|
`- Якорь периода: дата покупки ${formatDateRu(asOfDate)} попадает в налоговый период ${periodWindowLabel}; поэтому расчет ниже взят из книг продаж/покупок за это окно.`
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return `- Якорь периода: дата покупки из вопроса/контекста использована для выбора налогового периода ${periodWindowLabel}; сам расчет ниже взят из книг продаж/покупок за это окно.`;
|
return [
|
||||||
|
`- Якорь периода: дата покупки из вопроса/контекста использована для выбора налогового периода ${periodWindowLabel}; сам расчет ниже взят из книг продаж/покупок за это окно.`
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectRankingLimit(userMessage: string | null | undefined, fallback = 20): number {
|
function detectRankingLimit(userMessage: string | null | undefined, fallback = 20): number {
|
||||||
|
|
@ -1292,14 +1326,85 @@ function hasInventoryAccountPrefix(value: string | null | undefined, prefix: str
|
||||||
return normalized === prefix || normalized.startsWith(`${prefix}.`) || normalized.startsWith(prefix);
|
return normalized === prefix || normalized.startsWith(`${prefix}.`) || normalized.startsWith(prefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isInventoryOnHandSnapshotRow(row: ComposeStageRow): boolean {
|
||||||
|
const registrator = String(row.registrator ?? "").trim().toLowerCase();
|
||||||
|
return /(?:остатки\s+на\s+дату|stock\s*on\s*hand|balance\s+as\s+of)/iu.test(registrator);
|
||||||
|
}
|
||||||
|
|
||||||
function isInventoryPurchaseMovement(row: ComposeStageRow): boolean {
|
function isInventoryPurchaseMovement(row: ComposeStageRow): boolean {
|
||||||
return hasInventoryAccountPrefix(row.account_dt, "41.01");
|
return hasInventoryAccountPrefix(row.account_dt, "41.01") && !isInventoryOnHandSnapshotRow(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isInventorySaleMovement(row: ComposeStageRow): boolean {
|
function isInventorySaleMovement(row: ComposeStageRow): boolean {
|
||||||
return hasInventoryAccountPrefix(row.account_kt, "41.01");
|
return hasInventoryAccountPrefix(row.account_kt, "41.01");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface InventoryOnHandAgingScope {
|
||||||
|
itemKeys: Set<string>;
|
||||||
|
itemOrgKeys: Set<string>;
|
||||||
|
fullKeys: Set<string>;
|
||||||
|
orgKeys: Set<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inventoryAgingKeyPart(value: string | null | undefined): string {
|
||||||
|
return normalizeEntityToken(value) ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInventoryOnHandAgingScope(rows: ComposeStageRow[]): InventoryOnHandAgingScope {
|
||||||
|
const scope: InventoryOnHandAgingScope = {
|
||||||
|
itemKeys: new Set(),
|
||||||
|
itemOrgKeys: new Set(),
|
||||||
|
fullKeys: new Set(),
|
||||||
|
orgKeys: new Set()
|
||||||
|
};
|
||||||
|
for (const row of rows) {
|
||||||
|
if (!isInventoryOnHandSnapshotRow(row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const quantity = extractInventoryQuantity(row);
|
||||||
|
if (quantity === null || quantity <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const itemKey = inventoryAgingKeyPart(extractInventoryItemName(row));
|
||||||
|
if (!itemKey) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const organizationKey = inventoryAgingKeyPart(extractInventoryOrganizationName(row));
|
||||||
|
const warehouseKey = inventoryAgingKeyPart(extractInventoryWarehouseName(row));
|
||||||
|
scope.itemKeys.add(itemKey);
|
||||||
|
if (organizationKey) {
|
||||||
|
scope.orgKeys.add(organizationKey);
|
||||||
|
scope.itemOrgKeys.add(`${itemKey}|${organizationKey}`);
|
||||||
|
}
|
||||||
|
if (organizationKey || warehouseKey) {
|
||||||
|
scope.fullKeys.add(`${itemKey}|${warehouseKey}|${organizationKey}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inventoryPurchaseMatchesOnHandAgingScope(row: ComposeStageRow, scope: InventoryOnHandAgingScope): boolean {
|
||||||
|
if (scope.itemKeys.size === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const itemKey = inventoryAgingKeyPart(extractInventoryItemName(row));
|
||||||
|
if (!itemKey || !scope.itemKeys.has(itemKey)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const organizationKey = inventoryAgingKeyPart(extractInventoryOrganizationName(row));
|
||||||
|
const warehouseKey = inventoryAgingKeyPart(extractInventoryWarehouseName(row));
|
||||||
|
if (scope.fullKeys.has(`${itemKey}|${warehouseKey}|${organizationKey}`)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (organizationKey && scope.itemOrgKeys.has(`${itemKey}|${organizationKey}`)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (organizationKey && scope.orgKeys.size > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function looksLikeInventoryTraceDocumentToken(value: string): boolean {
|
function looksLikeInventoryTraceDocumentToken(value: string): boolean {
|
||||||
const normalized = String(value ?? "").trim();
|
const normalized = String(value ?? "").trim();
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
|
|
@ -1526,8 +1631,12 @@ function buildInventoryAgingByItemAggregate(
|
||||||
}
|
}
|
||||||
>();
|
>();
|
||||||
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
|
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
|
||||||
|
const onHandScope = buildInventoryOnHandAgingScope(rows);
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
|
if (!isInventoryPurchaseMovement(row) || !inventoryPurchaseMatchesOnHandAgingScope(row, onHandScope)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const item = extractInventoryItemName(row);
|
const item = extractInventoryItemName(row);
|
||||||
if (!item) {
|
if (!item) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -1613,19 +1722,11 @@ function formatInventoryAgingRows(items: InventoryAgingByItemAggregate[], asOfDa
|
||||||
const parts = [
|
const parts = [
|
||||||
`${index + 1}. ${item.item}`,
|
`${index + 1}. ${item.item}`,
|
||||||
`первая закупка: ${inventoryTraceDateLabel(item.firstPurchasePeriod)}`,
|
`первая закупка: ${inventoryTraceDateLabel(item.firstPurchasePeriod)}`,
|
||||||
`последняя закупка: ${inventoryTraceDateLabel(item.lastPurchasePeriod)}`,
|
`последняя закупка: ${inventoryTraceDateLabel(item.lastPurchasePeriod)}`
|
||||||
`документов: ${formatNumberWithDots(item.documentCount)}`,
|
|
||||||
`операций: ${formatNumberWithDots(item.operations)}`
|
|
||||||
];
|
];
|
||||||
if (item.ageDays !== null) {
|
if (item.ageDays !== null) {
|
||||||
parts.push(`возраст следа на ${formatDateRu(asOfDate)}: ${formatNumberWithDots(item.ageDays)} дн.`);
|
parts.push(`возраст следа на ${formatDateRu(asOfDate)}: ${formatNumberWithDots(item.ageDays)} дн.`);
|
||||||
}
|
}
|
||||||
if (item.warehouse) {
|
|
||||||
parts.push(`склад: ${item.warehouse}`);
|
|
||||||
}
|
|
||||||
if (item.organization) {
|
|
||||||
parts.push(`организация: ${item.organization}`);
|
|
||||||
}
|
|
||||||
if (item.counterparties.length > 0) {
|
if (item.counterparties.length > 0) {
|
||||||
parts.push(`поставщики: ${item.counterparties.slice(0, 3).join("; ")}`);
|
parts.push(`поставщики: ${item.counterparties.slice(0, 3).join("; ")}`);
|
||||||
}
|
}
|
||||||
|
|
@ -3934,16 +4035,30 @@ function composeFactualReplyBody(
|
||||||
const formatConfirmedMoney = (value: number): string => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
const formatConfirmedMoney = (value: number): string => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
||||||
const organizationLabel = normalizeOrganizationScopeValue(options.organizationHint);
|
const organizationLabel = normalizeOrganizationScopeValue(options.organizationHint);
|
||||||
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
|
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
|
||||||
const purchaseDateAnchorLine = buildVatPurchaseDateAnchorDisclosureLine(options, periodWindowLabel);
|
const purchaseDateAnchorLines = buildVatPurchaseDateAnchorDisclosureLines(options, periodWindowLabel);
|
||||||
|
const selectedPurchaseDate = normalizeIsoDateOnly(options.purchaseDateBridge?.selectedPurchaseDate);
|
||||||
|
const hasMultiplePurchaseDates = Boolean(
|
||||||
|
options.purchaseDateBridge?.hasMultiplePurchaseDates ||
|
||||||
|
(normalizeIsoDateOnly(options.purchaseDateBridge?.firstPurchaseDate) &&
|
||||||
|
normalizeIsoDateOnly(options.purchaseDateBridge?.lastPurchaseDate) &&
|
||||||
|
normalizeIsoDateOnly(options.purchaseDateBridge?.firstPurchaseDate) !==
|
||||||
|
normalizeIsoDateOnly(options.purchaseDateBridge?.lastPurchaseDate))
|
||||||
|
);
|
||||||
|
const purchaseBasisLabel = vatPurchaseDateBasisLabel(options.purchaseDateBridge?.basis, hasMultiplePurchaseDates);
|
||||||
|
const directVatLine = selectedPurchaseDate
|
||||||
|
? hasMultiplePurchaseDates
|
||||||
|
? `Коротко: если брать ${purchaseBasisLabel} ${formatDateRu(selectedPurchaseDate)}, подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`
|
||||||
|
: `Коротко: по дате покупки ${formatDateRu(selectedPurchaseDate)} подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`
|
||||||
|
: `Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`;
|
||||||
|
|
||||||
const lines = [
|
const lines = [
|
||||||
`Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`,
|
directVatLine,
|
||||||
"Расчет сделан по книгам продаж и покупок.",
|
"Расчет сделан по книгам продаж и покупок.",
|
||||||
"",
|
"",
|
||||||
"Что вошло в расчет:",
|
"Что вошло в расчет:",
|
||||||
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
|
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
|
||||||
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
|
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
|
||||||
...(purchaseDateAnchorLine ? [purchaseDateAnchorLine] : []),
|
...purchaseDateAnchorLines,
|
||||||
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
|
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
|
||||||
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
|
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
|
||||||
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`
|
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`
|
||||||
|
|
|
||||||
|
|
@ -548,7 +548,11 @@ export function composeCounterpartyAnalyticsReply(
|
||||||
const focus = deps.detectValueRankingFocus(options.userMessage);
|
const focus = deps.detectValueRankingFocus(options.userMessage);
|
||||||
const limit = deps.detectRankingLimit(options.userMessage, 20);
|
const limit = deps.detectRankingLimit(options.userMessage, 20);
|
||||||
const minOpsForAvgCheck = deps.detectMinOpsForAvgCheck(options.userMessage);
|
const minOpsForAvgCheck = deps.detectMinOpsForAvgCheck(options.userMessage);
|
||||||
const normalizedQuestion = deps.normalizeQuestionText(options.userMessage);
|
const questionSignalText = [options.rawUserMessage, options.userMessage]
|
||||||
|
.map((value) => String(value ?? "").trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
const normalizedQuestion = deps.normalizeQuestionText(questionSignalText || options.userMessage);
|
||||||
const asksSingleBestCounterparty =
|
const asksSingleBestCounterparty =
|
||||||
focus === "top_by_total" &&
|
focus === "top_by_total" &&
|
||||||
/(?:какой|кто|which|who|какой|кто)/iu.test(normalizedQuestion) &&
|
/(?:какой|кто|which|who|какой|кто)/iu.test(normalizedQuestion) &&
|
||||||
|
|
@ -572,6 +576,10 @@ export function composeCounterpartyAnalyticsReply(
|
||||||
const effectiveLimit = asksSingleBestCounterparty || semanticSingleBestCounterparty ? 1 : limit;
|
const effectiveLimit = asksSingleBestCounterparty || semanticSingleBestCounterparty ? 1 : limit;
|
||||||
const cashflowBoundaryLine = buildCashflowBoundaryLine(isSupplier);
|
const cashflowBoundaryLine = buildCashflowBoundaryLine(isSupplier);
|
||||||
const cashflowNextStepLine = buildCashflowNextStepLine(isSupplier, normalizedQuestion);
|
const cashflowNextStepLine = buildCashflowNextStepLine(isSupplier, normalizedQuestion);
|
||||||
|
const dependencyCue =
|
||||||
|
/(?:зависим|завязан|концентрац|ключев|основн|критич|dependency|concentration|key|critical)/iu.test(
|
||||||
|
normalizedQuestion
|
||||||
|
);
|
||||||
|
|
||||||
const byCounterparty = new Map<string, CounterpartyValuePoint>();
|
const byCounterparty = new Map<string, CounterpartyValuePoint>();
|
||||||
const byYear = new Map<number, CounterpartyYearPoint>();
|
const byYear = new Map<number, CounterpartyYearPoint>();
|
||||||
|
|
@ -864,13 +872,24 @@ export function composeCounterpartyAnalyticsReply(
|
||||||
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
||||||
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
||||||
: isSupplier
|
: isSupplier
|
||||||
? `Крупнейший поставщик по подтвержденным выплатам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
? dependencyCue
|
||||||
: `Самый доходный клиент ${rankingPeriodLabel} по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`;
|
? `Главный сигнал зависимости по исходящим платежам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||||
|
: `Крупнейший поставщик по подтвержденным выплатам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||||
|
: dependencyCue
|
||||||
|
? `Главный сигнал зависимости по входящим деньгам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||||
|
: `Самый доходный клиент ${rankingPeriodLabel} по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`;
|
||||||
lines.unshift(directAnswerLine);
|
lines.unshift(directAnswerLine);
|
||||||
|
if (dependencyCue) {
|
||||||
|
lines.splice(
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
"Это сигнал концентрации по найденным денежным строкам 1С, а не подтверждение договорной зависимости, надежности контрагента или чистой прибыли."
|
||||||
|
);
|
||||||
|
}
|
||||||
if (!isSupplier) {
|
if (!isSupplier) {
|
||||||
lines.splice(1, 0, cashflowBoundaryLine);
|
lines.splice(dependencyCue ? 2 : 1, 0, cashflowBoundaryLine);
|
||||||
if (cashflowNextStepLine) {
|
if (cashflowNextStepLine) {
|
||||||
lines.splice(2, 0, cashflowNextStepLine);
|
lines.splice(dependencyCue ? 3 : 2, 0, cashflowNextStepLine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -574,6 +574,24 @@ function hasRelativeYearHint(text: string): boolean {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveMonthPeriodFromIsoDate(isoDate: string | null | undefined): { period_from: string; period_to: string; as_of_date: string } | null {
|
||||||
|
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const year = Number(match[1]);
|
||||||
|
const month = Number(match[2]);
|
||||||
|
if (!Number.isFinite(year) || !Number.isFinite(month) || month < 1 || month > 12) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
||||||
|
return {
|
||||||
|
period_from: `${match[1]}-${match[2]}-01`,
|
||||||
|
period_to: `${match[1]}-${match[2]}-${String(lastDay).padStart(2, "0")}`,
|
||||||
|
as_of_date: `${match[1]}-${match[2]}-${String(lastDay).padStart(2, "0")}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function resolveRelativeMonthPeriodFromInventoryRoot(
|
function resolveRelativeMonthPeriodFromInventoryRoot(
|
||||||
userMessage: string,
|
userMessage: string,
|
||||||
followupContext: AddressFollowupContext | null
|
followupContext: AddressFollowupContext | null
|
||||||
|
|
@ -801,6 +819,36 @@ export function hasInventoryPurchaseDateVatBridgeCue(text: string): boolean {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasInventoryPurchaseDateVatBridgeContinuationCue(text: string): boolean {
|
||||||
|
const normalized = textWithRepairedVariant(String(text ?? ""))
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ё/g, "е");
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const mentionsPurchase = /(?:покупк|закупк|purchase)/iu.test(normalized);
|
||||||
|
const mentionsFirstOrLast = /(?:перв|последн|earliest|oldest|latest|last)/iu.test(normalized);
|
||||||
|
return mentionsPurchase && mentionsFirstOrLast;
|
||||||
|
}
|
||||||
|
|
||||||
|
function purchaseDateBridgeBasisFromMessage(userMessage: string): string | null {
|
||||||
|
const normalized = textWithRepairedVariant(String(userMessage ?? ""))
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ё/g, "е");
|
||||||
|
if (!normalized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (/(?:последн|latest|last)/iu.test(normalized) && /(?:покупк|закупк|purchase)/iu.test(normalized)) {
|
||||||
|
return "last_confirmed_purchase";
|
||||||
|
}
|
||||||
|
if (/(?:перв|earliest|oldest)/iu.test(normalized) && /(?:покупк|закупк|purchase)/iu.test(normalized)) {
|
||||||
|
return "first_confirmed_purchase";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function hasInventoryMarginRankingFollowupCue(text: string): boolean {
|
export function hasInventoryMarginRankingFollowupCue(text: string): boolean {
|
||||||
const normalized = textWithRepairedVariant(String(text ?? ""))
|
const normalized = textWithRepairedVariant(String(text ?? ""))
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
|
@ -968,6 +1016,34 @@ function mergeFollowupFilters(
|
||||||
): { filters: AddressFilterSet; reasons: string[] } {
|
): { filters: AddressFilterSet; reasons: string[] } {
|
||||||
const merged: AddressFilterSet = { ...current };
|
const merged: AddressFilterSet = { ...current };
|
||||||
const reasons: string[] = [];
|
const reasons: string[] = [];
|
||||||
|
const clearInventoryAgingOrganizationAliasItem = (organizationHint: string | null): void => {
|
||||||
|
if (intent !== "inventory_aging_by_purchase_date") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const explicitItemMention = /(?:^|[\s,.;:!?()\-\u2014])(?:товар(?:у|а|ом)?|позици(?:и|я|ю)|item|row|line)(?=$|[\s,.;:!?()\-\u2014])/iu.test(
|
||||||
|
String(userMessage ?? "")
|
||||||
|
);
|
||||||
|
const agingItem = toNonEmptyString(merged.item);
|
||||||
|
const agingOrganization = toNonEmptyString(merged.organization) ?? organizationHint;
|
||||||
|
const normalizedAgingItem = normalizeOrganizationScopeSearchText(agingItem ?? "");
|
||||||
|
const normalizedAgingOrganization = normalizeOrganizationScopeSearchText(agingOrganization ?? "");
|
||||||
|
const itemLooksLikeOrganizationScope = Boolean(
|
||||||
|
agingItem &&
|
||||||
|
agingOrganization &&
|
||||||
|
(organizationsLikelySameEntity(agingItem, agingOrganization) ||
|
||||||
|
(normalizedAgingItem &&
|
||||||
|
normalizedAgingOrganization &&
|
||||||
|
(normalizedAgingOrganization.includes(normalizedAgingItem) ||
|
||||||
|
normalizedAgingItem.includes(normalizedAgingOrganization))))
|
||||||
|
);
|
||||||
|
if (agingItem && itemLooksLikeOrganizationScope) {
|
||||||
|
delete merged.item;
|
||||||
|
reasons.push("item_cleared_as_organization_scope_alias_for_stock_aging");
|
||||||
|
} else if (agingItem && !explicitItemMention) {
|
||||||
|
delete merged.item;
|
||||||
|
reasons.push("item_cleared_for_stock_slice_aging");
|
||||||
|
}
|
||||||
|
};
|
||||||
if (!followupContext) {
|
if (!followupContext) {
|
||||||
if (
|
if (
|
||||||
(intent === "list_open_contracts" ||
|
(intent === "list_open_contracts" ||
|
||||||
|
|
@ -987,6 +1063,7 @@ function mergeFollowupFilters(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
clearInventoryAgingOrganizationAliasItem(null);
|
||||||
return { filters: merged, reasons };
|
return { filters: merged, reasons };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1001,6 +1078,13 @@ function mergeFollowupFilters(
|
||||||
const previousAsOfDate = toNonEmptyString(previous.as_of_date);
|
const previousAsOfDate = toNonEmptyString(previous.as_of_date);
|
||||||
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 previousPurchaseDateBridgeSelected = toNonEmptyString(previous.purchase_date_bridge_selected);
|
||||||
|
const previousPurchaseDateBridgeFirst = toNonEmptyString(previous.purchase_date_bridge_first);
|
||||||
|
const previousPurchaseDateBridgeLast = toNonEmptyString(previous.purchase_date_bridge_last);
|
||||||
|
const previousPurchaseDateBridgeBasis = toNonEmptyString(previous.purchase_date_bridge_basis);
|
||||||
|
const previousPurchaseDateBridgeHasMultiple =
|
||||||
|
previous.purchase_date_bridge_has_multiple === true ||
|
||||||
|
String(previous.purchase_date_bridge_has_multiple ?? "").trim().toLowerCase() === "true";
|
||||||
const relativeMonthFromInventoryRoot = resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext);
|
const relativeMonthFromInventoryRoot = resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext);
|
||||||
const relativeMonthFromFollowupYear = resolveRelativeMonthPeriodFromFollowupYear(userMessage, followupContext);
|
const relativeMonthFromFollowupYear = resolveRelativeMonthPeriodFromFollowupYear(userMessage, followupContext);
|
||||||
const allTimeRequested = hasAllTimeHint(userMessage);
|
const allTimeRequested = hasAllTimeHint(userMessage);
|
||||||
|
|
@ -1020,6 +1104,57 @@ function mergeFollowupFilters(
|
||||||
reasons.push("organization_from_followup_context");
|
reasons.push("organization_from_followup_context");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (intent === "vat_liability_confirmed_for_tax_period" && previousPurchaseDateBridgeSelected) {
|
||||||
|
const requestedBridgeBasis = purchaseDateBridgeBasisFromMessage(userMessage);
|
||||||
|
const shouldCarryPurchaseDateBridge =
|
||||||
|
!currentHasExplicitTemporalScope &&
|
||||||
|
(Boolean(requestedBridgeBasis) ||
|
||||||
|
hasInventoryPurchaseDateVatBridgeCue(userMessage) ||
|
||||||
|
hasInventoryPurchaseDateVatBridgeContinuationCue(userMessage));
|
||||||
|
if (!shouldCarryPurchaseDateBridge) {
|
||||||
|
delete merged.purchase_date_bridge_selected;
|
||||||
|
delete merged.purchase_date_bridge_first;
|
||||||
|
delete merged.purchase_date_bridge_last;
|
||||||
|
delete merged.purchase_date_bridge_basis;
|
||||||
|
delete merged.purchase_date_bridge_has_multiple;
|
||||||
|
reasons.push(
|
||||||
|
currentHasExplicitTemporalScope
|
||||||
|
? "purchase_date_bridge_suppressed_by_explicit_temporal_scope"
|
||||||
|
: "purchase_date_bridge_not_reused_without_bridge_cue"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const selectedBridgeDate =
|
||||||
|
requestedBridgeBasis === "last_confirmed_purchase"
|
||||||
|
? previousPurchaseDateBridgeLast ?? previousPurchaseDateBridgeSelected
|
||||||
|
: requestedBridgeBasis === "first_confirmed_purchase"
|
||||||
|
? previousPurchaseDateBridgeFirst ?? previousPurchaseDateBridgeSelected
|
||||||
|
: previousPurchaseDateBridgeSelected;
|
||||||
|
merged.purchase_date_bridge_selected = selectedBridgeDate;
|
||||||
|
const selectedBridgeWindow = resolveMonthPeriodFromIsoDate(selectedBridgeDate);
|
||||||
|
if (selectedBridgeWindow) {
|
||||||
|
merged.period_from = selectedBridgeWindow.period_from;
|
||||||
|
merged.period_to = selectedBridgeWindow.period_to;
|
||||||
|
merged.as_of_date = selectedBridgeWindow.as_of_date;
|
||||||
|
reasons.push("period_from_purchase_date_bridge_followup_context");
|
||||||
|
reasons.push("period_to_purchase_date_bridge_followup_context");
|
||||||
|
}
|
||||||
|
if (previousPurchaseDateBridgeFirst) {
|
||||||
|
merged.purchase_date_bridge_first = previousPurchaseDateBridgeFirst;
|
||||||
|
}
|
||||||
|
if (previousPurchaseDateBridgeLast) {
|
||||||
|
merged.purchase_date_bridge_last = previousPurchaseDateBridgeLast;
|
||||||
|
}
|
||||||
|
const effectiveBridgeBasis = requestedBridgeBasis ?? previousPurchaseDateBridgeBasis;
|
||||||
|
if (effectiveBridgeBasis) {
|
||||||
|
merged.purchase_date_bridge_basis = effectiveBridgeBasis;
|
||||||
|
}
|
||||||
|
if (previousPurchaseDateBridgeHasMultiple) {
|
||||||
|
merged.purchase_date_bridge_has_multiple = true;
|
||||||
|
}
|
||||||
|
reasons.push("purchase_date_bridge_from_followup_context");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
intent === "inventory_on_hand_as_of_date" &&
|
intent === "inventory_on_hand_as_of_date" &&
|
||||||
followupContext.previous_intent === "inventory_on_hand_as_of_date" &&
|
followupContext.previous_intent === "inventory_on_hand_as_of_date" &&
|
||||||
|
|
@ -1386,13 +1521,7 @@ function mergeFollowupFilters(
|
||||||
reasons.push("period_derived_from_inventory_root_frame_year");
|
reasons.push("period_derived_from_inventory_root_frame_year");
|
||||||
}
|
}
|
||||||
if (intent === "inventory_aging_by_purchase_date") {
|
if (intent === "inventory_aging_by_purchase_date") {
|
||||||
const explicitItemMention = /(?:^|[\s,.;:!?()\-\u2014])(?:товар(?:у|а|ом)?|позици(?:и|я|ю)|item|row|line)(?=$|[\s,.;:!?()\-\u2014])/iu.test(
|
clearInventoryAgingOrganizationAliasItem(previousOrganization);
|
||||||
String(userMessage ?? "")
|
|
||||||
);
|
|
||||||
if (toNonEmptyString(merged.item) && !explicitItemMention) {
|
|
||||||
delete merged.item;
|
|
||||||
reasons.push("item_cleared_for_stock_slice_aging");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!sameDateRequested &&
|
!sameDateRequested &&
|
||||||
|
|
@ -1646,7 +1775,13 @@ function deriveIntentWithFollowupContext(
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedMessage = String(userMessage ?? "");
|
const normalizedMessage = String(userMessage ?? "");
|
||||||
const hasFollowupSignal = hasAddressFollowupContextSignal(normalizedMessage);
|
const genericFollowupSignal = hasAddressFollowupContextSignal(normalizedMessage);
|
||||||
|
const previousFilters = followupContext.previous_filters ?? {};
|
||||||
|
const purchaseBridgeContinuationSignal =
|
||||||
|
followupContext.previous_intent === "vat_liability_confirmed_for_tax_period" &&
|
||||||
|
Boolean(toNonEmptyString(previousFilters.purchase_date_bridge_selected)) &&
|
||||||
|
hasInventoryPurchaseDateVatBridgeContinuationCue(normalizedMessage);
|
||||||
|
const hasFollowupSignal = genericFollowupSignal || purchaseBridgeContinuationSignal;
|
||||||
if (!hasFollowupSignal) {
|
if (!hasFollowupSignal) {
|
||||||
return detectedIntent;
|
return detectedIntent;
|
||||||
}
|
}
|
||||||
|
|
@ -1655,7 +1790,6 @@ function deriveIntentWithFollowupContext(
|
||||||
if (!sourceIntent && !fallbackIntent) {
|
if (!sourceIntent && !fallbackIntent) {
|
||||||
return detectedIntent;
|
return detectedIntent;
|
||||||
}
|
}
|
||||||
const previousFilters = followupContext.previous_filters ?? {};
|
|
||||||
const previousPeriodFrom = toNonEmptyString(previousFilters.period_from);
|
const previousPeriodFrom = toNonEmptyString(previousFilters.period_from);
|
||||||
const previousPeriodTo = toNonEmptyString(previousFilters.period_to);
|
const previousPeriodTo = toNonEmptyString(previousFilters.period_to);
|
||||||
const previousContract = toNonEmptyString(previousFilters.contract);
|
const previousContract = toNonEmptyString(previousFilters.contract);
|
||||||
|
|
@ -1672,6 +1806,7 @@ function deriveIntentWithFollowupContext(
|
||||||
isVatFollowup &&
|
isVatFollowup &&
|
||||||
hasSamePeriodHint(normalizedMessage) &&
|
hasSamePeriodHint(normalizedMessage) &&
|
||||||
Boolean(previousPeriodFrom || previousPeriodTo);
|
Boolean(previousPeriodFrom || previousPeriodTo);
|
||||||
|
const previousPurchaseDateBridgeSelected = toNonEmptyString(previousFilters.purchase_date_bridge_selected);
|
||||||
const previousIsInventoryFamily = isInventoryIntent(sourceIntent ?? undefined);
|
const previousIsInventoryFamily = isInventoryIntent(sourceIntent ?? undefined);
|
||||||
const rootIsInventoryFamily = isInventoryIntent(followupContext.root_intent ?? undefined);
|
const rootIsInventoryFamily = isInventoryIntent(followupContext.root_intent ?? undefined);
|
||||||
const inventoryLineageActive =
|
const inventoryLineageActive =
|
||||||
|
|
@ -1700,8 +1835,13 @@ function deriveIntentWithFollowupContext(
|
||||||
) || hasSelectedObjectInlineSnapshotMetadata(normalizedMessage);
|
) || hasSelectedObjectInlineSnapshotMetadata(normalizedMessage);
|
||||||
const staleInventoryLineageCanYieldToCounterparty =
|
const staleInventoryLineageCanYieldToCounterparty =
|
||||||
previousCounterpartyLaneActive && !hasExplicitInventoryItemReference;
|
previousCounterpartyLaneActive && !hasExplicitInventoryItemReference;
|
||||||
|
const vatPurchaseDateBridgeContinuation =
|
||||||
|
sourceIntent === "vat_liability_confirmed_for_tax_period" &&
|
||||||
|
Boolean(previousPurchaseDateBridgeSelected) &&
|
||||||
|
hasInventoryPurchaseDateVatBridgeContinuationCue(normalizedMessage);
|
||||||
const inventoryPurchaseDateVatBridge =
|
const inventoryPurchaseDateVatBridge =
|
||||||
inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage);
|
(inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage)) ||
|
||||||
|
vatPurchaseDateBridgeContinuation;
|
||||||
const marginRankingLineageActive =
|
const marginRankingLineageActive =
|
||||||
sourceIntent === "inventory_margin_ranking_for_nomenclature" ||
|
sourceIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
fallbackIntent === "inventory_margin_ranking_for_nomenclature" ||
|
fallbackIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
|
|
@ -1728,6 +1868,7 @@ function deriveIntentWithFollowupContext(
|
||||||
inventoryPurchaseDateVatBridge &&
|
inventoryPurchaseDateVatBridge &&
|
||||||
(detectedIntent.intent === "unknown" ||
|
(detectedIntent.intent === "unknown" ||
|
||||||
detectedIntent.intent === sourceIntent ||
|
detectedIntent.intent === sourceIntent ||
|
||||||
|
detectedIntent.intent === "account_balance_snapshot" ||
|
||||||
detectedIntent.intent === "vat_payable_confirmed_as_of_date" ||
|
detectedIntent.intent === "vat_payable_confirmed_as_of_date" ||
|
||||||
detectedIntent.intent === "vat_payable_forecast")
|
detectedIntent.intent === "vat_payable_forecast")
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,17 @@ function inventoryPartyListOrUnknown(parties: string[]): string {
|
||||||
return parties.length > 0 ? parties.slice(0, 4).join("; ") : "не выделен отдельным полем";
|
return parties.length > 0 ? parties.slice(0, 4).join("; ") : "не выделен отдельным полем";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeInventoryReplyEntityToken(value: string | null | undefined): string | null {
|
||||||
|
const normalized = String(value ?? "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ё/gu, "е")
|
||||||
|
.replace(/[^a-zа-я0-9]+/giu, " ")
|
||||||
|
.replace(/\s+/gu, " ")
|
||||||
|
.trim();
|
||||||
|
return normalized || null;
|
||||||
|
}
|
||||||
|
|
||||||
function sumInventoryRowAmount(rows: ComposeStageRow[]): number {
|
function sumInventoryRowAmount(rows: ComposeStageRow[]): number {
|
||||||
return rows.reduce((sum, row) => sum + (typeof row.amount === "number" && Number.isFinite(row.amount) ? row.amount : 0), 0);
|
return rows.reduce((sum, row) => sum + (typeof row.amount === "number" && Number.isFinite(row.amount) ? row.amount : 0), 0);
|
||||||
}
|
}
|
||||||
|
|
@ -550,30 +561,50 @@ export function composeInventoryReply(
|
||||||
|
|
||||||
if (intent === "inventory_aging_by_purchase_date") {
|
if (intent === "inventory_aging_by_purchase_date") {
|
||||||
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
||||||
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
const agingItems = deps.buildInventoryAgingByItemAggregate(rows, asOfDate);
|
||||||
|
const agingItemTokens = new Set(
|
||||||
|
agingItems
|
||||||
|
.map((item) => normalizeInventoryReplyEntityToken(item.item))
|
||||||
|
.filter((item): item is string => Boolean(item))
|
||||||
|
);
|
||||||
|
const purchaseRows = rows.filter((row) => {
|
||||||
|
if (!deps.isInventoryPurchaseMovement(row)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (agingItemTokens.size === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const item = deps.summarizeInventoryTraceRows([row]).item;
|
||||||
|
const itemToken = normalizeInventoryReplyEntityToken(item);
|
||||||
|
return Boolean(itemToken && agingItemTokens.has(itemToken));
|
||||||
|
});
|
||||||
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
|
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
|
||||||
const agingItems = deps.buildInventoryAgingByItemAggregate(purchaseRows, asOfDate);
|
|
||||||
const oldestPurchaseDate = agingItems[0]?.firstPurchasePeriod ?? summary.firstPeriod;
|
const oldestPurchaseDate = agingItems[0]?.firstPurchasePeriod ?? summary.firstPeriod;
|
||||||
const oldestPurchaseAgeDays = agingItems[0]?.ageDays ?? null;
|
const oldestPurchaseAgeDays = agingItems[0]?.ageDays ?? null;
|
||||||
|
const organizationLabel = agingItems.find((item) => item.organization)?.organization ?? null;
|
||||||
const oldestAnswerPreview = agingItems
|
const oldestAnswerPreview = agingItems
|
||||||
.slice(0, 3)
|
.slice(0, 3)
|
||||||
.map((item) => `${item.item} (${deps.inventoryTraceDateLabel(item.firstPurchasePeriod)})`)
|
.map((item) => `${item.item} (${deps.inventoryTraceDateLabel(item.firstPurchasePeriod)})`)
|
||||||
.join("; ");
|
.join("; ");
|
||||||
const directAnswerLine =
|
const directAnswerLine =
|
||||||
agingItems.length > 0
|
agingItems.length > 0
|
||||||
? `К самым старым закупкам в текущем подтвержденном срезе относятся позиции с самой ранней первой закупкой: ${oldestAnswerPreview}.`
|
? `Среди фактических положительных остатков есть давно закупавшиеся позиции: ${oldestAnswerPreview}.`
|
||||||
: "По доступному закупочному следу позиции со старыми закупками не материализованы.";
|
: "В фактическом положительном остатке не найдено позиций с подтвержденным старым закупочным следом.";
|
||||||
const lines: string[] = [directAnswerLine];
|
const lines: string[] = [directAnswerLine];
|
||||||
appendInventoryBulletSection(lines, "Сводка:", [
|
const summaryLines = [
|
||||||
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||||
`Самая ранняя первая закупка среди позиций: ${deps.inventoryTraceDateLabel(oldestPurchaseDate)}.`,
|
`Самая ранняя первая закупка среди позиций: ${deps.inventoryTraceDateLabel(oldestPurchaseDate)}.`,
|
||||||
`Самая поздняя найденная закупка: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
`Самая поздняя найденная закупка: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
||||||
`Позиций в выборке: ${deps.formatNumberWithDots(agingItems.length)}.`,
|
`Позиций в выборке: ${deps.formatNumberWithDots(agingItems.length)}.`,
|
||||||
`Закупочных документов: ${deps.formatNumberWithDots(summary.documents.length)}.`,
|
`Закупочных документов: ${deps.formatNumberWithDots(summary.documents.length)}.`,
|
||||||
`Закупочных операций: ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
`Закупочных операций: ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
||||||
]);
|
];
|
||||||
|
if (organizationLabel) {
|
||||||
|
summaryLines.splice(1, 0, `Организация: ${organizationLabel}.`);
|
||||||
|
}
|
||||||
|
appendInventoryBulletSection(lines, "Сводка:", summaryLines);
|
||||||
appendInventoryBulletSection(lines, "Ограничения:", [
|
appendInventoryBulletSection(lines, "Ограничения:", [
|
||||||
"Без партионного учета этот ответ показывает возраст закупочного следа по товарной позиции, а не возраст конкретного лота."
|
"Берем только позиции с положительным остатком на дату среза; без партионного учета это возраст закупочного следа по номенклатуре, а не доказанный возраст конкретной партии."
|
||||||
]);
|
]);
|
||||||
if (oldestPurchaseAgeDays !== null) {
|
if (oldestPurchaseAgeDays !== null) {
|
||||||
lines.push(`- Между самой ранней первой закупкой и датой среза прошло ${deps.formatNumberWithDots(oldestPurchaseAgeDays)} дн.`);
|
lines.push(`- Между самой ранней первой закупкой и датой среза прошло ${deps.formatNumberWithDots(oldestPurchaseAgeDays)} дн.`);
|
||||||
|
|
@ -582,10 +613,10 @@ export function composeInventoryReply(
|
||||||
lines.push(`- Поставщики, встречающиеся в наблюдаемом закупочном следе: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
lines.push(`- Поставщики, встречающиеся в наблюдаемом закупочном следе: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||||
}
|
}
|
||||||
if (agingItems.length > 0) {
|
if (agingItems.length > 0) {
|
||||||
appendInventorySection(lines, "Позиции от самых старых закупок:", deps.formatInventoryAgingRows(agingItems, asOfDate, 12));
|
appendInventorySection(lines, "Позиции от самых старых закупок:", deps.formatInventoryAgingRows(agingItems, asOfDate, 5));
|
||||||
} else {
|
} else {
|
||||||
appendInventorySection(lines, "Позиции от самых старых закупок:", [
|
appendInventorySection(lines, "Позиции от самых старых закупок:", [
|
||||||
"- В доступных данных не найдено закупочных движений для выбранного среза."
|
"- В доступных данных не найдено закупочных движений по позициям, которые есть в положительном остатке на дату среза."
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
return buildFactualSummaryReply(
|
return buildFactualSummaryReply(
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import type { AddressEvidenceStrength, AddressResponseType, AddressResultMode }
|
||||||
|
|
||||||
export interface ComposeFactualReplyOptions<TVatDirectSourceProbe = unknown> {
|
export interface ComposeFactualReplyOptions<TVatDirectSourceProbe = unknown> {
|
||||||
userMessage?: string;
|
userMessage?: string;
|
||||||
|
rawUserMessage?: string;
|
||||||
itemHint?: string;
|
itemHint?: string;
|
||||||
counterpartyHint?: string;
|
counterpartyHint?: string;
|
||||||
organizationHint?: string;
|
organizationHint?: string;
|
||||||
|
|
@ -11,6 +12,13 @@ export interface ComposeFactualReplyOptions<TVatDirectSourceProbe = unknown> {
|
||||||
asOfDate?: string;
|
asOfDate?: string;
|
||||||
requestedResultMode?: AddressResultMode;
|
requestedResultMode?: AddressResultMode;
|
||||||
vatDirectSourceProbe?: TVatDirectSourceProbe | null;
|
vatDirectSourceProbe?: TVatDirectSourceProbe | null;
|
||||||
|
purchaseDateBridge?: {
|
||||||
|
selectedPurchaseDate?: string | null;
|
||||||
|
firstPurchaseDate?: string | null;
|
||||||
|
lastPurchaseDate?: string | null;
|
||||||
|
basis?: string | null;
|
||||||
|
hasMultiplePurchaseDates?: boolean;
|
||||||
|
} | null;
|
||||||
emphasizeNumbers?: boolean;
|
emphasizeNumbers?: boolean;
|
||||||
useRubCurrency?: boolean;
|
useRubCurrency?: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,7 @@ export async function runAssistantAddressAttemptRuntime<ResponseType = unknown>(
|
||||||
runAddressLaneAttemptRuntimeSafe(
|
runAddressLaneAttemptRuntimeSafe(
|
||||||
buildAssistantAddressLaneAttemptRuntimeInput({
|
buildAssistantAddressLaneAttemptRuntimeInput({
|
||||||
messageUsed,
|
messageUsed,
|
||||||
|
rawUserMessage: input.userMessage,
|
||||||
carryMeta,
|
carryMeta,
|
||||||
analysisDateHint,
|
analysisDateHint,
|
||||||
llmSemanticHints,
|
llmSemanticHints,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import type { RunAssistantAddressLaneAttemptRuntimeInput } from "./assistantAddr
|
||||||
|
|
||||||
export interface BuildAssistantAddressLaneAttemptRuntimeInputInput {
|
export interface BuildAssistantAddressLaneAttemptRuntimeInputInput {
|
||||||
messageUsed: RunAssistantAddressLaneAttemptRuntimeInput["messageUsed"];
|
messageUsed: RunAssistantAddressLaneAttemptRuntimeInput["messageUsed"];
|
||||||
|
rawUserMessage: RunAssistantAddressLaneAttemptRuntimeInput["rawUserMessage"];
|
||||||
carryMeta: RunAssistantAddressLaneAttemptRuntimeInput["carryMeta"];
|
carryMeta: RunAssistantAddressLaneAttemptRuntimeInput["carryMeta"];
|
||||||
analysisDateHint: RunAssistantAddressLaneAttemptRuntimeInput["analysisDateHint"];
|
analysisDateHint: RunAssistantAddressLaneAttemptRuntimeInput["analysisDateHint"];
|
||||||
llmSemanticHints: RunAssistantAddressLaneAttemptRuntimeInput["llmSemanticHints"];
|
llmSemanticHints: RunAssistantAddressLaneAttemptRuntimeInput["llmSemanticHints"];
|
||||||
|
|
@ -17,6 +18,7 @@ export function buildAssistantAddressLaneAttemptRuntimeInput(
|
||||||
): RunAssistantAddressLaneAttemptRuntimeInput {
|
): RunAssistantAddressLaneAttemptRuntimeInput {
|
||||||
return {
|
return {
|
||||||
messageUsed: input.messageUsed,
|
messageUsed: input.messageUsed,
|
||||||
|
rawUserMessage: input.rawUserMessage,
|
||||||
carryMeta: input.carryMeta,
|
carryMeta: input.carryMeta,
|
||||||
analysisDateHint: input.analysisDateHint,
|
analysisDateHint: input.analysisDateHint,
|
||||||
llmSemanticHints: input.llmSemanticHints,
|
llmSemanticHints: input.llmSemanticHints,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ export function resolveAssistantAddressLaneAttemptFollowupContext(
|
||||||
|
|
||||||
export interface BuildAssistantAddressLaneAttemptQueryOptionsInput {
|
export interface BuildAssistantAddressLaneAttemptQueryOptionsInput {
|
||||||
analysisDateHint: RunAssistantAddressLaneAttemptRuntimeInput["analysisDateHint"];
|
analysisDateHint: RunAssistantAddressLaneAttemptRuntimeInput["analysisDateHint"];
|
||||||
|
rawUserMessage: RunAssistantAddressLaneAttemptRuntimeInput["rawUserMessage"];
|
||||||
scopedFollowupContext: Record<string, unknown> | null;
|
scopedFollowupContext: Record<string, unknown> | null;
|
||||||
llmSemanticHints: RunAssistantAddressLaneAttemptRuntimeInput["llmSemanticHints"];
|
llmSemanticHints: RunAssistantAddressLaneAttemptRuntimeInput["llmSemanticHints"];
|
||||||
activeOrganization: RunAssistantAddressLaneAttemptRuntimeInput["activeOrganization"];
|
activeOrganization: RunAssistantAddressLaneAttemptRuntimeInput["activeOrganization"];
|
||||||
|
|
@ -20,7 +21,8 @@ export function buildAssistantAddressLaneAttemptQueryOptions(
|
||||||
input: BuildAssistantAddressLaneAttemptQueryOptionsInput
|
input: BuildAssistantAddressLaneAttemptQueryOptionsInput
|
||||||
): Parameters<RunAssistantAddressLaneAttemptRuntimeInput["runAddressQueryTryHandle"]>[1] {
|
): Parameters<RunAssistantAddressLaneAttemptRuntimeInput["runAddressQueryTryHandle"]>[1] {
|
||||||
const base = {
|
const base = {
|
||||||
analysisDateHint: input.analysisDateHint
|
analysisDateHint: input.analysisDateHint,
|
||||||
|
rawUserMessage: input.rawUserMessage ?? undefined
|
||||||
} as Parameters<RunAssistantAddressLaneAttemptRuntimeInput["runAddressQueryTryHandle"]>[1];
|
} as Parameters<RunAssistantAddressLaneAttemptRuntimeInput["runAddressQueryTryHandle"]>[1];
|
||||||
if (input.scopedFollowupContext) {
|
if (input.scopedFollowupContext) {
|
||||||
base.followupContext = input.scopedFollowupContext;
|
base.followupContext = input.scopedFollowupContext;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
|
|
||||||
export interface RunAssistantAddressLaneAttemptRuntimeInput {
|
export interface RunAssistantAddressLaneAttemptRuntimeInput {
|
||||||
messageUsed: string;
|
messageUsed: string;
|
||||||
|
rawUserMessage?: string | null;
|
||||||
carryMeta: AssistantAddressCarryoverLike | null;
|
carryMeta: AssistantAddressCarryoverLike | null;
|
||||||
analysisDateHint: string | null;
|
analysisDateHint: string | null;
|
||||||
llmSemanticHints?: Record<string, unknown> | null;
|
llmSemanticHints?: Record<string, unknown> | null;
|
||||||
|
|
@ -24,6 +25,7 @@ export interface RunAssistantAddressLaneAttemptRuntimeInput {
|
||||||
llmSemanticHints?: Record<string, unknown> | null;
|
llmSemanticHints?: Record<string, unknown> | null;
|
||||||
activeOrganization?: string | null;
|
activeOrganization?: string | null;
|
||||||
knownOrganizations?: string[];
|
knownOrganizations?: string[];
|
||||||
|
rawUserMessage?: string | null;
|
||||||
}
|
}
|
||||||
) => Promise<AssistantAddressLaneLike | null>;
|
) => Promise<AssistantAddressLaneLike | null>;
|
||||||
}
|
}
|
||||||
|
|
@ -43,7 +45,8 @@ export async function runAssistantAddressLaneAttemptRuntime(
|
||||||
scopedFollowupContext,
|
scopedFollowupContext,
|
||||||
llmSemanticHints: input.llmSemanticHints ?? null,
|
llmSemanticHints: input.llmSemanticHints ?? null,
|
||||||
activeOrganization: input.activeOrganization,
|
activeOrganization: input.activeOrganization,
|
||||||
knownOrganizations: input.knownOrganizations
|
knownOrganizations: input.knownOrganizations,
|
||||||
|
rawUserMessage: input.rawUserMessage ?? input.messageUsed
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -120,12 +120,25 @@ function cleanComparisonScopeCompanyLine(line: string, organization: string | nu
|
||||||
return clean.trim();
|
return clean.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasComparisonScopeProofCue(value: unknown): boolean {
|
||||||
|
const text = toNullableString(value)?.toLocaleLowerCase("ru-RU").replace(/\s+/gu, " ").trim();
|
||||||
|
if (!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:\bсравни\b|собери\s+коротк\p{L}*\s+итог|коротк\p{L}*\s+сравн|что\s+.*подтверд\p{L}*.*(?:компан|контрагент|свк)|что\s+.*отдельно\s+по\s+(?:группа\s+свк|выбранн\p{L}*\s+контрагент)|какие\s+выводы\s+можно\s+делать\s+и\s+какие\s+нельзя)/iu.test(
|
||||||
|
text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function buildComparisonScopeProofReply(input: {
|
function buildComparisonScopeProofReply(input: {
|
||||||
baseReply: string;
|
baseReply: string;
|
||||||
debug: Record<string, unknown>;
|
debug: Record<string, unknown>;
|
||||||
session: unknown;
|
session: unknown;
|
||||||
userMessage?: unknown;
|
userMessage?: unknown;
|
||||||
}): { reply: string; audit: Record<string, unknown> } | null {
|
}): { reply: string; audit: Record<string, unknown> } | null {
|
||||||
|
if (!hasComparisonScopeProofCue(input.userMessage)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const entryPoint = toRecordObject(input.debug.assistant_mcp_discovery_entry_point_v1);
|
const entryPoint = toRecordObject(input.debug.assistant_mcp_discovery_entry_point_v1);
|
||||||
const turnInput = toRecordObject(entryPoint?.turn_input);
|
const turnInput = toRecordObject(entryPoint?.turn_input);
|
||||||
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
|
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ function hasCompactCashflowFollowupSignal(text: string | null): boolean {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return /(?:\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0440\u0435\u0437\p{L}*|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433|\u043f\u0440\u0438\u0448\p{L}*[\s\S]{0,80}\u0443\u0448\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u043f\u043e\u043b\u0443\u0447\p{L}*[\s\S]{0,80}\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*[\s\S]{0,80}\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b)/iu.test(value);
|
return /(?:\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0440\u0435\u0437\p{L}*|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433|\u043f\u0440\u0438\u0448\p{L}*[\s\S]{0,80}\u0443\u0448\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u043f\u043e\u043b\u0443\u0447\p{L}*[\s\S]{0,80}\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*[\s\S]{0,80}\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b|кто\s+больше\s+всего\s+зан[её]с|кому\s+больше\s+всего\s+ушл\p{L}*|банк\s+не\s+называй\s+обычн\p{L}*\s+клиент\p{L}*|главн\p{L}*\s+клиент|главн\p{L}*\s+поставщик)/iu.test(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function dateScopeToFilterWindow(dateScope: string | null): Record<string, string> | null {
|
function dateScopeToFilterWindow(dateScope: string | null): Record<string, string> | null {
|
||||||
|
|
@ -169,6 +169,37 @@ function dateScopeToFilterWindow(dateScope: string | null): Record<string, strin
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filterWindowFromExtractedFilters(filters: Record<string, unknown> | null): Record<string, string> | null {
|
||||||
|
if (!filters) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const asOfDate = typeof filters.as_of_date === "string" ? filters.as_of_date.trim() : "";
|
||||||
|
if (/^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(asOfDate)) {
|
||||||
|
return { as_of_date: asOfDate };
|
||||||
|
}
|
||||||
|
const periodFrom = typeof filters.period_from === "string" ? filters.period_from.trim() : "";
|
||||||
|
const periodTo = typeof filters.period_to === "string" ? filters.period_to.trim() : "";
|
||||||
|
if (/^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(periodFrom) && /^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(periodTo)) {
|
||||||
|
return {
|
||||||
|
period_from: periodFrom,
|
||||||
|
period_to: periodTo
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasBoundedFilterWindow(filters: Record<string, unknown> | null): boolean {
|
||||||
|
if (!filters) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (typeof filters.as_of_date === "string" && /^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(filters.as_of_date.trim())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const periodFrom = typeof filters.period_from === "string" ? filters.period_from.trim() : "";
|
||||||
|
const periodTo = typeof filters.period_to === "string" ? filters.period_to.trim() : "";
|
||||||
|
return /^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(periodFrom) && /^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(periodTo);
|
||||||
|
}
|
||||||
|
|
||||||
function looksLikeReliableOrganizationScope(value: string | null): boolean {
|
function looksLikeReliableOrganizationScope(value: string | null): boolean {
|
||||||
const text = compactLower(value);
|
const text = compactLower(value);
|
||||||
if (!text) {
|
if (!text) {
|
||||||
|
|
@ -197,12 +228,17 @@ function inferBusinessOverviewDiscoveryContextFromSessionItems(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const turnMeaningRef = toRecordObject(turnInput?.turn_meaning_ref);
|
const turnMeaningRef = toRecordObject(turnInput?.turn_meaning_ref);
|
||||||
const filterWindow = dateScopeToFilterWindow(toNonEmptyString(turnMeaningRef?.explicit_date_scope));
|
const debugFilters = toRecordObject(debug?.extracted_filters);
|
||||||
|
const filterWindow =
|
||||||
|
dateScopeToFilterWindow(toNonEmptyString(turnMeaningRef?.explicit_date_scope)) ??
|
||||||
|
filterWindowFromExtractedFilters(debugFilters);
|
||||||
if (!filterWindow) {
|
if (!filterWindow) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const previousFilters: Record<string, unknown> = { ...filterWindow };
|
const previousFilters: Record<string, unknown> = { ...filterWindow };
|
||||||
const organization = toNonEmptyString(turnMeaningRef?.explicit_organization_scope);
|
const organization =
|
||||||
|
toNonEmptyString(turnMeaningRef?.explicit_organization_scope) ??
|
||||||
|
toNonEmptyString(debugFilters?.organization);
|
||||||
if (looksLikeReliableOrganizationScope(organization)) {
|
if (looksLikeReliableOrganizationScope(organization)) {
|
||||||
previousFilters.organization = organization;
|
previousFilters.organization = organization;
|
||||||
}
|
}
|
||||||
|
|
@ -229,11 +265,7 @@ function mergeBusinessOverviewDateContextForCompactCashflow(input: {
|
||||||
return input.followupContext;
|
return input.followupContext;
|
||||||
}
|
}
|
||||||
const existingFilters = toRecordObject(input.followupContext?.previous_filters);
|
const existingFilters = toRecordObject(input.followupContext?.previous_filters);
|
||||||
if (
|
if (hasBoundedFilterWindow(existingFilters)) {
|
||||||
input.toNonEmptyString(existingFilters?.period_from) ||
|
|
||||||
input.toNonEmptyString(existingFilters?.period_to) ||
|
|
||||||
input.toNonEmptyString(existingFilters?.as_of_date)
|
|
||||||
) {
|
|
||||||
return input.followupContext;
|
return input.followupContext;
|
||||||
}
|
}
|
||||||
const inferred = inferBusinessOverviewDiscoveryContextFromSessionItems(input.sessionItems, input.toNonEmptyString);
|
const inferred = inferBusinessOverviewDiscoveryContextFromSessionItems(input.sessionItems, input.toNonEmptyString);
|
||||||
|
|
@ -244,12 +276,12 @@ function mergeBusinessOverviewDateContextForCompactCashflow(input: {
|
||||||
...inferred,
|
...inferred,
|
||||||
...(input.followupContext ?? {}),
|
...(input.followupContext ?? {}),
|
||||||
previous_filters: {
|
previous_filters: {
|
||||||
...(toRecordObject(inferred.previous_filters) ?? {}),
|
...(toRecordObject(input.followupContext?.previous_filters) ?? {}),
|
||||||
...(toRecordObject(input.followupContext?.previous_filters) ?? {})
|
...(toRecordObject(inferred.previous_filters) ?? {})
|
||||||
},
|
},
|
||||||
root_filters: {
|
root_filters: {
|
||||||
...(toRecordObject(inferred.root_filters) ?? {}),
|
...(toRecordObject(input.followupContext?.root_filters) ?? {}),
|
||||||
...(toRecordObject(input.followupContext?.root_filters) ?? {})
|
...(toRecordObject(inferred.root_filters) ?? {})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -281,6 +313,16 @@ function sameEntityHint(expected: string | null, actual: string | null): boolean
|
||||||
return left === right || left.includes(right) || right.includes(left);
|
return left === right || left.includes(right) || right.includes(left);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasComparisonScopeProofCue(text: string | null): boolean {
|
||||||
|
const value = compactLower(text);
|
||||||
|
if (!value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:\bсравни\b|собери\s+коротк\p{L}*\s+итог|коротк\p{L}*\s+сравн|что\s+.*подтверд\p{L}*.*(?:компан|контрагент|свк)|что\s+.*отдельно\s+по\s+(?:группа\s+свк|выбранн\p{L}*\s+контрагент)|какие\s+выводы\s+можно\s+делать\s+и\s+какие\s+нельзя)/iu.test(
|
||||||
|
value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function isBusinessOverviewDiscoveryFollowup(
|
function isBusinessOverviewDiscoveryFollowup(
|
||||||
followupContext: Record<string, unknown> | null,
|
followupContext: Record<string, unknown> | null,
|
||||||
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
|
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
|
||||||
|
|
@ -489,6 +531,7 @@ function mergeBusinessOverviewProofBundlesFromNavigationState(input: {
|
||||||
followupContext: Record<string, unknown> | null;
|
followupContext: Record<string, unknown> | null;
|
||||||
sessionAddressNavigationState: unknown;
|
sessionAddressNavigationState: unknown;
|
||||||
predecomposeContract: Record<string, unknown> | null;
|
predecomposeContract: Record<string, unknown> | null;
|
||||||
|
userMessage: string;
|
||||||
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"];
|
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"];
|
||||||
}): Record<string, unknown> | null {
|
}): Record<string, unknown> | null {
|
||||||
const currentValueBundle = toRecordObject(input.followupContext?.previous_discovery_bidirectional_value_flow);
|
const currentValueBundle = toRecordObject(input.followupContext?.previous_discovery_bidirectional_value_flow);
|
||||||
|
|
@ -508,6 +551,9 @@ function mergeBusinessOverviewProofBundlesFromNavigationState(input: {
|
||||||
const entities = toRecordObject(input.predecomposeContract?.entities);
|
const entities = toRecordObject(input.predecomposeContract?.entities);
|
||||||
const hasCurrentOrganizationSelection = Boolean(input.toNonEmptyString(entities?.organization));
|
const hasCurrentOrganizationSelection = Boolean(input.toNonEmptyString(entities?.organization));
|
||||||
const businessOverviewFollowup = isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString);
|
const businessOverviewFollowup = isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString);
|
||||||
|
if (!hasComparisonScopeProofCue(input.userMessage)) {
|
||||||
|
return input.followupContext;
|
||||||
|
}
|
||||||
if (!businessOverviewFollowup && !hasCurrentOrganizationSelection) {
|
if (!businessOverviewFollowup && !hasCurrentOrganizationSelection) {
|
||||||
return input.followupContext;
|
return input.followupContext;
|
||||||
}
|
}
|
||||||
|
|
@ -550,9 +596,13 @@ function mergeBusinessOverviewProofBundlesFromNavigationState(input: {
|
||||||
|
|
||||||
function mergeBusinessOverviewProofBundlesFromSessionItems(input: {
|
function mergeBusinessOverviewProofBundlesFromSessionItems(input: {
|
||||||
followupContext: Record<string, unknown> | null;
|
followupContext: Record<string, unknown> | null;
|
||||||
|
userMessage: string;
|
||||||
sessionItems: unknown[];
|
sessionItems: unknown[];
|
||||||
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"];
|
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"];
|
||||||
}): Record<string, unknown> | null {
|
}): Record<string, unknown> | null {
|
||||||
|
if (!hasComparisonScopeProofCue(input.userMessage)) {
|
||||||
|
return input.followupContext;
|
||||||
|
}
|
||||||
if (!isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString)) {
|
if (!isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString)) {
|
||||||
return input.followupContext;
|
return input.followupContext;
|
||||||
}
|
}
|
||||||
|
|
@ -853,6 +903,7 @@ export async function buildAssistantAddressOrchestrationRuntime(
|
||||||
});
|
});
|
||||||
const discoveryFollowupContextWithProofBundles = mergeBusinessOverviewProofBundlesFromSessionItems({
|
const discoveryFollowupContextWithProofBundles = mergeBusinessOverviewProofBundlesFromSessionItems({
|
||||||
followupContext: discoveryFollowupContext,
|
followupContext: discoveryFollowupContext,
|
||||||
|
userMessage: input.userMessage,
|
||||||
sessionItems: input.sessionItems,
|
sessionItems: input.sessionItems,
|
||||||
toNonEmptyString: input.toNonEmptyString
|
toNonEmptyString: input.toNonEmptyString
|
||||||
});
|
});
|
||||||
|
|
@ -860,6 +911,7 @@ export async function buildAssistantAddressOrchestrationRuntime(
|
||||||
followupContext: discoveryFollowupContextWithProofBundles,
|
followupContext: discoveryFollowupContextWithProofBundles,
|
||||||
sessionAddressNavigationState: input.sessionAddressNavigationState,
|
sessionAddressNavigationState: input.sessionAddressNavigationState,
|
||||||
predecomposeContract,
|
predecomposeContract,
|
||||||
|
userMessage: input.userMessage,
|
||||||
toNonEmptyString: input.toNonEmptyString
|
toNonEmptyString: input.toNonEmptyString
|
||||||
});
|
});
|
||||||
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(
|
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,21 @@ function normalizeAddressRuntimeMetaForDeep(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripRoutineShortAnswerPrefix(value: string): string {
|
||||||
|
return String(value ?? "")
|
||||||
|
.split(/\r?\n/g)
|
||||||
|
.map((line) =>
|
||||||
|
line
|
||||||
|
.replace(
|
||||||
|
/^((?:\s|\uFEFF)*(?:[-*]\s*)?)(?:\u041a\u043e\u0440\u043e\u0442\u043a\u043e:\s*)+/u,
|
||||||
|
"$1"
|
||||||
|
)
|
||||||
|
.replace(/(:\s*)\u041a\u043e\u0440\u043e\u0442\u043a\u043e:\s*/u, "$1")
|
||||||
|
)
|
||||||
|
.join("\n")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
export function runAssistantDeepTurnResponseRuntime(
|
export function runAssistantDeepTurnResponseRuntime(
|
||||||
input: RunAssistantDeepTurnResponseRuntimeInput
|
input: RunAssistantDeepTurnResponseRuntimeInput
|
||||||
): RunAssistantDeepTurnResponseRuntimeOutput {
|
): RunAssistantDeepTurnResponseRuntimeOutput {
|
||||||
|
|
@ -243,9 +258,11 @@ export function runAssistantDeepTurnResponseRuntime(
|
||||||
currentReplySource: "deep_analysis",
|
currentReplySource: "deep_analysis",
|
||||||
addressRuntimeMeta: addressRuntimeMetaForDeep as unknown as Record<string, unknown> | null
|
addressRuntimeMeta: addressRuntimeMetaForDeep as unknown as Record<string, unknown> | null
|
||||||
});
|
});
|
||||||
const assistantReply = mcpDiscoveryResponsePolicy.applied
|
const assistantReply = stripRoutineShortAnswerPrefix(
|
||||||
? mcpDiscoveryResponsePolicy.reply_text
|
mcpDiscoveryResponsePolicy.applied
|
||||||
: packagingRuntime.safeAssistantReply;
|
? input.sanitizeReply(mcpDiscoveryResponsePolicy.reply_text, packagingRuntime.safeAssistantReply)
|
||||||
|
: packagingRuntime.safeAssistantReply
|
||||||
|
);
|
||||||
const replyType = mcpDiscoveryResponsePolicy.applied ? "partial_coverage" : input.composition.reply_type;
|
const replyType = mcpDiscoveryResponsePolicy.applied ? "partial_coverage" : input.composition.reply_type;
|
||||||
const debug = {
|
const debug = {
|
||||||
...packagingRuntime.debug,
|
...packagingRuntime.debug,
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,16 @@ function requestsCounterpartyLeaderSurface(
|
||||||
return /(?:(?:кто|как\p{L}*|покаж\p{L}*|назов\p{L}*|раскро\p{L}*)[\s\S]{0,100}(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*|топ)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:топ[-\s]*(?:клиент|заказчик|поставщик|получател))|(?:клиент|поставщик)[\s\S]{0,80}(?:главн|крупнейш|основн|ведущ|топ))/iu.test(text);
|
return /(?:(?:кто|как\p{L}*|покаж\p{L}*|назов\p{L}*|раскро\p{L}*)[\s\S]{0,100}(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*|топ)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:топ[-\s]*(?:клиент|заказчик|поставщик|получател))|(?:клиент|поставщик)[\s\S]{0,80}(?:главн|крупнейш|основн|ведущ|топ))/iu.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function requestsBusinessOverviewNextChecks(
|
||||||
|
turnMeaning: Record<string, unknown> | null,
|
||||||
|
graph: Record<string, unknown> | null
|
||||||
|
): boolean {
|
||||||
|
const text = normalizedTurnAndGraphText(turnMeaning, graph);
|
||||||
|
return /(?:что\s+(?:дальше\s+)?провер\w*|что\s+провер\w*\s+дальше|следующ\w*\s+шаг|в\s+первую\s+очередь\s+провер\w*|next\s+checks?|next\s+steps?)/iu.test(
|
||||||
|
text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function requestsCompactCashflowAnswer(
|
function requestsCompactCashflowAnswer(
|
||||||
turnMeaning: Record<string, unknown> | null,
|
turnMeaning: Record<string, unknown> | null,
|
||||||
graph: Record<string, unknown> | null
|
graph: Record<string, unknown> | null
|
||||||
|
|
@ -527,6 +537,14 @@ function businessOverviewPeriodText(overview: Record<string, unknown>): string {
|
||||||
return period ? `за ${period}` : "за все доступное проверенное окно";
|
return period ? `за ${period}` : "за все доступное проверенное окно";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function businessOverviewPeriodPhrase(value: string | null | undefined): string {
|
||||||
|
const normalized = toNonEmptyString(value);
|
||||||
|
if (!normalized) {
|
||||||
|
return "за все доступное проверенное окно";
|
||||||
|
}
|
||||||
|
return /^за\s+/iu.test(normalized) ? normalized : `за ${normalized}`;
|
||||||
|
}
|
||||||
|
|
||||||
function strongestIncomingYear(overview: Record<string, unknown>): Record<string, unknown> | null {
|
function strongestIncomingYear(overview: Record<string, unknown>): Record<string, unknown> | null {
|
||||||
const years = Array.isArray(overview.yearly_breakdown) ? overview.yearly_breakdown : [];
|
const years = Array.isArray(overview.yearly_breakdown) ? overview.yearly_breakdown : [];
|
||||||
const sorted = years
|
const sorted = years
|
||||||
|
|
@ -1031,7 +1049,12 @@ function buildCompactBusinessOverviewReply(
|
||||||
const compactCashflowRequested = directMoneyOnlyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
|
const compactCashflowRequested = directMoneyOnlyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
|
||||||
const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph);
|
const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph);
|
||||||
const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph);
|
const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph);
|
||||||
|
const nextChecksRequested = requestsBusinessOverviewNextChecks(turnMeaning, graph);
|
||||||
const rawMessage = toNonEmptyString(turnMeaning?.raw_message) ?? toNonEmptyString(turnMeaning?.effective_message);
|
const rawMessage = toNonEmptyString(turnMeaning?.raw_message) ?? toNonEmptyString(turnMeaning?.effective_message);
|
||||||
|
const earningsAmountRequested = /(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0434\u043e\u0445\u043e\u0434\p{L}*)/iu.test(
|
||||||
|
normalizeQuestionText(rawMessage ?? "")
|
||||||
|
);
|
||||||
|
const accountingFinancialResultForEarnings = toRecordObject(overview.accounting_financial_result);
|
||||||
const rawMessageComparable = compactComparable(rawMessage);
|
const rawMessageComparable = compactComparable(rawMessage);
|
||||||
const organizationScopeComparable = compactComparable(organizationScope);
|
const organizationScopeComparable = compactComparable(organizationScope);
|
||||||
const plainOrganizationClarificationSelection = Boolean(
|
const plainOrganizationClarificationSelection = Boolean(
|
||||||
|
|
@ -1045,18 +1068,65 @@ function buildCompactBusinessOverviewReply(
|
||||||
!/(?:сравн|подтвержд|деньг|сколько|что\s+|покаж|дай|вывод|нельзя|клиент|поставщик|\?)/iu.test(rawMessage)
|
!/(?:сравн|подтвержд|деньг|сколько|что\s+|покаж|дай|вывод|нельзя|клиент|поставщик|\?)/iu.test(rawMessage)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
compactCashflowRequested &&
|
||||||
|
!rankingNeed &&
|
||||||
|
!profitMarginBoundary &&
|
||||||
|
!cashflowPolarityRequested &&
|
||||||
|
earningsAmountRequested &&
|
||||||
|
accountingFinancialResultForEarnings
|
||||||
|
) {
|
||||||
|
const direction = toNonEmptyString(accountingFinancialResultForEarnings.final_result_direction);
|
||||||
|
const amount = moneyText(accountingFinancialResultForEarnings.final_result_amount_human_ru);
|
||||||
|
const periodScope = toNonEmptyString(accountingFinancialResultForEarnings.period_scope) ?? period;
|
||||||
|
const marginPct =
|
||||||
|
typeof accountingFinancialResultForEarnings.net_margin_to_revenue_pct === "number" &&
|
||||||
|
Number.isFinite(accountingFinancialResultForEarnings.net_margin_to_revenue_pct)
|
||||||
|
? `${accountingFinancialResultForEarnings.net_margin_to_revenue_pct}%`
|
||||||
|
: null;
|
||||||
|
const directionText =
|
||||||
|
direction === "profit"
|
||||||
|
? "\u0443\u0447\u0435\u0442\u043d\u0430\u044f \u043f\u0440\u0438\u0431\u044b\u043b\u044c"
|
||||||
|
: direction === "loss"
|
||||||
|
? "\u0443\u0447\u0435\u0442\u043d\u044b\u0439 \u0443\u0431\u044b\u0442\u043e\u043a"
|
||||||
|
: "\u043d\u0443\u043b\u0435\u0432\u043e\u0439 \u0443\u0447\u0435\u0442\u043d\u044b\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442";
|
||||||
|
const amountText = amount
|
||||||
|
? direction === "loss"
|
||||||
|
? `\u043c\u0438\u043d\u0443\u0441 ${amount}`
|
||||||
|
: amount
|
||||||
|
: "\u0441\u0443\u043c\u043c\u0430 \u043d\u0435 \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u043d\u0430";
|
||||||
|
lines.push(
|
||||||
|
`${organizationPrefix}\u0437\u0430 ${periodScope} \u043f\u043e \u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044e \u0441\u0447\u0435\u0442\u043e\u0432 90/91/99 \u0432 1\u0421 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0430 ${directionText}: ${amountText}${marginPct ? `; \u043c\u0430\u0440\u0436\u0430 \u043a \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043d\u043e\u0439 \u0432\u044b\u0440\u0443\u0447\u043a\u0435 ${marginPct}` : ""}.`
|
||||||
|
);
|
||||||
|
if (incomingAmount || outgoingAmount || netAmount) {
|
||||||
|
lines.push(
|
||||||
|
`\u0414\u043b\u044f \u0441\u0432\u0435\u0440\u043a\u0438 \u043f\u043e \u0434\u0435\u043d\u044c\u0433\u0430\u043c: \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 ${incomingAmount ?? "0 \u0440\u0443\u0431."}; \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438/\u0441\u043f\u0438\u0441\u0430\u043b\u0438 ${outgoingAmount ?? "0 \u0440\u0443\u0431."}; \u0434\u0435\u043d\u0435\u0436\u043d\u043e\u0435 \u043d\u0435\u0442\u0442\u043e ${sentenceAmount(netAmount) ?? netAmount ?? "0 \u0440\u0443\u0431."}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
lines.push(
|
||||||
|
"\u0412\u0430\u0436\u043d\u043e: \u0443\u0447\u0435\u0442\u043d\u044b\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u0438 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435 \u0434\u0435\u043d\u0435\u0433 - \u0440\u0430\u0437\u043d\u044b\u0435 \u043c\u0435\u0442\u0440\u0438\u043a\u0438; \u043d\u0435 \u0441\u043c\u0435\u0448\u0438\u0432\u0430\u044e \u0438\u0445 \u0432 \u043e\u0434\u043d\u0443 \"\u043f\u0440\u0438\u0431\u044b\u043b\u044c\"."
|
||||||
|
);
|
||||||
|
return joinBusinessReplyLines(lines);
|
||||||
|
}
|
||||||
|
|
||||||
if (compactCashflowRequested && !rankingNeed && (incomingAmount || outgoingAmount || netAmount)) {
|
if (compactCashflowRequested && !rankingNeed && (incomingAmount || outgoingAmount || netAmount)) {
|
||||||
const netDisplay = sentenceAmount(netAmount) ?? netAmount ?? "0 \u0440\u0443\u0431.";
|
const netDisplay = sentenceAmount(netAmount) ?? netAmount ?? "0 \u0440\u0443\u0431.";
|
||||||
const signedNetDisplay =
|
const signedNetDisplay =
|
||||||
cashflowPolarityRequested && netDisplay && !String(netDisplay).trim().startsWith("-")
|
cashflowPolarityRequested && netDisplay && !String(netDisplay).trim().startsWith("-")
|
||||||
? `+${netDisplay}`
|
? `+${netDisplay}`
|
||||||
: netDisplay;
|
: netDisplay;
|
||||||
const polarityLead = cashflowPolarityRequested
|
const limitedEarningsAnswer = Boolean(earningsAmountRequested && !accountingFinancialResultForEarnings && limitLine);
|
||||||
? `\u041a\u043e\u0440\u043e\u0442\u043a\u043e: \u043f\u043e \u0434\u0435\u043d\u044c\u0433\u0430\u043c \u043f\u043b\u044e\u0441. `
|
const polarityLead = limitedEarningsAnswer
|
||||||
: "\u041a\u043e\u0440\u043e\u0442\u043a\u043e: ";
|
? "\u041a\u043e\u0440\u043e\u0442\u043a\u043e: \u0442\u043e\u0447\u043d\u0443\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0437\u0430 \u044d\u0442\u043e \u043e\u043a\u043d\u043e \u043d\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u044e; \u0432 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u043d\u043e\u043c \u0434\u0435\u043d\u0435\u0436\u043d\u043e\u043c \u0441\u0440\u0435\u0437\u0435 "
|
||||||
|
: cashflowPolarityRequested
|
||||||
|
? `\u041a\u043e\u0440\u043e\u0442\u043a\u043e: \u043f\u043e \u0434\u0435\u043d\u044c\u0433\u0430\u043c \u043f\u043b\u044e\u0441. `
|
||||||
|
: "\u041a\u043e\u0440\u043e\u0442\u043a\u043e: ";
|
||||||
lines.push(
|
lines.push(
|
||||||
`${polarityLead}${organizationPrefix}${period} \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 ${incomingAmount ?? "0 \u0440\u0443\u0431."}; \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438/\u0441\u043f\u0438\u0441\u0430\u043b\u0438 ${outgoingAmount ?? "0 \u0440\u0443\u0431."}; \u0434\u0435\u043d\u0435\u0436\u043d\u043e\u0435 \u043d\u0435\u0442\u0442\u043e ${signedNetDisplay}.`
|
`${polarityLead}${organizationPrefix}${period} \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 ${incomingAmount ?? "0 \u0440\u0443\u0431."}; \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438/\u0441\u043f\u0438\u0441\u0430\u043b\u0438 ${outgoingAmount ?? "0 \u0440\u0443\u0431."}; \u0434\u0435\u043d\u0435\u0436\u043d\u043e\u0435 \u043d\u0435\u0442\u0442\u043e ${signedNetDisplay}.`
|
||||||
);
|
);
|
||||||
|
if (limitLine) {
|
||||||
|
lines.push(limitLine);
|
||||||
|
}
|
||||||
lines.push(
|
lines.push(
|
||||||
cashflowPolarityRequested
|
cashflowPolarityRequested
|
||||||
? "\u042d\u0442\u043e \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u043f\u043e\u0442\u043e\u043a \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c 1\u0421, \u043d\u0435 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442."
|
? "\u042d\u0442\u043e \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u043f\u043e\u0442\u043e\u043a \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c 1\u0421, \u043d\u0435 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442."
|
||||||
|
|
@ -1116,12 +1186,8 @@ function buildCompactBusinessOverviewReply(
|
||||||
);
|
);
|
||||||
return joinBusinessReplyLines(lines);
|
return joinBusinessReplyLines(lines);
|
||||||
}
|
}
|
||||||
const headline = toNonEmptyString(draft.headline);
|
|
||||||
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
|
|
||||||
lines.push(
|
lines.push(
|
||||||
cleanHeadline
|
"Коротко: чистую прибыль и маржу не подтверждаю по текущему срезу 1С; видны только ограниченные денежные обороты и расчетное нетто/операционный сигнал, а не полный отчет о прибыли и не бухгалтерский финансовый результат."
|
||||||
? `Коротко: ${localizeLine(cleanHeadline)}`
|
|
||||||
: "Коротко: нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только ограниченный операционный денежный/товарный сигнал, а не полный отчет о прибыли и не бухгалтерский финансовый результат."
|
|
||||||
);
|
);
|
||||||
const boundaryLines = userFacingLines([
|
const boundaryLines = userFacingLines([
|
||||||
...toStringList(draft.confirmed_lines),
|
...toStringList(draft.confirmed_lines),
|
||||||
|
|
@ -1221,10 +1287,13 @@ function buildCompactBusinessOverviewReply(
|
||||||
? `${vendorProcurementQuality.top_non_financial_supplier_share_pct}%`
|
? `${vendorProcurementQuality.top_non_financial_supplier_share_pct}%`
|
||||||
: null;
|
: null;
|
||||||
const periodScope = toNonEmptyString(vendorProcurementQuality.period_scope) ?? period;
|
const periodScope = toNonEmptyString(vendorProcurementQuality.period_scope) ?? period;
|
||||||
const totalText = totalOutgoing ? `; всего исходящих платежей в проверенном срезе ${totalOutgoing}` : "";
|
const periodPhrase = businessOverviewPeriodPhrase(periodScope);
|
||||||
|
const totalText = totalOutgoing
|
||||||
|
? `; всего исходящих платежей в проверенном срезе ${sentenceAmount(totalOutgoing)}`
|
||||||
|
: "";
|
||||||
if (status === "financial_institution_leads_outgoing_cash") {
|
if (status === "financial_institution_leads_outgoing_cash") {
|
||||||
lines.push(
|
lines.push(
|
||||||
`Коротко: проверка концентрации закупок/исходящих платежей за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`
|
`Коротко: проверка концентрации закупок/исходящих платежей ${periodPhrase} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`
|
||||||
);
|
);
|
||||||
const financialHintText = financialFlowHintTextRuFromRecord(topOutgoingRecord);
|
const financialHintText = financialFlowHintTextRuFromRecord(topOutgoingRecord);
|
||||||
if (financialHintText) {
|
if (financialHintText) {
|
||||||
|
|
@ -1237,11 +1306,11 @@ function buildCompactBusinessOverviewReply(
|
||||||
}
|
}
|
||||||
} else if (status === "reviewed_procurement_concentration") {
|
} else if (status === "reviewed_procurement_concentration") {
|
||||||
lines.push(
|
lines.push(
|
||||||
`Коротко: точный риск зависимости от одного поставщика не подтвержден полностью; проверка концентрации закупок/исходящих платежей за ${periodScope} нашла крупнейшего получателя исходящего потока: ${topOutgoingName ?? nonFinancialName ?? "получатель не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : nonFinancialShare ? ` держит около ${nonFinancialShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}${totalText}.`
|
`Коротко: точный риск зависимости от одного поставщика не подтвержден полностью; проверка концентрации закупок/исходящих платежей ${periodPhrase} нашла крупнейшего получателя исходящего потока: ${topOutgoingName ?? nonFinancialName ?? "получатель не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : nonFinancialShare ? ` держит около ${nonFinancialShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}${totalText}.`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
lines.push(
|
lines.push(
|
||||||
`Коротко: проверка концентрации закупок/исходящих платежей за ${periodScope} выполнена, но надежной небанковской концентрации поставщика по найденным исходящим платежам не хватает${totalText}.`
|
`Коротко: проверка концентрации закупок/исходящих платежей ${periodPhrase} выполнена, но надежной небанковской концентрации поставщика по найденным исходящим платежам не хватает${totalText}.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const contractText =
|
const contractText =
|
||||||
|
|
@ -1251,7 +1320,7 @@ function buildCompactBusinessOverviewReply(
|
||||||
: ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts} договоров.`
|
: ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts} договоров.`
|
||||||
: "";
|
: "";
|
||||||
lines.push(
|
lines.push(
|
||||||
`Что не доказано этим срезом: надежность поставщика, качество поставок, договорные условия, назначение каждого платежа и полная структура всех расходов.${contractText}`
|
`Это не аудит поставщиков: срез не доказывает надежность поставщика, качество поставок, договорные условия, назначение каждого платежа и полную структуру всех расходов.${contractText}`
|
||||||
);
|
);
|
||||||
return joinBusinessReplyLines(lines);
|
return joinBusinessReplyLines(lines);
|
||||||
}
|
}
|
||||||
|
|
@ -1305,6 +1374,26 @@ function buildCompactBusinessOverviewReply(
|
||||||
return joinBusinessReplyLines(lines);
|
return joinBusinessReplyLines(lines);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!separateSubject &&
|
||||||
|
!crossScopeExecutiveSummary &&
|
||||||
|
!counterpartyLeaderSurfaceRequested &&
|
||||||
|
!rankingNeed &&
|
||||||
|
nextChecksRequested
|
||||||
|
) {
|
||||||
|
const subject = organizationScope ? `по компании ${organizationScope}` : "по компании";
|
||||||
|
lines.push(
|
||||||
|
`В первую очередь ${subject} надо проверить чистую прибыль через закрытие 90/91/99, зависимость от топ-3 клиентов и поставщиков, долги/зависшие расчеты, складские остатки и НДС-позицию.`
|
||||||
|
);
|
||||||
|
lines.push(
|
||||||
|
"Смысл порядка: сначала отделить бухгалтерский финрезультат от денежного нетто, затем проверить концентрацию потоков и только после этого углубляться в склад, долги и налоговую позицию."
|
||||||
|
);
|
||||||
|
if (limitLine) {
|
||||||
|
lines.push(limitLine);
|
||||||
|
}
|
||||||
|
return joinBusinessReplyLines(lines);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!separateSubject &&
|
!separateSubject &&
|
||||||
!crossScopeExecutiveSummary &&
|
!crossScopeExecutiveSummary &&
|
||||||
|
|
@ -1315,9 +1404,9 @@ function buildCompactBusinessOverviewReply(
|
||||||
const subject = organizationScope ?? "компания";
|
const subject = organizationScope ?? "компания";
|
||||||
const periodWithoutPrefix = period.replace(/^за\s+/iu, "");
|
const periodWithoutPrefix = period.replace(/^за\s+/iu, "");
|
||||||
lines.push(
|
lines.push(
|
||||||
`Коротко: по доступным данным ${subject} выглядит как бизнес с крупными контрактными денежными потоками и заметной зависимостью от нескольких крупных контрагентов, а не как равномерный поток мелких продаж.`
|
`Коротко: по ограниченному проверенному 1С-срезу ${subject} выглядит как бизнес с крупными контрактными денежными потоками и заметной зависимостью от нескольких крупных контрагентов, а не как равномерный поток мелких продаж; это не аудиторское заключение и не подтвержденная чистая прибыль.`
|
||||||
);
|
);
|
||||||
lines.push("Что видно:");
|
lines.push("Что видно в ограниченном денежном срезе:");
|
||||||
if (incomingAmount) {
|
if (incomingAmount) {
|
||||||
lines.push(`- входящие деньги за ${periodWithoutPrefix}: ${incomingAmount};`);
|
lines.push(`- входящие деньги за ${periodWithoutPrefix}: ${incomingAmount};`);
|
||||||
}
|
}
|
||||||
|
|
@ -1353,9 +1442,11 @@ function buildCompactBusinessOverviewReply(
|
||||||
if (debtLine) {
|
if (debtLine) {
|
||||||
lines.push(`- ${localizeLine(debtLine)}`);
|
lines.push(`- ${localizeLine(debtLine)}`);
|
||||||
}
|
}
|
||||||
lines.push(
|
if (!limitLine) {
|
||||||
"Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль."
|
lines.push(
|
||||||
);
|
"Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль."
|
||||||
|
);
|
||||||
|
}
|
||||||
const missingOverviewFamilies: string[] = [];
|
const missingOverviewFamilies: string[] = [];
|
||||||
if (!taxLine) {
|
if (!taxLine) {
|
||||||
missingOverviewFamilies.push("НДС/налоговая позиция без отдельного точного расчета");
|
missingOverviewFamilies.push("НДС/налоговая позиция без отдельного точного расчета");
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,21 @@ function hasInternalMechanics(value: string): boolean {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripRoutineShortAnswerPrefix(value: string): string {
|
||||||
|
return String(value ?? "")
|
||||||
|
.split(/\r?\n/g)
|
||||||
|
.map((line) =>
|
||||||
|
line
|
||||||
|
.replace(
|
||||||
|
/^((?:\s|\uFEFF)*(?:[-*]\s*)?)(?:\u041a\u043e\u0440\u043e\u0442\u043a\u043e:\s*)+/u,
|
||||||
|
"$1"
|
||||||
|
)
|
||||||
|
.replace(/(:\s*)\u041a\u043e\u0440\u043e\u0442\u043a\u043e:\s*/u, "$1")
|
||||||
|
)
|
||||||
|
.join("\n")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
function isMcpDiscoveryEntryPointContract(value: unknown): value is AssistantMcpDiscoveryRuntimeEntryPointContract {
|
function isMcpDiscoveryEntryPointContract(value: unknown): value is AssistantMcpDiscoveryRuntimeEntryPointContract {
|
||||||
const record = toRecordObject(value);
|
const record = toRecordObject(value);
|
||||||
return (
|
return (
|
||||||
|
|
@ -1052,12 +1067,13 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||||
}
|
}
|
||||||
|
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_applied");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_applied");
|
||||||
|
const replyText = stripRoutineShortAnswerPrefix(String(candidate.reply_text));
|
||||||
return {
|
return {
|
||||||
schema_version: ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION,
|
schema_version: ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION,
|
||||||
policy_owner: "assistantMcpDiscoveryResponsePolicy",
|
policy_owner: "assistantMcpDiscoveryResponsePolicy",
|
||||||
decision: "apply_candidate",
|
decision: "apply_candidate",
|
||||||
applied: true,
|
applied: true,
|
||||||
reply_text: String(candidate.reply_text),
|
reply_text: replyText || String(candidate.reply_text),
|
||||||
reply_source: "mcp_discovery_response_candidate_guarded",
|
reply_source: "mcp_discovery_response_candidate_guarded",
|
||||||
candidate,
|
candidate,
|
||||||
reason_codes: reasonCodes
|
reason_codes: reasonCodes
|
||||||
|
|
|
||||||
|
|
@ -225,6 +225,9 @@ function isGarbageSemanticAnchorCandidate(value: string | null): boolean {
|
||||||
/^(?:и\s+)?кто\s+(?:главн\p{L}*|основн\p{L}*|крупн\p{L}*)\s+(?:клиент|покупател|поставщик|контрагент)(?:\s+в)?$/iu.test(
|
/^(?:и\s+)?кто\s+(?:главн\p{L}*|основн\p{L}*|крупн\p{L}*)\s+(?:клиент|покупател|поставщик|контрагент)(?:\s+в)?$/iu.test(
|
||||||
text
|
text
|
||||||
) ||
|
) ||
|
||||||
|
/^(?:и\s+)?(?:главн\p{L}*|основн\p{L}*|крупн\p{L}*)\s+(?:клиент|покупател|поставщик|контрагент)(?:\s+в)?$/iu.test(
|
||||||
|
text
|
||||||
|
) ||
|
||||||
/^(?:или\s+)?(?:обычн\p{L}*\s+)?(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент)(?:\s+или\s+(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент))?$/iu.test(
|
/^(?:или\s+)?(?:обычн\p{L}*\s+)?(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент)(?:\s+или\s+(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент))?$/iu.test(
|
||||||
text
|
text
|
||||||
) ||
|
) ||
|
||||||
|
|
@ -900,6 +903,22 @@ function hasDebtDueDateBoundaryFollowupSignal(text: string): boolean {
|
||||||
return hasDueDateCue && hasFollowupShape;
|
return hasDueDateCue && hasFollowupShape;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasSupplierDependencyFollowupSignal(text: string): boolean {
|
||||||
|
if (!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const normalized = compactLower(text);
|
||||||
|
const hasSupplierOrOutgoingCue =
|
||||||
|
/(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0437\u0430\u043a\u0443\u043f|\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u043f\u043b\u0430\u0442[\u0435\u0451]\u0436|\u0437\u0430\u043f\u043b\u0430\u0442|supplier|vendor|procurement|outgoing|payout)/iu.test(
|
||||||
|
normalized
|
||||||
|
);
|
||||||
|
const hasDependencyCue =
|
||||||
|
/(?:\u0437\u0430\u0432\u0438\u0441\u0438\u043c|\u0437\u0430\u0432\u044f\u0437\u0430\u043d|\u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446|\u043a\u043b\u044e\u0447\u0435\u0432|\u043e\u0441\u043d\u043e\u0432\u043d|\u043a\u0440\u0438\u0442\u0438\u0447|dependency|concentration|key|critical)/iu.test(
|
||||||
|
normalized
|
||||||
|
);
|
||||||
|
return hasSupplierOrOutgoingCue && hasDependencyCue;
|
||||||
|
}
|
||||||
|
|
||||||
function hasOrganizationLevelDebtDueDateOverviewSignal(text: string): boolean {
|
function hasOrganizationLevelDebtDueDateOverviewSignal(text: string): boolean {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -1079,7 +1098,7 @@ function hasBusinessOverviewContinuationSignal(text: string): boolean {
|
||||||
normalized
|
normalized
|
||||||
);
|
);
|
||||||
const hasAnalystContinuationCue =
|
const hasAnalystContinuationCue =
|
||||||
/(?:можно\s+ли|если\s+нет|proxy|прокси|аудит|оцен|что\s+думаешь|нормальн\p{L}*\s+прибыл|прибыл|марж|рентаб|ликвидн|просроч|качество\s+долг|риск|налогов\p{L}*\s+вывод)/iu.test(
|
/(?:можно\s+ли|если\s+нет|proxy|прокси|аудит|оцен|что\s+думаешь|нормальн\p{L}*\s+прибыл|прибыл|марж|рентаб|ликвидн|просроч|качество\s+долг|риск|зависим|завязан|концентрац|налогов\p{L}*\s+вывод)/iu.test(
|
||||||
normalized
|
normalized
|
||||||
);
|
);
|
||||||
const hasTaxContinuationCue =
|
const hasTaxContinuationCue =
|
||||||
|
|
@ -1865,7 +1884,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
hasOrganizationLevelDebtDueDateOverviewSignal(rawText)) ||
|
hasOrganizationLevelDebtDueDateOverviewSignal(rawText)) ||
|
||||||
debtDueDateFollowupBusinessOverviewSignal;
|
debtDueDateFollowupBusinessOverviewSignal;
|
||||||
const supplierQualityBusinessOverviewSignal =
|
const supplierQualityBusinessOverviewSignal =
|
||||||
(rawBusinessOverviewSignal || seededBusinessOverviewSignal) && hasOrganizationLevelSupplierQualityOverviewSignal(rawText);
|
((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
||||||
|
(hasOrganizationLevelSupplierQualityOverviewSignal(rawText) || hasSupplierDependencyFollowupSignal(rawText))) ||
|
||||||
|
(businessOverviewContinuationSignal && hasSupplierDependencyFollowupSignal(rawText));
|
||||||
const profitMarginFollowupBusinessOverviewSignal =
|
const profitMarginFollowupBusinessOverviewSignal =
|
||||||
businessOverviewContinuationSignal && hasProfitMarginBoundaryFollowupSignal(rawText);
|
businessOverviewContinuationSignal && hasProfitMarginBoundaryFollowupSignal(rawText);
|
||||||
const profitMarginBusinessOverviewSignal =
|
const profitMarginBusinessOverviewSignal =
|
||||||
|
|
@ -2458,6 +2479,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
? null
|
? null
|
||||||
: explicitCurrentCounterpartyOverridesFollowupEntity
|
: explicitCurrentCounterpartyOverridesFollowupEntity
|
||||||
? null
|
? null
|
||||||
|
: businessOverviewSuppressesFollowupCounterparty
|
||||||
|
? rawMetadataScopeHint
|
||||||
: rawMetadataScopeHint ??
|
: rawMetadataScopeHint ??
|
||||||
followupSeed.metadataScopeHint ??
|
followupSeed.metadataScopeHint ??
|
||||||
followupSeed.discoveryEntity ??
|
followupSeed.discoveryEntity ??
|
||||||
|
|
@ -2685,7 +2708,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
normalizedPredecomposeDateScope &&
|
normalizedPredecomposeDateScope &&
|
||||||
normalizedPredecomposeDateScope.startsWith(`${rawDateScope}-`)
|
normalizedPredecomposeDateScope.startsWith(`${rawDateScope}-`)
|
||||||
);
|
);
|
||||||
const followupAllTimeScopeApplied = normalizedFollowupDateScope === "all_time_scope";
|
const followupAllTimeScopeApplied = normalizedFollowupDateScope === "all_time_scope" && !currentTurnCarriesExplicitPeriod;
|
||||||
const explicitDateScope =
|
const explicitDateScope =
|
||||||
rawAllTimeScopeSignal || followupAllTimeScopeApplied
|
rawAllTimeScopeSignal || followupAllTimeScopeApplied
|
||||||
? null
|
? null
|
||||||
|
|
@ -3081,6 +3104,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
if (debtDueDateFollowupBusinessOverviewSignal) {
|
if (debtDueDateFollowupBusinessOverviewSignal) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_business_overview_debt_due_date_followup_boundary");
|
pushReason(reasonCodes, "mcp_discovery_business_overview_debt_due_date_followup_boundary");
|
||||||
}
|
}
|
||||||
|
if (supplierQualityBusinessOverviewSignal && hasSupplierDependencyFollowupSignal(rawText)) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_business_overview_supplier_dependency_followup_boundary");
|
||||||
|
}
|
||||||
if (explicitVatSuppressesBusinessOverviewContinuation) {
|
if (explicitVatSuppressesBusinessOverviewContinuation) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_suppressed_by_explicit_vat_question");
|
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_suppressed_by_explicit_vat_question");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1982,7 +1982,11 @@ function repairAddressMojibake(value) {
|
||||||
}
|
}
|
||||||
function sanitizeOutgoingAssistantText(value, fallback = "Не смог сформировать читаемый ответ. Уточните запрос.") {
|
function sanitizeOutgoingAssistantText(value, fallback = "Не смог сформировать читаемый ответ. Уточните запрос.") {
|
||||||
const repaired = repairAddressMojibake(String(value ?? ""));
|
const repaired = repairAddressMojibake(String(value ?? ""));
|
||||||
const sanitized = String((0, answerComposer_1.sanitizeAssistantReplyForUserFacing)(repaired) ?? "").trim();
|
const sanitized = String((0, answerComposer_1.sanitizeAssistantReplyForUserFacing)(repaired) ?? "")
|
||||||
|
.split(/\r?\n/g)
|
||||||
|
.map((line) => line.replace(/^Коротко:\s*/u, "").replace(/(:\s*)Коротко:\s*/u, "$1"))
|
||||||
|
.join("\n")
|
||||||
|
.trim();
|
||||||
if (sanitized) {
|
if (sanitized) {
|
||||||
return sanitized;
|
return sanitized;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -301,7 +301,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
return `${match[3]}-${match[2]}-${match[1]}`;
|
return `${match[3]}-${match[2]}-${match[1]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeMonthWindowFromIso(isoDate) {
|
function computeMonthWindowFromIso(isoDate, bridgeFacts = {}) {
|
||||||
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -318,11 +318,17 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
purchase_date: isoDate,
|
purchase_date: isoDate,
|
||||||
period_from: `${year}-${mm}-01`,
|
period_from: `${year}-${mm}-01`,
|
||||||
period_to: `${year}-${mm}-${dd}`,
|
period_to: `${year}-${mm}-${dd}`,
|
||||||
as_of_date: `${year}-${mm}-${dd}`
|
as_of_date: `${year}-${mm}-${dd}`,
|
||||||
|
purchase_date_bridge_selected: isoDate,
|
||||||
|
...bridgeFacts
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractEarliestDmyDateFromEntityRefs(entityRefs) {
|
function uniqueSortedIsoDates(dates) {
|
||||||
|
return Array.from(new Set(dates.filter(Boolean))).sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractDmyDatesFromEntityRefs(entityRefs) {
|
||||||
const dates = [];
|
const dates = [];
|
||||||
for (const entityRef of Array.isArray(entityRefs) ? entityRefs : []) {
|
for (const entityRef of Array.isArray(entityRefs) ? entityRefs : []) {
|
||||||
const value = deps.toNonEmptyString(entityRef?.value);
|
const value = deps.toNonEmptyString(entityRef?.value);
|
||||||
|
|
@ -340,40 +346,96 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (dates.length === 0) {
|
return uniqueSortedIsoDates(dates);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return dates.sort()[0] ?? null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractPurchaseDateBridgeWindow(previousAddressItem, addressNavigationState) {
|
function detectPurchaseDateBridgeBasis(userMessage, alternateMessage) {
|
||||||
|
const text = [userMessage, alternateMessage]
|
||||||
|
.map((item) => deps.compactWhitespace(deps.repairAddressMojibake(String(item ?? "")).toLowerCase()).replace(/ё/g, "е"))
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
if (/(?:последн|latest|last|сам[а-я\s-]*поздн)/iu.test(text)) {
|
||||||
|
return "last_confirmed_purchase";
|
||||||
|
}
|
||||||
|
if (/(?:перв|earliest|oldest|ранн|сам[а-я\s-]*стар)/iu.test(text)) {
|
||||||
|
return "first_confirmed_purchase";
|
||||||
|
}
|
||||||
|
return "first_confirmed_purchase_default";
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPurchaseDateBridgeFacts(previousAddressItem, addressNavigationState, userMessage, alternateMessage) {
|
||||||
|
const previousFilters = readAddressDebugFilters(previousAddressItem?.debug ?? null);
|
||||||
|
let firstIsoDate = deps.toNonEmptyString(previousFilters?.purchase_date_bridge_first);
|
||||||
|
let lastIsoDate = deps.toNonEmptyString(previousFilters?.purchase_date_bridge_last);
|
||||||
|
const selectedIsoDateFromFilters = deps.toNonEmptyString(previousFilters?.purchase_date_bridge_selected);
|
||||||
|
if (!firstIsoDate && selectedIsoDateFromFilters) {
|
||||||
|
firstIsoDate = selectedIsoDateFromFilters;
|
||||||
|
}
|
||||||
|
if (!lastIsoDate && selectedIsoDateFromFilters) {
|
||||||
|
lastIsoDate = selectedIsoDateFromFilters;
|
||||||
|
}
|
||||||
const replyText = deps.toNonEmptyString(previousAddressItem?.text);
|
const replyText = deps.toNonEmptyString(previousAddressItem?.text);
|
||||||
if (replyText) {
|
if (replyText && (!firstIsoDate || !lastIsoDate)) {
|
||||||
const repairedText = deps.repairAddressMojibake(replyText);
|
const repairedText = deps.repairAddressMojibake(replyText);
|
||||||
const explicitFirstDateMatch = repairedText.match(/первая\s+найденная\s+дата\s+закупки:\s*(\d{2}\.\d{2}\.\d{4})/iu);
|
const explicitFirstDateMatch = repairedText.match(
|
||||||
const explicitFirstDateIso = explicitFirstDateMatch ? parseDmyDateToIso(explicitFirstDateMatch[1]) : null;
|
/первая\s+(?:найденная|подтвержденная)\s+дата\s+закупки:\s*(\d{2}\.\d{2}\.\d{4})/iu
|
||||||
if (explicitFirstDateIso) {
|
);
|
||||||
return computeMonthWindowFromIso(explicitFirstDateIso);
|
const explicitLastDateMatch = repairedText.match(
|
||||||
}
|
/последняя\s+(?:найденная|подтвержденная)\s+дата\s+закупки:\s*(\d{2}\.\d{2}\.\d{4})/iu
|
||||||
|
);
|
||||||
|
firstIsoDate = explicitFirstDateMatch ? parseDmyDateToIso(explicitFirstDateMatch[1]) : null;
|
||||||
|
lastIsoDate = explicitLastDateMatch ? parseDmyDateToIso(explicitLastDateMatch[1]) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const navigationSessionState = resolveNavigationSessionContextState(
|
if (!firstIsoDate || !lastIsoDate) {
|
||||||
addressNavigationState,
|
const navigationSessionState = resolveNavigationSessionContextState(
|
||||||
deps.toNonEmptyString,
|
addressNavigationState,
|
||||||
deps.normalizeOrganizationScopeValue
|
deps.toNonEmptyString,
|
||||||
);
|
deps.normalizeOrganizationScopeValue
|
||||||
const focusObject = navigationSessionState.focusObject;
|
);
|
||||||
const preferredResultSetId =
|
const focusObject = navigationSessionState.focusObject;
|
||||||
deps.toNonEmptyString(focusObject?.provenanceResultSetId) ?? navigationSessionState.activeResultSetId;
|
const preferredResultSetId =
|
||||||
const resultSets = Array.isArray(addressNavigationState?.result_sets) ? addressNavigationState.result_sets : [];
|
deps.toNonEmptyString(focusObject?.provenanceResultSetId) ?? navigationSessionState.activeResultSetId;
|
||||||
const preferredResultSet =
|
const resultSets = Array.isArray(addressNavigationState?.result_sets) ? addressNavigationState.result_sets : [];
|
||||||
(preferredResultSetId
|
const preferredResultSet =
|
||||||
? resultSets.find((item) => deps.toNonEmptyString(item?.result_set_id) === preferredResultSetId) ?? null
|
(preferredResultSetId
|
||||||
: null) ??
|
? resultSets.find((item) => deps.toNonEmptyString(item?.result_set_id) === preferredResultSetId) ?? null
|
||||||
resultSets.find((item) => deps.toNonEmptyString(item?.intent) === "inventory_purchase_provenance_for_item") ??
|
: null) ??
|
||||||
null;
|
resultSets.find((item) => deps.toNonEmptyString(item?.intent) === "inventory_purchase_provenance_for_item") ??
|
||||||
const earliestIsoDate = extractEarliestDmyDateFromEntityRefs(preferredResultSet?.entity_refs);
|
null;
|
||||||
return earliestIsoDate ? computeMonthWindowFromIso(earliestIsoDate) : null;
|
const datesFromResultSet = extractDmyDatesFromEntityRefs(preferredResultSet?.entity_refs);
|
||||||
|
firstIsoDate = firstIsoDate ?? datesFromResultSet[0] ?? null;
|
||||||
|
lastIsoDate = lastIsoDate ?? datesFromResultSet[datesFromResultSet.length - 1] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!firstIsoDate && !lastIsoDate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedFirst = firstIsoDate ?? lastIsoDate;
|
||||||
|
const normalizedLast = lastIsoDate ?? firstIsoDate;
|
||||||
|
const basis = detectPurchaseDateBridgeBasis(userMessage, alternateMessage);
|
||||||
|
const selectedIsoDate = basis === "last_confirmed_purchase" ? normalizedLast : normalizedFirst;
|
||||||
|
return {
|
||||||
|
selectedIsoDate,
|
||||||
|
firstIsoDate: normalizedFirst,
|
||||||
|
lastIsoDate: normalizedLast,
|
||||||
|
basis,
|
||||||
|
hasMultiplePurchaseDates: Boolean(normalizedFirst && normalizedLast && normalizedFirst !== normalizedLast)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPurchaseDateBridgeWindow(previousAddressItem, addressNavigationState, userMessage, alternateMessage) {
|
||||||
|
const facts = extractPurchaseDateBridgeFacts(previousAddressItem, addressNavigationState, userMessage, alternateMessage);
|
||||||
|
if (!facts?.selectedIsoDate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return computeMonthWindowFromIso(facts.selectedIsoDate, {
|
||||||
|
purchase_date_bridge_first: facts.firstIsoDate,
|
||||||
|
purchase_date_bridge_last: facts.lastIsoDate,
|
||||||
|
purchase_date_bridge_basis: facts.basis,
|
||||||
|
purchase_date_bridge_has_multiple: facts.hasMultiplePurchaseDates
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function isUsableFollowupSourceDebug(debug) {
|
function isUsableFollowupSourceDebug(debug) {
|
||||||
|
|
@ -605,11 +667,20 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasInventoryItemFocusHint) {
|
function hasInventoryPurchaseDateVatBridgeSignal(
|
||||||
|
userMessage,
|
||||||
|
alternateMessage,
|
||||||
|
sourceIntentHint,
|
||||||
|
hasInventoryItemFocusHint,
|
||||||
|
sourceHasPurchaseDateBridgeHint = false
|
||||||
|
) {
|
||||||
|
const vatPurchaseBridgeContinuation =
|
||||||
|
sourceIntentHint === "vat_liability_confirmed_for_tax_period" && sourceHasPurchaseDateBridgeHint;
|
||||||
if (
|
if (
|
||||||
sourceIntentHint !== "inventory_purchase_provenance_for_item" &&
|
sourceIntentHint !== "inventory_purchase_provenance_for_item" &&
|
||||||
!hasInventoryItemFocusHint &&
|
!hasInventoryItemFocusHint &&
|
||||||
!deps.isInventorySelectedObjectIntent(sourceIntentHint)
|
!deps.isInventorySelectedObjectIntent(sourceIntentHint) &&
|
||||||
|
!vatPurchaseBridgeContinuation
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -621,10 +692,13 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
}
|
}
|
||||||
return samples.some(
|
return samples.some(
|
||||||
(sample) =>
|
(sample) =>
|
||||||
/(?:ндс|vat)/iu.test(sample) &&
|
((/(?:ндс|vat)/iu.test(sample) &&
|
||||||
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/iu.test(
|
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/iu.test(
|
||||||
sample
|
sample
|
||||||
)
|
)) ||
|
||||||
|
((vatPurchaseBridgeContinuation || sourceIntentHint === "vat_liability_confirmed_for_tax_period") &&
|
||||||
|
/(?:перв|последн|earliest|oldest|latest|last)/iu.test(sample) &&
|
||||||
|
/(?:покупк|закупк|purchase)/iu.test(sample)))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -932,10 +1006,17 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
|
sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
|
||||||
const navigationSessionState = earlyNavigationSessionState;
|
const navigationSessionState = earlyNavigationSessionState;
|
||||||
const navigationFocusObjectHint = navigationSessionState.focusObject;
|
const navigationFocusObjectHint = navigationSessionState.focusObject;
|
||||||
|
const sourceFiltersHint = readAddressDebugFilters(carryoverSourceDebug);
|
||||||
|
const sourceHasPurchaseDateBridgeHint = Boolean(
|
||||||
|
deps.toNonEmptyString(sourceFiltersHint?.purchase_date_bridge_selected)
|
||||||
|
);
|
||||||
const hasNavigationInventoryItemFocusHint = Boolean(
|
const hasNavigationInventoryItemFocusHint = Boolean(
|
||||||
deps.toNonEmptyString(navigationFocusObjectHint?.label) &&
|
deps.toNonEmptyString(navigationFocusObjectHint?.label) &&
|
||||||
deps.toNonEmptyString(navigationFocusObjectHint?.objectType) === "item" &&
|
deps.toNonEmptyString(navigationFocusObjectHint?.objectType) === "item" &&
|
||||||
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||||
|
sourceIntentHint === "vat_liability_confirmed_for_tax_period" ||
|
||||||
|
sourceIntentHint === "vat_payable_forecast" ||
|
||||||
|
sourceIntentHint === "vat_payable_confirmed_as_of_date" ||
|
||||||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
deps.isInventorySelectedObjectIntent(sourceIntentHint))
|
deps.isInventorySelectedObjectIntent(sourceIntentHint))
|
||||||
);
|
);
|
||||||
|
|
@ -943,7 +1024,8 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
userMessage,
|
userMessage,
|
||||||
alternateMessage,
|
alternateMessage,
|
||||||
sourceIntentHint,
|
sourceIntentHint,
|
||||||
hasNavigationInventoryItemFocusHint
|
hasNavigationInventoryItemFocusHint,
|
||||||
|
sourceHasPurchaseDateBridgeHint
|
||||||
);
|
);
|
||||||
const inventoryMarginRankingFollowup = hasInventoryMarginRankingFollowupSignal(
|
const inventoryMarginRankingFollowup = hasInventoryMarginRankingFollowupSignal(
|
||||||
userMessage,
|
userMessage,
|
||||||
|
|
@ -1457,10 +1539,21 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
readAddressDebugItem(carryoverSourceDebug, deps.toNonEmptyString) ??
|
readAddressDebugItem(carryoverSourceDebug, deps.toNonEmptyString) ??
|
||||||
deps.toNonEmptyString(previousFilters.item)
|
deps.toNonEmptyString(previousFilters.item)
|
||||||
) ?? previousAddressItem;
|
) ?? previousAddressItem;
|
||||||
const purchaseBridgeWindow = extractPurchaseDateBridgeWindow(purchaseBridgeItem, addressNavigationState);
|
const purchaseBridgeWindow = extractPurchaseDateBridgeWindow(
|
||||||
|
purchaseBridgeItem,
|
||||||
|
addressNavigationState,
|
||||||
|
userMessage,
|
||||||
|
alternateMessage
|
||||||
|
);
|
||||||
if (purchaseBridgeWindow) {
|
if (purchaseBridgeWindow) {
|
||||||
previousFilters.period_from = purchaseBridgeWindow.period_from;
|
previousFilters.period_from = purchaseBridgeWindow.period_from;
|
||||||
previousFilters.period_to = purchaseBridgeWindow.period_to;
|
previousFilters.period_to = purchaseBridgeWindow.period_to;
|
||||||
|
previousFilters.as_of_date = purchaseBridgeWindow.as_of_date;
|
||||||
|
previousFilters.purchase_date_bridge_selected = purchaseBridgeWindow.purchase_date_bridge_selected;
|
||||||
|
previousFilters.purchase_date_bridge_first = purchaseBridgeWindow.purchase_date_bridge_first;
|
||||||
|
previousFilters.purchase_date_bridge_last = purchaseBridgeWindow.purchase_date_bridge_last;
|
||||||
|
previousFilters.purchase_date_bridge_basis = purchaseBridgeWindow.purchase_date_bridge_basis;
|
||||||
|
previousFilters.purchase_date_bridge_has_multiple = purchaseBridgeWindow.purchase_date_bridge_has_multiple;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
previousFilters = applyTemporalCarryoverFilters(
|
previousFilters = applyTemporalCarryoverFilters(
|
||||||
|
|
|
||||||
|
|
@ -426,6 +426,25 @@ function detectBroadBusinessEvaluation(text) {
|
||||||
family: "broad_business_evaluation"
|
family: "broad_business_evaluation"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
/(?:что\s+(?:дальше\s+)?провер\w*|что\s+провер\w*\s+дальше|следующ\w*\s+шаг|в\s+первую\s+очередь\s+провер\w*|next\s+checks?|next\s+steps?)/iu.test(
|
||||||
|
normalized
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
family: "broad_business_evaluation"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
/(?:поставщик|закуп|исходящ|плат[её]ж|supplier|vendor|procurement|outgoing|payout)/iu.test(normalized) &&
|
||||||
|
/(?:зависим|завязан|концентрац|ключев|основн|критич|dependency|concentration|key|critical)/iu.test(
|
||||||
|
normalized
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
family: "broad_business_evaluation"
|
||||||
|
};
|
||||||
|
}
|
||||||
if (hasOrganizationLevelEarningsOverviewSignal(normalized)) {
|
if (hasOrganizationLevelEarningsOverviewSignal(normalized)) {
|
||||||
return {
|
return {
|
||||||
family: "broad_business_evaluation"
|
family: "broad_business_evaluation"
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,11 @@ export interface AddressFilterSet {
|
||||||
warehouse?: string;
|
warehouse?: string;
|
||||||
document_type?: string;
|
document_type?: string;
|
||||||
document_ref?: string;
|
document_ref?: string;
|
||||||
|
purchase_date_bridge_selected?: string;
|
||||||
|
purchase_date_bridge_first?: string;
|
||||||
|
purchase_date_bridge_last?: string;
|
||||||
|
purchase_date_bridge_basis?: string;
|
||||||
|
purchase_date_bridge_has_multiple?: boolean;
|
||||||
status?: string;
|
status?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
sort?: "period_desc" | "period_asc";
|
sort?: "period_desc" | "period_asc";
|
||||||
|
|
|
||||||
|
|
@ -244,7 +244,12 @@ describe("address follow-up temporal regressions", () => {
|
||||||
organization: 'ООО "Альтернатива Плюс"',
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
period_from: "2015-02-01",
|
period_from: "2015-02-01",
|
||||||
period_to: "2015-02-28",
|
period_to: "2015-02-28",
|
||||||
as_of_date: "2016-03-31"
|
as_of_date: "2015-02-28",
|
||||||
|
purchase_date_bridge_selected: "2015-02-05",
|
||||||
|
purchase_date_bridge_first: "2015-02-05",
|
||||||
|
purchase_date_bridge_last: "2015-07-22",
|
||||||
|
purchase_date_bridge_basis: "first_confirmed_purchase_default",
|
||||||
|
purchase_date_bridge_has_multiple: true
|
||||||
},
|
},
|
||||||
previous_anchor_type: "item",
|
previous_anchor_type: "item",
|
||||||
previous_anchor_value: "Рабочая станция универсального специалиста"
|
previous_anchor_value: "Рабочая станция универсального специалиста"
|
||||||
|
|
@ -254,7 +259,98 @@ describe("address follow-up temporal regressions", () => {
|
||||||
expect(result?.intent.intent).toBe("vat_liability_confirmed_for_tax_period");
|
expect(result?.intent.intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||||
expect(result?.filters.extracted_filters.period_from).toBe("2015-02-01");
|
expect(result?.filters.extracted_filters.period_from).toBe("2015-02-01");
|
||||||
expect(result?.filters.extracted_filters.period_to).toBe("2015-02-28");
|
expect(result?.filters.extracted_filters.period_to).toBe("2015-02-28");
|
||||||
expect(result?.baseReasons).toContain("period_from_followup_context");
|
expect(result?.filters.extracted_filters.as_of_date).toBe("2015-02-28");
|
||||||
|
expect(result?.filters.extracted_filters.purchase_date_bridge_selected).toBe("2015-02-05");
|
||||||
|
expect(result?.filters.extracted_filters.purchase_date_bridge_has_multiple).toBe(true);
|
||||||
|
expect(result?.baseReasons).toContain("period_from_purchase_date_bridge_followup_context");
|
||||||
|
expect(result?.baseReasons).toContain("purchase_date_bridge_from_followup_context");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not reuse purchase-date VAT bridge when the next VAT question has an explicit period", () => {
|
||||||
|
const result = runAddressDecomposeStage("а какой ндс мы должны сгрузить на март 2020?", {
|
||||||
|
previous_intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
target_intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
previous_filters: {
|
||||||
|
item: "Рабочая станция универсального специалиста",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
period_from: "2015-02-01",
|
||||||
|
period_to: "2015-02-28",
|
||||||
|
as_of_date: "2015-02-28",
|
||||||
|
purchase_date_bridge_selected: "2015-02-05",
|
||||||
|
purchase_date_bridge_first: "2015-02-05",
|
||||||
|
purchase_date_bridge_last: "2015-07-22",
|
||||||
|
purchase_date_bridge_basis: "first_confirmed_purchase_default",
|
||||||
|
purchase_date_bridge_has_multiple: true
|
||||||
|
},
|
||||||
|
previous_anchor_type: "organization",
|
||||||
|
previous_anchor_value: 'ООО "Альтернатива Плюс"'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.intent.intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||||
|
expect(result?.filters.extracted_filters.period_from).toBe("2020-03-01");
|
||||||
|
expect(result?.filters.extracted_filters.period_to).toBe("2020-03-31");
|
||||||
|
expect(result?.filters.extracted_filters.as_of_date).toBeUndefined();
|
||||||
|
expect(result?.filters.extracted_filters.purchase_date_bridge_selected).toBeUndefined();
|
||||||
|
expect(result?.baseReasons).toContain("purchase_date_bridge_suppressed_by_explicit_temporal_scope");
|
||||||
|
expect(result?.baseReasons).not.toContain("period_from_purchase_date_bridge_followup_context");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps first confirmed purchase VAT recalculation in the VAT lane without repeating VAT wording", () => {
|
||||||
|
const result = runAddressDecomposeStage("если у нее несколько закупок, посчитай по первой подтвержденной покупке", {
|
||||||
|
previous_intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
target_intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
previous_filters: {
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
period_from: "2015-02-01",
|
||||||
|
period_to: "2015-02-28",
|
||||||
|
as_of_date: "2015-02-28",
|
||||||
|
purchase_date_bridge_selected: "2015-02-05",
|
||||||
|
purchase_date_bridge_first: "2015-02-05",
|
||||||
|
purchase_date_bridge_last: "2015-07-22",
|
||||||
|
purchase_date_bridge_basis: "first_confirmed_purchase_default",
|
||||||
|
purchase_date_bridge_has_multiple: true
|
||||||
|
},
|
||||||
|
previous_anchor_type: "organization",
|
||||||
|
previous_anchor_value: 'ООО "Альтернатива Плюс"'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.intent.intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||||
|
expect(result?.filters.extracted_filters.purchase_date_bridge_selected).toBe("2015-02-05");
|
||||||
|
expect(result?.filters.extracted_filters.purchase_date_bridge_basis).toBe("first_confirmed_purchase");
|
||||||
|
expect(result?.filters.extracted_filters.period_from).toBe("2015-02-01");
|
||||||
|
expect(result?.filters.extracted_filters.period_to).toBe("2015-02-28");
|
||||||
|
expect(result?.baseReasons).toContain("intent_adjusted_to_inventory_purchase_date_vat_bridge");
|
||||||
|
expect(result?.baseReasons).toContain("purchase_date_bridge_from_followup_context");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps last confirmed purchase VAT recalculation and moves the tax period to the last purchase month", () => {
|
||||||
|
const result = runAddressDecomposeStage("а теперь по последней подтвержденной покупке", {
|
||||||
|
previous_intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
target_intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
previous_filters: {
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
period_from: "2015-02-01",
|
||||||
|
period_to: "2015-02-28",
|
||||||
|
as_of_date: "2015-02-28",
|
||||||
|
purchase_date_bridge_selected: "2015-02-05",
|
||||||
|
purchase_date_bridge_first: "2015-02-05",
|
||||||
|
purchase_date_bridge_last: "2015-07-22",
|
||||||
|
purchase_date_bridge_basis: "first_confirmed_purchase",
|
||||||
|
purchase_date_bridge_has_multiple: true
|
||||||
|
},
|
||||||
|
previous_anchor_type: "organization",
|
||||||
|
previous_anchor_value: 'ООО "Альтернатива Плюс"'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.intent.intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||||
|
expect(result?.filters.extracted_filters.purchase_date_bridge_selected).toBe("2015-07-22");
|
||||||
|
expect(result?.filters.extracted_filters.purchase_date_bridge_basis).toBe("last_confirmed_purchase");
|
||||||
|
expect(result?.filters.extracted_filters.period_from).toBe("2015-07-01");
|
||||||
|
expect(result?.filters.extracted_filters.period_to).toBe("2015-07-31");
|
||||||
|
expect(result?.filters.extracted_filters.as_of_date).toBe("2015-07-31");
|
||||||
});
|
});
|
||||||
it("clears counterparty noise from bare organization clarification selections that resume inventory", () => {
|
it("clears counterparty noise from bare organization clarification selections that resume inventory", () => {
|
||||||
const result = runAddressDecomposeStage("АЛЬТЕРНАТИВА", {
|
const result = runAddressDecomposeStage("АЛЬТЕРНАТИВА", {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,44 @@ import { runAddressDecomposeStage } from "../src/services/address_runtime/decomp
|
||||||
import { composeFactualReply } from "../src/services/address_runtime/composeStage";
|
import { composeFactualReply } from "../src/services/address_runtime/composeStage";
|
||||||
|
|
||||||
describe("inventory aging follow-up", () => {
|
describe("inventory aging follow-up", () => {
|
||||||
|
it("clears organization alias item on root old-stock questions with LLM organization semantics", () => {
|
||||||
|
const result = runAddressDecomposeStage(
|
||||||
|
'Есть ли у ООО "Альтернатива Плюс" остатки товара, которые закупались очень давно',
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
scope_target_kind: "organization",
|
||||||
|
scope_target_text: 'ООО "Альтернатива Плюс"',
|
||||||
|
date_scope_kind: "implicit_current",
|
||||||
|
self_scope_detected: false,
|
||||||
|
selected_object_scope_detected: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.intent.intent).toBe("inventory_aging_by_purchase_date");
|
||||||
|
expect(result?.filters.extracted_filters.organization).toBe('ООО "Альтернатива Плюс"');
|
||||||
|
expect(result?.filters.extracted_filters.item).toBeUndefined();
|
||||||
|
expect(result?.baseReasons).toContain("item_cleared_as_organization_scope_alias_for_stock_aging");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not mistake an explicit organization name for an inventory item in old-stock questions", () => {
|
||||||
|
const result = runAddressDecomposeStage('Есть ли у ООО "Альтернатива Плюс" остатки товара, которые закупались очень давно', {
|
||||||
|
previous_intent: "inventory_aging_by_purchase_date",
|
||||||
|
previous_filters: {
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
as_of_date: "2026-05-25"
|
||||||
|
},
|
||||||
|
previous_anchor_type: "organization",
|
||||||
|
previous_anchor_value: 'ООО "Альтернатива Плюс"'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.intent.intent).toBe("inventory_aging_by_purchase_date");
|
||||||
|
expect(result?.filters.extracted_filters.organization).toBe('ООО "Альтернатива Плюс"');
|
||||||
|
expect(result?.filters.extracted_filters.item).toBeUndefined();
|
||||||
|
expect(result?.baseReasons).toContain("item_cleared_as_organization_scope_alias_for_stock_aging");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps carried date window for 'на эту дату' aging follow-up", () => {
|
it("keeps carried date window for 'на эту дату' aging follow-up", () => {
|
||||||
const result = runAddressDecomposeStage("Какие остатки по товарам на эту дату относятся к старым закупкам", {
|
const result = runAddressDecomposeStage("Какие остатки по товарам на эту дату относятся к старым закупкам", {
|
||||||
previous_intent: "inventory_purchase_provenance_for_item",
|
previous_intent: "inventory_purchase_provenance_for_item",
|
||||||
|
|
|
||||||
|
|
@ -2182,6 +2182,49 @@ describe("address compose stage utf8 headers", () => {
|
||||||
expect(reply.text).toContain("книг продаж/покупок");
|
expect(reply.text).toContain("книг продаж/покупок");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("discloses ambiguous inventory purchase-date VAT bridge instead of hiding the selected date", () => {
|
||||||
|
const reply = composeFactualReply(
|
||||||
|
"vat_liability_confirmed_for_tax_period",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
period: "2015-02-28T23:59:59Z",
|
||||||
|
registrator: "VAT_BOOK_SALES",
|
||||||
|
account_dt: "68.02",
|
||||||
|
account_kt: "",
|
||||||
|
amount: 3500000,
|
||||||
|
analytics: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
period: "2015-02-28T23:59:59Z",
|
||||||
|
registrator: "VAT_BOOK_PURCHASES",
|
||||||
|
account_dt: "19",
|
||||||
|
account_kt: "",
|
||||||
|
amount: 868612,
|
||||||
|
analytics: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
userMessage: "ндс можешь прикинуть на дату покупки рабочей станции?",
|
||||||
|
periodFrom: "2015-02-01",
|
||||||
|
periodTo: "2015-02-28",
|
||||||
|
asOfDate: "2015-02-28",
|
||||||
|
organizationHint: "ООО Альтернатива Плюс",
|
||||||
|
purchaseDateBridge: {
|
||||||
|
selectedPurchaseDate: "2015-02-05",
|
||||||
|
firstPurchaseDate: "2015-02-05",
|
||||||
|
lastPurchaseDate: "2015-07-22",
|
||||||
|
basis: "first_confirmed_purchase_default",
|
||||||
|
hasMultiplePurchaseDates: true
|
||||||
|
},
|
||||||
|
useRubCurrency: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(reply.text).toContain("если брать первую подтвержденную дату закупки 05.02.2015");
|
||||||
|
expect(reply.text).toContain("у позиции несколько подтвержденных дат закупки");
|
||||||
|
expect(reply.text).toContain("05.02.2015..22.07.2015");
|
||||||
|
});
|
||||||
|
|
||||||
it("formats VAT forecast amounts in rubles and emphasizes numbers when requested", () => {
|
it("formats VAT forecast amounts in rubles and emphasizes numbers when requested", () => {
|
||||||
const reply = composeFactualReply(
|
const reply = composeFactualReply(
|
||||||
"vat_payable_forecast",
|
"vat_payable_forecast",
|
||||||
|
|
@ -2906,6 +2949,12 @@ describe("address intent resolver expansion (M2.3a)", () => {
|
||||||
expect(result.reasons).toContain("unicode_business_overview_supplier_quality_deferred_to_discovery");
|
expect(result.reasons).toContain("unicode_business_overview_supplier_quality_deferred_to_discovery");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("defers supplier dependency follow-up wording to business overview discovery instead of bank ops by pseudo-counterparty", () => {
|
||||||
|
const result = resolveAddressIntent("а по поставщикам и исходящим платежам от кого зависимость?");
|
||||||
|
expect(result.intent).toBe("unknown");
|
||||||
|
expect(result.reasons).toContain("unicode_business_overview_supplier_quality_deferred_to_discovery");
|
||||||
|
});
|
||||||
|
|
||||||
it("routes reconciliation mismatch wording into open contracts intent", () => {
|
it("routes reconciliation mismatch wording into open contracts intent", () => {
|
||||||
const result = resolveAddressIntent(
|
const result = resolveAddressIntent(
|
||||||
"Покажи контрагентов, по которым сальдо скорее всего не совпадет с их актом сверки. Может, стоит поторопиться и запросить сверку?"
|
"Покажи контрагентов, по которым сальдо скорее всего не совпадет с их актом сверки. Может, стоит поторопиться и запросить сверку?"
|
||||||
|
|
@ -4059,6 +4108,67 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 9000
|
||||||
expect(result?.debug.route_expectation_status).toBe("matched");
|
expect(result?.debug.route_expectation_status).toBe("matched");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("normalizes carried monthly debt period into VAT tax quarter for same-period follow-up", async () => {
|
||||||
|
const service = new AddressQueryService();
|
||||||
|
const result = await service.tryHandle("а какой ндс мы должны примерно заплатить за этот период?", {
|
||||||
|
followupContext: {
|
||||||
|
previous_intent: "receivables_confirmed_as_of_date",
|
||||||
|
previous_filters: {
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
period_from: "2017-05-01",
|
||||||
|
period_to: "2017-05-31",
|
||||||
|
as_of_date: "2017-05-31"
|
||||||
|
},
|
||||||
|
previous_anchor_type: "organization",
|
||||||
|
previous_anchor_value: 'ООО "Альтернатива Плюс"',
|
||||||
|
target_intent: "vat_liability_confirmed_for_tax_period"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result?.handled).toBe(true);
|
||||||
|
expect(result?.debug.detected_intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||||
|
expect(result?.debug.selected_recipe).toBe("address_vat_liability_confirmed_tax_period_v1");
|
||||||
|
expect(result?.debug.extracted_filters.period_from).toBe("2017-04-01");
|
||||||
|
expect(result?.debug.extracted_filters.period_to).toBe("2017-06-30");
|
||||||
|
expect(result?.debug.reasons).toContain("period_from_from_followup_context");
|
||||||
|
expect(result?.debug.reasons).toContain("period_derived_from_tax_quarter_for_confirmed_vat_liability");
|
||||||
|
expect(String(result?.reply_text ?? "")).toContain("01.04.2017..30.06.2017");
|
||||||
|
}, 35000);
|
||||||
|
|
||||||
|
it("normalizes purchase-date VAT bridge month into the selected date tax quarter", async () => {
|
||||||
|
const service = new AddressQueryService();
|
||||||
|
const result = await service.tryHandle("ндс можешь прикинуть на дату покупки рабочей станции?", {
|
||||||
|
followupContext: {
|
||||||
|
previous_intent: "inventory_purchase_provenance_for_item",
|
||||||
|
target_intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
previous_filters: {
|
||||||
|
item: "Рабочая станция универсального специалиста",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
period_from: "2015-02-01",
|
||||||
|
period_to: "2015-02-28",
|
||||||
|
as_of_date: "2015-02-28",
|
||||||
|
purchase_date_bridge_selected: "2015-02-05",
|
||||||
|
purchase_date_bridge_first: "2015-02-05",
|
||||||
|
purchase_date_bridge_last: "2015-07-22",
|
||||||
|
purchase_date_bridge_basis: "first_confirmed_purchase_default",
|
||||||
|
purchase_date_bridge_has_multiple: true
|
||||||
|
},
|
||||||
|
previous_anchor_type: "item",
|
||||||
|
previous_anchor_value: "Рабочая станция универсального специалиста"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result?.handled).toBe(true);
|
||||||
|
expect(result?.debug.detected_intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||||
|
expect(result?.debug.extracted_filters.period_from).toBe("2015-01-01");
|
||||||
|
expect(result?.debug.extracted_filters.period_to).toBe("2015-03-31");
|
||||||
|
expect(result?.debug.extracted_filters.purchase_date_bridge_selected).toBe("2015-02-05");
|
||||||
|
expect(result?.debug.reasons).toContain("period_from_purchase_date_bridge_followup_context");
|
||||||
|
expect(result?.debug.reasons).toContain("period_derived_from_tax_quarter_for_confirmed_vat_liability");
|
||||||
|
expect(String(result?.reply_text ?? "")).toContain("05.02.2015");
|
||||||
|
expect(String(result?.reply_text ?? "")).toContain("01.01.2015..31.03.2015");
|
||||||
|
}, 35000);
|
||||||
|
|
||||||
it("routes customer lifecycle question into dedicated aggregate recipe", async () => {
|
it("routes customer lifecycle question into dedicated aggregate recipe", async () => {
|
||||||
const service = new AddressQueryService();
|
const service = new AddressQueryService();
|
||||||
const result = await service.tryHandle("Какие заказчики работали с нами в 2020 году?");
|
const result = await service.tryHandle("Какие заказчики работали с нами в 2020 году?");
|
||||||
|
|
@ -5741,6 +5851,24 @@ it("routes old purchase residue questions to aging-by-purchase-date", () => {
|
||||||
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Остатки.Счет.Код, \"\"), 1, 5) = \"41.01\"");
|
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Остатки.Счет.Код, \"\"), 1, 5) = \"41.01\"");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("builds old-stock aging query from positive stock snapshot plus purchase history", () => {
|
||||||
|
const selected = selectAddressRecipe("inventory_aging_by_purchase_date", {
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
as_of_date: "2026-04-18"
|
||||||
|
});
|
||||||
|
expect(selected.selected_recipe?.recipe_id).toBe("address_inventory_aging_by_purchase_date_v1");
|
||||||
|
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
as_of_date: "2026-04-18"
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(plan.query).toContain("РегистрБухгалтерии.Хозрасчетный.Остатки(");
|
||||||
|
expect(plan.query).toContain("Документ.ПоступлениеТоваровУслуг.Товары");
|
||||||
|
expect(plan.query).toContain("Остатки.КоличествоРазвернутыйОстатокДт > 0");
|
||||||
|
expect(plan.query).toContain("Товары.Номенклатура В");
|
||||||
|
expect(plan.query).toContain("Товары.Ссылка.Дата <= ДАТАВРЕМЯ(2026, 4, 18, 23, 59, 59)");
|
||||||
|
});
|
||||||
|
|
||||||
it("renders confirmed inventory-on-hand snapshot from normalized rows", () => {
|
it("renders confirmed inventory-on-hand snapshot from normalized rows", () => {
|
||||||
const reply = composeFactualReply(
|
const reply = composeFactualReply(
|
||||||
"inventory_on_hand_as_of_date",
|
"inventory_on_hand_as_of_date",
|
||||||
|
|
@ -5776,6 +5904,59 @@ it("routes old purchase residue questions to aging-by-purchase-date", () => {
|
||||||
expect(reply.semantics?.balance_confirmed).toBe(true);
|
expect(reply.semantics?.balance_confirmed).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders old-stock aging only for items that are still in positive stock", () => {
|
||||||
|
const reply = composeFactualReply(
|
||||||
|
"inventory_aging_by_purchase_date",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
period: "2015-02-05T00:00:00Z",
|
||||||
|
registrator: "Поступление товаров и услуг 00000000001 от 05.02.2015",
|
||||||
|
account_dt: "41.01",
|
||||||
|
account_kt: "",
|
||||||
|
amount: 1000,
|
||||||
|
analytics: ["Старый проданный товар", 'ООО "Альтернатива Плюс"', "Поставщик А"],
|
||||||
|
item: "Старый проданный товар",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
counterparty: "Поставщик А",
|
||||||
|
quantity: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
period: "2017-03-10T00:00:00Z",
|
||||||
|
registrator: "Поступление товаров и услуг 00000000002 от 10.03.2017",
|
||||||
|
account_dt: "41.01",
|
||||||
|
account_kt: "",
|
||||||
|
amount: 2500,
|
||||||
|
analytics: ["Товар в остатке", 'ООО "Альтернатива Плюс"', "Поставщик Б"],
|
||||||
|
item: "Товар в остатке",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
counterparty: "Поставщик Б",
|
||||||
|
quantity: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
period: "2026-04-18T23:59:59Z",
|
||||||
|
registrator: "Остатки на дату",
|
||||||
|
account_dt: "41.01",
|
||||||
|
account_kt: "",
|
||||||
|
amount: 2500,
|
||||||
|
analytics: ["Товар в остатке", "Основной склад", 'ООО "Альтернатива Плюс"'],
|
||||||
|
item: "Товар в остатке",
|
||||||
|
warehouse: "Основной склад",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
quantity: 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
asOfDate: "2026-04-18",
|
||||||
|
useRubCurrency: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
||||||
|
expect(reply.text.split("\n")[0]).toContain("Товар в остатке");
|
||||||
|
expect(reply.text).toContain("положительным остатком");
|
||||||
|
expect(reply.text).not.toContain("Старый проданный товар");
|
||||||
|
});
|
||||||
|
|
||||||
it("sorts inventory-on-hand positions by amount before rendering the top list", () => {
|
it("sorts inventory-on-hand positions by amount before rendering the top list", () => {
|
||||||
const reply = composeFactualReply(
|
const reply = composeFactualReply(
|
||||||
"inventory_on_hand_as_of_date",
|
"inventory_on_hand_as_of_date",
|
||||||
|
|
|
||||||
|
|
@ -270,7 +270,7 @@ describe("address reply builders regressions", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const firstLine = result?.text.split("\n")[0] ?? "";
|
const firstLine = result?.text.split("\n")[0] ?? "";
|
||||||
expect(firstLine).toContain("К самым старым закупкам");
|
expect(firstLine).toContain("фактических положительных остатков");
|
||||||
expect(firstLine).not.toContain("2026-04-18");
|
expect(firstLine).not.toContain("2026-04-18");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -245,4 +245,90 @@ describe("assistant deep turn response runtime adapter", () => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sanitizes guarded MCP discovery candidates before finalizing the visible reply", () => {
|
||||||
|
const runPackagingRuntime = vi.fn(() => ({
|
||||||
|
messageId: "msg-a1",
|
||||||
|
investigationStateSnapshot: null,
|
||||||
|
droppedIntentSegments: [],
|
||||||
|
analysisContextForContract: null,
|
||||||
|
routesForDebug: [],
|
||||||
|
resolvedExecutionState: [],
|
||||||
|
safeAssistantReplyBase: "bad-base",
|
||||||
|
safeAssistantReply: "bad deep partial answer",
|
||||||
|
debug: { trace_id: "trace-1" },
|
||||||
|
assistantItem: {
|
||||||
|
message_id: "msg-a1",
|
||||||
|
session_id: "asst-1",
|
||||||
|
role: "assistant",
|
||||||
|
text: "bad deep partial answer",
|
||||||
|
reply_type: "partial_coverage",
|
||||||
|
created_at: "2026-04-10T00:00:00.000Z",
|
||||||
|
trace_id: "trace-1",
|
||||||
|
debug: null
|
||||||
|
},
|
||||||
|
deepAnalysisLogDetails: {}
|
||||||
|
}));
|
||||||
|
const sanitizeReply = vi.fn((value: string) =>
|
||||||
|
value.replace(/^\u041a\u043e\u0440\u043e\u0442\u043a\u043e:\s*/u, "")
|
||||||
|
);
|
||||||
|
const runFinalizeDeepTurn = vi.fn((input) => ({
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
session_id: "asst-1",
|
||||||
|
assistant_reply: input.assistantReply,
|
||||||
|
reply_type: input.replyType,
|
||||||
|
conversation_item: input.assistantItem,
|
||||||
|
conversation: [],
|
||||||
|
debug: input.debug
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const runtime = runAssistantDeepTurnResponseRuntime(
|
||||||
|
buildBaseInput({
|
||||||
|
composition: { reply_type: "partial_coverage" },
|
||||||
|
sanitizeReply,
|
||||||
|
addressRuntimeMetaForDeep: {
|
||||||
|
mcpDiscoveryRuntimeEntryPoint: {
|
||||||
|
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||||
|
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
|
||||||
|
entry_status: "bridge_executed",
|
||||||
|
hot_runtime_wired: false,
|
||||||
|
discovery_attempted: true,
|
||||||
|
turn_input: { should_run_discovery: true },
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
user_facing_response_allowed: true,
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
requires_user_clarification: false,
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference",
|
||||||
|
headline: "Коротко: Discovery answer",
|
||||||
|
confirmed_lines: ["Confirmed fact"],
|
||||||
|
inference_lines: [],
|
||||||
|
unknown_lines: [],
|
||||||
|
limitation_lines: [],
|
||||||
|
next_step_line: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reason_codes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
runPackagingRuntime,
|
||||||
|
runFinalizeDeepTurn
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(sanitizeReply).toHaveBeenCalledWith(expect.not.stringContaining("Коротко:"), "bad deep partial answer");
|
||||||
|
expect(runtime.response.assistant_reply).toContain("Discovery answer");
|
||||||
|
expect(runtime.response.assistant_reply).not.toContain("Коротко:");
|
||||||
|
expect(runFinalizeDeepTurn).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
assistantReply: expect.not.stringContaining("Коротко:"),
|
||||||
|
assistantItem: expect.objectContaining({
|
||||||
|
text: expect.not.stringContaining("Коротко:")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,8 @@ describe("assistant MCP discovery response candidate", () => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(candidate.reply_text).toContain("нельзя точно подтвердить чистую прибыль");
|
expect(candidate.reply_text).toContain("чистую прибыль и маржу не подтверждаю");
|
||||||
|
expect(candidate.reply_text).toContain("расчетное нетто");
|
||||||
expect(candidate.reply_text).toContain("полный отчет о прибылях и убытках");
|
expect(candidate.reply_text).toContain("полный отчет о прибылях и убытках");
|
||||||
expect(candidate.reply_text).toContain("себестоимости");
|
expect(candidate.reply_text).toContain("себестоимости");
|
||||||
expect(candidate.reply_text).not.toContain("47 628 853");
|
expect(candidate.reply_text).not.toContain("47 628 853");
|
||||||
|
|
@ -464,6 +465,64 @@ describe("assistant MCP discovery response candidate", () => {
|
||||||
expect(candidate.reply_text).not.toContain("business_overview_route_template_v1");
|
expect(candidate.reply_text).not.toContain("business_overview_route_template_v1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("formats reviewed supplier dependency answers without duplicated period or punctuation", () => {
|
||||||
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
|
entryPoint({
|
||||||
|
turn_input: {
|
||||||
|
adapter_status: "ready",
|
||||||
|
turn_meaning_ref: {
|
||||||
|
asked_domain_family: "business_overview",
|
||||||
|
asked_action_family: "vendor_risk_procurement_boundary",
|
||||||
|
unsupported_but_understood_family: "vendor_risk_procurement_boundary"
|
||||||
|
},
|
||||||
|
data_need_graph: {
|
||||||
|
business_fact_family: "business_overview",
|
||||||
|
ranking_need: null,
|
||||||
|
reason_codes: ["data_need_graph_family_business_overview"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
user_facing_response_allowed: true,
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
requires_user_clarification: false,
|
||||||
|
pilot: {
|
||||||
|
pilot_scope: "business_overview_route_template_v1",
|
||||||
|
derived_business_overview: {
|
||||||
|
vendor_procurement_quality: {
|
||||||
|
period_scope: "за все доступное проверенное окно",
|
||||||
|
total_outgoing_amount_human_ru: "137 963 720,37 руб.",
|
||||||
|
top_outgoing_counterparty: {
|
||||||
|
axis_value: "ГТК-Интер, ООО",
|
||||||
|
total_amount_human_ru: "44 943 460 руб."
|
||||||
|
},
|
||||||
|
top_outgoing_share_pct: 32.58,
|
||||||
|
used_contracts: 291,
|
||||||
|
total_contracts: 394,
|
||||||
|
used_contract_share_pct: 73.86,
|
||||||
|
evidence_status: "reviewed_procurement_concentration"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference",
|
||||||
|
headline: "Vendor route.",
|
||||||
|
confirmed_lines: [],
|
||||||
|
inference_lines: [],
|
||||||
|
unknown_lines: [],
|
||||||
|
limitation_lines: [],
|
||||||
|
next_step_line: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(candidate.reply_text).toContain("ГТК-Интер, ООО держит около 32.58% (44 943 460 руб.)");
|
||||||
|
expect(candidate.reply_text).toContain("Это не аудит поставщиков");
|
||||||
|
expect(candidate.reply_text).not.toContain("за за");
|
||||||
|
expect(candidate.reply_text).not.toContain("руб..");
|
||||||
|
});
|
||||||
|
|
||||||
it("uses a compact direct answer for business-overview top year questions", () => {
|
it("uses a compact direct answer for business-overview top year questions", () => {
|
||||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
entryPoint({
|
entryPoint({
|
||||||
|
|
@ -705,6 +764,77 @@ describe("assistant MCP discovery response candidate", () => {
|
||||||
expect(candidate.reply_text).not.toContain("wide overview");
|
expect(candidate.reply_text).not.toContain("wide overview");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("leads earnings wording with accounting result when explicit-period proof is available", () => {
|
||||||
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
|
entryPoint({
|
||||||
|
turn_input: {
|
||||||
|
adapter_status: "ready",
|
||||||
|
turn_meaning_ref: {
|
||||||
|
raw_message: "а за 2017 мы скок заработали?",
|
||||||
|
asked_action_family: "broad_evaluation",
|
||||||
|
unsupported_but_understood_family: "broad_business_evaluation",
|
||||||
|
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||||
|
explicit_date_scope: "2017"
|
||||||
|
},
|
||||||
|
data_need_graph: {
|
||||||
|
business_fact_family: "business_overview",
|
||||||
|
ranking_need: null,
|
||||||
|
reason_codes: [
|
||||||
|
"data_need_graph_family_business_overview",
|
||||||
|
"data_need_graph_business_overview_direct_money_answer"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
user_facing_response_allowed: true,
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
requires_user_clarification: false,
|
||||||
|
pilot: {
|
||||||
|
pilot_scope: "business_overview_route_template_v1",
|
||||||
|
derived_business_overview: {
|
||||||
|
period_scope: "2017",
|
||||||
|
incoming_customer_revenue: {
|
||||||
|
total_amount_human_ru: "16 932 063,96 руб."
|
||||||
|
},
|
||||||
|
outgoing_supplier_payout: {
|
||||||
|
total_amount_human_ru: "15 617 432,52 руб."
|
||||||
|
},
|
||||||
|
net_amount_human_ru: "1 314 631,44 руб.",
|
||||||
|
net_direction: "net_incoming",
|
||||||
|
top_customers: [],
|
||||||
|
top_suppliers: [],
|
||||||
|
accounting_financial_result: {
|
||||||
|
final_result_direction: "profit",
|
||||||
|
final_result_amount_human_ru: "116 938,23 руб.",
|
||||||
|
net_margin_to_revenue_pct: 1.03,
|
||||||
|
period_scope: "2017"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference",
|
||||||
|
headline: "wide overview should not leak",
|
||||||
|
confirmed_lines: [],
|
||||||
|
inference_lines: [],
|
||||||
|
unknown_lines: [],
|
||||||
|
limitation_lines: [],
|
||||||
|
next_step_line: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(candidate.reply_text).toContain("учетная прибыль");
|
||||||
|
expect(candidate.reply_text).toContain("116 938,23");
|
||||||
|
expect(candidate.reply_text).toContain("маржа");
|
||||||
|
expect(candidate.reply_text).toContain("1.03%");
|
||||||
|
expect(candidate.reply_text).toContain("денежное нетто");
|
||||||
|
expect(candidate.reply_text).toContain("1 314 631,44");
|
||||||
|
expect(candidate.reply_text).toContain("разные метрики");
|
||||||
|
expect(candidate.reply_text).not.toContain("wide overview");
|
||||||
|
});
|
||||||
|
|
||||||
it("uses compact cashflow output for one-line no-breakdown wording", () => {
|
it("uses compact cashflow output for one-line no-breakdown wording", () => {
|
||||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
entryPoint({
|
entryPoint({
|
||||||
|
|
@ -779,6 +909,72 @@ describe("assistant MCP discovery response candidate", () => {
|
||||||
expect(candidate.reply_text).not.toContain("wide overview");
|
expect(candidate.reply_text).not.toContain("wide overview");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("marks all-time earnings wording as limited when cashflow probes hit row limits", () => {
|
||||||
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
|
entryPoint({
|
||||||
|
turn_input: {
|
||||||
|
adapter_status: "ready",
|
||||||
|
turn_meaning_ref: {
|
||||||
|
raw_message: "сколько вообще денег мы заработали за все время?",
|
||||||
|
asked_action_family: "broad_evaluation",
|
||||||
|
unsupported_but_understood_family: "broad_business_evaluation",
|
||||||
|
explicit_organization_scope: "ООО Альтернатива Плюс"
|
||||||
|
},
|
||||||
|
data_need_graph: {
|
||||||
|
business_fact_family: "business_overview",
|
||||||
|
ranking_need: null,
|
||||||
|
reason_codes: [
|
||||||
|
"data_need_graph_family_business_overview",
|
||||||
|
"data_need_graph_business_overview_direct_money_answer"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
user_facing_response_allowed: true,
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
requires_user_clarification: false,
|
||||||
|
pilot: {
|
||||||
|
pilot_scope: "business_overview_route_template_v1",
|
||||||
|
derived_business_overview: {
|
||||||
|
incoming_customer_revenue: {
|
||||||
|
total_amount_human_ru: "285 819 547,57 руб.",
|
||||||
|
coverage_limited_by_probe_limit: true
|
||||||
|
},
|
||||||
|
outgoing_supplier_payout: {
|
||||||
|
total_amount_human_ru: "137 963 720,37 руб.",
|
||||||
|
coverage_limited_by_probe_limit: true
|
||||||
|
},
|
||||||
|
net_amount_human_ru: "147 855 827,20 руб.",
|
||||||
|
net_direction: "net_incoming",
|
||||||
|
top_customers: [],
|
||||||
|
top_suppliers: [],
|
||||||
|
accounting_financial_result: null,
|
||||||
|
coverage_limited_by_probe_limit: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference",
|
||||||
|
headline: "wide overview should not leak",
|
||||||
|
confirmed_lines: [],
|
||||||
|
inference_lines: [],
|
||||||
|
unknown_lines: [],
|
||||||
|
limitation_lines: [],
|
||||||
|
next_step_line: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(candidate.reply_text).toContain("точную учетную прибыль");
|
||||||
|
expect(candidate.reply_text).toContain("не подтверждаю");
|
||||||
|
expect(candidate.reply_text).toContain("ограниченном денежном срезе");
|
||||||
|
expect(candidate.reply_text).toContain("285 819 547,57");
|
||||||
|
expect(candidate.reply_text).toContain("147 855 827,20");
|
||||||
|
expect(candidate.reply_text).toContain("не считаю полным бухгалтерским оборотом");
|
||||||
|
expect(candidate.reply_text).not.toContain("wide overview");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps broad overview layers when direct-money reason is present", () => {
|
it("keeps broad overview layers when direct-money reason is present", () => {
|
||||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
entryPoint({
|
entryPoint({
|
||||||
|
|
@ -866,7 +1062,7 @@ describe("assistant MCP discovery response candidate", () => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(candidate.reply_text).toContain("Что видно:");
|
expect(candidate.reply_text).toContain("Что видно в ограниченном денежном срезе:");
|
||||||
expect(candidate.reply_text).toContain("НДС:");
|
expect(candidate.reply_text).toContain("НДС:");
|
||||||
expect(candidate.reply_text).toContain("Долги:");
|
expect(candidate.reply_text).toContain("Долги:");
|
||||||
expect(candidate.reply_text).toContain("Склад:");
|
expect(candidate.reply_text).toContain("Склад:");
|
||||||
|
|
@ -875,6 +1071,67 @@ describe("assistant MCP discovery response candidate", () => {
|
||||||
expect(candidate.reply_text).not.toContain("wide overview");
|
expect(candidate.reply_text).not.toContain("wide overview");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("answers business overview next-check follow-ups with a concrete analyst plan first", () => {
|
||||||
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
|
entryPoint({
|
||||||
|
turn_input: {
|
||||||
|
adapter_status: "ready",
|
||||||
|
turn_meaning_ref: {
|
||||||
|
raw_message: "что дальше проверять в первую очередь?",
|
||||||
|
asked_action_family: "broad_evaluation",
|
||||||
|
unsupported_but_understood_family: "broad_business_evaluation",
|
||||||
|
explicit_organization_scope: "ООО Альтернатива Плюс"
|
||||||
|
},
|
||||||
|
data_need_graph: {
|
||||||
|
business_fact_family: "business_overview",
|
||||||
|
ranking_need: null,
|
||||||
|
reason_codes: ["data_need_graph_family_business_overview"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
user_facing_response_allowed: true,
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
requires_user_clarification: false,
|
||||||
|
pilot: {
|
||||||
|
pilot_scope: "business_overview_route_template_v1",
|
||||||
|
derived_business_overview: {
|
||||||
|
incoming_customer_revenue: {
|
||||||
|
total_amount_human_ru: "285 819 547,57 руб.",
|
||||||
|
coverage_limited_by_probe_limit: true
|
||||||
|
},
|
||||||
|
outgoing_supplier_payout: {
|
||||||
|
total_amount_human_ru: "137 963 720,37 руб.",
|
||||||
|
coverage_limited_by_probe_limit: true
|
||||||
|
},
|
||||||
|
net_amount_human_ru: "147 855 827,20 руб.",
|
||||||
|
net_direction: "net_incoming",
|
||||||
|
top_customers: [{ axis_value: "Комитет государственных услуг г. Москвы", total_amount_human_ru: "133 839 880,82 руб." }],
|
||||||
|
top_suppliers: [{ axis_value: "ГТК-Интер, ООО", total_amount_human_ru: "44 943 460 руб." }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference",
|
||||||
|
headline: "generic advice should not leak",
|
||||||
|
confirmed_lines: [],
|
||||||
|
inference_lines: [],
|
||||||
|
unknown_lines: [],
|
||||||
|
limitation_lines: [],
|
||||||
|
next_step_line: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const firstLine = candidate.reply_text?.split("\n")[0] ?? "";
|
||||||
|
expect(firstLine).toContain("чистую прибыль через закрытие 90/91/99");
|
||||||
|
expect(firstLine).toContain("топ-3");
|
||||||
|
expect(firstLine).toContain("долги");
|
||||||
|
expect(firstLine).toContain("складские остатки");
|
||||||
|
expect(candidate.reply_text).toContain("не считаю полным бухгалтерским оборотом");
|
||||||
|
expect(candidate.reply_text).not.toContain("generic advice");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps counterparty leaders for money followups instead of compact cashflow", () => {
|
it("keeps counterparty leaders for money followups instead of compact cashflow", () => {
|
||||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
entryPoint({
|
entryPoint({
|
||||||
|
|
@ -1288,6 +1545,11 @@ describe("assistant MCP discovery response candidate", () => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const firstLine = candidate.reply_text?.split("\n")[0] ?? "";
|
||||||
|
expect(firstLine).toContain("по ограниченному проверенному 1С-срезу");
|
||||||
|
expect(firstLine).toContain("не аудиторское заключение");
|
||||||
|
expect(firstLine).toContain("не подтвержденная чистая прибыль");
|
||||||
|
expect(candidate.reply_text).toContain("Что видно в ограниченном денежном срезе");
|
||||||
expect(candidate.reply_text).toContain("Что не подтверждено в этом срезе");
|
expect(candidate.reply_text).toContain("Что не подтверждено в этом срезе");
|
||||||
expect(candidate.reply_text).toContain("НДС");
|
expect(candidate.reply_text).toContain("НДС");
|
||||||
expect(candidate.reply_text).toContain("долги");
|
expect(candidate.reply_text).toContain("долги");
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,37 @@ describe("assistant MCP discovery response policy", () => {
|
||||||
expect(result.reason_codes).toContain("mcp_discovery_response_policy_candidate_applied");
|
expect(result.reason_codes).toContain("mcp_discovery_response_policy_candidate_applied");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("removes routine short-answer prefix from applied guarded candidates", () => {
|
||||||
|
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||||
|
currentReply: "route is not wired",
|
||||||
|
currentReplySource: "deterministic_unsupported_current_turn_boundary",
|
||||||
|
modeDecisionReason: "unsupported_current_turn_meaning_boundary",
|
||||||
|
addressRuntimeMeta: {
|
||||||
|
mcpDiscoveryRuntimeEntryPoint: entryPoint({
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
user_facing_response_allowed: true,
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
requires_user_clarification: false,
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference",
|
||||||
|
headline: "Коротко: Confirmed scoped answer.",
|
||||||
|
confirmed_lines: ["Коротко: Confirmed fact"],
|
||||||
|
inference_lines: [],
|
||||||
|
unknown_lines: [],
|
||||||
|
limitation_lines: [],
|
||||||
|
next_step_line: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.applied).toBe(true);
|
||||||
|
expect(result.reply_text).toContain("Confirmed scoped answer.");
|
||||||
|
expect(result.reply_text).not.toContain("Коротко:");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps the current reply when the turn is not an unsupported boundary", () => {
|
it("keeps the current reply when the turn is not an unsupported boundary", () => {
|
||||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||||
currentReply: "regular chat",
|
currentReply: "regular chat",
|
||||||
|
|
|
||||||
|
|
@ -2637,6 +2637,70 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||||
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
|
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps supplier dependency follow-ups in business overview instead of pseudo-counterparty bank operations", () => {
|
||||||
|
const orgName = "ООО Альтернатива Плюс";
|
||||||
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
|
userMessage: "а по поставщикам и исходящим платежам от кого зависимость?",
|
||||||
|
assistantTurnMeaning: {
|
||||||
|
asked_domain_family: "counterparty",
|
||||||
|
asked_action_family: "list_movements",
|
||||||
|
explicit_intent_candidate: "bank_operations_by_counterparty",
|
||||||
|
explicit_entity_candidates: ["поставщикам"]
|
||||||
|
},
|
||||||
|
followupContext: {
|
||||||
|
previous_discovery_pilot_scope: "business_overview_route_template_v1",
|
||||||
|
previous_discovery_loop_selected_chain_id: "business_overview",
|
||||||
|
previous_filters: {
|
||||||
|
organization: orgName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.adapter_status).toBe("ready");
|
||||||
|
expect(result.should_run_discovery).toBe(true);
|
||||||
|
expect(result.semantic_data_need).toBe("business overview evidence with bounded analyst interpretation");
|
||||||
|
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
|
||||||
|
expect(result.turn_meaning_ref).toMatchObject({
|
||||||
|
asked_domain_family: "business_overview",
|
||||||
|
asked_action_family: "vendor_risk_procurement_boundary",
|
||||||
|
explicit_organization_scope: orgName,
|
||||||
|
unsupported_but_understood_family: "vendor_risk_procurement_boundary",
|
||||||
|
stale_replay_forbidden: true
|
||||||
|
});
|
||||||
|
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||||
|
expect(result.reason_codes).toContain("mcp_discovery_business_overview_supplier_dependency_followup_boundary");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps supplier dependency follow-ups in the procurement boundary after broad-business meaning normalization", () => {
|
||||||
|
const orgName = "ООО Альтернатива Плюс";
|
||||||
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
|
userMessage: "а по поставщикам и исходящим платежам от кого зависимость?",
|
||||||
|
assistantTurnMeaning: {
|
||||||
|
asked_domain_family: "business_overview",
|
||||||
|
asked_action_family: "broad_evaluation",
|
||||||
|
explicit_organization_scope: orgName,
|
||||||
|
unsupported_but_understood_family: "broad_business_evaluation"
|
||||||
|
},
|
||||||
|
followupContext: {
|
||||||
|
previous_discovery_pilot_scope: "business_overview_route_template_v1",
|
||||||
|
previous_filters: {
|
||||||
|
organization: orgName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.adapter_status).toBe("ready");
|
||||||
|
expect(result.should_run_discovery).toBe(true);
|
||||||
|
expect(result.turn_meaning_ref).toMatchObject({
|
||||||
|
asked_domain_family: "business_overview",
|
||||||
|
asked_action_family: "vendor_risk_procurement_boundary",
|
||||||
|
explicit_organization_scope: orgName,
|
||||||
|
unsupported_but_understood_family: "vendor_risk_procurement_boundary",
|
||||||
|
stale_replay_forbidden: true
|
||||||
|
});
|
||||||
|
expect(result.reason_codes).toContain("mcp_discovery_business_overview_supplier_dependency_followup_boundary");
|
||||||
|
});
|
||||||
|
|
||||||
it("resumes an open-scope total clarification loop from saved state when the user resolves the pending period with all-time wording", () => {
|
it("resumes an open-scope total clarification loop from saved state when the user resolves the pending period with all-time wording", () => {
|
||||||
const orgName = "ООО Альтернатива Плюс";
|
const orgName = "ООО Альтернатива Плюс";
|
||||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
|
|
|
||||||
|
|
@ -214,6 +214,152 @@ describe("assistantTransitionPolicy", () => {
|
||||||
expect(contract.decision).toBe("continue_previous");
|
expect(contract.decision).toBe("continue_previous");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("carries explicit purchase-date VAT bridge facts without silently losing ambiguity", () => {
|
||||||
|
const policy = buildPolicy({
|
||||||
|
findLastAddressAssistantItem: () => ({
|
||||||
|
role: "assistant",
|
||||||
|
text: [
|
||||||
|
"По позиции Рабочая станция до 31.03.2016 однозначный поставщик не подтвержден.",
|
||||||
|
"- Первая найденная дата закупки: 05.02.2015.",
|
||||||
|
"- Последняя найденная дата закупки: 22.07.2015."
|
||||||
|
].join("\n"),
|
||||||
|
debug: {
|
||||||
|
detected_intent: "inventory_purchase_provenance_for_item",
|
||||||
|
extracted_filters: {
|
||||||
|
item: "Рабочая станция",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
as_of_date: "2016-03-31"
|
||||||
|
},
|
||||||
|
anchor_type: "item",
|
||||||
|
anchor_value_resolved: "Рабочая станция"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||||
|
"ндс можешь прикинуть на дату покупки рабочей станции?",
|
||||||
|
[],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(carryover?.followupContext?.target_intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||||
|
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||||||
|
period_from: "2015-02-01",
|
||||||
|
period_to: "2015-02-28",
|
||||||
|
as_of_date: "2015-02-28",
|
||||||
|
purchase_date_bridge_selected: "2015-02-05",
|
||||||
|
purchase_date_bridge_first: "2015-02-05",
|
||||||
|
purchase_date_bridge_last: "2015-07-22",
|
||||||
|
purchase_date_bridge_basis: "first_confirmed_purchase_default",
|
||||||
|
purchase_date_bridge_has_multiple: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can retarget a follow-up VAT recalculation to the last confirmed purchase date", () => {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
text: [
|
||||||
|
"По позиции Рабочая станция до 31.03.2016 однозначный поставщик не подтвержден.",
|
||||||
|
"- Первая найденная дата закупки: 05.02.2015.",
|
||||||
|
"- Последняя найденная дата закупки: 22.07.2015."
|
||||||
|
].join("\n"),
|
||||||
|
debug: {
|
||||||
|
detected_intent: "inventory_purchase_provenance_for_item",
|
||||||
|
extracted_filters: {
|
||||||
|
item: "Рабочая станция",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
text: "Коротко: если брать первую подтвержденную дату закупки 05.02.2015, НДС к уплате 100.",
|
||||||
|
debug: {
|
||||||
|
detected_intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
extracted_filters: {
|
||||||
|
item: "Рабочая станция",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
purchase_date_bridge_selected: "2015-02-05"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const policy = buildPolicy({
|
||||||
|
findLastAddressAssistantItem: (sourceItems: unknown[]) => {
|
||||||
|
const list = sourceItems as Record<string, unknown>[];
|
||||||
|
return list[list.length - 1];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||||
|
"а теперь по последней подтвержденной покупке",
|
||||||
|
items,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
session_context: {
|
||||||
|
active_focus_object: {
|
||||||
|
object_type: "item",
|
||||||
|
label: "Рабочая станция"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(carryover?.followupContext?.target_intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||||
|
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||||||
|
period_from: "2015-07-01",
|
||||||
|
period_to: "2015-07-31",
|
||||||
|
as_of_date: "2015-07-31",
|
||||||
|
purchase_date_bridge_selected: "2015-07-22",
|
||||||
|
purchase_date_bridge_basis: "last_confirmed_purchase"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("continues purchase-date VAT recalculation from prior VAT bridge filters without relying on active item focus", () => {
|
||||||
|
const policy = buildPolicy({
|
||||||
|
findLastAddressAssistantItem: () => ({
|
||||||
|
role: "assistant",
|
||||||
|
text: "Коротко: если брать первую подтвержденную дату закупки 05.02.2015, НДС к уплате 100.",
|
||||||
|
debug: {
|
||||||
|
detected_intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
extracted_filters: {
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
period_from: "2015-02-01",
|
||||||
|
period_to: "2015-02-28",
|
||||||
|
as_of_date: "2015-02-28",
|
||||||
|
purchase_date_bridge_selected: "2015-02-05",
|
||||||
|
purchase_date_bridge_first: "2015-02-05",
|
||||||
|
purchase_date_bridge_last: "2015-07-22",
|
||||||
|
purchase_date_bridge_basis: "first_confirmed_purchase_default",
|
||||||
|
purchase_date_bridge_has_multiple: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||||
|
"если у нее несколько закупок, посчитай по первой подтвержденной покупке",
|
||||||
|
[],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(carryover?.followupContext?.target_intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||||
|
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||||||
|
period_from: "2015-02-01",
|
||||||
|
period_to: "2015-02-28",
|
||||||
|
as_of_date: "2015-02-28",
|
||||||
|
purchase_date_bridge_selected: "2015-02-05",
|
||||||
|
purchase_date_bridge_basis: "first_confirmed_purchase",
|
||||||
|
purchase_date_bridge_has_multiple: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("retargets displayed counterparty follow-up through shared referenced-entity carryover", () => {
|
it("retargets displayed counterparty follow-up through shared referenced-entity carryover", () => {
|
||||||
const policy = buildPolicy({
|
const policy = buildPolicy({
|
||||||
findLastAddressAssistantItem: () => ({
|
findLastAddressAssistantItem: () => ({
|
||||||
|
|
|
||||||
|
|
@ -303,6 +303,22 @@ describe("assistantTurnMeaningPolicy", () => {
|
||||||
expect(supplierQuality.asked_action_family).toBe("broad_evaluation");
|
expect(supplierQuality.asked_action_family).toBe("broad_evaluation");
|
||||||
expect(supplierQuality.unsupported_but_understood_family).toBe("broad_business_evaluation");
|
expect(supplierQuality.unsupported_but_understood_family).toBe("broad_business_evaluation");
|
||||||
expect(supplierQuality.reason_codes).toContain("broad_business_evaluation_current_turn_signal");
|
expect(supplierQuality.reason_codes).toContain("broad_business_evaluation_current_turn_signal");
|
||||||
|
|
||||||
|
const supplierDependencyFollowup = policy.resolveAssistantTurnMeaning({
|
||||||
|
rawUserMessage: "а по поставщикам и исходящим платежам от кого зависимость?"
|
||||||
|
});
|
||||||
|
expect(supplierDependencyFollowup.explicit_intent_candidate).toBeNull();
|
||||||
|
expect(supplierDependencyFollowup.asked_domain_family).toBe("business_summary");
|
||||||
|
expect(supplierDependencyFollowup.asked_action_family).toBe("broad_evaluation");
|
||||||
|
expect(supplierDependencyFollowup.unsupported_but_understood_family).toBe("broad_business_evaluation");
|
||||||
|
|
||||||
|
const nextChecks = policy.resolveAssistantTurnMeaning({
|
||||||
|
rawUserMessage: "что дальше проверять в первую очередь?"
|
||||||
|
});
|
||||||
|
expect(nextChecks.explicit_intent_candidate).toBeNull();
|
||||||
|
expect(nextChecks.asked_domain_family).toBe("business_summary");
|
||||||
|
expect(nextChecks.asked_action_family).toBe("broad_evaluation");
|
||||||
|
expect(nextChecks.unsupported_but_understood_family).toBe("broad_business_evaluation");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps selected-object profitability as an inventory exact intent instead of company overview", () => {
|
it("keeps selected-object profitability as an inventory exact intent instead of company overview", () => {
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,40 @@ describe("counterparty analytics reply builders", () => {
|
||||||
expect(reply.text).not.toContain("1. 2020 |");
|
expect(reply.text).not.toContain("1. 2020 |");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("preserves raw dependency wording when the address lane receives a canonicalized inflow ranking", () => {
|
||||||
|
const reply = composeFactualReply(
|
||||||
|
"customer_revenue_and_payments",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
period: "2020-03-01T00:00:00Z",
|
||||||
|
registrator: "Поступление 1",
|
||||||
|
account_dt: "",
|
||||||
|
account_kt: "",
|
||||||
|
amount: 500,
|
||||||
|
analytics: ["Малый клиент", "Договор 1"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
period: "2020-03-02T00:00:00Z",
|
||||||
|
registrator: "Поступление 2",
|
||||||
|
account_dt: "",
|
||||||
|
account_kt: "",
|
||||||
|
amount: 1200,
|
||||||
|
analytics: ["Главный клиент", "Договор 2"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
rawUserMessage: "от кого мы больше всего зависим по входящим деньгам?",
|
||||||
|
userMessage: "определить контрагентов с наибольшим объемом входящих денежных потоков"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(reply.responseType).toBe("FACTUAL_LIST");
|
||||||
|
expect(reply.text.split("\n")[0]).toContain("Главный сигнал зависимости по входящим деньгам");
|
||||||
|
expect(reply.text.split("\n")[0]).toContain("Главный клиент");
|
||||||
|
expect(reply.text).toContain("сигнал концентрации");
|
||||||
|
expect(reply.text).toContain("не чистая прибыль");
|
||||||
|
});
|
||||||
|
|
||||||
it("explains organization activity age as 1C activity rather than legal age", () => {
|
it("explains organization activity age as 1C activity rather than legal age", () => {
|
||||||
const reply = composeFactualReply(
|
const reply = composeFactualReply(
|
||||||
"counterparty_activity_lifecycle",
|
"counterparty_activity_lifecycle",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"schema_version": "shared_llm_connection_v1",
|
"schema_version": "shared_llm_connection_v1",
|
||||||
"updated_at": "2026-04-18T14:51:55.518Z",
|
"updated_at": "2026-05-25T13:27:06.297Z",
|
||||||
"connection": {
|
"connection": {
|
||||||
"llmProvider": "local",
|
"llmProvider": "local",
|
||||||
"model": "unsloth/qwen3-30b-a3b-instruct-2507",
|
"model": "unsloth/qwen3-30b-a3b-instruct-2507",
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -4,7 +4,7 @@
|
||||||
<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-BdTHVNKv.js"></script>
|
<script type="module" crossorigin src="/assets/index-B26u5hxP.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-D-Jp8Dx0.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-D-Jp8Dx0.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -312,7 +312,6 @@ export const apiClient = {
|
||||||
|
|
||||||
async sendAssistantMessage(input: {
|
async sendAssistantMessage(input: {
|
||||||
connection: ConnectionState;
|
connection: ConnectionState;
|
||||||
prompts: PromptState;
|
|
||||||
userMessage: string;
|
userMessage: string;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
promptVersion?: string;
|
promptVersion?: string;
|
||||||
|
|
@ -336,10 +335,6 @@ export const apiClient = {
|
||||||
temperature: input.connection.temperature,
|
temperature: input.connection.temperature,
|
||||||
maxOutputTokens: input.connection.maxOutputTokens,
|
maxOutputTokens: input.connection.maxOutputTokens,
|
||||||
promptVersion: input.promptVersion ?? "address_query_runtime_v1",
|
promptVersion: input.promptVersion ?? "address_query_runtime_v1",
|
||||||
systemPrompt: input.prompts.systemPrompt,
|
|
||||||
developerPrompt: input.prompts.developerPrompt,
|
|
||||||
domainPrompt: input.prompts.domainPrompt,
|
|
||||||
fewShotExamples: input.prompts.fewShotExamples,
|
|
||||||
context: {
|
context: {
|
||||||
period_hint: input.context?.periodHint ?? "",
|
period_hint: input.context?.periodHint ?? "",
|
||||||
business_context: input.context?.businessContext ?? ""
|
business_context: input.context?.businessContext ?? ""
|
||||||
|
|
@ -362,7 +357,6 @@ export const apiClient = {
|
||||||
model?: string;
|
model?: string;
|
||||||
assistant_prompt_version?: string;
|
assistant_prompt_version?: string;
|
||||||
decomposition_prompt_version?: string;
|
decomposition_prompt_version?: string;
|
||||||
prompt_fingerprint?: string;
|
|
||||||
};
|
};
|
||||||
}): Promise<{ ok: boolean; generation: AutoGenHistoryRecord }> {
|
}): Promise<{ ok: boolean; generation: AutoGenHistoryRecord }> {
|
||||||
return request("/autoruns/autogen/save-assistant-session", {
|
return request("/autoruns/autogen/save-assistant-session", {
|
||||||
|
|
@ -534,35 +528,5 @@ export const apiClient = {
|
||||||
return request(`/autoruns/autogen/history/${encodeURIComponent(generationId)}`, {
|
return request(`/autoruns/autogen/history/${encodeURIComponent(generationId)}`, {
|
||||||
method: "DELETE"
|
method: "DELETE"
|
||||||
});
|
});
|
||||||
},
|
|
||||||
|
|
||||||
async generateAutoRunQuestions(input: {
|
|
||||||
mode: AutoGenMode;
|
|
||||||
count: number;
|
|
||||||
domain?: string;
|
|
||||||
persist_to_eval_cases?: boolean;
|
|
||||||
generated_by?: string;
|
|
||||||
llm?: {
|
|
||||||
llm_provider?: "openai" | "local";
|
|
||||||
api_key?: string;
|
|
||||||
model?: string;
|
|
||||||
base_url?: string;
|
|
||||||
temperature?: number;
|
|
||||||
max_output_tokens?: number;
|
|
||||||
};
|
|
||||||
context?: {
|
|
||||||
llm_provider?: string;
|
|
||||||
model?: string;
|
|
||||||
assistant_prompt_version?: string;
|
|
||||||
decomposition_prompt_version?: string;
|
|
||||||
prompt_fingerprint?: string;
|
|
||||||
autogen_personality_id?: string;
|
|
||||||
autogen_personality_prompt?: string;
|
|
||||||
};
|
|
||||||
}): Promise<{ ok: boolean; generation: AutoGenHistoryRecord }> {
|
|
||||||
return request("/autoruns/autogen/generate", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(input)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -244,7 +244,7 @@ type UnifiedCommentListItem =
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_AUTOGEN_SETTINGS: AutoGenSettingsState = {
|
const DEFAULT_AUTOGEN_SETTINGS: AutoGenSettingsState = {
|
||||||
mode: "codex_creative",
|
mode: "saved_user_sessions",
|
||||||
count: 24,
|
count: 24,
|
||||||
personalityId: "general",
|
personalityId: "general",
|
||||||
personalityPrompts: buildDefaultPersonalityPrompts(),
|
personalityPrompts: buildDefaultPersonalityPrompts(),
|
||||||
|
|
@ -723,7 +723,6 @@ export function AutoRunsHistoryPanel({
|
||||||
const [dragOverQuestionIndex, setDragOverQuestionIndex] = useState<number | null>(null);
|
const [dragOverQuestionIndex, setDragOverQuestionIndex] = useState<number | null>(null);
|
||||||
const [activeAsyncJob, setActiveAsyncJob] = useState<AsyncEvalRunJob | null>(null);
|
const [activeAsyncJob, setActiveAsyncJob] = useState<AsyncEvalRunJob | null>(null);
|
||||||
const [postAnalysis, setPostAnalysis] = useState<AutoRunPostAnalysisResponse | null>(null);
|
const [postAnalysis, setPostAnalysis] = useState<AutoRunPostAnalysisResponse | null>(null);
|
||||||
const [autoGenBusy, setAutoGenBusy] = useState(false);
|
|
||||||
const [autogenRunBusy, setAutogenRunBusy] = useState(false);
|
const [autogenRunBusy, setAutogenRunBusy] = useState(false);
|
||||||
const [postAnalysisBusy, setPostAnalysisBusy] = useState(false);
|
const [postAnalysisBusy, setPostAnalysisBusy] = useState(false);
|
||||||
const [autogenHistoryBusy, setAutogenHistoryBusy] = useState(false);
|
const [autogenHistoryBusy, setAutogenHistoryBusy] = useState(false);
|
||||||
|
|
@ -801,10 +800,6 @@ export function AutoRunsHistoryPanel({
|
||||||
const asyncJobPollTimerRef = useRef<number | null>(null);
|
const asyncJobPollTimerRef = useRef<number | null>(null);
|
||||||
const questionEditorRef = useRef<HTMLInputElement | null>(null);
|
const questionEditorRef = useRef<HTMLInputElement | null>(null);
|
||||||
const isSavedUserSessionsMode = autoGenSettings.mode === "saved_user_sessions";
|
const isSavedUserSessionsMode = autoGenSettings.mode === "saved_user_sessions";
|
||||||
const selectedPersonality = useMemo(
|
|
||||||
() => autogenPersonalities.find((item) => item.id === autoGenSettings.personalityId) ?? autogenPersonalities[0] ?? AUTOGEN_PERSONALITIES[0],
|
|
||||||
[autoGenSettings.personalityId, autogenPersonalities]
|
|
||||||
);
|
|
||||||
const visibleAutoGenHistory = useMemo(
|
const visibleAutoGenHistory = useMemo(
|
||||||
() => autoGenHistory.filter((item) => item.mode === autoGenSettings.mode),
|
() => autoGenHistory.filter((item) => item.mode === autoGenSettings.mode),
|
||||||
[autoGenHistory, autoGenSettings.mode]
|
[autoGenHistory, autoGenSettings.mode]
|
||||||
|
|
@ -1104,7 +1099,6 @@ export function AutoRunsHistoryPanel({
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.sendAssistantMessage({
|
const response = await apiClient.sendAssistantMessage({
|
||||||
connection,
|
connection,
|
||||||
prompts,
|
|
||||||
userMessage,
|
userMessage,
|
||||||
sessionId: assistantLiveSessionId || undefined,
|
sessionId: assistantLiveSessionId || undefined,
|
||||||
promptVersion: assistantPromptVersion,
|
promptVersion: assistantPromptVersion,
|
||||||
|
|
@ -1132,8 +1126,7 @@ export function AutoRunsHistoryPanel({
|
||||||
assistantPromptVersion,
|
assistantPromptVersion,
|
||||||
connection,
|
connection,
|
||||||
loadAssistantLiveAnnotationsForSession,
|
loadAssistantLiveAnnotationsForSession,
|
||||||
log,
|
log
|
||||||
prompts
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const openAssistantLiveSaveModal = useCallback(() => {
|
const openAssistantLiveSaveModal = useCallback(() => {
|
||||||
|
|
@ -1164,13 +1157,6 @@ export function AutoRunsHistoryPanel({
|
||||||
|
|
||||||
setAssistantLiveSaveModal((prev) => ({ ...prev, saving: true, error: "" }));
|
setAssistantLiveSaveModal((prev) => ({ ...prev, saving: true, error: "" }));
|
||||||
try {
|
try {
|
||||||
const promptFingerprint = [
|
|
||||||
prompts.systemPrompt,
|
|
||||||
prompts.developerPrompt,
|
|
||||||
prompts.domainPrompt,
|
|
||||||
prompts.schemaNotes,
|
|
||||||
prompts.fewShotExamples
|
|
||||||
].join("||");
|
|
||||||
const payload = await apiClient.saveAutoRunAssistantSession({
|
const payload = await apiClient.saveAutoRunAssistantSession({
|
||||||
session_id: sessionId,
|
session_id: sessionId,
|
||||||
title,
|
title,
|
||||||
|
|
@ -1179,8 +1165,7 @@ export function AutoRunsHistoryPanel({
|
||||||
llm_provider: connection.llmProvider,
|
llm_provider: connection.llmProvider,
|
||||||
model: connection.model,
|
model: connection.model,
|
||||||
assistant_prompt_version: assistantPromptVersion,
|
assistant_prompt_version: assistantPromptVersion,
|
||||||
decomposition_prompt_version: decompositionPromptVersion,
|
decomposition_prompt_version: decompositionPromptVersion
|
||||||
prompt_fingerprint: promptFingerprint
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setAutoGenHistory((prev) => [payload.generation, ...prev.filter((item) => item.generation_id !== payload.generation.generation_id)]);
|
setAutoGenHistory((prev) => [payload.generation, ...prev.filter((item) => item.generation_id !== payload.generation.generation_id)]);
|
||||||
|
|
@ -1202,12 +1187,7 @@ export function AutoRunsHistoryPanel({
|
||||||
connection.llmProvider,
|
connection.llmProvider,
|
||||||
connection.model,
|
connection.model,
|
||||||
decompositionPromptVersion,
|
decompositionPromptVersion,
|
||||||
log,
|
log
|
||||||
prompts.developerPrompt,
|
|
||||||
prompts.domainPrompt,
|
|
||||||
prompts.fewShotExamples,
|
|
||||||
prompts.schemaNotes,
|
|
||||||
prompts.systemPrompt
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const commitLimitInput = useCallback(
|
const commitLimitInput = useCallback(
|
||||||
|
|
@ -1363,87 +1343,6 @@ export function AutoRunsHistoryPanel({
|
||||||
}
|
}
|
||||||
}, [filters.fromLocal, filters.mode, filters.promptContains, filters.target, filters.toLocal, filters.useMock, log, selectedRunId]);
|
}, [filters.fromLocal, filters.mode, filters.promptContains, filters.target, filters.toLocal, filters.useMock, log, selectedRunId]);
|
||||||
|
|
||||||
const generateAutogenBatch = useCallback(async () => {
|
|
||||||
setAutoGenBusy(true);
|
|
||||||
setErrorText("");
|
|
||||||
try {
|
|
||||||
if (autoGenSettings.mode === "saved_user_sessions") {
|
|
||||||
throw new Error("Пользовательские сессии сохраняются из живого чата, а не генерируются автоматически.");
|
|
||||||
}
|
|
||||||
const activePersonalityPrompt = autoGenSettings.personalityPrompts[autoGenSettings.personalityId] ?? "";
|
|
||||||
const promptFingerprint = [
|
|
||||||
prompts.systemPrompt,
|
|
||||||
prompts.developerPrompt,
|
|
||||||
prompts.domainPrompt,
|
|
||||||
prompts.schemaNotes,
|
|
||||||
prompts.fewShotExamples
|
|
||||||
]
|
|
||||||
.join("\n")
|
|
||||||
.slice(0, 900);
|
|
||||||
const payload = await apiClient.generateAutoRunQuestions({
|
|
||||||
mode: autoGenSettings.mode,
|
|
||||||
count: autoGenSettings.count,
|
|
||||||
domain: selectedPersonality.domain || undefined,
|
|
||||||
persist_to_eval_cases: autoGenSettings.persistToEvalCases,
|
|
||||||
generated_by: autoGenSettings.generatedBy.trim() || undefined,
|
|
||||||
llm: {
|
|
||||||
llm_provider: connection.llmProvider,
|
|
||||||
api_key: connection.apiKey,
|
|
||||||
model: connection.model,
|
|
||||||
base_url: connection.baseUrl,
|
|
||||||
temperature: connection.temperature,
|
|
||||||
max_output_tokens: connection.maxOutputTokens
|
|
||||||
},
|
|
||||||
context: {
|
|
||||||
llm_provider: connection.llmProvider,
|
|
||||||
model: connection.model,
|
|
||||||
assistant_prompt_version: assistantPromptVersion,
|
|
||||||
decomposition_prompt_version: decompositionPromptVersion,
|
|
||||||
prompt_fingerprint: promptFingerprint,
|
|
||||||
autogen_personality_id: selectedPersonality.id,
|
|
||||||
autogen_personality_prompt: activePersonalityPrompt.trim() || undefined
|
|
||||||
}
|
|
||||||
});
|
|
||||||
log(
|
|
||||||
`Generated ${payload.generation.count} questions (${payload.generation.mode}) id=${payload.generation.generation_id}` +
|
|
||||||
(payload.generation.saved_case_set_file ? ` saved=${payload.generation.saved_case_set_file}` : "")
|
|
||||||
);
|
|
||||||
setSelectedAutogenGenerationId(payload.generation.generation_id);
|
|
||||||
setEditableGeneratedQuestions([...(payload.generation.questions ?? [])]);
|
|
||||||
await loadAutoGenHistory();
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
|
||||||
setErrorText(`Автогенерация: ${message}`);
|
|
||||||
log(`Autogen generate error: ${message}`);
|
|
||||||
} finally {
|
|
||||||
setAutoGenBusy(false);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
assistantPromptVersion,
|
|
||||||
autoGenSettings.count,
|
|
||||||
autoGenSettings.generatedBy,
|
|
||||||
autoGenSettings.mode,
|
|
||||||
autoGenSettings.personalityId,
|
|
||||||
autoGenSettings.personalityPrompts,
|
|
||||||
autoGenSettings.persistToEvalCases,
|
|
||||||
connection.apiKey,
|
|
||||||
connection.baseUrl,
|
|
||||||
connection.llmProvider,
|
|
||||||
connection.maxOutputTokens,
|
|
||||||
connection.model,
|
|
||||||
connection.temperature,
|
|
||||||
decompositionPromptVersion,
|
|
||||||
loadAutoGenHistory,
|
|
||||||
log,
|
|
||||||
prompts.developerPrompt,
|
|
||||||
prompts.domainPrompt,
|
|
||||||
prompts.fewShotExamples,
|
|
||||||
prompts.schemaNotes,
|
|
||||||
prompts.systemPrompt,
|
|
||||||
selectedPersonality.domain,
|
|
||||||
selectedPersonality.id
|
|
||||||
]);
|
|
||||||
|
|
||||||
const loadCaseDialog = useCallback(
|
const loadCaseDialog = useCallback(
|
||||||
async (runId: string, caseId: string) => {
|
async (runId: string, caseId: string) => {
|
||||||
if (isLiveRunId(runId)) {
|
if (isLiveRunId(runId)) {
|
||||||
|
|
@ -2477,12 +2376,7 @@ export function AutoRunsHistoryPanel({
|
||||||
: prev.personalityId;
|
: prev.personalityId;
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
mode:
|
mode: "saved_user_sessions",
|
||||||
parsed.autoGenSettings?.mode === "codex_creative" ||
|
|
||||||
parsed.autoGenSettings?.mode === "qwen_seed" ||
|
|
||||||
parsed.autoGenSettings?.mode === "saved_user_sessions"
|
|
||||||
? parsed.autoGenSettings.mode
|
|
||||||
: prev.mode,
|
|
||||||
count:
|
count:
|
||||||
typeof parsed.autoGenSettings?.count === "number"
|
typeof parsed.autoGenSettings?.count === "number"
|
||||||
? Math.max(1, Math.min(200, parsed.autoGenSettings.count))
|
? Math.max(1, Math.min(200, parsed.autoGenSettings.count))
|
||||||
|
|
@ -2767,7 +2661,7 @@ export function AutoRunsHistoryPanel({
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="autoruns-group-heading">
|
<div className="autoruns-group-heading">
|
||||||
<h4>Автопрогоны</h4>
|
<h4>Сохранённые сессии</h4>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="autoruns-group-toggle"
|
className="autoruns-group-toggle"
|
||||||
|
|
@ -2780,20 +2674,19 @@ export function AutoRunsHistoryPanel({
|
||||||
</div>
|
</div>
|
||||||
{autogenGroupExpanded ? (
|
{autogenGroupExpanded ? (
|
||||||
<>
|
<>
|
||||||
<div className="autoruns-form-grid">
|
<div className="autoruns-form-grid">
|
||||||
<label>
|
<label>
|
||||||
Режимы
|
Режим
|
||||||
<select
|
<select
|
||||||
value={autoGenSettings.mode}
|
value={autoGenSettings.mode}
|
||||||
onChange={(event) => setAutoGenSettings((prev) => ({ ...prev, mode: event.target.value as AutoGenMode }))}
|
disabled
|
||||||
>
|
onChange={(event) => setAutoGenSettings((prev) => ({ ...prev, mode: event.target.value as AutoGenMode }))}
|
||||||
<option value="codex_creative">codex_creative</option>
|
>
|
||||||
<option value="qwen_seed">qwen_seed</option>
|
<option value="saved_user_sessions">Пользовательские сессии</option>
|
||||||
<option value="saved_user_sessions">Пользовательские сессии</option>
|
</select>
|
||||||
</select>
|
</label>
|
||||||
</label>
|
{!isSavedUserSessionsMode ? (
|
||||||
{!isSavedUserSessionsMode ? (
|
<>
|
||||||
<>
|
|
||||||
<label>
|
<label>
|
||||||
Кол-во
|
Кол-во
|
||||||
<input
|
<input
|
||||||
|
|
@ -2892,28 +2785,22 @@ export function AutoRunsHistoryPanel({
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="button-row">
|
<div className="button-row">
|
||||||
{!isSavedUserSessionsMode ? (
|
<button type="button" className="tab" disabled={autogenHistoryBusy} onClick={() => void loadAutoGenHistory()}>
|
||||||
<>
|
{autogenHistoryBusy ? "Обновляю..." : "Обновить историю"}
|
||||||
<button type="button" disabled={autoGenBusy} onClick={() => void generateAutogenBatch()}>
|
|
||||||
{autoGenBusy ? "Генерирую..." : "Сгенерировать пачку"}
|
|
||||||
</button>
|
|
||||||
<button type="button" className="tab" disabled={autogenHistoryBusy} onClick={() => void loadAutoGenHistory()}>
|
|
||||||
{autogenHistoryBusy ? "Обновляю..." : "Обновить историю"}
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="autoruns-run-launch-btn"
|
|
||||||
style={isSavedUserSessionsMode ? { display: "none" } : undefined}
|
|
||||||
disabled={
|
|
||||||
autogenStopBusy ||
|
|
||||||
(!autogenRunBusy && (editableGeneratedQuestions.length === 0 || !selectedAutogenGeneration))
|
|
||||||
}
|
|
||||||
onClick={() => void (autogenRunBusy ? stopAutogenCampaign() : runAutogenCampaign())}
|
|
||||||
>
|
|
||||||
{autogenRunBusy ? (autogenStopBusy ? "Останавливаю..." : "Остановить прогон") : "Запустить прогон"}
|
|
||||||
</button>
|
</button>
|
||||||
|
{!isSavedUserSessionsMode ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="autoruns-run-launch-btn"
|
||||||
|
disabled={
|
||||||
|
autogenStopBusy ||
|
||||||
|
(!autogenRunBusy && (editableGeneratedQuestions.length === 0 || !selectedAutogenGeneration))
|
||||||
|
}
|
||||||
|
onClick={() => void (autogenRunBusy ? stopAutogenCampaign() : runAutogenCampaign())}
|
||||||
|
>
|
||||||
|
{autogenRunBusy ? (autogenStopBusy ? "Останавливаю..." : "Остановить прогон") : "Запустить прогон"}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="autoruns-form-grid">
|
<div className="autoruns-form-grid">
|
||||||
|
|
|
||||||
|
|
@ -111,8 +111,8 @@ export function PromptPanel({
|
||||||
<section className="embedded-panel-section">
|
<section className="embedded-panel-section">
|
||||||
<div className="embedded-panel-section-header">
|
<div className="embedded-panel-section-header">
|
||||||
<div>
|
<div>
|
||||||
<h4>Prompt Manager</h4>
|
<h4>Normalizer / eval prompts</h4>
|
||||||
<p>Системный, developer и domain уровни управляются отдельно.</p>
|
<p>Эти поля управляют декомпозицией и eval-прогонами, а не live-ответом ассистента.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{content}
|
{content}
|
||||||
|
|
@ -121,7 +121,7 @@ export function PromptPanel({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelFrame title="Prompt Manager" subtitle="Системный, developer и domain уровни управляются отдельно.">
|
<PanelFrame title="Normalizer / eval prompts" subtitle="Эти поля управляют декомпозицией и eval-прогонами, а не live-ответом ассистента.">
|
||||||
{content}
|
{content}
|
||||||
</PanelFrame>
|
</PanelFrame>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
||||||
import argparse
|
import argparse
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -435,9 +436,22 @@ def dump_json(payload: Any) -> str:
|
||||||
return json.dumps(payload, ensure_ascii=False, indent=2)
|
return json.dumps(payload, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_windows_extended_path(file_path: Path) -> Path:
|
||||||
|
if os.name == "nt":
|
||||||
|
resolved = str(file_path.resolve())
|
||||||
|
if not resolved.startswith("\\\\?\\"):
|
||||||
|
if resolved.startswith("\\\\"):
|
||||||
|
resolved = "\\\\?\\UNC\\" + resolved.lstrip("\\")
|
||||||
|
else:
|
||||||
|
resolved = "\\\\?\\" + resolved
|
||||||
|
return Path(resolved)
|
||||||
|
return file_path
|
||||||
|
|
||||||
|
|
||||||
def write_text(file_path: Path, text: str) -> None:
|
def write_text(file_path: Path, text: str) -> None:
|
||||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
target_path = resolve_windows_extended_path(file_path)
|
||||||
file_path.write_text(text, encoding="utf-8", newline="\n")
|
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
target_path.write_text(text, encoding="utf-8", newline="\n")
|
||||||
|
|
||||||
|
|
||||||
def write_json(file_path: Path, payload: Any) -> None:
|
def write_json(file_path: Path, payload: Any) -> None:
|
||||||
|
|
@ -3016,6 +3030,14 @@ def build_scenario_step_state(
|
||||||
"entries": entries,
|
"entries": entries,
|
||||||
}
|
}
|
||||||
step_state["execution_status"] = derive_step_execution_status(reply_type if isinstance(reply_type, str) else None, debug)
|
step_state["execution_status"] = derive_step_execution_status(reply_type if isinstance(reply_type, str) else None, debug)
|
||||||
|
if (
|
||||||
|
"answer_inspection" in normalize_string_list(step_state.get("semantic_tags"))
|
||||||
|
and step_state["execution_status"] == "needs_exact_capability"
|
||||||
|
and reply_type in {"factual", "factual_with_explanation"}
|
||||||
|
and assistant_text.strip()
|
||||||
|
):
|
||||||
|
step_state["execution_status"] = "exact"
|
||||||
|
step_state["answer_inspection_chat_validated"] = True
|
||||||
step_state["acceptance_status"] = step_state["execution_status"]
|
step_state["acceptance_status"] = step_state["execution_status"]
|
||||||
step_state["status"] = step_state["acceptance_status"]
|
step_state["status"] = step_state["acceptance_status"]
|
||||||
return validate_step_contract(step_state)
|
return validate_step_contract(step_state)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
@ -12,6 +15,27 @@ import domain_truth_harness as dth
|
||||||
|
|
||||||
|
|
||||||
class DomainCaseLoopStepStateTests(unittest.TestCase):
|
class DomainCaseLoopStepStateTests(unittest.TestCase):
|
||||||
|
@unittest.skipIf(os.name != "nt", "extended-length path handling is Windows-specific")
|
||||||
|
def test_write_json_supports_long_windows_artifact_paths(self) -> None:
|
||||||
|
temp_dir = tempfile.mkdtemp()
|
||||||
|
try:
|
||||||
|
base = Path(temp_dir)
|
||||||
|
long_path = (
|
||||||
|
base
|
||||||
|
/ ("artifact_root_" + "a" * 80)
|
||||||
|
/ ("scenario_" + "b" * 80)
|
||||||
|
/ ("step_" + "c" * 80)
|
||||||
|
/ "assistant_response.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
dcl.write_json(long_path, {"ok": True})
|
||||||
|
|
||||||
|
extended_path = dcl.resolve_windows_extended_path(long_path)
|
||||||
|
self.assertTrue(extended_path.exists())
|
||||||
|
self.assertIn('"ok": true', extended_path.read_text(encoding="utf-8-sig"))
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(dcl.resolve_windows_extended_path(Path(temp_dir)), ignore_errors=True)
|
||||||
|
|
||||||
def test_preserves_mcp_catalog_alignment_debug_fields(self) -> None:
|
def test_preserves_mcp_catalog_alignment_debug_fields(self) -> None:
|
||||||
step_state = dcl.build_scenario_step_state(
|
step_state = dcl.build_scenario_step_state(
|
||||||
scenario_id="planner_alignment_demo",
|
scenario_id="planner_alignment_demo",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue