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) => {
|
||||
try {
|
||||
const body = toRecord(req.body);
|
||||
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
|
||||
});
|
||||
throw new http_1.ApiError("AUTOGEN_QUESTION_GENERATION_DISABLED", "Автогенерация вопросов отключена: используйте сохранение живых пользовательских сессий.", 410);
|
||||
}
|
||||
catch (error) {
|
||||
next(error);
|
||||
|
|
|
|||
|
|
@ -1869,6 +1869,10 @@ function resolveUnicodeAddressIntentBridge(text) {
|
|||
/(?:хвост|задержк|проблем|систематическ)/iu.test(normalized)) {
|
||||
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) {
|
||||
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")}`
|
||||
};
|
||||
}
|
||||
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) {
|
||||
const isoDate = normalizeIsoDateForQuery(value);
|
||||
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" &&
|
||||
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);
|
||||
if (taxQuarterWindow) {
|
||||
filters.extracted_filters = {
|
||||
|
|
@ -3176,8 +3202,14 @@ class AddressQueryService {
|
|||
shadowRouteAudit
|
||||
});
|
||||
}
|
||||
const rawUserMessageForReply = typeof options.rawUserMessage === "string" && options.rawUserMessage.trim().length > 0
|
||||
? options.rawUserMessage
|
||||
: undefined;
|
||||
const composeOptionsFromFilters = (filterSet, options = {}) => ({
|
||||
userMessage,
|
||||
rawUserMessage: typeof options.rawUserMessage === "string" && options.rawUserMessage.trim().length > 0
|
||||
? options.rawUserMessage
|
||||
: rawUserMessageForReply,
|
||||
itemHint: typeof filterSet.item === "string" ? filterSet.item : undefined,
|
||||
counterpartyHint: typeof options.counterpartyHint === "string"
|
||||
? options.counterpartyHint
|
||||
|
|
@ -3195,6 +3227,17 @@ class AddressQueryService {
|
|||
asOfDate: typeof filterSet.as_of_date === "string" ? filterSet.as_of_date : undefined,
|
||||
requestedResultMode,
|
||||
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,
|
||||
useRubCurrency: options.useRubCurrency ?? undefined
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1413,6 +1413,85 @@ function buildInventoryMovementQuery(filters, resolvedLimit, side) {
|
|||
.replace("__WHERE_CLAUSE__", buildWhereClause(filters, "Движения.Период", [inventoryCondition, itemCondition].filter((item) => Boolean(item))))
|
||||
.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) {
|
||||
const warehouse = typeof filters.warehouse === "string" ? filters.warehouse.trim() : "";
|
||||
if (!warehouse) {
|
||||
|
|
@ -1922,7 +2001,7 @@ function buildAddressRecipePlan(recipe, filters) {
|
|||
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||
? buildInventoryAgingByPurchaseDateQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_quality_events_profile"
|
||||
? buildInventoryQualityEventsQuery(filters, resolvedLimit)
|
||||
: 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);
|
||||
}
|
||||
function buildVatPurchaseDateAnchorDisclosureLine(options, periodWindowLabel) {
|
||||
if (!periodWindowLabel || !needsVatPurchaseDateAnchorDisclosure(options.userMessage)) {
|
||||
return null;
|
||||
function vatPurchaseDateBasisLabel(basis, hasMultiplePurchaseDates) {
|
||||
if (!hasMultiplePurchaseDates) {
|
||||
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 periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
||||
|
|
@ -565,9 +589,13 @@ function buildVatPurchaseDateAnchorDisclosureLine(options, periodWindowLabel) {
|
|||
toTs !== null &&
|
||||
asOfTs >= fromTs &&
|
||||
asOfTs <= toTs) {
|
||||
return `- Якорь периода: дата покупки ${formatDateRu(asOfDate)} попадает в налоговый период ${periodWindowLabel}; поэтому расчет ниже взят из книг продаж/покупок за это окно.`;
|
||||
return [
|
||||
`- Якорь периода: дата покупки ${formatDateRu(asOfDate)} попадает в налоговый период ${periodWindowLabel}; поэтому расчет ниже взят из книг продаж/покупок за это окно.`
|
||||
];
|
||||
}
|
||||
return `- Якорь периода: дата покупки из вопроса/контекста использована для выбора налогового периода ${periodWindowLabel}; сам расчет ниже взят из книг продаж/покупок за это окно.`;
|
||||
return [
|
||||
`- Якорь периода: дата покупки из вопроса/контекста использована для выбора налогового периода ${periodWindowLabel}; сам расчет ниже взят из книг продаж/покупок за это окно.`
|
||||
];
|
||||
}
|
||||
function detectRankingLimit(userMessage, fallback = 20) {
|
||||
const text = normalizeQuestionText(userMessage);
|
||||
|
|
@ -996,12 +1024,72 @@ function hasInventoryAccountPrefix(value, prefix) {
|
|||
.replace(",", ".");
|
||||
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) {
|
||||
return hasInventoryAccountPrefix(row.account_dt, "41.01");
|
||||
return hasInventoryAccountPrefix(row.account_dt, "41.01") && !isInventoryOnHandSnapshotRow(row);
|
||||
}
|
||||
function isInventorySaleMovement(row) {
|
||||
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) {
|
||||
const normalized = String(value ?? "").trim();
|
||||
if (!normalized) {
|
||||
|
|
@ -1162,7 +1250,11 @@ function formatCounterpartyItemFlowRows(rows, limit = 12) {
|
|||
function buildInventoryAgingByItemAggregate(rows, asOfDate) {
|
||||
const byItem = new Map();
|
||||
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
|
||||
const onHandScope = buildInventoryOnHandAgingScope(rows);
|
||||
for (const row of rows) {
|
||||
if (!isInventoryPurchaseMovement(row) || !inventoryPurchaseMatchesOnHandAgingScope(row, onHandScope)) {
|
||||
continue;
|
||||
}
|
||||
const item = extractInventoryItemName(row);
|
||||
if (!item) {
|
||||
continue;
|
||||
|
|
@ -1245,19 +1337,11 @@ function formatInventoryAgingRows(items, asOfDate, limit = 10) {
|
|||
const parts = [
|
||||
`${index + 1}. ${item.item}`,
|
||||
`первая закупка: ${inventoryTraceDateLabel(item.firstPurchasePeriod)}`,
|
||||
`последняя закупка: ${inventoryTraceDateLabel(item.lastPurchasePeriod)}`,
|
||||
`документов: ${formatNumberWithDots(item.documentCount)}`,
|
||||
`операций: ${formatNumberWithDots(item.operations)}`
|
||||
`последняя закупка: ${inventoryTraceDateLabel(item.lastPurchasePeriod)}`
|
||||
];
|
||||
if (item.ageDays !== null) {
|
||||
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) {
|
||||
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 organizationLabel = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.organizationHint);
|
||||
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 = [
|
||||
`Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`,
|
||||
directVatLine,
|
||||
"Расчет сделан по книгам продаж и покупок.",
|
||||
"",
|
||||
"Что вошло в расчет:",
|
||||
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
|
||||
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
|
||||
...(purchaseDateAnchorLine ? [purchaseDateAnchorLine] : []),
|
||||
...purchaseDateAnchorLines,
|
||||
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
|
||||
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
|
||||
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`
|
||||
|
|
|
|||
|
|
@ -406,7 +406,11 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
|||
const focus = deps.detectValueRankingFocus(options.userMessage);
|
||||
const limit = deps.detectRankingLimit(options.userMessage, 20);
|
||||
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" &&
|
||||
/(?:какой|кто|which|who|какой|кто)/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 cashflowBoundaryLine = buildCashflowBoundaryLine(isSupplier);
|
||||
const cashflowNextStepLine = buildCashflowNextStepLine(isSupplier, normalizedQuestion);
|
||||
const dependencyCue = /(?:зависим|завязан|концентрац|ключев|основн|критич|dependency|concentration|key|critical)/iu.test(normalizedQuestion);
|
||||
const byCounterparty = new Map();
|
||||
const byYear = new Map();
|
||||
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} операциям). Это не полноценный сравнительный рейтинг.`
|
||||
: isSupplier
|
||||
? `Крупнейший поставщик по подтвержденным выплатам ${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} операциям).`
|
||||
: dependencyCue
|
||||
? `Главный сигнал зависимости по входящим деньгам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||
: `Самый доходный клиент ${rankingPeriodLabel} по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`;
|
||||
lines.unshift(directAnswerLine);
|
||||
if (dependencyCue) {
|
||||
lines.splice(1, 0, "Это сигнал концентрации по найденным денежным строкам 1С, а не подтверждение договорной зависимости, надежности контрагента или чистой прибыли.");
|
||||
}
|
||||
if (!isSupplier) {
|
||||
lines.splice(1, 0, cashflowBoundaryLine);
|
||||
lines.splice(dependencyCue ? 2 : 1, 0, cashflowBoundaryLine);
|
||||
if (cashflowNextStepLine) {
|
||||
lines.splice(2, 0, cashflowNextStepLine);
|
||||
lines.splice(dependencyCue ? 3 : 2, 0, cashflowNextStepLine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -460,6 +460,23 @@ function resolveYearFromFilters(filters) {
|
|||
function hasRelativeYearHint(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) {
|
||||
if (!followupContext || !isInventoryRootFrameIntent(followupContext.root_intent)) {
|
||||
return null;
|
||||
|
|
@ -629,6 +646,34 @@ function hasInventoryPurchaseDateVatBridgeCue(text) {
|
|||
return (/(?:ндс|vat)/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) {
|
||||
const normalized = textWithRepairedVariant(String(text ?? ""))
|
||||
.toLowerCase()
|
||||
|
|
@ -745,6 +790,31 @@ function isShortDebtRoleMirrorFollowup(intent, text, followupContext) {
|
|||
function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
const merged = { ...current };
|
||||
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 ((intent === "list_open_contracts" ||
|
||||
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");
|
||||
}
|
||||
}
|
||||
clearInventoryAgingOrganizationAliasItem(null);
|
||||
return { filters: merged, reasons };
|
||||
}
|
||||
const previous = followupContext.previous_filters ?? {};
|
||||
|
|
@ -773,6 +844,12 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
|||
const previousAsOfDate = toNonEmptyString(previous.as_of_date);
|
||||
const previousPeriodFrom = toNonEmptyString(previous.period_from);
|
||||
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 relativeMonthFromFollowupYear = resolveRelativeMonthPeriodFromFollowupYear(userMessage, followupContext);
|
||||
const allTimeRequested = hasAllTimeHint(userMessage);
|
||||
|
|
@ -790,6 +867,53 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
|||
merged.organization = previousOrganization;
|
||||
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" &&
|
||||
followupContext.previous_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");
|
||||
}
|
||||
if (intent === "inventory_aging_by_purchase_date") {
|
||||
const explicitItemMention = /(?:^|[\s,.;:!?()\-\u2014])(?:товар(?:у|а|ом)?|позици(?:и|я|ю)|item|row|line)(?=$|[\s,.;:!?()\-\u2014])/iu.test(String(userMessage ?? ""));
|
||||
if (toNonEmptyString(merged.item) && !explicitItemMention) {
|
||||
delete merged.item;
|
||||
reasons.push("item_cleared_for_stock_slice_aging");
|
||||
}
|
||||
clearInventoryAgingOrganizationAliasItem(previousOrganization);
|
||||
}
|
||||
if (!sameDateRequested &&
|
||||
hasFollowupSignalForConfirmed &&
|
||||
|
|
@ -1317,7 +1437,12 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
|||
return detectedIntent;
|
||||
}
|
||||
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) {
|
||||
return detectedIntent;
|
||||
}
|
||||
|
|
@ -1326,7 +1451,6 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
|||
if (!sourceIntent && !fallbackIntent) {
|
||||
return detectedIntent;
|
||||
}
|
||||
const previousFilters = followupContext.previous_filters ?? {};
|
||||
const previousPeriodFrom = toNonEmptyString(previousFilters.period_from);
|
||||
const previousPeriodTo = toNonEmptyString(previousFilters.period_to);
|
||||
const previousContract = toNonEmptyString(previousFilters.contract);
|
||||
|
|
@ -1340,6 +1464,7 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
|||
const samePeriodVatFollowup = isVatFollowup &&
|
||||
hasSamePeriodHint(normalizedMessage) &&
|
||||
Boolean(previousPeriodFrom || previousPeriodTo);
|
||||
const previousPurchaseDateBridgeSelected = toNonEmptyString(previousFilters.purchase_date_bridge_selected);
|
||||
const previousIsInventoryFamily = isInventoryIntent(sourceIntent ?? undefined);
|
||||
const rootIsInventoryFamily = isInventoryIntent(followupContext.root_intent ?? undefined);
|
||||
const inventoryLineageActive = previousIsInventoryFamily ||
|
||||
|
|
@ -1361,7 +1486,11 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
|||
sourceIntent === "open_items_by_counterparty_or_contract");
|
||||
const hasExplicitInventoryItemReference = /(?:товар|номенклатур|позици|склад|остат|sku|item|product|товар|номенклатур|позици|склад|остат)/iu.test(normalizedMessage) || hasSelectedObjectInlineSnapshotMetadata(normalizedMessage);
|
||||
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" ||
|
||||
fallbackIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
followupContext.root_intent === "inventory_margin_ranking_for_nomenclature";
|
||||
|
|
@ -1382,6 +1511,7 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
|||
if (inventoryPurchaseDateVatBridge &&
|
||||
(detectedIntent.intent === "unknown" ||
|
||||
detectedIntent.intent === sourceIntent ||
|
||||
detectedIntent.intent === "account_balance_snapshot" ||
|
||||
detectedIntent.intent === "vat_payable_confirmed_as_of_date" ||
|
||||
detectedIntent.intent === "vat_payable_forecast")) {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -57,6 +57,16 @@ function inventoryRequestedPartyMatches(requested, actualParties) {
|
|||
function inventoryPartyListOrUnknown(parties) {
|
||||
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) {
|
||||
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") {
|
||||
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 agingItems = deps.buildInventoryAgingByItemAggregate(purchaseRows, asOfDate);
|
||||
const oldestPurchaseDate = agingItems[0]?.firstPurchasePeriod ?? summary.firstPeriod;
|
||||
const oldestPurchaseAgeDays = agingItems[0]?.ageDays ?? null;
|
||||
const organizationLabel = agingItems.find((item) => item.organization)?.organization ?? null;
|
||||
const oldestAnswerPreview = agingItems
|
||||
.slice(0, 3)
|
||||
.map((item) => `${item.item} (${deps.inventoryTraceDateLabel(item.firstPurchasePeriod)})`)
|
||||
.join("; ");
|
||||
const directAnswerLine = agingItems.length > 0
|
||||
? `К самым старым закупкам в текущем подтвержденном срезе относятся позиции с самой ранней первой закупкой: ${oldestAnswerPreview}.`
|
||||
: "По доступному закупочному следу позиции со старыми закупками не материализованы.";
|
||||
? `Среди фактических положительных остатков есть давно закупавшиеся позиции: ${oldestAnswerPreview}.`
|
||||
: "В фактическом положительном остатке не найдено позиций с подтвержденным старым закупочным следом.";
|
||||
const lines = [directAnswerLine];
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Сводка:", [
|
||||
const summaryLines = [
|
||||
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
`Самая ранняя первая закупка среди позиций: ${deps.inventoryTraceDateLabel(oldestPurchaseDate)}.`,
|
||||
`Самая поздняя найденная закупка: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
||||
`Позиций в выборке: ${deps.formatNumberWithDots(agingItems.length)}.`,
|
||||
`Закупочных документов: ${deps.formatNumberWithDots(summary.documents.length)}.`,
|
||||
`Закупочных операций: ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
||||
]);
|
||||
];
|
||||
if (organizationLabel) {
|
||||
summaryLines.splice(1, 0, `Организация: ${organizationLabel}.`);
|
||||
}
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Сводка:", summaryLines);
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Ограничения:", [
|
||||
"Без партионного учета этот ответ показывает возраст закупочного следа по товарной позиции, а не возраст конкретного лота."
|
||||
"Берем только позиции с положительным остатком на дату среза; без партионного учета это возраст закупочного следа по номенклатуре, а не доказанный возраст конкретной партии."
|
||||
]);
|
||||
if (oldestPurchaseAgeDays !== null) {
|
||||
lines.push(`- Между самой ранней первой закупкой и датой среза прошло ${deps.formatNumberWithDots(oldestPurchaseAgeDays)} дн.`);
|
||||
|
|
@ -411,11 +439,11 @@ function composeInventoryReply(intent, rows, options, deps) {
|
|||
lines.push(`- Поставщики, встречающиеся в наблюдаемом закупочном следе: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||
}
|
||||
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 {
|
||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Позиции от самых старых закупок:", [
|
||||
"- В доступных данных не найдено закупочных движений для выбранного среза."
|
||||
"- В доступных данных не найдено закупочных движений по позициям, которые есть в положительном остатке на дату среза."
|
||||
]);
|
||||
}
|
||||
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)({
|
||||
messageUsed,
|
||||
rawUserMessage: input.userMessage,
|
||||
carryMeta,
|
||||
analysisDateHint,
|
||||
llmSemanticHints,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ exports.buildAssistantAddressLaneAttemptRuntimeInput = buildAssistantAddressLane
|
|||
function buildAssistantAddressLaneAttemptRuntimeInput(input) {
|
||||
return {
|
||||
messageUsed: input.messageUsed,
|
||||
rawUserMessage: input.rawUserMessage,
|
||||
carryMeta: input.carryMeta,
|
||||
analysisDateHint: input.analysisDateHint,
|
||||
llmSemanticHints: input.llmSemanticHints,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ function resolveAssistantAddressLaneAttemptFollowupContext(carryMeta) {
|
|||
}
|
||||
function buildAssistantAddressLaneAttemptQueryOptions(input) {
|
||||
const base = {
|
||||
analysisDateHint: input.analysisDateHint
|
||||
analysisDateHint: input.analysisDateHint,
|
||||
rawUserMessage: input.rawUserMessage ?? undefined
|
||||
};
|
||||
if (input.scopedFollowupContext) {
|
||||
base.followupContext = input.scopedFollowupContext;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ async function runAssistantAddressLaneAttemptRuntime(input) {
|
|||
scopedFollowupContext,
|
||||
llmSemanticHints: input.llmSemanticHints ?? null,
|
||||
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();
|
||||
}
|
||||
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) {
|
||||
if (!hasComparisonScopeProofCue(input.userMessage)) {
|
||||
return null;
|
||||
}
|
||||
const entryPoint = toRecordObject(input.debug.assistant_mcp_discovery_entry_point_v1);
|
||||
const turnInput = toRecordObject(entryPoint?.turn_input);
|
||||
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ function hasCompactCashflowFollowupSignal(text) {
|
|||
if (!value) {
|
||||
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) {
|
||||
if (!dateScope) {
|
||||
|
|
@ -79,6 +79,35 @@ function dateScopeToFilterWindow(dateScope) {
|
|||
}
|
||||
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) {
|
||||
const text = compactLower(value);
|
||||
if (!text) {
|
||||
|
|
@ -103,12 +132,15 @@ function inferBusinessOverviewDiscoveryContextFromSessionItems(sessionItems, toN
|
|||
continue;
|
||||
}
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
const previousFilters = { ...filterWindow };
|
||||
const organization = toNonEmptyString(turnMeaningRef?.explicit_organization_scope);
|
||||
const organization = toNonEmptyString(turnMeaningRef?.explicit_organization_scope) ??
|
||||
toNonEmptyString(debugFilters?.organization);
|
||||
if (looksLikeReliableOrganizationScope(organization)) {
|
||||
previousFilters.organization = organization;
|
||||
}
|
||||
|
|
@ -129,9 +161,7 @@ function mergeBusinessOverviewDateContextForCompactCashflow(input) {
|
|||
return input.followupContext;
|
||||
}
|
||||
const existingFilters = toRecordObject(input.followupContext?.previous_filters);
|
||||
if (input.toNonEmptyString(existingFilters?.period_from) ||
|
||||
input.toNonEmptyString(existingFilters?.period_to) ||
|
||||
input.toNonEmptyString(existingFilters?.as_of_date)) {
|
||||
if (hasBoundedFilterWindow(existingFilters)) {
|
||||
return input.followupContext;
|
||||
}
|
||||
const inferred = inferBusinessOverviewDiscoveryContextFromSessionItems(input.sessionItems, input.toNonEmptyString);
|
||||
|
|
@ -142,12 +172,12 @@ function mergeBusinessOverviewDateContextForCompactCashflow(input) {
|
|||
...inferred,
|
||||
...(input.followupContext ?? {}),
|
||||
previous_filters: {
|
||||
...(toRecordObject(inferred.previous_filters) ?? {}),
|
||||
...(toRecordObject(input.followupContext?.previous_filters) ?? {})
|
||||
...(toRecordObject(input.followupContext?.previous_filters) ?? {}),
|
||||
...(toRecordObject(inferred.previous_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);
|
||||
}
|
||||
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) {
|
||||
if (!followupContext) {
|
||||
return false;
|
||||
|
|
@ -344,6 +381,9 @@ function mergeBusinessOverviewProofBundlesFromNavigationState(input) {
|
|||
const entities = toRecordObject(input.predecomposeContract?.entities);
|
||||
const hasCurrentOrganizationSelection = Boolean(input.toNonEmptyString(entities?.organization));
|
||||
const businessOverviewFollowup = isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString);
|
||||
if (!hasComparisonScopeProofCue(input.userMessage)) {
|
||||
return input.followupContext;
|
||||
}
|
||||
if (!businessOverviewFollowup && !hasCurrentOrganizationSelection) {
|
||||
return input.followupContext;
|
||||
}
|
||||
|
|
@ -376,6 +416,9 @@ function mergeBusinessOverviewProofBundlesFromNavigationState(input) {
|
|||
};
|
||||
}
|
||||
function mergeBusinessOverviewProofBundlesFromSessionItems(input) {
|
||||
if (!hasComparisonScopeProofCue(input.userMessage)) {
|
||||
return input.followupContext;
|
||||
}
|
||||
if (!isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString)) {
|
||||
return input.followupContext;
|
||||
}
|
||||
|
|
@ -576,6 +619,7 @@ async function buildAssistantAddressOrchestrationRuntime(input) {
|
|||
});
|
||||
const discoveryFollowupContextWithProofBundles = mergeBusinessOverviewProofBundlesFromSessionItems({
|
||||
followupContext: discoveryFollowupContext,
|
||||
userMessage: input.userMessage,
|
||||
sessionItems: input.sessionItems,
|
||||
toNonEmptyString: input.toNonEmptyString
|
||||
});
|
||||
|
|
@ -583,6 +627,7 @@ async function buildAssistantAddressOrchestrationRuntime(input) {
|
|||
followupContext: discoveryFollowupContextWithProofBundles,
|
||||
sessionAddressNavigationState: input.sessionAddressNavigationState,
|
||||
predecomposeContract,
|
||||
userMessage: input.userMessage,
|
||||
toNonEmptyString: input.toNonEmptyString
|
||||
});
|
||||
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(input.userMessage, addressInputMessage, carryover, addressPreDecompose);
|
||||
|
|
|
|||
|
|
@ -74,6 +74,15 @@ function normalizeAddressRuntimeMetaForDeep(value) {
|
|||
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) {
|
||||
const runPackagingRuntimeSafe = input.runPackagingRuntime ?? assistantDeepTurnPackagingRuntimeAdapter_1.runAssistantDeepTurnPackagingRuntime;
|
||||
const runFinalizeDeepTurnSafe = input.runFinalizeDeepTurn ?? assistantDeepTurnFinalizeRuntimeAdapter_1.finalizeAssistantDeepTurn;
|
||||
|
|
@ -125,9 +134,9 @@ function runAssistantDeepTurnResponseRuntime(input) {
|
|||
currentReplySource: "deep_analysis",
|
||||
addressRuntimeMeta: addressRuntimeMetaForDeep
|
||||
});
|
||||
const assistantReply = mcpDiscoveryResponsePolicy.applied
|
||||
? mcpDiscoveryResponsePolicy.reply_text
|
||||
: packagingRuntime.safeAssistantReply;
|
||||
const assistantReply = stripRoutineShortAnswerPrefix(mcpDiscoveryResponsePolicy.applied
|
||||
? input.sanitizeReply(mcpDiscoveryResponsePolicy.reply_text, packagingRuntime.safeAssistantReply)
|
||||
: packagingRuntime.safeAssistantReply);
|
||||
const replyType = mcpDiscoveryResponsePolicy.applied ? "partial_coverage" : input.composition.reply_type;
|
||||
const 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);
|
||||
}
|
||||
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) {
|
||||
const text = normalizeQuestionText([
|
||||
turnMeaning?.raw_message,
|
||||
|
|
@ -429,6 +433,13 @@ function businessOverviewPeriodText(overview) {
|
|||
const period = toNonEmptyString(overview.period_scope);
|
||||
return period ? `за ${period}` : "за все доступное проверенное окно";
|
||||
}
|
||||
function businessOverviewPeriodPhrase(value) {
|
||||
const normalized = toNonEmptyString(value);
|
||||
if (!normalized) {
|
||||
return "за все доступное проверенное окно";
|
||||
}
|
||||
return /^за\s+/iu.test(normalized) ? normalized : `за ${normalized}`;
|
||||
}
|
||||
function strongestIncomingYear(overview) {
|
||||
const years = Array.isArray(overview.yearly_breakdown) ? overview.yearly_breakdown : [];
|
||||
const sorted = years
|
||||
|
|
@ -857,7 +868,10 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
|||
const compactCashflowRequested = directMoneyOnlyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
|
||||
const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph);
|
||||
const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph);
|
||||
const nextChecksRequested = requestsBusinessOverviewNextChecks(turnMeaning, graph);
|
||||
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 organizationScopeComparable = compactComparable(organizationScope);
|
||||
const plainOrganizationClarificationSelection = Boolean(separateSubject &&
|
||||
|
|
@ -868,15 +882,51 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
|||
rawMessageComparable.includes(organizationScopeComparable) &&
|
||||
rawMessage.length <= 90 &&
|
||||
!/(?:сравн|подтвержд|деньг|сколько|что\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)) {
|
||||
const netDisplay = sentenceAmount(netAmount) ?? netAmount ?? "0 \u0440\u0443\u0431.";
|
||||
const signedNetDisplay = cashflowPolarityRequested && netDisplay && !String(netDisplay).trim().startsWith("-")
|
||||
? `+${netDisplay}`
|
||||
: netDisplay;
|
||||
const polarityLead = 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: ";
|
||||
const limitedEarningsAnswer = Boolean(earningsAmountRequested && !accountingFinancialResultForEarnings && limitLine);
|
||||
const polarityLead = limitedEarningsAnswer
|
||||
? "\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}.`);
|
||||
if (limitLine) {
|
||||
lines.push(limitLine);
|
||||
}
|
||||
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 \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С, а не внешний аудит и не юридически подтвержденная отчетность.");
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
const headline = toNonEmptyString(draft.headline);
|
||||
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
|
||||
lines.push(cleanHeadline
|
||||
? `Коротко: ${localizeLine(cleanHeadline)}`
|
||||
: "Коротко: нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только ограниченный операционный денежный/товарный сигнал, а не полный отчет о прибыли и не бухгалтерский финансовый результат.");
|
||||
lines.push("Коротко: чистую прибыль и маржу не подтверждаю по текущему срезу 1С; видны только ограниченные денежные обороты и расчетное нетто/операционный сигнал, а не полный отчет о прибыли и не бухгалтерский финансовый результат.");
|
||||
const boundaryLines = userFacingLines([
|
||||
...toStringList(draft.confirmed_lines),
|
||||
...toStringList(draft.inference_lines),
|
||||
|
|
@ -1005,9 +1051,12 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
|||
? `${vendorProcurementQuality.top_non_financial_supplier_share_pct}%`
|
||||
: null;
|
||||
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") {
|
||||
lines.push(`Коротко: проверка концентрации закупок/исходящих платежей за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`);
|
||||
lines.push(`Коротко: проверка концентрации закупок/исходящих платежей ${periodPhrase} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`);
|
||||
const financialHintText = financialFlowHintTextRuFromRecord(topOutgoingRecord);
|
||||
if (financialHintText) {
|
||||
lines.push(financialHintText);
|
||||
|
|
@ -1017,17 +1066,17 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
|||
}
|
||||
}
|
||||
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 {
|
||||
lines.push(`Коротко: проверка концентрации закупок/исходящих платежей за ${periodScope} выполнена, но надежной небанковской концентрации поставщика по найденным исходящим платежам не хватает${totalText}.`);
|
||||
lines.push(`Коротко: проверка концентрации закупок/исходящих платежей ${periodPhrase} выполнена, но надежной небанковской концентрации поставщика по найденным исходящим платежам не хватает${totalText}.`);
|
||||
}
|
||||
const contractText = typeof vendorProcurementQuality.used_contracts === "number" && Number.isFinite(vendorProcurementQuality.used_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} договоров.`
|
||||
: "";
|
||||
lines.push(`Что не доказано этим срезом: надежность поставщика, качество поставок, договорные условия, назначение каждого платежа и полная структура всех расходов.${contractText}`);
|
||||
lines.push(`Это не аудит поставщиков: срез не доказывает надежность поставщика, качество поставок, договорные условия, назначение каждого платежа и полную структуру всех расходов.${contractText}`);
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
const supplierBasis = topSupplier
|
||||
|
|
@ -1068,6 +1117,19 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
|||
lines.push("Проверить нужно отдельно: складской срез на дату, учетную политику резервов, списания и ликвидационную стоимость; косвенные признаки нельзя выдавать за доказанный факт резерва.");
|
||||
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 &&
|
||||
!crossScopeExecutiveSummary &&
|
||||
!counterpartyLeaderSurfaceRequested &&
|
||||
|
|
@ -1075,8 +1137,8 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
|||
(actionFamily === "broad_evaluation" || unsupportedFamily === "broad_business_evaluation")) {
|
||||
const subject = organizationScope ?? "компания";
|
||||
const periodWithoutPrefix = period.replace(/^за\s+/iu, "");
|
||||
lines.push(`Коротко: по доступным данным ${subject} выглядит как бизнес с крупными контрактными денежными потоками и заметной зависимостью от нескольких крупных контрагентов, а не как равномерный поток мелких продаж.`);
|
||||
lines.push("Что видно:");
|
||||
lines.push(`Коротко: по ограниченному проверенному 1С-срезу ${subject} выглядит как бизнес с крупными контрактными денежными потоками и заметной зависимостью от нескольких крупных контрагентов, а не как равномерный поток мелких продаж; это не аудиторское заключение и не подтвержденная чистая прибыль.`);
|
||||
lines.push("Что видно в ограниченном денежном срезе:");
|
||||
if (incomingAmount) {
|
||||
lines.push(`- входящие деньги за ${periodWithoutPrefix}: ${incomingAmount};`);
|
||||
}
|
||||
|
|
@ -1108,7 +1170,9 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
|||
if (debtLine) {
|
||||
lines.push(`- ${localizeLine(debtLine)}`);
|
||||
}
|
||||
lines.push("Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль.");
|
||||
if (!limitLine) {
|
||||
lines.push("Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль.");
|
||||
}
|
||||
const missingOverviewFamilies = [];
|
||||
if (!taxLine) {
|
||||
missingOverviewFamilies.push("НДС/налоговая позиция без отдельного точного расчета");
|
||||
|
|
|
|||
|
|
@ -52,6 +52,15 @@ function hasInternalMechanics(value) {
|
|||
text.includes("catalog_") ||
|
||||
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) {
|
||||
const record = toRecordObject(value);
|
||||
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");
|
||||
const replyText = stripRoutineShortAnswerPrefix(String(candidate.reply_text));
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryResponsePolicy",
|
||||
decision: "apply_candidate",
|
||||
applied: true,
|
||||
reply_text: String(candidate.reply_text),
|
||||
reply_text: replyText || String(candidate.reply_text),
|
||||
reply_source: "mcp_discovery_response_candidate_guarded",
|
||||
candidate,
|
||||
reason_codes: reasonCodes
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@ function isGarbageSemanticAnchorCandidate(value) {
|
|||
}
|
||||
if (/^(?:по\s+)?(?:этим|этими)\s+данн\p{L}*$/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+хватает)/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);
|
||||
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) {
|
||||
if (!text) {
|
||||
return false;
|
||||
|
|
@ -770,7 +780,7 @@ function hasBusinessOverviewContinuationSignal(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 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 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) &&
|
||||
|
|
@ -1338,7 +1348,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
const debtDueDateBusinessOverviewSignal = ((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
||||
hasOrganizationLevelDebtDueDateOverviewSignal(rawText)) ||
|
||||
debtDueDateFollowupBusinessOverviewSignal;
|
||||
const supplierQualityBusinessOverviewSignal = (rawBusinessOverviewSignal || seededBusinessOverviewSignal) && hasOrganizationLevelSupplierQualityOverviewSignal(rawText);
|
||||
const supplierQualityBusinessOverviewSignal = ((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
||||
(hasOrganizationLevelSupplierQualityOverviewSignal(rawText) || hasSupplierDependencyFollowupSignal(rawText))) ||
|
||||
(businessOverviewContinuationSignal && hasSupplierDependencyFollowupSignal(rawText));
|
||||
const profitMarginFollowupBusinessOverviewSignal = businessOverviewContinuationSignal && hasProfitMarginBoundaryFollowupSignal(rawText);
|
||||
const profitMarginBusinessOverviewSignal = ((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
||||
hasOrganizationLevelProfitMarginBoundaryOverviewSignal(rawText)) ||
|
||||
|
|
@ -1823,11 +1835,13 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
? null
|
||||
: explicitCurrentCounterpartyOverridesFollowupEntity
|
||||
? null
|
||||
: rawMetadataScopeHint ??
|
||||
followupSeed.metadataScopeHint ??
|
||||
followupSeed.discoveryEntity ??
|
||||
followupSeed.metadataSelectedEntitySet ??
|
||||
null;
|
||||
: businessOverviewSuppressesFollowupCounterparty
|
||||
? rawMetadataScopeHint
|
||||
: rawMetadataScopeHint ??
|
||||
followupSeed.metadataScopeHint ??
|
||||
followupSeed.discoveryEntity ??
|
||||
followupSeed.metadataSelectedEntitySet ??
|
||||
null;
|
||||
const metadataScopedLaneWithoutSubject = Boolean((metadataGroundedMovementLaneApplicable ||
|
||||
metadataGroundedDocumentLaneApplicable ||
|
||||
explicitVatMovementEvidenceSignal) &&
|
||||
|
|
@ -2013,7 +2027,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
/^\d{4}$/.test(rawDateScope) &&
|
||||
normalizedPredecomposeDateScope &&
|
||||
normalizedPredecomposeDateScope.startsWith(`${rawDateScope}-`));
|
||||
const followupAllTimeScopeApplied = normalizedFollowupDateScope === "all_time_scope";
|
||||
const followupAllTimeScopeApplied = normalizedFollowupDateScope === "all_time_scope" && !currentTurnCarriesExplicitPeriod;
|
||||
const explicitDateScope = rawAllTimeScopeSignal || followupAllTimeScopeApplied
|
||||
? null
|
||||
: normalizedAssistantTurnMeaningDateScope ??
|
||||
|
|
@ -2386,6 +2400,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
if (debtDueDateFollowupBusinessOverviewSignal) {
|
||||
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) {
|
||||
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_suppressed_by_explicit_vat_question");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2025,7 +2025,11 @@ function repairAddressMojibake(value) {
|
|||
}
|
||||
function sanitizeOutgoingAssistantText(value, fallback = "Не смог сформировать читаемый ответ. Уточните запрос.") {
|
||||
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) {
|
||||
return sanitized;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ function createAssistantTransitionPolicy(deps) {
|
|||
}
|
||||
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})$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
|
|
@ -199,10 +199,15 @@ function createAssistantTransitionPolicy(deps) {
|
|||
purchase_date: isoDate,
|
||||
period_from: `${year}-${mm}-01`,
|
||||
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 = [];
|
||||
for (const entityRef of Array.isArray(entityRefs) ? entityRefs : []) {
|
||||
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 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) {
|
||||
const replyText = deps.toNonEmptyString(previousAddressItem?.text);
|
||||
if (replyText) {
|
||||
const repairedText = deps.repairAddressMojibake(replyText);
|
||||
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);
|
||||
}
|
||||
function extractPurchaseDateBridgeWindow(previousAddressItem, addressNavigationState, userMessage, alternateMessage) {
|
||||
const facts = extractPurchaseDateBridgeFacts(previousAddressItem, addressNavigationState, userMessage, alternateMessage);
|
||||
if (!facts?.selectedIsoDate) {
|
||||
return null;
|
||||
}
|
||||
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 earliestIsoDate = extractEarliestDmyDateFromEntityRefs(preferredResultSet?.entity_refs);
|
||||
return earliestIsoDate ? computeMonthWindowFromIso(earliestIsoDate) : 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) {
|
||||
if (!debug || typeof debug !== "object") {
|
||||
|
|
@ -460,10 +513,12 @@ function createAssistantTransitionPolicy(deps) {
|
|||
}
|
||||
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" &&
|
||||
!hasInventoryItemFocusHint &&
|
||||
!deps.isInventorySelectedObjectIntent(sourceIntentHint)) {
|
||||
!deps.isInventorySelectedObjectIntent(sourceIntentHint) &&
|
||||
!vatPurchaseBridgeContinuation) {
|
||||
return false;
|
||||
}
|
||||
const samples = [userMessage, alternateMessage]
|
||||
|
|
@ -472,8 +527,11 @@ function createAssistantTransitionPolicy(deps) {
|
|||
if (samples.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return samples.some((sample) => /(?:ндс|vat)/iu.test(sample) &&
|
||||
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/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)) ||
|
||||
((vatPurchaseBridgeContinuation || sourceIntentHint === "vat_liability_confirmed_for_tax_period") &&
|
||||
/(?:перв|последн|earliest|oldest|latest|last)/iu.test(sample) &&
|
||||
/(?:покупк|закупк|purchase)/iu.test(sample))));
|
||||
}
|
||||
function hasInventoryRootRestatementLikeSignal(userMessage, sourceIntentHint, hasInventoryRootFrame) {
|
||||
if (!hasInventoryRootFrame) {
|
||||
|
|
@ -701,12 +759,17 @@ function createAssistantTransitionPolicy(deps) {
|
|||
const hasBusinessOverviewCarryoverSourceHint = sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
|
||||
const navigationSessionState = earlyNavigationSessionState;
|
||||
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) &&
|
||||
deps.toNonEmptyString(navigationFocusObjectHint?.objectType) === "item" &&
|
||||
(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" ||
|
||||
deps.isInventorySelectedObjectIntent(sourceIntentHint)));
|
||||
const inventoryPurchaseDateVatBridge = hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasNavigationInventoryItemFocusHint);
|
||||
const inventoryPurchaseDateVatBridge = hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasNavigationInventoryItemFocusHint, sourceHasPurchaseDateBridgeHint);
|
||||
const inventoryMarginRankingFollowup = hasInventoryMarginRankingFollowupSignal(userMessage, alternateMessage, sourceIntentHint);
|
||||
let inventoryShortFollowupPrimary = (deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
||||
deps.hasShortInventoryObjectFollowupSignal(userMessage);
|
||||
|
|
@ -1092,10 +1155,16 @@ function createAssistantTransitionPolicy(deps) {
|
|||
: findRecentInventoryPurchaseProvenanceItem(items, deps.toNonEmptyString(navigationFocusObjectLabel) ??
|
||||
(0, assistantContinuityPolicy_1.readAddressDebugItem)(carryoverSourceDebug, deps.toNonEmptyString) ??
|
||||
deps.toNonEmptyString(previousFilters.item)) ?? previousAddressItem;
|
||||
const purchaseBridgeWindow = extractPurchaseDateBridgeWindow(purchaseBridgeItem, addressNavigationState);
|
||||
const purchaseBridgeWindow = extractPurchaseDateBridgeWindow(purchaseBridgeItem, addressNavigationState, userMessage, alternateMessage);
|
||||
if (purchaseBridgeWindow) {
|
||||
previousFilters.period_from = purchaseBridgeWindow.period_from;
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -309,6 +309,17 @@ function detectBroadBusinessEvaluation(text) {
|
|||
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)) {
|
||||
return {
|
||||
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) => {
|
||||
try {
|
||||
const body = toRecord(req.body);
|
||||
if (!body) {
|
||||
throw new 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 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
|
||||
throw new ApiError(
|
||||
"AUTOGEN_QUESTION_GENERATION_DISABLED",
|
||||
"Автогенерация вопросов отключена: используйте сохранение живых пользовательских сессий.",
|
||||
410
|
||||
);
|
||||
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) {
|
||||
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) {
|
||||
return unicodeBridgeResolution(
|
||||
"list_documents_by_counterparty",
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ interface AddressTryHandleOptions {
|
|||
llmSemanticHints?: AddressLlmSemanticHints | null;
|
||||
activeOrganization?: string | null;
|
||||
knownOrganizations?: string[];
|
||||
rawUserMessage?: string | null;
|
||||
}
|
||||
|
||||
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 {
|
||||
const isoDate = normalizeIsoDateForQuery(value);
|
||||
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 (
|
||||
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);
|
||||
if (taxQuarterWindow) {
|
||||
|
|
@ -3914,6 +3944,10 @@ export class AddressQueryService {
|
|||
shadowRouteAudit
|
||||
});
|
||||
}
|
||||
const rawUserMessageForReply =
|
||||
typeof options.rawUserMessage === "string" && options.rawUserMessage.trim().length > 0
|
||||
? options.rawUserMessage
|
||||
: undefined;
|
||||
const composeOptionsFromFilters = (
|
||||
filterSet: AddressFilterSet,
|
||||
options: {
|
||||
|
|
@ -3922,9 +3956,14 @@ export class AddressQueryService {
|
|||
useRubCurrency?: boolean;
|
||||
counterpartyHint?: string;
|
||||
accountHint?: string;
|
||||
rawUserMessage?: string | null;
|
||||
} = {}
|
||||
) => ({
|
||||
userMessage,
|
||||
rawUserMessage:
|
||||
typeof options.rawUserMessage === "string" && options.rawUserMessage.trim().length > 0
|
||||
? options.rawUserMessage
|
||||
: rawUserMessageForReply,
|
||||
itemHint: typeof filterSet.item === "string" ? filterSet.item : undefined,
|
||||
counterpartyHint:
|
||||
typeof options.counterpartyHint === "string"
|
||||
|
|
@ -3944,6 +3983,18 @@ export class AddressQueryService {
|
|||
asOfDate: typeof filterSet.as_of_date === "string" ? filterSet.as_of_date : undefined,
|
||||
requestedResultMode,
|
||||
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,
|
||||
useRubCurrency: options.useRubCurrency ?? undefined
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1520,6 +1520,91 @@ function buildInventoryMovementQuery(
|
|||
.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 {
|
||||
const warehouse = typeof filters.warehouse === "string" ? filters.warehouse.trim() : "";
|
||||
if (!warehouse) {
|
||||
|
|
@ -2199,7 +2284,7 @@ export function buildAddressRecipePlan(
|
|||
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||
? buildInventoryAgingByPurchaseDateQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_quality_events_profile"
|
||||
? buildInventoryQualityEventsQuery(filters, resolvedLimit)
|
||||
: 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,
|
||||
periodWindowLabel: string | null
|
||||
): string | null {
|
||||
if (!periodWindowLabel || !needsVatPurchaseDateAnchorDisclosure(options.userMessage)) {
|
||||
return null;
|
||||
): string[] {
|
||||
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);
|
||||
|
|
@ -765,10 +795,14 @@ function buildVatPurchaseDateAnchorDisclosureLine(
|
|||
asOfTs >= fromTs &&
|
||||
asOfTs <= toTs
|
||||
) {
|
||||
return `- Якорь периода: дата покупки ${formatDateRu(asOfDate)} попадает в налоговый период ${periodWindowLabel}; поэтому расчет ниже взят из книг продаж/покупок за это окно.`;
|
||||
return [
|
||||
`- Якорь периода: дата покупки ${formatDateRu(asOfDate)} попадает в налоговый период ${periodWindowLabel}; поэтому расчет ниже взят из книг продаж/покупок за это окно.`
|
||||
];
|
||||
}
|
||||
|
||||
return `- Якорь периода: дата покупки из вопроса/контекста использована для выбора налогового периода ${periodWindowLabel}; сам расчет ниже взят из книг продаж/покупок за это окно.`;
|
||||
return [
|
||||
`- Якорь периода: дата покупки из вопроса/контекста использована для выбора налогового периода ${periodWindowLabel}; сам расчет ниже взят из книг продаж/покупок за это окно.`
|
||||
];
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
return hasInventoryAccountPrefix(row.account_dt, "41.01");
|
||||
return hasInventoryAccountPrefix(row.account_dt, "41.01") && !isInventoryOnHandSnapshotRow(row);
|
||||
}
|
||||
|
||||
function isInventorySaleMovement(row: ComposeStageRow): boolean {
|
||||
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 {
|
||||
const normalized = String(value ?? "").trim();
|
||||
if (!normalized) {
|
||||
|
|
@ -1526,8 +1631,12 @@ function buildInventoryAgingByItemAggregate(
|
|||
}
|
||||
>();
|
||||
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
|
||||
const onHandScope = buildInventoryOnHandAgingScope(rows);
|
||||
|
||||
for (const row of rows) {
|
||||
if (!isInventoryPurchaseMovement(row) || !inventoryPurchaseMatchesOnHandAgingScope(row, onHandScope)) {
|
||||
continue;
|
||||
}
|
||||
const item = extractInventoryItemName(row);
|
||||
if (!item) {
|
||||
continue;
|
||||
|
|
@ -1613,19 +1722,11 @@ function formatInventoryAgingRows(items: InventoryAgingByItemAggregate[], asOfDa
|
|||
const parts = [
|
||||
`${index + 1}. ${item.item}`,
|
||||
`первая закупка: ${inventoryTraceDateLabel(item.firstPurchasePeriod)}`,
|
||||
`последняя закупка: ${inventoryTraceDateLabel(item.lastPurchasePeriod)}`,
|
||||
`документов: ${formatNumberWithDots(item.documentCount)}`,
|
||||
`операций: ${formatNumberWithDots(item.operations)}`
|
||||
`последняя закупка: ${inventoryTraceDateLabel(item.lastPurchasePeriod)}`
|
||||
];
|
||||
if (item.ageDays !== null) {
|
||||
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) {
|
||||
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 organizationLabel = normalizeOrganizationScopeValue(options.organizationHint);
|
||||
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 = [
|
||||
`Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`,
|
||||
directVatLine,
|
||||
"Расчет сделан по книгам продаж и покупок.",
|
||||
"",
|
||||
"Что вошло в расчет:",
|
||||
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
|
||||
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
|
||||
...(purchaseDateAnchorLine ? [purchaseDateAnchorLine] : []),
|
||||
...purchaseDateAnchorLines,
|
||||
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
|
||||
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
|
||||
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`
|
||||
|
|
|
|||
|
|
@ -548,7 +548,11 @@ export function composeCounterpartyAnalyticsReply(
|
|||
const focus = deps.detectValueRankingFocus(options.userMessage);
|
||||
const limit = deps.detectRankingLimit(options.userMessage, 20);
|
||||
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" &&
|
||||
/(?:какой|кто|which|who|какой|кто)/iu.test(normalizedQuestion) &&
|
||||
|
|
@ -572,6 +576,10 @@ export function composeCounterpartyAnalyticsReply(
|
|||
const effectiveLimit = asksSingleBestCounterparty || semanticSingleBestCounterparty ? 1 : limit;
|
||||
const cashflowBoundaryLine = buildCashflowBoundaryLine(isSupplier);
|
||||
const cashflowNextStepLine = buildCashflowNextStepLine(isSupplier, normalizedQuestion);
|
||||
const dependencyCue =
|
||||
/(?:зависим|завязан|концентрац|ключев|основн|критич|dependency|concentration|key|critical)/iu.test(
|
||||
normalizedQuestion
|
||||
);
|
||||
|
||||
const byCounterparty = new Map<string, CounterpartyValuePoint>();
|
||||
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} операциям). Это не полноценный сравнительный рейтинг.`
|
||||
: isSupplier
|
||||
? `Крупнейший поставщик по подтвержденным выплатам ${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} операциям).`
|
||||
: dependencyCue
|
||||
? `Главный сигнал зависимости по входящим деньгам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||
: `Самый доходный клиент ${rankingPeriodLabel} по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`;
|
||||
lines.unshift(directAnswerLine);
|
||||
if (dependencyCue) {
|
||||
lines.splice(
|
||||
1,
|
||||
0,
|
||||
"Это сигнал концентрации по найденным денежным строкам 1С, а не подтверждение договорной зависимости, надежности контрагента или чистой прибыли."
|
||||
);
|
||||
}
|
||||
if (!isSupplier) {
|
||||
lines.splice(1, 0, cashflowBoundaryLine);
|
||||
lines.splice(dependencyCue ? 2 : 1, 0, cashflowBoundaryLine);
|
||||
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(
|
||||
userMessage: string,
|
||||
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 {
|
||||
const normalized = textWithRepairedVariant(String(text ?? ""))
|
||||
.toLowerCase()
|
||||
|
|
@ -968,6 +1016,34 @@ function mergeFollowupFilters(
|
|||
): { filters: AddressFilterSet; reasons: string[] } {
|
||||
const merged: AddressFilterSet = { ...current };
|
||||
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 (
|
||||
(intent === "list_open_contracts" ||
|
||||
|
|
@ -987,6 +1063,7 @@ function mergeFollowupFilters(
|
|||
);
|
||||
}
|
||||
}
|
||||
clearInventoryAgingOrganizationAliasItem(null);
|
||||
return { filters: merged, reasons };
|
||||
}
|
||||
|
||||
|
|
@ -1001,6 +1078,13 @@ function mergeFollowupFilters(
|
|||
const previousAsOfDate = toNonEmptyString(previous.as_of_date);
|
||||
const previousPeriodFrom = toNonEmptyString(previous.period_from);
|
||||
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 relativeMonthFromFollowupYear = resolveRelativeMonthPeriodFromFollowupYear(userMessage, followupContext);
|
||||
const allTimeRequested = hasAllTimeHint(userMessage);
|
||||
|
|
@ -1020,6 +1104,57 @@ function mergeFollowupFilters(
|
|||
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" &&
|
||||
followupContext.previous_intent === "inventory_on_hand_as_of_date" &&
|
||||
|
|
@ -1386,13 +1521,7 @@ function mergeFollowupFilters(
|
|||
reasons.push("period_derived_from_inventory_root_frame_year");
|
||||
}
|
||||
if (intent === "inventory_aging_by_purchase_date") {
|
||||
const explicitItemMention = /(?:^|[\s,.;:!?()\-\u2014])(?:товар(?:у|а|ом)?|позици(?:и|я|ю)|item|row|line)(?=$|[\s,.;:!?()\-\u2014])/iu.test(
|
||||
String(userMessage ?? "")
|
||||
);
|
||||
if (toNonEmptyString(merged.item) && !explicitItemMention) {
|
||||
delete merged.item;
|
||||
reasons.push("item_cleared_for_stock_slice_aging");
|
||||
}
|
||||
clearInventoryAgingOrganizationAliasItem(previousOrganization);
|
||||
}
|
||||
if (
|
||||
!sameDateRequested &&
|
||||
|
|
@ -1646,7 +1775,13 @@ function deriveIntentWithFollowupContext(
|
|||
}
|
||||
|
||||
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) {
|
||||
return detectedIntent;
|
||||
}
|
||||
|
|
@ -1655,7 +1790,6 @@ function deriveIntentWithFollowupContext(
|
|||
if (!sourceIntent && !fallbackIntent) {
|
||||
return detectedIntent;
|
||||
}
|
||||
const previousFilters = followupContext.previous_filters ?? {};
|
||||
const previousPeriodFrom = toNonEmptyString(previousFilters.period_from);
|
||||
const previousPeriodTo = toNonEmptyString(previousFilters.period_to);
|
||||
const previousContract = toNonEmptyString(previousFilters.contract);
|
||||
|
|
@ -1672,6 +1806,7 @@ function deriveIntentWithFollowupContext(
|
|||
isVatFollowup &&
|
||||
hasSamePeriodHint(normalizedMessage) &&
|
||||
Boolean(previousPeriodFrom || previousPeriodTo);
|
||||
const previousPurchaseDateBridgeSelected = toNonEmptyString(previousFilters.purchase_date_bridge_selected);
|
||||
const previousIsInventoryFamily = isInventoryIntent(sourceIntent ?? undefined);
|
||||
const rootIsInventoryFamily = isInventoryIntent(followupContext.root_intent ?? undefined);
|
||||
const inventoryLineageActive =
|
||||
|
|
@ -1700,8 +1835,13 @@ function deriveIntentWithFollowupContext(
|
|||
) || hasSelectedObjectInlineSnapshotMetadata(normalizedMessage);
|
||||
const staleInventoryLineageCanYieldToCounterparty =
|
||||
previousCounterpartyLaneActive && !hasExplicitInventoryItemReference;
|
||||
const vatPurchaseDateBridgeContinuation =
|
||||
sourceIntent === "vat_liability_confirmed_for_tax_period" &&
|
||||
Boolean(previousPurchaseDateBridgeSelected) &&
|
||||
hasInventoryPurchaseDateVatBridgeContinuationCue(normalizedMessage);
|
||||
const inventoryPurchaseDateVatBridge =
|
||||
inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage);
|
||||
(inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage)) ||
|
||||
vatPurchaseDateBridgeContinuation;
|
||||
const marginRankingLineageActive =
|
||||
sourceIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
fallbackIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
|
|
@ -1728,6 +1868,7 @@ function deriveIntentWithFollowupContext(
|
|||
inventoryPurchaseDateVatBridge &&
|
||||
(detectedIntent.intent === "unknown" ||
|
||||
detectedIntent.intent === sourceIntent ||
|
||||
detectedIntent.intent === "account_balance_snapshot" ||
|
||||
detectedIntent.intent === "vat_payable_confirmed_as_of_date" ||
|
||||
detectedIntent.intent === "vat_payable_forecast")
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -134,6 +134,17 @@ function inventoryPartyListOrUnknown(parties: string[]): string {
|
|||
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 {
|
||||
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") {
|
||||
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 agingItems = deps.buildInventoryAgingByItemAggregate(purchaseRows, asOfDate);
|
||||
const oldestPurchaseDate = agingItems[0]?.firstPurchasePeriod ?? summary.firstPeriod;
|
||||
const oldestPurchaseAgeDays = agingItems[0]?.ageDays ?? null;
|
||||
const organizationLabel = agingItems.find((item) => item.organization)?.organization ?? null;
|
||||
const oldestAnswerPreview = agingItems
|
||||
.slice(0, 3)
|
||||
.map((item) => `${item.item} (${deps.inventoryTraceDateLabel(item.firstPurchasePeriod)})`)
|
||||
.join("; ");
|
||||
const directAnswerLine =
|
||||
agingItems.length > 0
|
||||
? `К самым старым закупкам в текущем подтвержденном срезе относятся позиции с самой ранней первой закупкой: ${oldestAnswerPreview}.`
|
||||
: "По доступному закупочному следу позиции со старыми закупками не материализованы.";
|
||||
? `Среди фактических положительных остатков есть давно закупавшиеся позиции: ${oldestAnswerPreview}.`
|
||||
: "В фактическом положительном остатке не найдено позиций с подтвержденным старым закупочным следом.";
|
||||
const lines: string[] = [directAnswerLine];
|
||||
appendInventoryBulletSection(lines, "Сводка:", [
|
||||
const summaryLines = [
|
||||
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
`Самая ранняя первая закупка среди позиций: ${deps.inventoryTraceDateLabel(oldestPurchaseDate)}.`,
|
||||
`Самая поздняя найденная закупка: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
||||
`Позиций в выборке: ${deps.formatNumberWithDots(agingItems.length)}.`,
|
||||
`Закупочных документов: ${deps.formatNumberWithDots(summary.documents.length)}.`,
|
||||
`Закупочных операций: ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
||||
]);
|
||||
];
|
||||
if (organizationLabel) {
|
||||
summaryLines.splice(1, 0, `Организация: ${organizationLabel}.`);
|
||||
}
|
||||
appendInventoryBulletSection(lines, "Сводка:", summaryLines);
|
||||
appendInventoryBulletSection(lines, "Ограничения:", [
|
||||
"Без партионного учета этот ответ показывает возраст закупочного следа по товарной позиции, а не возраст конкретного лота."
|
||||
"Берем только позиции с положительным остатком на дату среза; без партионного учета это возраст закупочного следа по номенклатуре, а не доказанный возраст конкретной партии."
|
||||
]);
|
||||
if (oldestPurchaseAgeDays !== null) {
|
||||
lines.push(`- Между самой ранней первой закупкой и датой среза прошло ${deps.formatNumberWithDots(oldestPurchaseAgeDays)} дн.`);
|
||||
|
|
@ -582,10 +613,10 @@ export function composeInventoryReply(
|
|||
lines.push(`- Поставщики, встречающиеся в наблюдаемом закупочном следе: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||
}
|
||||
if (agingItems.length > 0) {
|
||||
appendInventorySection(lines, "Позиции от самых старых закупок:", deps.formatInventoryAgingRows(agingItems, asOfDate, 12));
|
||||
appendInventorySection(lines, "Позиции от самых старых закупок:", deps.formatInventoryAgingRows(agingItems, asOfDate, 5));
|
||||
} else {
|
||||
appendInventorySection(lines, "Позиции от самых старых закупок:", [
|
||||
"- В доступных данных не найдено закупочных движений для выбранного среза."
|
||||
"- В доступных данных не найдено закупочных движений по позициям, которые есть в положительном остатке на дату среза."
|
||||
]);
|
||||
}
|
||||
return buildFactualSummaryReply(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { AddressEvidenceStrength, AddressResponseType, AddressResultMode }
|
|||
|
||||
export interface ComposeFactualReplyOptions<TVatDirectSourceProbe = unknown> {
|
||||
userMessage?: string;
|
||||
rawUserMessage?: string;
|
||||
itemHint?: string;
|
||||
counterpartyHint?: string;
|
||||
organizationHint?: string;
|
||||
|
|
@ -11,6 +12,13 @@ export interface ComposeFactualReplyOptions<TVatDirectSourceProbe = unknown> {
|
|||
asOfDate?: string;
|
||||
requestedResultMode?: AddressResultMode;
|
||||
vatDirectSourceProbe?: TVatDirectSourceProbe | null;
|
||||
purchaseDateBridge?: {
|
||||
selectedPurchaseDate?: string | null;
|
||||
firstPurchaseDate?: string | null;
|
||||
lastPurchaseDate?: string | null;
|
||||
basis?: string | null;
|
||||
hasMultiplePurchaseDates?: boolean;
|
||||
} | null;
|
||||
emphasizeNumbers?: boolean;
|
||||
useRubCurrency?: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -224,6 +224,7 @@ export async function runAssistantAddressAttemptRuntime<ResponseType = unknown>(
|
|||
runAddressLaneAttemptRuntimeSafe(
|
||||
buildAssistantAddressLaneAttemptRuntimeInput({
|
||||
messageUsed,
|
||||
rawUserMessage: input.userMessage,
|
||||
carryMeta,
|
||||
analysisDateHint,
|
||||
llmSemanticHints,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { RunAssistantAddressLaneAttemptRuntimeInput } from "./assistantAddr
|
|||
|
||||
export interface BuildAssistantAddressLaneAttemptRuntimeInputInput {
|
||||
messageUsed: RunAssistantAddressLaneAttemptRuntimeInput["messageUsed"];
|
||||
rawUserMessage: RunAssistantAddressLaneAttemptRuntimeInput["rawUserMessage"];
|
||||
carryMeta: RunAssistantAddressLaneAttemptRuntimeInput["carryMeta"];
|
||||
analysisDateHint: RunAssistantAddressLaneAttemptRuntimeInput["analysisDateHint"];
|
||||
llmSemanticHints: RunAssistantAddressLaneAttemptRuntimeInput["llmSemanticHints"];
|
||||
|
|
@ -17,6 +18,7 @@ export function buildAssistantAddressLaneAttemptRuntimeInput(
|
|||
): RunAssistantAddressLaneAttemptRuntimeInput {
|
||||
return {
|
||||
messageUsed: input.messageUsed,
|
||||
rawUserMessage: input.rawUserMessage,
|
||||
carryMeta: input.carryMeta,
|
||||
analysisDateHint: input.analysisDateHint,
|
||||
llmSemanticHints: input.llmSemanticHints,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export function resolveAssistantAddressLaneAttemptFollowupContext(
|
|||
|
||||
export interface BuildAssistantAddressLaneAttemptQueryOptionsInput {
|
||||
analysisDateHint: RunAssistantAddressLaneAttemptRuntimeInput["analysisDateHint"];
|
||||
rawUserMessage: RunAssistantAddressLaneAttemptRuntimeInput["rawUserMessage"];
|
||||
scopedFollowupContext: Record<string, unknown> | null;
|
||||
llmSemanticHints: RunAssistantAddressLaneAttemptRuntimeInput["llmSemanticHints"];
|
||||
activeOrganization: RunAssistantAddressLaneAttemptRuntimeInput["activeOrganization"];
|
||||
|
|
@ -20,7 +21,8 @@ export function buildAssistantAddressLaneAttemptQueryOptions(
|
|||
input: BuildAssistantAddressLaneAttemptQueryOptionsInput
|
||||
): Parameters<RunAssistantAddressLaneAttemptRuntimeInput["runAddressQueryTryHandle"]>[1] {
|
||||
const base = {
|
||||
analysisDateHint: input.analysisDateHint
|
||||
analysisDateHint: input.analysisDateHint,
|
||||
rawUserMessage: input.rawUserMessage ?? undefined
|
||||
} as Parameters<RunAssistantAddressLaneAttemptRuntimeInput["runAddressQueryTryHandle"]>[1];
|
||||
if (input.scopedFollowupContext) {
|
||||
base.followupContext = input.scopedFollowupContext;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
|
||||
export interface RunAssistantAddressLaneAttemptRuntimeInput {
|
||||
messageUsed: string;
|
||||
rawUserMessage?: string | null;
|
||||
carryMeta: AssistantAddressCarryoverLike | null;
|
||||
analysisDateHint: string | null;
|
||||
llmSemanticHints?: Record<string, unknown> | null;
|
||||
|
|
@ -24,6 +25,7 @@ export interface RunAssistantAddressLaneAttemptRuntimeInput {
|
|||
llmSemanticHints?: Record<string, unknown> | null;
|
||||
activeOrganization?: string | null;
|
||||
knownOrganizations?: string[];
|
||||
rawUserMessage?: string | null;
|
||||
}
|
||||
) => Promise<AssistantAddressLaneLike | null>;
|
||||
}
|
||||
|
|
@ -43,7 +45,8 @@ export async function runAssistantAddressLaneAttemptRuntime(
|
|||
scopedFollowupContext,
|
||||
llmSemanticHints: input.llmSemanticHints ?? null,
|
||||
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();
|
||||
}
|
||||
|
||||
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: {
|
||||
baseReply: string;
|
||||
debug: Record<string, unknown>;
|
||||
session: unknown;
|
||||
userMessage?: unknown;
|
||||
}): { 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 turnInput = toRecordObject(entryPoint?.turn_input);
|
||||
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ function hasCompactCashflowFollowupSignal(text: string | null): boolean {
|
|||
if (!value) {
|
||||
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 {
|
||||
|
|
@ -169,6 +169,37 @@ function dateScopeToFilterWindow(dateScope: string | null): Record<string, strin
|
|||
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 {
|
||||
const text = compactLower(value);
|
||||
if (!text) {
|
||||
|
|
@ -197,12 +228,17 @@ function inferBusinessOverviewDiscoveryContextFromSessionItems(
|
|||
continue;
|
||||
}
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
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)) {
|
||||
previousFilters.organization = organization;
|
||||
}
|
||||
|
|
@ -229,11 +265,7 @@ function mergeBusinessOverviewDateContextForCompactCashflow(input: {
|
|||
return input.followupContext;
|
||||
}
|
||||
const existingFilters = toRecordObject(input.followupContext?.previous_filters);
|
||||
if (
|
||||
input.toNonEmptyString(existingFilters?.period_from) ||
|
||||
input.toNonEmptyString(existingFilters?.period_to) ||
|
||||
input.toNonEmptyString(existingFilters?.as_of_date)
|
||||
) {
|
||||
if (hasBoundedFilterWindow(existingFilters)) {
|
||||
return input.followupContext;
|
||||
}
|
||||
const inferred = inferBusinessOverviewDiscoveryContextFromSessionItems(input.sessionItems, input.toNonEmptyString);
|
||||
|
|
@ -244,12 +276,12 @@ function mergeBusinessOverviewDateContextForCompactCashflow(input: {
|
|||
...inferred,
|
||||
...(input.followupContext ?? {}),
|
||||
previous_filters: {
|
||||
...(toRecordObject(inferred.previous_filters) ?? {}),
|
||||
...(toRecordObject(input.followupContext?.previous_filters) ?? {})
|
||||
...(toRecordObject(input.followupContext?.previous_filters) ?? {}),
|
||||
...(toRecordObject(inferred.previous_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);
|
||||
}
|
||||
|
||||
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(
|
||||
followupContext: Record<string, unknown> | null,
|
||||
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
|
||||
|
|
@ -489,6 +531,7 @@ function mergeBusinessOverviewProofBundlesFromNavigationState(input: {
|
|||
followupContext: Record<string, unknown> | null;
|
||||
sessionAddressNavigationState: unknown;
|
||||
predecomposeContract: Record<string, unknown> | null;
|
||||
userMessage: string;
|
||||
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"];
|
||||
}): Record<string, unknown> | null {
|
||||
const currentValueBundle = toRecordObject(input.followupContext?.previous_discovery_bidirectional_value_flow);
|
||||
|
|
@ -508,6 +551,9 @@ function mergeBusinessOverviewProofBundlesFromNavigationState(input: {
|
|||
const entities = toRecordObject(input.predecomposeContract?.entities);
|
||||
const hasCurrentOrganizationSelection = Boolean(input.toNonEmptyString(entities?.organization));
|
||||
const businessOverviewFollowup = isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString);
|
||||
if (!hasComparisonScopeProofCue(input.userMessage)) {
|
||||
return input.followupContext;
|
||||
}
|
||||
if (!businessOverviewFollowup && !hasCurrentOrganizationSelection) {
|
||||
return input.followupContext;
|
||||
}
|
||||
|
|
@ -550,9 +596,13 @@ function mergeBusinessOverviewProofBundlesFromNavigationState(input: {
|
|||
|
||||
function mergeBusinessOverviewProofBundlesFromSessionItems(input: {
|
||||
followupContext: Record<string, unknown> | null;
|
||||
userMessage: string;
|
||||
sessionItems: unknown[];
|
||||
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"];
|
||||
}): Record<string, unknown> | null {
|
||||
if (!hasComparisonScopeProofCue(input.userMessage)) {
|
||||
return input.followupContext;
|
||||
}
|
||||
if (!isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString)) {
|
||||
return input.followupContext;
|
||||
}
|
||||
|
|
@ -853,6 +903,7 @@ export async function buildAssistantAddressOrchestrationRuntime(
|
|||
});
|
||||
const discoveryFollowupContextWithProofBundles = mergeBusinessOverviewProofBundlesFromSessionItems({
|
||||
followupContext: discoveryFollowupContext,
|
||||
userMessage: input.userMessage,
|
||||
sessionItems: input.sessionItems,
|
||||
toNonEmptyString: input.toNonEmptyString
|
||||
});
|
||||
|
|
@ -860,6 +911,7 @@ export async function buildAssistantAddressOrchestrationRuntime(
|
|||
followupContext: discoveryFollowupContextWithProofBundles,
|
||||
sessionAddressNavigationState: input.sessionAddressNavigationState,
|
||||
predecomposeContract,
|
||||
userMessage: input.userMessage,
|
||||
toNonEmptyString: input.toNonEmptyString
|
||||
});
|
||||
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(
|
||||
input: RunAssistantDeepTurnResponseRuntimeInput
|
||||
): RunAssistantDeepTurnResponseRuntimeOutput {
|
||||
|
|
@ -243,9 +258,11 @@ export function runAssistantDeepTurnResponseRuntime(
|
|||
currentReplySource: "deep_analysis",
|
||||
addressRuntimeMeta: addressRuntimeMetaForDeep as unknown as Record<string, unknown> | null
|
||||
});
|
||||
const assistantReply = mcpDiscoveryResponsePolicy.applied
|
||||
? mcpDiscoveryResponsePolicy.reply_text
|
||||
: packagingRuntime.safeAssistantReply;
|
||||
const assistantReply = stripRoutineShortAnswerPrefix(
|
||||
mcpDiscoveryResponsePolicy.applied
|
||||
? input.sanitizeReply(mcpDiscoveryResponsePolicy.reply_text, packagingRuntime.safeAssistantReply)
|
||||
: packagingRuntime.safeAssistantReply
|
||||
);
|
||||
const replyType = mcpDiscoveryResponsePolicy.applied ? "partial_coverage" : input.composition.reply_type;
|
||||
const 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);
|
||||
}
|
||||
|
||||
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(
|
||||
turnMeaning: Record<string, unknown> | null,
|
||||
graph: Record<string, unknown> | null
|
||||
|
|
@ -527,6 +537,14 @@ function businessOverviewPeriodText(overview: Record<string, unknown>): string {
|
|||
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 {
|
||||
const years = Array.isArray(overview.yearly_breakdown) ? overview.yearly_breakdown : [];
|
||||
const sorted = years
|
||||
|
|
@ -1031,7 +1049,12 @@ function buildCompactBusinessOverviewReply(
|
|||
const compactCashflowRequested = directMoneyOnlyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
|
||||
const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph);
|
||||
const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph);
|
||||
const nextChecksRequested = requestsBusinessOverviewNextChecks(turnMeaning, graph);
|
||||
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 organizationScopeComparable = compactComparable(organizationScope);
|
||||
const plainOrganizationClarificationSelection = Boolean(
|
||||
|
|
@ -1045,18 +1068,65 @@ function buildCompactBusinessOverviewReply(
|
|||
!/(?:сравн|подтвержд|деньг|сколько|что\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)) {
|
||||
const netDisplay = sentenceAmount(netAmount) ?? netAmount ?? "0 \u0440\u0443\u0431.";
|
||||
const signedNetDisplay =
|
||||
cashflowPolarityRequested && netDisplay && !String(netDisplay).trim().startsWith("-")
|
||||
? `+${netDisplay}`
|
||||
: netDisplay;
|
||||
const polarityLead = 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: ";
|
||||
const limitedEarningsAnswer = Boolean(earningsAmountRequested && !accountingFinancialResultForEarnings && limitLine);
|
||||
const polarityLead = limitedEarningsAnswer
|
||||
? "\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}.`
|
||||
);
|
||||
if (limitLine) {
|
||||
lines.push(limitLine);
|
||||
}
|
||||
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."
|
||||
|
|
@ -1116,12 +1186,8 @@ function buildCompactBusinessOverviewReply(
|
|||
);
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
const headline = toNonEmptyString(draft.headline);
|
||||
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
|
||||
lines.push(
|
||||
cleanHeadline
|
||||
? `Коротко: ${localizeLine(cleanHeadline)}`
|
||||
: "Коротко: нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только ограниченный операционный денежный/товарный сигнал, а не полный отчет о прибыли и не бухгалтерский финансовый результат."
|
||||
"Коротко: чистую прибыль и маржу не подтверждаю по текущему срезу 1С; видны только ограниченные денежные обороты и расчетное нетто/операционный сигнал, а не полный отчет о прибыли и не бухгалтерский финансовый результат."
|
||||
);
|
||||
const boundaryLines = userFacingLines([
|
||||
...toStringList(draft.confirmed_lines),
|
||||
|
|
@ -1221,10 +1287,13 @@ function buildCompactBusinessOverviewReply(
|
|||
? `${vendorProcurementQuality.top_non_financial_supplier_share_pct}%`
|
||||
: null;
|
||||
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") {
|
||||
lines.push(
|
||||
`Коротко: проверка концентрации закупок/исходящих платежей за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`
|
||||
`Коротко: проверка концентрации закупок/исходящих платежей ${periodPhrase} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`
|
||||
);
|
||||
const financialHintText = financialFlowHintTextRuFromRecord(topOutgoingRecord);
|
||||
if (financialHintText) {
|
||||
|
|
@ -1237,11 +1306,11 @@ function buildCompactBusinessOverviewReply(
|
|||
}
|
||||
} else if (status === "reviewed_procurement_concentration") {
|
||||
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 {
|
||||
lines.push(
|
||||
`Коротко: проверка концентрации закупок/исходящих платежей за ${periodScope} выполнена, но надежной небанковской концентрации поставщика по найденным исходящим платежам не хватает${totalText}.`
|
||||
`Коротко: проверка концентрации закупок/исходящих платежей ${periodPhrase} выполнена, но надежной небанковской концентрации поставщика по найденным исходящим платежам не хватает${totalText}.`
|
||||
);
|
||||
}
|
||||
const contractText =
|
||||
|
|
@ -1251,7 +1320,7 @@ function buildCompactBusinessOverviewReply(
|
|||
: ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts} договоров.`
|
||||
: "";
|
||||
lines.push(
|
||||
`Что не доказано этим срезом: надежность поставщика, качество поставок, договорные условия, назначение каждого платежа и полная структура всех расходов.${contractText}`
|
||||
`Это не аудит поставщиков: срез не доказывает надежность поставщика, качество поставок, договорные условия, назначение каждого платежа и полную структуру всех расходов.${contractText}`
|
||||
);
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
|
|
@ -1305,6 +1374,26 @@ function buildCompactBusinessOverviewReply(
|
|||
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 &&
|
||||
!crossScopeExecutiveSummary &&
|
||||
|
|
@ -1315,9 +1404,9 @@ function buildCompactBusinessOverviewReply(
|
|||
const subject = organizationScope ?? "компания";
|
||||
const periodWithoutPrefix = period.replace(/^за\s+/iu, "");
|
||||
lines.push(
|
||||
`Коротко: по доступным данным ${subject} выглядит как бизнес с крупными контрактными денежными потоками и заметной зависимостью от нескольких крупных контрагентов, а не как равномерный поток мелких продаж.`
|
||||
`Коротко: по ограниченному проверенному 1С-срезу ${subject} выглядит как бизнес с крупными контрактными денежными потоками и заметной зависимостью от нескольких крупных контрагентов, а не как равномерный поток мелких продаж; это не аудиторское заключение и не подтвержденная чистая прибыль.`
|
||||
);
|
||||
lines.push("Что видно:");
|
||||
lines.push("Что видно в ограниченном денежном срезе:");
|
||||
if (incomingAmount) {
|
||||
lines.push(`- входящие деньги за ${periodWithoutPrefix}: ${incomingAmount};`);
|
||||
}
|
||||
|
|
@ -1353,9 +1442,11 @@ function buildCompactBusinessOverviewReply(
|
|||
if (debtLine) {
|
||||
lines.push(`- ${localizeLine(debtLine)}`);
|
||||
}
|
||||
lines.push(
|
||||
"Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль."
|
||||
);
|
||||
if (!limitLine) {
|
||||
lines.push(
|
||||
"Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль."
|
||||
);
|
||||
}
|
||||
const missingOverviewFamilies: string[] = [];
|
||||
if (!taxLine) {
|
||||
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 {
|
||||
const record = toRecordObject(value);
|
||||
return (
|
||||
|
|
@ -1052,12 +1067,13 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
|||
}
|
||||
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_applied");
|
||||
const replyText = stripRoutineShortAnswerPrefix(String(candidate.reply_text));
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryResponsePolicy",
|
||||
decision: "apply_candidate",
|
||||
applied: true,
|
||||
reply_text: String(candidate.reply_text),
|
||||
reply_text: replyText || String(candidate.reply_text),
|
||||
reply_source: "mcp_discovery_response_candidate_guarded",
|
||||
candidate,
|
||||
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(
|
||||
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
|
||||
) ||
|
||||
|
|
@ -900,6 +903,22 @@ function hasDebtDueDateBoundaryFollowupSignal(text: string): boolean {
|
|||
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 {
|
||||
if (!text) {
|
||||
return false;
|
||||
|
|
@ -1079,7 +1098,7 @@ function hasBusinessOverviewContinuationSignal(text: string): boolean {
|
|||
normalized
|
||||
);
|
||||
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
|
||||
);
|
||||
const hasTaxContinuationCue =
|
||||
|
|
@ -1865,7 +1884,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
hasOrganizationLevelDebtDueDateOverviewSignal(rawText)) ||
|
||||
debtDueDateFollowupBusinessOverviewSignal;
|
||||
const supplierQualityBusinessOverviewSignal =
|
||||
(rawBusinessOverviewSignal || seededBusinessOverviewSignal) && hasOrganizationLevelSupplierQualityOverviewSignal(rawText);
|
||||
((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
||||
(hasOrganizationLevelSupplierQualityOverviewSignal(rawText) || hasSupplierDependencyFollowupSignal(rawText))) ||
|
||||
(businessOverviewContinuationSignal && hasSupplierDependencyFollowupSignal(rawText));
|
||||
const profitMarginFollowupBusinessOverviewSignal =
|
||||
businessOverviewContinuationSignal && hasProfitMarginBoundaryFollowupSignal(rawText);
|
||||
const profitMarginBusinessOverviewSignal =
|
||||
|
|
@ -2458,6 +2479,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
? null
|
||||
: explicitCurrentCounterpartyOverridesFollowupEntity
|
||||
? null
|
||||
: businessOverviewSuppressesFollowupCounterparty
|
||||
? rawMetadataScopeHint
|
||||
: rawMetadataScopeHint ??
|
||||
followupSeed.metadataScopeHint ??
|
||||
followupSeed.discoveryEntity ??
|
||||
|
|
@ -2685,7 +2708,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
normalizedPredecomposeDateScope &&
|
||||
normalizedPredecomposeDateScope.startsWith(`${rawDateScope}-`)
|
||||
);
|
||||
const followupAllTimeScopeApplied = normalizedFollowupDateScope === "all_time_scope";
|
||||
const followupAllTimeScopeApplied = normalizedFollowupDateScope === "all_time_scope" && !currentTurnCarriesExplicitPeriod;
|
||||
const explicitDateScope =
|
||||
rawAllTimeScopeSignal || followupAllTimeScopeApplied
|
||||
? null
|
||||
|
|
@ -3081,6 +3104,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
if (debtDueDateFollowupBusinessOverviewSignal) {
|
||||
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) {
|
||||
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_suppressed_by_explicit_vat_question");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1982,7 +1982,11 @@ function repairAddressMojibake(value) {
|
|||
}
|
||||
function sanitizeOutgoingAssistantText(value, fallback = "Не смог сформировать читаемый ответ. Уточните запрос.") {
|
||||
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) {
|
||||
return sanitized;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -301,7 +301,7 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
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})$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
|
|
@ -318,11 +318,17 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
purchase_date: isoDate,
|
||||
period_from: `${year}-${mm}-01`,
|
||||
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 = [];
|
||||
for (const entityRef of Array.isArray(entityRefs) ? entityRefs : []) {
|
||||
const value = deps.toNonEmptyString(entityRef?.value);
|
||||
|
|
@ -340,40 +346,96 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (dates.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return dates.sort()[0] ?? null;
|
||||
return uniqueSortedIsoDates(dates);
|
||||
}
|
||||
|
||||
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);
|
||||
if (replyText) {
|
||||
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 explicitFirstDateIso = explicitFirstDateMatch ? parseDmyDateToIso(explicitFirstDateMatch[1]) : null;
|
||||
if (explicitFirstDateIso) {
|
||||
return computeMonthWindowFromIso(explicitFirstDateIso);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
const navigationSessionState = 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 earliestIsoDate = extractEarliestDmyDateFromEntityRefs(preferredResultSet?.entity_refs);
|
||||
return earliestIsoDate ? computeMonthWindowFromIso(earliestIsoDate) : null;
|
||||
if (!firstIsoDate || !lastIsoDate) {
|
||||
const navigationSessionState = 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;
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
@ -605,11 +667,20 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
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" &&
|
||||
!hasInventoryItemFocusHint &&
|
||||
!deps.isInventorySelectedObjectIntent(sourceIntentHint)
|
||||
!deps.isInventorySelectedObjectIntent(sourceIntentHint) &&
|
||||
!vatPurchaseBridgeContinuation
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -621,10 +692,13 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
}
|
||||
return samples.some(
|
||||
(sample) =>
|
||||
/(?:ндс|vat)/iu.test(sample) &&
|
||||
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/iu.test(
|
||||
sample
|
||||
)
|
||||
((/(?:ндс|vat)/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)))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -932,10 +1006,17 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
|
||||
const navigationSessionState = earlyNavigationSessionState;
|
||||
const navigationFocusObjectHint = navigationSessionState.focusObject;
|
||||
const sourceFiltersHint = readAddressDebugFilters(carryoverSourceDebug);
|
||||
const sourceHasPurchaseDateBridgeHint = Boolean(
|
||||
deps.toNonEmptyString(sourceFiltersHint?.purchase_date_bridge_selected)
|
||||
);
|
||||
const hasNavigationInventoryItemFocusHint = Boolean(
|
||||
deps.toNonEmptyString(navigationFocusObjectHint?.label) &&
|
||||
deps.toNonEmptyString(navigationFocusObjectHint?.objectType) === "item" &&
|
||||
(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" ||
|
||||
deps.isInventorySelectedObjectIntent(sourceIntentHint))
|
||||
);
|
||||
|
|
@ -943,7 +1024,8 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
userMessage,
|
||||
alternateMessage,
|
||||
sourceIntentHint,
|
||||
hasNavigationInventoryItemFocusHint
|
||||
hasNavigationInventoryItemFocusHint,
|
||||
sourceHasPurchaseDateBridgeHint
|
||||
);
|
||||
const inventoryMarginRankingFollowup = hasInventoryMarginRankingFollowupSignal(
|
||||
userMessage,
|
||||
|
|
@ -1457,10 +1539,21 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
readAddressDebugItem(carryoverSourceDebug, deps.toNonEmptyString) ??
|
||||
deps.toNonEmptyString(previousFilters.item)
|
||||
) ?? previousAddressItem;
|
||||
const purchaseBridgeWindow = extractPurchaseDateBridgeWindow(purchaseBridgeItem, addressNavigationState);
|
||||
const purchaseBridgeWindow = extractPurchaseDateBridgeWindow(
|
||||
purchaseBridgeItem,
|
||||
addressNavigationState,
|
||||
userMessage,
|
||||
alternateMessage
|
||||
);
|
||||
if (purchaseBridgeWindow) {
|
||||
previousFilters.period_from = purchaseBridgeWindow.period_from;
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -426,6 +426,25 @@ function detectBroadBusinessEvaluation(text) {
|
|||
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)) {
|
||||
return {
|
||||
family: "broad_business_evaluation"
|
||||
|
|
|
|||
|
|
@ -163,6 +163,11 @@ export interface AddressFilterSet {
|
|||
warehouse?: string;
|
||||
document_type?: 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;
|
||||
limit?: number;
|
||||
sort?: "period_desc" | "period_asc";
|
||||
|
|
|
|||
|
|
@ -244,7 +244,12 @@ describe("address follow-up temporal regressions", () => {
|
|||
organization: 'ООО "Альтернатива Плюс"',
|
||||
period_from: "2015-02-01",
|
||||
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_value: "Рабочая станция универсального специалиста"
|
||||
|
|
@ -254,7 +259,98 @@ describe("address follow-up temporal regressions", () => {
|
|||
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_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", () => {
|
||||
const result = runAddressDecomposeStage("АЛЬТЕРНАТИВА", {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,44 @@ import { runAddressDecomposeStage } from "../src/services/address_runtime/decomp
|
|||
import { composeFactualReply } from "../src/services/address_runtime/composeStage";
|
||||
|
||||
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", () => {
|
||||
const result = runAddressDecomposeStage("Какие остатки по товарам на эту дату относятся к старым закупкам", {
|
||||
previous_intent: "inventory_purchase_provenance_for_item",
|
||||
|
|
|
|||
|
|
@ -2182,6 +2182,49 @@ describe("address compose stage utf8 headers", () => {
|
|||
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", () => {
|
||||
const reply = composeFactualReply(
|
||||
"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");
|
||||
});
|
||||
|
||||
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", () => {
|
||||
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");
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const service = new AddressQueryService();
|
||||
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\"");
|
||||
});
|
||||
|
||||
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", () => {
|
||||
const reply = composeFactualReply(
|
||||
"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);
|
||||
});
|
||||
|
||||
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", () => {
|
||||
const reply = composeFactualReply(
|
||||
"inventory_on_hand_as_of_date",
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ describe("address reply builders regressions", () => {
|
|||
);
|
||||
|
||||
const firstLine = result?.text.split("\n")[0] ?? "";
|
||||
expect(firstLine).toContain("К самым старым закупкам");
|
||||
expect(firstLine).toContain("фактических положительных остатков");
|
||||
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).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");
|
||||
});
|
||||
|
||||
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", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
|
|
@ -705,6 +764,77 @@ describe("assistant MCP discovery response candidate", () => {
|
|||
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", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
|
|
@ -779,6 +909,72 @@ describe("assistant MCP discovery response candidate", () => {
|
|||
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", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
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("Склад:");
|
||||
|
|
@ -875,6 +1071,67 @@ describe("assistant MCP discovery response candidate", () => {
|
|||
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", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
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("долги");
|
||||
|
|
|
|||
|
|
@ -48,6 +48,37 @@ describe("assistant MCP discovery response policy", () => {
|
|||
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", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: "regular chat",
|
||||
|
|
|
|||
|
|
@ -2637,6 +2637,70 @@ describe("assistant MCP discovery turn input adapter", () => {
|
|||
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", () => {
|
||||
const orgName = "ООО Альтернатива Плюс";
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
|
|
|
|||
|
|
@ -214,6 +214,152 @@ describe("assistantTransitionPolicy", () => {
|
|||
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", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => ({
|
||||
|
|
|
|||
|
|
@ -303,6 +303,22 @@ describe("assistantTurnMeaningPolicy", () => {
|
|||
expect(supplierQuality.asked_action_family).toBe("broad_evaluation");
|
||||
expect(supplierQuality.unsupported_but_understood_family).toBe("broad_business_evaluation");
|
||||
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", () => {
|
||||
|
|
|
|||
|
|
@ -149,6 +149,40 @@ describe("counterparty analytics reply builders", () => {
|
|||
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", () => {
|
||||
const reply = composeFactualReply(
|
||||
"counterparty_activity_lifecycle",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"schema_version": "shared_llm_connection_v1",
|
||||
"updated_at": "2026-04-18T14:51:55.518Z",
|
||||
"updated_at": "2026-05-25T13:27:06.297Z",
|
||||
"connection": {
|
||||
"llmProvider": "local",
|
||||
"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 name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -312,7 +312,6 @@ export const apiClient = {
|
|||
|
||||
async sendAssistantMessage(input: {
|
||||
connection: ConnectionState;
|
||||
prompts: PromptState;
|
||||
userMessage: string;
|
||||
sessionId?: string;
|
||||
promptVersion?: string;
|
||||
|
|
@ -336,10 +335,6 @@ export const apiClient = {
|
|||
temperature: input.connection.temperature,
|
||||
maxOutputTokens: input.connection.maxOutputTokens,
|
||||
promptVersion: input.promptVersion ?? "address_query_runtime_v1",
|
||||
systemPrompt: input.prompts.systemPrompt,
|
||||
developerPrompt: input.prompts.developerPrompt,
|
||||
domainPrompt: input.prompts.domainPrompt,
|
||||
fewShotExamples: input.prompts.fewShotExamples,
|
||||
context: {
|
||||
period_hint: input.context?.periodHint ?? "",
|
||||
business_context: input.context?.businessContext ?? ""
|
||||
|
|
@ -362,7 +357,6 @@ export const apiClient = {
|
|||
model?: string;
|
||||
assistant_prompt_version?: string;
|
||||
decomposition_prompt_version?: string;
|
||||
prompt_fingerprint?: string;
|
||||
};
|
||||
}): Promise<{ ok: boolean; generation: AutoGenHistoryRecord }> {
|
||||
return request("/autoruns/autogen/save-assistant-session", {
|
||||
|
|
@ -534,35 +528,5 @@ export const apiClient = {
|
|||
return request(`/autoruns/autogen/history/${encodeURIComponent(generationId)}`, {
|
||||
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 = {
|
||||
mode: "codex_creative",
|
||||
mode: "saved_user_sessions",
|
||||
count: 24,
|
||||
personalityId: "general",
|
||||
personalityPrompts: buildDefaultPersonalityPrompts(),
|
||||
|
|
@ -723,7 +723,6 @@ export function AutoRunsHistoryPanel({
|
|||
const [dragOverQuestionIndex, setDragOverQuestionIndex] = useState<number | null>(null);
|
||||
const [activeAsyncJob, setActiveAsyncJob] = useState<AsyncEvalRunJob | null>(null);
|
||||
const [postAnalysis, setPostAnalysis] = useState<AutoRunPostAnalysisResponse | null>(null);
|
||||
const [autoGenBusy, setAutoGenBusy] = useState(false);
|
||||
const [autogenRunBusy, setAutogenRunBusy] = useState(false);
|
||||
const [postAnalysisBusy, setPostAnalysisBusy] = useState(false);
|
||||
const [autogenHistoryBusy, setAutogenHistoryBusy] = useState(false);
|
||||
|
|
@ -801,10 +800,6 @@ export function AutoRunsHistoryPanel({
|
|||
const asyncJobPollTimerRef = useRef<number | null>(null);
|
||||
const questionEditorRef = useRef<HTMLInputElement | null>(null);
|
||||
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(
|
||||
() => autoGenHistory.filter((item) => item.mode === autoGenSettings.mode),
|
||||
[autoGenHistory, autoGenSettings.mode]
|
||||
|
|
@ -1104,7 +1099,6 @@ export function AutoRunsHistoryPanel({
|
|||
try {
|
||||
const response = await apiClient.sendAssistantMessage({
|
||||
connection,
|
||||
prompts,
|
||||
userMessage,
|
||||
sessionId: assistantLiveSessionId || undefined,
|
||||
promptVersion: assistantPromptVersion,
|
||||
|
|
@ -1132,8 +1126,7 @@ export function AutoRunsHistoryPanel({
|
|||
assistantPromptVersion,
|
||||
connection,
|
||||
loadAssistantLiveAnnotationsForSession,
|
||||
log,
|
||||
prompts
|
||||
log
|
||||
]);
|
||||
|
||||
const openAssistantLiveSaveModal = useCallback(() => {
|
||||
|
|
@ -1164,13 +1157,6 @@ export function AutoRunsHistoryPanel({
|
|||
|
||||
setAssistantLiveSaveModal((prev) => ({ ...prev, saving: true, error: "" }));
|
||||
try {
|
||||
const promptFingerprint = [
|
||||
prompts.systemPrompt,
|
||||
prompts.developerPrompt,
|
||||
prompts.domainPrompt,
|
||||
prompts.schemaNotes,
|
||||
prompts.fewShotExamples
|
||||
].join("||");
|
||||
const payload = await apiClient.saveAutoRunAssistantSession({
|
||||
session_id: sessionId,
|
||||
title,
|
||||
|
|
@ -1179,8 +1165,7 @@ export function AutoRunsHistoryPanel({
|
|||
llm_provider: connection.llmProvider,
|
||||
model: connection.model,
|
||||
assistant_prompt_version: assistantPromptVersion,
|
||||
decomposition_prompt_version: decompositionPromptVersion,
|
||||
prompt_fingerprint: promptFingerprint
|
||||
decomposition_prompt_version: decompositionPromptVersion
|
||||
}
|
||||
});
|
||||
setAutoGenHistory((prev) => [payload.generation, ...prev.filter((item) => item.generation_id !== payload.generation.generation_id)]);
|
||||
|
|
@ -1202,12 +1187,7 @@ export function AutoRunsHistoryPanel({
|
|||
connection.llmProvider,
|
||||
connection.model,
|
||||
decompositionPromptVersion,
|
||||
log,
|
||||
prompts.developerPrompt,
|
||||
prompts.domainPrompt,
|
||||
prompts.fewShotExamples,
|
||||
prompts.schemaNotes,
|
||||
prompts.systemPrompt
|
||||
log
|
||||
]);
|
||||
|
||||
const commitLimitInput = useCallback(
|
||||
|
|
@ -1363,87 +1343,6 @@ export function AutoRunsHistoryPanel({
|
|||
}
|
||||
}, [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(
|
||||
async (runId: string, caseId: string) => {
|
||||
if (isLiveRunId(runId)) {
|
||||
|
|
@ -2477,12 +2376,7 @@ export function AutoRunsHistoryPanel({
|
|||
: prev.personalityId;
|
||||
return {
|
||||
...prev,
|
||||
mode:
|
||||
parsed.autoGenSettings?.mode === "codex_creative" ||
|
||||
parsed.autoGenSettings?.mode === "qwen_seed" ||
|
||||
parsed.autoGenSettings?.mode === "saved_user_sessions"
|
||||
? parsed.autoGenSettings.mode
|
||||
: prev.mode,
|
||||
mode: "saved_user_sessions",
|
||||
count:
|
||||
typeof parsed.autoGenSettings?.count === "number"
|
||||
? Math.max(1, Math.min(200, parsed.autoGenSettings.count))
|
||||
|
|
@ -2767,7 +2661,7 @@ export function AutoRunsHistoryPanel({
|
|||
) : null}
|
||||
|
||||
<div className="autoruns-group-heading">
|
||||
<h4>Автопрогоны</h4>
|
||||
<h4>Сохранённые сессии</h4>
|
||||
<button
|
||||
type="button"
|
||||
className="autoruns-group-toggle"
|
||||
|
|
@ -2780,20 +2674,19 @@ export function AutoRunsHistoryPanel({
|
|||
</div>
|
||||
{autogenGroupExpanded ? (
|
||||
<>
|
||||
<div className="autoruns-form-grid">
|
||||
<label>
|
||||
Режимы
|
||||
<select
|
||||
value={autoGenSettings.mode}
|
||||
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>
|
||||
</select>
|
||||
</label>
|
||||
{!isSavedUserSessionsMode ? (
|
||||
<>
|
||||
<div className="autoruns-form-grid">
|
||||
<label>
|
||||
Режим
|
||||
<select
|
||||
value={autoGenSettings.mode}
|
||||
disabled
|
||||
onChange={(event) => setAutoGenSettings((prev) => ({ ...prev, mode: event.target.value as AutoGenMode }))}
|
||||
>
|
||||
<option value="saved_user_sessions">Пользовательские сессии</option>
|
||||
</select>
|
||||
</label>
|
||||
{!isSavedUserSessionsMode ? (
|
||||
<>
|
||||
<label>
|
||||
Кол-во
|
||||
<input
|
||||
|
|
@ -2892,28 +2785,22 @@ export function AutoRunsHistoryPanel({
|
|||
) : null}
|
||||
|
||||
<div className="button-row">
|
||||
{!isSavedUserSessionsMode ? (
|
||||
<>
|
||||
<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 type="button" className="tab" disabled={autogenHistoryBusy} onClick={() => void loadAutoGenHistory()}>
|
||||
{autogenHistoryBusy ? "Обновляю..." : "Обновить историю"}
|
||||
</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 className="autoruns-form-grid">
|
||||
|
|
|
|||
|
|
@ -111,8 +111,8 @@ export function PromptPanel({
|
|||
<section className="embedded-panel-section">
|
||||
<div className="embedded-panel-section-header">
|
||||
<div>
|
||||
<h4>Prompt Manager</h4>
|
||||
<p>Системный, developer и domain уровни управляются отдельно.</p>
|
||||
<h4>Normalizer / eval prompts</h4>
|
||||
<p>Эти поля управляют декомпозицией и eval-прогонами, а не live-ответом ассистента.</p>
|
||||
</div>
|
||||
</div>
|
||||
{content}
|
||||
|
|
@ -121,7 +121,7 @@ export function PromptPanel({
|
|||
}
|
||||
|
||||
return (
|
||||
<PanelFrame title="Prompt Manager" subtitle="Системный, developer и domain уровни управляются отдельно.">
|
||||
<PanelFrame title="Normalizer / eval prompts" subtitle="Эти поля управляют декомпозицией и eval-прогонами, а не live-ответом ассистента.">
|
||||
{content}
|
||||
</PanelFrame>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||
import argparse
|
||||
import fnmatch
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
|
@ -435,9 +436,22 @@ def dump_json(payload: Any) -> str:
|
|||
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:
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
file_path.write_text(text, encoding="utf-8", newline="\n")
|
||||
target_path = resolve_windows_extended_path(file_path)
|
||||
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:
|
||||
|
|
@ -3016,6 +3030,14 @@ def build_scenario_step_state(
|
|||
"entries": entries,
|
||||
}
|
||||
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["status"] = step_state["acceptance_status"]
|
||||
return validate_step_contract(step_state)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -12,6 +15,27 @@ import domain_truth_harness as dth
|
|||
|
||||
|
||||
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:
|
||||
step_state = dcl.build_scenario_step_state(
|
||||
scenario_id="planner_alignment_demo",
|
||||
|
|
|
|||
Loading…
Reference in New Issue