Починить Hm-прогон ассистента по НДС, остаткам и MCP-ответам
This commit is contained in:
parent
36f7b4df03
commit
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"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -2302,91 +2302,7 @@ function buildAutoRunsRouter(services, openaiClient = new openaiResponsesClient_
|
||||||
});
|
});
|
||||||
router.post("/api/autoruns/autogen/generate", async (req, res, next) => {
|
router.post("/api/autoruns/autogen/generate", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const body = toRecord(req.body);
|
throw new http_1.ApiError("AUTOGEN_QUESTION_GENERATION_DISABLED", "Автогенерация вопросов отключена: используйте сохранение живых пользовательских сессий.", 410);
|
||||||
if (!body) {
|
|
||||||
throw new http_1.ApiError("INVALID_AUTOGEN_PAYLOAD", "JSON body is required", 400);
|
|
||||||
}
|
|
||||||
const mode = parseAutoGenMode(body.mode);
|
|
||||||
const count = parseAutogenCount(body.count);
|
|
||||||
const domain = parseAutogenDomain(body.domain);
|
|
||||||
const persistCaseSet = toBooleanSafe(body.persist_to_eval_cases) ?? true;
|
|
||||||
const generatedBy = parseAnnotationAuthor(body.generated_by);
|
|
||||||
const context = toRecord(body.context);
|
|
||||||
const llmConfig = parseAutogenLlmRuntimeConfig(body, context);
|
|
||||||
const personalityPrompt = toStringSafe(context?.autogen_personality_prompt);
|
|
||||||
if (mode === "saved_user_sessions") {
|
|
||||||
throw new http_1.ApiError("AUTOGEN_MODE_NOT_SUPPORTED", "Use `/api/autoruns/autogen/save-assistant-session` to save user sessions.", 400);
|
|
||||||
}
|
|
||||||
let questions = [];
|
|
||||||
if (mode === "qwen_seed") {
|
|
||||||
if (!llmConfig) {
|
|
||||||
throw new http_1.ApiError("AUTOGEN_LLM_CONFIG_REQUIRED", "Для режима qwen_seed нужен активный LLM-контур (provider/model/baseUrl) из настроек подключения.", 400);
|
|
||||||
}
|
|
||||||
questions = await generateQwenSeedQuestionsLive({
|
|
||||||
count,
|
|
||||||
domain,
|
|
||||||
personalityPrompt,
|
|
||||||
llmConfig,
|
|
||||||
client: openaiClient
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
questions = generateCodexCreativeQuestions(count, domain);
|
|
||||||
}
|
|
||||||
questions = Array.from(new Set(questions.map((item) => sanitizeGeneratedQuestion(item)).filter((item) => item.length > 0))).slice(0, count);
|
|
||||||
const generationId = generateAutogenId();
|
|
||||||
let savedCaseSetFile = null;
|
|
||||||
if (persistCaseSet) {
|
|
||||||
if (!fs_1.default.existsSync(config_1.EVAL_CASES_DIR)) {
|
|
||||||
fs_1.default.mkdirSync(config_1.EVAL_CASES_DIR, { recursive: true });
|
|
||||||
}
|
|
||||||
const fileName = buildAutogenCaseSetFileName(mode, generationId);
|
|
||||||
const filePath = path_1.default.resolve(config_1.EVAL_CASES_DIR, fileName);
|
|
||||||
const payload = buildAutogenCaseSetPayload({
|
|
||||||
generationId,
|
|
||||||
mode,
|
|
||||||
domain,
|
|
||||||
questions
|
|
||||||
});
|
|
||||||
fs_1.default.writeFileSync(filePath, JSON.stringify(payload, null, 2), "utf-8");
|
|
||||||
savedCaseSetFile = fileName;
|
|
||||||
}
|
|
||||||
const record = {
|
|
||||||
generation_id: generationId,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
mode,
|
|
||||||
title: null,
|
|
||||||
count: questions.length,
|
|
||||||
domain,
|
|
||||||
questions,
|
|
||||||
generated_by: generatedBy,
|
|
||||||
saved_case_set_file: savedCaseSetFile,
|
|
||||||
context: context
|
|
||||||
? {
|
|
||||||
llm_provider: toStringSafe(context.llm_provider),
|
|
||||||
model: toStringSafe(context.model),
|
|
||||||
assistant_prompt_version: toStringSafe(context.assistant_prompt_version),
|
|
||||||
decomposition_prompt_version: toStringSafe(context.decomposition_prompt_version),
|
|
||||||
prompt_fingerprint: toStringSafe(context.prompt_fingerprint)
|
|
||||||
? repairAutogenMojibake(String(context.prompt_fingerprint))
|
|
||||||
: null,
|
|
||||||
autogen_personality_id: toStringSafe(context.autogen_personality_id),
|
|
||||||
autogen_personality_prompt: toStringSafe(context.autogen_personality_prompt)
|
|
||||||
? repairAutogenMojibake(String(context.autogen_personality_prompt))
|
|
||||||
: null,
|
|
||||||
source_session_id: null,
|
|
||||||
saved_session_file: null,
|
|
||||||
saved_case_set_kind: "single_turn_list"
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
};
|
|
||||||
const history = readAutoGenHistory();
|
|
||||||
history.unshift(record);
|
|
||||||
writeAutoGenHistory(history.slice(0, 500));
|
|
||||||
(0, http_1.ok)(res, {
|
|
||||||
ok: true,
|
|
||||||
generation: record
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
|
|
|
||||||
|
|
@ -3195,6 +3195,17 @@ class AddressQueryService {
|
||||||
asOfDate: typeof filterSet.as_of_date === "string" ? filterSet.as_of_date : undefined,
|
asOfDate: typeof filterSet.as_of_date === "string" ? filterSet.as_of_date : undefined,
|
||||||
requestedResultMode,
|
requestedResultMode,
|
||||||
vatDirectSourceProbe: options.vatDirectSourceProbe ?? undefined,
|
vatDirectSourceProbe: options.vatDirectSourceProbe ?? undefined,
|
||||||
|
purchaseDateBridge: typeof filterSet.purchase_date_bridge_selected === "string" ||
|
||||||
|
typeof filterSet.purchase_date_bridge_first === "string" ||
|
||||||
|
typeof filterSet.purchase_date_bridge_last === "string"
|
||||||
|
? {
|
||||||
|
selectedPurchaseDate: filterSet.purchase_date_bridge_selected,
|
||||||
|
firstPurchaseDate: filterSet.purchase_date_bridge_first,
|
||||||
|
lastPurchaseDate: filterSet.purchase_date_bridge_last,
|
||||||
|
basis: filterSet.purchase_date_bridge_basis,
|
||||||
|
hasMultiplePurchaseDates: filterSet.purchase_date_bridge_has_multiple === true
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
emphasizeNumbers: options.emphasizeNumbers ?? undefined,
|
emphasizeNumbers: options.emphasizeNumbers ?? undefined,
|
||||||
useRubCurrency: options.useRubCurrency ?? undefined
|
useRubCurrency: options.useRubCurrency ?? undefined
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1413,6 +1413,85 @@ function buildInventoryMovementQuery(filters, resolvedLimit, side) {
|
||||||
.replace("__WHERE_CLAUSE__", buildWhereClause(filters, "Движения.Период", [inventoryCondition, itemCondition].filter((item) => Boolean(item))))
|
.replace("__WHERE_CLAUSE__", buildWhereClause(filters, "Движения.Период", [inventoryCondition, itemCondition].filter((item) => Boolean(item))))
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||||
}
|
}
|
||||||
|
function buildStaticWhereClause(conditions, indent = " ") {
|
||||||
|
const cleaned = conditions.map((condition) => String(condition ?? "").trim()).filter((condition) => condition.length > 0);
|
||||||
|
return cleaned.length > 0 ? `ГДЕ\n${indent}${cleaned.join(`\n${indent}И `)}` : "";
|
||||||
|
}
|
||||||
|
function resolveInventoryAgingAsOfExpr(filters) {
|
||||||
|
const now = new Date();
|
||||||
|
return ((typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||||
|
? toDateTimeExpr(filters.as_of_date, true)
|
||||||
|
: null) ??
|
||||||
|
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||||
|
? toDateTimeExpr(filters.period_to, true)
|
||||||
|
: null) ??
|
||||||
|
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||||
|
? toDateTimeExpr(filters.period_from, true)
|
||||||
|
: null) ??
|
||||||
|
`ДАТАВРЕМЯ(${now.getFullYear()}, ${now.getMonth() + 1}, ${now.getDate()}, 23, 59, 59)`);
|
||||||
|
}
|
||||||
|
function buildInventoryAgingByPurchaseDateQuery(filters, resolvedLimit) {
|
||||||
|
const asOfExpr = resolveInventoryAgingAsOfExpr(filters);
|
||||||
|
const inventoryAccountPredicate = buildAccountPrefixPredicate("Остатки.Счет", ["41.01"]);
|
||||||
|
const onHandScopeConditions = [
|
||||||
|
"Остатки.КоличествоРазвернутыйОстатокДт > 0",
|
||||||
|
inventoryAccountPredicate,
|
||||||
|
buildOrganizationReferenceCondition(filters, ["Остатки.Организация"]),
|
||||||
|
buildWarehouseReferenceCondition(filters, ["Остатки.Субконто3"]),
|
||||||
|
buildInventoryItemReferenceCondition(filters, ["Остатки.Субконто1"])
|
||||||
|
];
|
||||||
|
const onHandWhereClause = buildStaticWhereClause(onHandScopeConditions);
|
||||||
|
const currentStockItemSubquery = [
|
||||||
|
"(ВЫБРАТЬ РАЗЛИЧНЫЕ",
|
||||||
|
" Остатки.Субконто1",
|
||||||
|
" ИЗ",
|
||||||
|
` РегистрБухгалтерии.Хозрасчетный.Остатки(${asOfExpr}, , , ) КАК Остатки`,
|
||||||
|
buildStaticWhereClause(onHandScopeConditions, " "),
|
||||||
|
")"
|
||||||
|
].join("\n");
|
||||||
|
const purchaseWhereClause = buildStaticWhereClause([
|
||||||
|
"Товары.Ссылка.Проведен = ИСТИНА",
|
||||||
|
`Товары.Ссылка.Дата <= ${asOfExpr}`,
|
||||||
|
buildOrganizationReferenceCondition(filters, ["Товары.Ссылка.Организация"]),
|
||||||
|
buildInventoryItemReferenceCondition(filters, ["Товары.Номенклатура"]),
|
||||||
|
`Товары.Номенклатура В ${currentStockItemSubquery}`
|
||||||
|
]);
|
||||||
|
return `
|
||||||
|
ВЫБРАТЬ ПЕРВЫЕ ${resolvedLimit}
|
||||||
|
${asOfExpr} КАК Период,
|
||||||
|
"Остатки на дату" КАК Регистратор,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетДт,
|
||||||
|
"" КАК СчетКт,
|
||||||
|
Остатки.СуммаРазвернутыйОстатокДт КАК Сумма,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК Номенклатура,
|
||||||
|
"" КАК Контрагент,
|
||||||
|
"" КАК Договор,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК Склад,
|
||||||
|
Остатки.КоличествоРазвернутыйОстатокДт КАК Количество
|
||||||
|
ИЗ
|
||||||
|
РегистрБухгалтерии.Хозрасчетный.Остатки(${asOfExpr}, , , ) КАК Остатки
|
||||||
|
${onHandWhereClause}
|
||||||
|
ОБЪЕДИНИТЬ ВСЕ
|
||||||
|
ВЫБРАТЬ ПЕРВЫЕ ${resolvedLimit}
|
||||||
|
Товары.Ссылка.Дата КАК Период,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка) КАК Регистратор,
|
||||||
|
"41.01" КАК СчетДт,
|
||||||
|
"" КАК СчетКт,
|
||||||
|
Товары.Сумма КАК Сумма,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Товары.Номенклатура) КАК Номенклатура,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Контрагент) КАК Контрагент,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.ДоговорКонтрагента) КАК Договор,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Организация) КАК Организация,
|
||||||
|
"" КАК Склад,
|
||||||
|
Товары.Количество КАК Количество
|
||||||
|
ИЗ
|
||||||
|
Документ.ПоступлениеТоваровУслуг.Товары КАК Товары
|
||||||
|
${purchaseWhereClause}
|
||||||
|
УПОРЯДОЧИТЬ ПО
|
||||||
|
Период ВОЗР
|
||||||
|
`.trim();
|
||||||
|
}
|
||||||
function buildWarehouseReferenceCondition(filters, fieldPaths) {
|
function buildWarehouseReferenceCondition(filters, fieldPaths) {
|
||||||
const warehouse = typeof filters.warehouse === "string" ? filters.warehouse.trim() : "";
|
const warehouse = typeof filters.warehouse === "string" ? filters.warehouse.trim() : "";
|
||||||
if (!warehouse) {
|
if (!warehouse) {
|
||||||
|
|
@ -1922,7 +2001,7 @@ function buildAddressRecipePlan(recipe, filters) {
|
||||||
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
||||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
||||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
? buildInventoryAgingByPurchaseDateQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "inventory_quality_events_profile"
|
: recipe.query_template === "inventory_quality_events_profile"
|
||||||
? buildInventoryQualityEventsQuery(filters, resolvedLimit)
|
? buildInventoryQualityEventsQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "contracts_by_counterparty_profile"
|
: recipe.query_template === "contracts_by_counterparty_profile"
|
||||||
|
|
|
||||||
|
|
@ -549,9 +549,33 @@ function needsVatPurchaseDateAnchorDisclosure(userMessage) {
|
||||||
}
|
}
|
||||||
return /(?:дата|дату|дате|момент)\s+(?:покуп|закуп)|(?:покуп|закуп)\S*\s+(?:дат|момент)|purchase\s+date|date\s+of\s+purchase/iu.test(text);
|
return /(?:дата|дату|дате|момент)\s+(?:покуп|закуп)|(?:покуп|закуп)\S*\s+(?:дат|момент)|purchase\s+date|date\s+of\s+purchase/iu.test(text);
|
||||||
}
|
}
|
||||||
function buildVatPurchaseDateAnchorDisclosureLine(options, periodWindowLabel) {
|
function vatPurchaseDateBasisLabel(basis, hasMultiplePurchaseDates) {
|
||||||
if (!periodWindowLabel || !needsVatPurchaseDateAnchorDisclosure(options.userMessage)) {
|
if (!hasMultiplePurchaseDates) {
|
||||||
return null;
|
return "подтвержденную дату закупки";
|
||||||
|
}
|
||||||
|
if (basis === "last_confirmed_purchase") {
|
||||||
|
return "последнюю подтвержденную дату закупки";
|
||||||
|
}
|
||||||
|
return "первую подтвержденную дату закупки";
|
||||||
|
}
|
||||||
|
function buildVatPurchaseDateAnchorDisclosureLines(options, periodWindowLabel) {
|
||||||
|
const bridge = options.purchaseDateBridge ?? null;
|
||||||
|
const selectedPurchaseDate = normalizeIsoDateOnly(bridge?.selectedPurchaseDate);
|
||||||
|
const firstPurchaseDate = normalizeIsoDateOnly(bridge?.firstPurchaseDate);
|
||||||
|
const lastPurchaseDate = normalizeIsoDateOnly(bridge?.lastPurchaseDate);
|
||||||
|
const hasMultiplePurchaseDates = Boolean(bridge?.hasMultiplePurchaseDates || (firstPurchaseDate && lastPurchaseDate && firstPurchaseDate !== lastPurchaseDate));
|
||||||
|
if (!periodWindowLabel || (!selectedPurchaseDate && !needsVatPurchaseDateAnchorDisclosure(options.userMessage))) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (selectedPurchaseDate) {
|
||||||
|
const basisLabel = vatPurchaseDateBasisLabel(bridge?.basis, hasMultiplePurchaseDates);
|
||||||
|
const lines = [
|
||||||
|
`- Якорь периода: для расчета я использую ${basisLabel} ${formatDateRu(selectedPurchaseDate)}; налоговый период ${periodWindowLabel}; сам расчет ниже взят из книг продаж/покупок за это окно.`
|
||||||
|
];
|
||||||
|
if (hasMultiplePurchaseDates && firstPurchaseDate && lastPurchaseDate) {
|
||||||
|
lines.push(`- Важно: у позиции несколько подтвержденных дат закупки (${formatDateRu(firstPurchaseDate)}..${formatDateRu(lastPurchaseDate)}); это расчет по выбранному якорю, а не единственная возможная дата.`);
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
}
|
}
|
||||||
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
||||||
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
||||||
|
|
@ -565,9 +589,13 @@ function buildVatPurchaseDateAnchorDisclosureLine(options, periodWindowLabel) {
|
||||||
toTs !== null &&
|
toTs !== null &&
|
||||||
asOfTs >= fromTs &&
|
asOfTs >= fromTs &&
|
||||||
asOfTs <= toTs) {
|
asOfTs <= toTs) {
|
||||||
return `- Якорь периода: дата покупки ${formatDateRu(asOfDate)} попадает в налоговый период ${periodWindowLabel}; поэтому расчет ниже взят из книг продаж/покупок за это окно.`;
|
return [
|
||||||
|
`- Якорь периода: дата покупки ${formatDateRu(asOfDate)} попадает в налоговый период ${periodWindowLabel}; поэтому расчет ниже взят из книг продаж/покупок за это окно.`
|
||||||
|
];
|
||||||
}
|
}
|
||||||
return `- Якорь периода: дата покупки из вопроса/контекста использована для выбора налогового периода ${periodWindowLabel}; сам расчет ниже взят из книг продаж/покупок за это окно.`;
|
return [
|
||||||
|
`- Якорь периода: дата покупки из вопроса/контекста использована для выбора налогового периода ${periodWindowLabel}; сам расчет ниже взят из книг продаж/покупок за это окно.`
|
||||||
|
];
|
||||||
}
|
}
|
||||||
function detectRankingLimit(userMessage, fallback = 20) {
|
function detectRankingLimit(userMessage, fallback = 20) {
|
||||||
const text = normalizeQuestionText(userMessage);
|
const text = normalizeQuestionText(userMessage);
|
||||||
|
|
@ -996,12 +1024,72 @@ function hasInventoryAccountPrefix(value, prefix) {
|
||||||
.replace(",", ".");
|
.replace(",", ".");
|
||||||
return normalized === prefix || normalized.startsWith(`${prefix}.`) || normalized.startsWith(prefix);
|
return normalized === prefix || normalized.startsWith(`${prefix}.`) || normalized.startsWith(prefix);
|
||||||
}
|
}
|
||||||
|
function isInventoryOnHandSnapshotRow(row) {
|
||||||
|
const registrator = String(row.registrator ?? "").trim().toLowerCase();
|
||||||
|
return /(?:остатки\s+на\s+дату|stock\s*on\s*hand|balance\s+as\s+of)/iu.test(registrator);
|
||||||
|
}
|
||||||
function isInventoryPurchaseMovement(row) {
|
function isInventoryPurchaseMovement(row) {
|
||||||
return hasInventoryAccountPrefix(row.account_dt, "41.01");
|
return hasInventoryAccountPrefix(row.account_dt, "41.01") && !isInventoryOnHandSnapshotRow(row);
|
||||||
}
|
}
|
||||||
function isInventorySaleMovement(row) {
|
function isInventorySaleMovement(row) {
|
||||||
return hasInventoryAccountPrefix(row.account_kt, "41.01");
|
return hasInventoryAccountPrefix(row.account_kt, "41.01");
|
||||||
}
|
}
|
||||||
|
function inventoryAgingKeyPart(value) {
|
||||||
|
return normalizeEntityToken(value) ?? "";
|
||||||
|
}
|
||||||
|
function buildInventoryOnHandAgingScope(rows) {
|
||||||
|
const scope = {
|
||||||
|
itemKeys: new Set(),
|
||||||
|
itemOrgKeys: new Set(),
|
||||||
|
fullKeys: new Set(),
|
||||||
|
orgKeys: new Set()
|
||||||
|
};
|
||||||
|
for (const row of rows) {
|
||||||
|
if (!isInventoryOnHandSnapshotRow(row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const quantity = extractInventoryQuantity(row);
|
||||||
|
if (quantity === null || quantity <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const itemKey = inventoryAgingKeyPart(extractInventoryItemName(row));
|
||||||
|
if (!itemKey) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const organizationKey = inventoryAgingKeyPart(extractInventoryOrganizationName(row));
|
||||||
|
const warehouseKey = inventoryAgingKeyPart(extractInventoryWarehouseName(row));
|
||||||
|
scope.itemKeys.add(itemKey);
|
||||||
|
if (organizationKey) {
|
||||||
|
scope.orgKeys.add(organizationKey);
|
||||||
|
scope.itemOrgKeys.add(`${itemKey}|${organizationKey}`);
|
||||||
|
}
|
||||||
|
if (organizationKey || warehouseKey) {
|
||||||
|
scope.fullKeys.add(`${itemKey}|${warehouseKey}|${organizationKey}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return scope;
|
||||||
|
}
|
||||||
|
function inventoryPurchaseMatchesOnHandAgingScope(row, scope) {
|
||||||
|
if (scope.itemKeys.size === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const itemKey = inventoryAgingKeyPart(extractInventoryItemName(row));
|
||||||
|
if (!itemKey || !scope.itemKeys.has(itemKey)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const organizationKey = inventoryAgingKeyPart(extractInventoryOrganizationName(row));
|
||||||
|
const warehouseKey = inventoryAgingKeyPart(extractInventoryWarehouseName(row));
|
||||||
|
if (scope.fullKeys.has(`${itemKey}|${warehouseKey}|${organizationKey}`)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (organizationKey && scope.itemOrgKeys.has(`${itemKey}|${organizationKey}`)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (organizationKey && scope.orgKeys.size > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
function looksLikeInventoryTraceDocumentToken(value) {
|
function looksLikeInventoryTraceDocumentToken(value) {
|
||||||
const normalized = String(value ?? "").trim();
|
const normalized = String(value ?? "").trim();
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
|
|
@ -1162,7 +1250,11 @@ function formatCounterpartyItemFlowRows(rows, limit = 12) {
|
||||||
function buildInventoryAgingByItemAggregate(rows, asOfDate) {
|
function buildInventoryAgingByItemAggregate(rows, asOfDate) {
|
||||||
const byItem = new Map();
|
const byItem = new Map();
|
||||||
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
|
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
|
||||||
|
const onHandScope = buildInventoryOnHandAgingScope(rows);
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
|
if (!isInventoryPurchaseMovement(row) || !inventoryPurchaseMatchesOnHandAgingScope(row, onHandScope)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const item = extractInventoryItemName(row);
|
const item = extractInventoryItemName(row);
|
||||||
if (!item) {
|
if (!item) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -1245,19 +1337,11 @@ function formatInventoryAgingRows(items, asOfDate, limit = 10) {
|
||||||
const parts = [
|
const parts = [
|
||||||
`${index + 1}. ${item.item}`,
|
`${index + 1}. ${item.item}`,
|
||||||
`первая закупка: ${inventoryTraceDateLabel(item.firstPurchasePeriod)}`,
|
`первая закупка: ${inventoryTraceDateLabel(item.firstPurchasePeriod)}`,
|
||||||
`последняя закупка: ${inventoryTraceDateLabel(item.lastPurchasePeriod)}`,
|
`последняя закупка: ${inventoryTraceDateLabel(item.lastPurchasePeriod)}`
|
||||||
`документов: ${formatNumberWithDots(item.documentCount)}`,
|
|
||||||
`операций: ${formatNumberWithDots(item.operations)}`
|
|
||||||
];
|
];
|
||||||
if (item.ageDays !== null) {
|
if (item.ageDays !== null) {
|
||||||
parts.push(`возраст следа на ${formatDateRu(asOfDate)}: ${formatNumberWithDots(item.ageDays)} дн.`);
|
parts.push(`возраст следа на ${formatDateRu(asOfDate)}: ${formatNumberWithDots(item.ageDays)} дн.`);
|
||||||
}
|
}
|
||||||
if (item.warehouse) {
|
|
||||||
parts.push(`склад: ${item.warehouse}`);
|
|
||||||
}
|
|
||||||
if (item.organization) {
|
|
||||||
parts.push(`организация: ${item.organization}`);
|
|
||||||
}
|
|
||||||
if (item.counterparties.length > 0) {
|
if (item.counterparties.length > 0) {
|
||||||
parts.push(`поставщики: ${item.counterparties.slice(0, 3).join("; ")}`);
|
parts.push(`поставщики: ${item.counterparties.slice(0, 3).join("; ")}`);
|
||||||
}
|
}
|
||||||
|
|
@ -3060,15 +3144,27 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||||
const formatConfirmedMoney = (value) => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
const formatConfirmedMoney = (value) => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
||||||
const organizationLabel = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.organizationHint);
|
const organizationLabel = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.organizationHint);
|
||||||
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
|
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
|
||||||
const purchaseDateAnchorLine = buildVatPurchaseDateAnchorDisclosureLine(options, periodWindowLabel);
|
const purchaseDateAnchorLines = buildVatPurchaseDateAnchorDisclosureLines(options, periodWindowLabel);
|
||||||
|
const selectedPurchaseDate = normalizeIsoDateOnly(options.purchaseDateBridge?.selectedPurchaseDate);
|
||||||
|
const hasMultiplePurchaseDates = Boolean(options.purchaseDateBridge?.hasMultiplePurchaseDates ||
|
||||||
|
(normalizeIsoDateOnly(options.purchaseDateBridge?.firstPurchaseDate) &&
|
||||||
|
normalizeIsoDateOnly(options.purchaseDateBridge?.lastPurchaseDate) &&
|
||||||
|
normalizeIsoDateOnly(options.purchaseDateBridge?.firstPurchaseDate) !==
|
||||||
|
normalizeIsoDateOnly(options.purchaseDateBridge?.lastPurchaseDate)));
|
||||||
|
const purchaseBasisLabel = vatPurchaseDateBasisLabel(options.purchaseDateBridge?.basis, hasMultiplePurchaseDates);
|
||||||
|
const directVatLine = selectedPurchaseDate
|
||||||
|
? hasMultiplePurchaseDates
|
||||||
|
? `Коротко: если брать ${purchaseBasisLabel} ${formatDateRu(selectedPurchaseDate)}, подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`
|
||||||
|
: `Коротко: по дате покупки ${formatDateRu(selectedPurchaseDate)} подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`
|
||||||
|
: `Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`;
|
||||||
const lines = [
|
const lines = [
|
||||||
`Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`,
|
directVatLine,
|
||||||
"Расчет сделан по книгам продаж и покупок.",
|
"Расчет сделан по книгам продаж и покупок.",
|
||||||
"",
|
"",
|
||||||
"Что вошло в расчет:",
|
"Что вошло в расчет:",
|
||||||
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
|
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
|
||||||
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
|
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
|
||||||
...(purchaseDateAnchorLine ? [purchaseDateAnchorLine] : []),
|
...purchaseDateAnchorLines,
|
||||||
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
|
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
|
||||||
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
|
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
|
||||||
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`
|
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`
|
||||||
|
|
|
||||||
|
|
@ -460,6 +460,23 @@ function resolveYearFromFilters(filters) {
|
||||||
function hasRelativeYearHint(text) {
|
function hasRelativeYearHint(text) {
|
||||||
return /(?:эт(?:от|ого)(?:\s+же)?\s+год|этого\s+же\s+года|того\s+же\s+года|this\s+year|same\s+year|that\s+year)/iu.test(String(text ?? ""));
|
return /(?:эт(?:от|ого)(?:\s+же)?\s+год|этого\s+же\s+года|того\s+же\s+года|this\s+year|same\s+year|that\s+year)/iu.test(String(text ?? ""));
|
||||||
}
|
}
|
||||||
|
function resolveMonthPeriodFromIsoDate(isoDate) {
|
||||||
|
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const year = Number(match[1]);
|
||||||
|
const month = Number(match[2]);
|
||||||
|
if (!Number.isFinite(year) || !Number.isFinite(month) || month < 1 || month > 12) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
||||||
|
return {
|
||||||
|
period_from: `${match[1]}-${match[2]}-01`,
|
||||||
|
period_to: `${match[1]}-${match[2]}-${String(lastDay).padStart(2, "0")}`,
|
||||||
|
as_of_date: `${match[1]}-${match[2]}-${String(lastDay).padStart(2, "0")}`
|
||||||
|
};
|
||||||
|
}
|
||||||
function resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext) {
|
function resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext) {
|
||||||
if (!followupContext || !isInventoryRootFrameIntent(followupContext.root_intent)) {
|
if (!followupContext || !isInventoryRootFrameIntent(followupContext.root_intent)) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -629,6 +646,34 @@ function hasInventoryPurchaseDateVatBridgeCue(text) {
|
||||||
return (/(?:ндс|vat)/iu.test(normalized) &&
|
return (/(?:ндс|vat)/iu.test(normalized) &&
|
||||||
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/iu.test(normalized));
|
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/iu.test(normalized));
|
||||||
}
|
}
|
||||||
|
function hasInventoryPurchaseDateVatBridgeContinuationCue(text) {
|
||||||
|
const normalized = textWithRepairedVariant(String(text ?? ""))
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ё/g, "е");
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const mentionsPurchase = /(?:покупк|закупк|purchase)/iu.test(normalized);
|
||||||
|
const mentionsFirstOrLast = /(?:перв|последн|earliest|oldest|latest|last)/iu.test(normalized);
|
||||||
|
return mentionsPurchase && mentionsFirstOrLast;
|
||||||
|
}
|
||||||
|
function purchaseDateBridgeBasisFromMessage(userMessage) {
|
||||||
|
const normalized = textWithRepairedVariant(String(userMessage ?? ""))
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ё/g, "е");
|
||||||
|
if (!normalized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (/(?:последн|latest|last)/iu.test(normalized) && /(?:покупк|закупк|purchase)/iu.test(normalized)) {
|
||||||
|
return "last_confirmed_purchase";
|
||||||
|
}
|
||||||
|
if (/(?:перв|earliest|oldest)/iu.test(normalized) && /(?:покупк|закупк|purchase)/iu.test(normalized)) {
|
||||||
|
return "first_confirmed_purchase";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
function hasInventoryMarginRankingFollowupCue(text) {
|
function hasInventoryMarginRankingFollowupCue(text) {
|
||||||
const normalized = textWithRepairedVariant(String(text ?? ""))
|
const normalized = textWithRepairedVariant(String(text ?? ""))
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
|
@ -745,6 +790,31 @@ function isShortDebtRoleMirrorFollowup(intent, text, followupContext) {
|
||||||
function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
const merged = { ...current };
|
const merged = { ...current };
|
||||||
const reasons = [];
|
const reasons = [];
|
||||||
|
const clearInventoryAgingOrganizationAliasItem = (organizationHint) => {
|
||||||
|
if (intent !== "inventory_aging_by_purchase_date") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const explicitItemMention = /(?:^|[\s,.;:!?()\-\u2014])(?:товар(?:у|а|ом)?|позици(?:и|я|ю)|item|row|line)(?=$|[\s,.;:!?()\-\u2014])/iu.test(String(userMessage ?? ""));
|
||||||
|
const agingItem = toNonEmptyString(merged.item);
|
||||||
|
const agingOrganization = toNonEmptyString(merged.organization) ?? organizationHint;
|
||||||
|
const normalizedAgingItem = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeSearchText)(agingItem ?? "");
|
||||||
|
const normalizedAgingOrganization = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeSearchText)(agingOrganization ?? "");
|
||||||
|
const itemLooksLikeOrganizationScope = Boolean(agingItem &&
|
||||||
|
agingOrganization &&
|
||||||
|
((0, assistantOrganizationMatcher_1.organizationsLikelySameEntity)(agingItem, agingOrganization) ||
|
||||||
|
(normalizedAgingItem &&
|
||||||
|
normalizedAgingOrganization &&
|
||||||
|
(normalizedAgingOrganization.includes(normalizedAgingItem) ||
|
||||||
|
normalizedAgingItem.includes(normalizedAgingOrganization)))));
|
||||||
|
if (agingItem && itemLooksLikeOrganizationScope) {
|
||||||
|
delete merged.item;
|
||||||
|
reasons.push("item_cleared_as_organization_scope_alias_for_stock_aging");
|
||||||
|
}
|
||||||
|
else if (agingItem && !explicitItemMention) {
|
||||||
|
delete merged.item;
|
||||||
|
reasons.push("item_cleared_for_stock_slice_aging");
|
||||||
|
}
|
||||||
|
};
|
||||||
if (!followupContext) {
|
if (!followupContext) {
|
||||||
if ((intent === "list_open_contracts" ||
|
if ((intent === "list_open_contracts" ||
|
||||||
intent === "open_contracts_confirmed_as_of_date" ||
|
intent === "open_contracts_confirmed_as_of_date" ||
|
||||||
|
|
@ -760,6 +830,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
: "as_of_date_derived_from_period_for_open_contracts");
|
: "as_of_date_derived_from_period_for_open_contracts");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
clearInventoryAgingOrganizationAliasItem(null);
|
||||||
return { filters: merged, reasons };
|
return { filters: merged, reasons };
|
||||||
}
|
}
|
||||||
const previous = followupContext.previous_filters ?? {};
|
const previous = followupContext.previous_filters ?? {};
|
||||||
|
|
@ -773,6 +844,12 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
const previousAsOfDate = toNonEmptyString(previous.as_of_date);
|
const previousAsOfDate = toNonEmptyString(previous.as_of_date);
|
||||||
const previousPeriodFrom = toNonEmptyString(previous.period_from);
|
const previousPeriodFrom = toNonEmptyString(previous.period_from);
|
||||||
const previousPeriodTo = toNonEmptyString(previous.period_to);
|
const previousPeriodTo = toNonEmptyString(previous.period_to);
|
||||||
|
const previousPurchaseDateBridgeSelected = toNonEmptyString(previous.purchase_date_bridge_selected);
|
||||||
|
const previousPurchaseDateBridgeFirst = toNonEmptyString(previous.purchase_date_bridge_first);
|
||||||
|
const previousPurchaseDateBridgeLast = toNonEmptyString(previous.purchase_date_bridge_last);
|
||||||
|
const previousPurchaseDateBridgeBasis = toNonEmptyString(previous.purchase_date_bridge_basis);
|
||||||
|
const previousPurchaseDateBridgeHasMultiple = previous.purchase_date_bridge_has_multiple === true ||
|
||||||
|
String(previous.purchase_date_bridge_has_multiple ?? "").trim().toLowerCase() === "true";
|
||||||
const relativeMonthFromInventoryRoot = resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext);
|
const relativeMonthFromInventoryRoot = resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext);
|
||||||
const relativeMonthFromFollowupYear = resolveRelativeMonthPeriodFromFollowupYear(userMessage, followupContext);
|
const relativeMonthFromFollowupYear = resolveRelativeMonthPeriodFromFollowupYear(userMessage, followupContext);
|
||||||
const allTimeRequested = hasAllTimeHint(userMessage);
|
const allTimeRequested = hasAllTimeHint(userMessage);
|
||||||
|
|
@ -790,6 +867,53 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
merged.organization = previousOrganization;
|
merged.organization = previousOrganization;
|
||||||
reasons.push("organization_from_followup_context");
|
reasons.push("organization_from_followup_context");
|
||||||
}
|
}
|
||||||
|
if (intent === "vat_liability_confirmed_for_tax_period" && previousPurchaseDateBridgeSelected) {
|
||||||
|
const requestedBridgeBasis = purchaseDateBridgeBasisFromMessage(userMessage);
|
||||||
|
const shouldCarryPurchaseDateBridge = !currentHasExplicitTemporalScope &&
|
||||||
|
(Boolean(requestedBridgeBasis) ||
|
||||||
|
hasInventoryPurchaseDateVatBridgeCue(userMessage) ||
|
||||||
|
hasInventoryPurchaseDateVatBridgeContinuationCue(userMessage));
|
||||||
|
if (!shouldCarryPurchaseDateBridge) {
|
||||||
|
delete merged.purchase_date_bridge_selected;
|
||||||
|
delete merged.purchase_date_bridge_first;
|
||||||
|
delete merged.purchase_date_bridge_last;
|
||||||
|
delete merged.purchase_date_bridge_basis;
|
||||||
|
delete merged.purchase_date_bridge_has_multiple;
|
||||||
|
reasons.push(currentHasExplicitTemporalScope
|
||||||
|
? "purchase_date_bridge_suppressed_by_explicit_temporal_scope"
|
||||||
|
: "purchase_date_bridge_not_reused_without_bridge_cue");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const selectedBridgeDate = requestedBridgeBasis === "last_confirmed_purchase"
|
||||||
|
? previousPurchaseDateBridgeLast ?? previousPurchaseDateBridgeSelected
|
||||||
|
: requestedBridgeBasis === "first_confirmed_purchase"
|
||||||
|
? previousPurchaseDateBridgeFirst ?? previousPurchaseDateBridgeSelected
|
||||||
|
: previousPurchaseDateBridgeSelected;
|
||||||
|
merged.purchase_date_bridge_selected = selectedBridgeDate;
|
||||||
|
const selectedBridgeWindow = resolveMonthPeriodFromIsoDate(selectedBridgeDate);
|
||||||
|
if (selectedBridgeWindow) {
|
||||||
|
merged.period_from = selectedBridgeWindow.period_from;
|
||||||
|
merged.period_to = selectedBridgeWindow.period_to;
|
||||||
|
merged.as_of_date = selectedBridgeWindow.as_of_date;
|
||||||
|
reasons.push("period_from_purchase_date_bridge_followup_context");
|
||||||
|
reasons.push("period_to_purchase_date_bridge_followup_context");
|
||||||
|
}
|
||||||
|
if (previousPurchaseDateBridgeFirst) {
|
||||||
|
merged.purchase_date_bridge_first = previousPurchaseDateBridgeFirst;
|
||||||
|
}
|
||||||
|
if (previousPurchaseDateBridgeLast) {
|
||||||
|
merged.purchase_date_bridge_last = previousPurchaseDateBridgeLast;
|
||||||
|
}
|
||||||
|
const effectiveBridgeBasis = requestedBridgeBasis ?? previousPurchaseDateBridgeBasis;
|
||||||
|
if (effectiveBridgeBasis) {
|
||||||
|
merged.purchase_date_bridge_basis = effectiveBridgeBasis;
|
||||||
|
}
|
||||||
|
if (previousPurchaseDateBridgeHasMultiple) {
|
||||||
|
merged.purchase_date_bridge_has_multiple = true;
|
||||||
|
}
|
||||||
|
reasons.push("purchase_date_bridge_from_followup_context");
|
||||||
|
}
|
||||||
|
}
|
||||||
if (intent === "inventory_on_hand_as_of_date" &&
|
if (intent === "inventory_on_hand_as_of_date" &&
|
||||||
followupContext.previous_intent === "inventory_on_hand_as_of_date" &&
|
followupContext.previous_intent === "inventory_on_hand_as_of_date" &&
|
||||||
followupContext.target_intent === "inventory_on_hand_as_of_date" &&
|
followupContext.target_intent === "inventory_on_hand_as_of_date" &&
|
||||||
|
|
@ -1099,11 +1223,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
reasons.push("period_derived_from_inventory_root_frame_year");
|
reasons.push("period_derived_from_inventory_root_frame_year");
|
||||||
}
|
}
|
||||||
if (intent === "inventory_aging_by_purchase_date") {
|
if (intent === "inventory_aging_by_purchase_date") {
|
||||||
const explicitItemMention = /(?:^|[\s,.;:!?()\-\u2014])(?:товар(?:у|а|ом)?|позици(?:и|я|ю)|item|row|line)(?=$|[\s,.;:!?()\-\u2014])/iu.test(String(userMessage ?? ""));
|
clearInventoryAgingOrganizationAliasItem(previousOrganization);
|
||||||
if (toNonEmptyString(merged.item) && !explicitItemMention) {
|
|
||||||
delete merged.item;
|
|
||||||
reasons.push("item_cleared_for_stock_slice_aging");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!sameDateRequested &&
|
if (!sameDateRequested &&
|
||||||
hasFollowupSignalForConfirmed &&
|
hasFollowupSignalForConfirmed &&
|
||||||
|
|
@ -1317,7 +1437,12 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||||
return detectedIntent;
|
return detectedIntent;
|
||||||
}
|
}
|
||||||
const normalizedMessage = String(userMessage ?? "");
|
const normalizedMessage = String(userMessage ?? "");
|
||||||
const hasFollowupSignal = hasAddressFollowupContextSignal(normalizedMessage);
|
const genericFollowupSignal = hasAddressFollowupContextSignal(normalizedMessage);
|
||||||
|
const previousFilters = followupContext.previous_filters ?? {};
|
||||||
|
const purchaseBridgeContinuationSignal = followupContext.previous_intent === "vat_liability_confirmed_for_tax_period" &&
|
||||||
|
Boolean(toNonEmptyString(previousFilters.purchase_date_bridge_selected)) &&
|
||||||
|
hasInventoryPurchaseDateVatBridgeContinuationCue(normalizedMessage);
|
||||||
|
const hasFollowupSignal = genericFollowupSignal || purchaseBridgeContinuationSignal;
|
||||||
if (!hasFollowupSignal) {
|
if (!hasFollowupSignal) {
|
||||||
return detectedIntent;
|
return detectedIntent;
|
||||||
}
|
}
|
||||||
|
|
@ -1326,7 +1451,6 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||||
if (!sourceIntent && !fallbackIntent) {
|
if (!sourceIntent && !fallbackIntent) {
|
||||||
return detectedIntent;
|
return detectedIntent;
|
||||||
}
|
}
|
||||||
const previousFilters = followupContext.previous_filters ?? {};
|
|
||||||
const previousPeriodFrom = toNonEmptyString(previousFilters.period_from);
|
const previousPeriodFrom = toNonEmptyString(previousFilters.period_from);
|
||||||
const previousPeriodTo = toNonEmptyString(previousFilters.period_to);
|
const previousPeriodTo = toNonEmptyString(previousFilters.period_to);
|
||||||
const previousContract = toNonEmptyString(previousFilters.contract);
|
const previousContract = toNonEmptyString(previousFilters.contract);
|
||||||
|
|
@ -1340,6 +1464,7 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||||
const samePeriodVatFollowup = isVatFollowup &&
|
const samePeriodVatFollowup = isVatFollowup &&
|
||||||
hasSamePeriodHint(normalizedMessage) &&
|
hasSamePeriodHint(normalizedMessage) &&
|
||||||
Boolean(previousPeriodFrom || previousPeriodTo);
|
Boolean(previousPeriodFrom || previousPeriodTo);
|
||||||
|
const previousPurchaseDateBridgeSelected = toNonEmptyString(previousFilters.purchase_date_bridge_selected);
|
||||||
const previousIsInventoryFamily = isInventoryIntent(sourceIntent ?? undefined);
|
const previousIsInventoryFamily = isInventoryIntent(sourceIntent ?? undefined);
|
||||||
const rootIsInventoryFamily = isInventoryIntent(followupContext.root_intent ?? undefined);
|
const rootIsInventoryFamily = isInventoryIntent(followupContext.root_intent ?? undefined);
|
||||||
const inventoryLineageActive = previousIsInventoryFamily ||
|
const inventoryLineageActive = previousIsInventoryFamily ||
|
||||||
|
|
@ -1361,7 +1486,11 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||||
sourceIntent === "open_items_by_counterparty_or_contract");
|
sourceIntent === "open_items_by_counterparty_or_contract");
|
||||||
const hasExplicitInventoryItemReference = /(?:товар|номенклатур|позици|склад|остат|sku|item|product|товар|номенклатур|позици|склад|остат)/iu.test(normalizedMessage) || hasSelectedObjectInlineSnapshotMetadata(normalizedMessage);
|
const hasExplicitInventoryItemReference = /(?:товар|номенклатур|позици|склад|остат|sku|item|product|товар|номенклатур|позици|склад|остат)/iu.test(normalizedMessage) || hasSelectedObjectInlineSnapshotMetadata(normalizedMessage);
|
||||||
const staleInventoryLineageCanYieldToCounterparty = previousCounterpartyLaneActive && !hasExplicitInventoryItemReference;
|
const staleInventoryLineageCanYieldToCounterparty = previousCounterpartyLaneActive && !hasExplicitInventoryItemReference;
|
||||||
const inventoryPurchaseDateVatBridge = inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage);
|
const vatPurchaseDateBridgeContinuation = sourceIntent === "vat_liability_confirmed_for_tax_period" &&
|
||||||
|
Boolean(previousPurchaseDateBridgeSelected) &&
|
||||||
|
hasInventoryPurchaseDateVatBridgeContinuationCue(normalizedMessage);
|
||||||
|
const inventoryPurchaseDateVatBridge = (inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage)) ||
|
||||||
|
vatPurchaseDateBridgeContinuation;
|
||||||
const marginRankingLineageActive = sourceIntent === "inventory_margin_ranking_for_nomenclature" ||
|
const marginRankingLineageActive = sourceIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
fallbackIntent === "inventory_margin_ranking_for_nomenclature" ||
|
fallbackIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
followupContext.root_intent === "inventory_margin_ranking_for_nomenclature";
|
followupContext.root_intent === "inventory_margin_ranking_for_nomenclature";
|
||||||
|
|
@ -1382,6 +1511,7 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||||
if (inventoryPurchaseDateVatBridge &&
|
if (inventoryPurchaseDateVatBridge &&
|
||||||
(detectedIntent.intent === "unknown" ||
|
(detectedIntent.intent === "unknown" ||
|
||||||
detectedIntent.intent === sourceIntent ||
|
detectedIntent.intent === sourceIntent ||
|
||||||
|
detectedIntent.intent === "account_balance_snapshot" ||
|
||||||
detectedIntent.intent === "vat_payable_confirmed_as_of_date" ||
|
detectedIntent.intent === "vat_payable_confirmed_as_of_date" ||
|
||||||
detectedIntent.intent === "vat_payable_forecast")) {
|
detectedIntent.intent === "vat_payable_forecast")) {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,16 @@ function inventoryRequestedPartyMatches(requested, actualParties) {
|
||||||
function inventoryPartyListOrUnknown(parties) {
|
function inventoryPartyListOrUnknown(parties) {
|
||||||
return parties.length > 0 ? parties.slice(0, 4).join("; ") : "не выделен отдельным полем";
|
return parties.length > 0 ? parties.slice(0, 4).join("; ") : "не выделен отдельным полем";
|
||||||
}
|
}
|
||||||
|
function normalizeInventoryReplyEntityToken(value) {
|
||||||
|
const normalized = String(value ?? "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ё/gu, "е")
|
||||||
|
.replace(/[^a-zа-я0-9]+/giu, " ")
|
||||||
|
.replace(/\s+/gu, " ")
|
||||||
|
.trim();
|
||||||
|
return normalized || null;
|
||||||
|
}
|
||||||
function sumInventoryRowAmount(rows) {
|
function sumInventoryRowAmount(rows) {
|
||||||
return rows.reduce((sum, row) => sum + (typeof row.amount === "number" && Number.isFinite(row.amount) ? row.amount : 0), 0);
|
return rows.reduce((sum, row) => sum + (typeof row.amount === "number" && Number.isFinite(row.amount) ? row.amount : 0), 0);
|
||||||
}
|
}
|
||||||
|
|
@ -380,29 +390,47 @@ function composeInventoryReply(intent, rows, options, deps) {
|
||||||
}
|
}
|
||||||
if (intent === "inventory_aging_by_purchase_date") {
|
if (intent === "inventory_aging_by_purchase_date") {
|
||||||
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
||||||
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
const agingItems = deps.buildInventoryAgingByItemAggregate(rows, asOfDate);
|
||||||
|
const agingItemTokens = new Set(agingItems
|
||||||
|
.map((item) => normalizeInventoryReplyEntityToken(item.item))
|
||||||
|
.filter((item) => Boolean(item)));
|
||||||
|
const purchaseRows = rows.filter((row) => {
|
||||||
|
if (!deps.isInventoryPurchaseMovement(row)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (agingItemTokens.size === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const item = deps.summarizeInventoryTraceRows([row]).item;
|
||||||
|
const itemToken = normalizeInventoryReplyEntityToken(item);
|
||||||
|
return Boolean(itemToken && agingItemTokens.has(itemToken));
|
||||||
|
});
|
||||||
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
|
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
|
||||||
const agingItems = deps.buildInventoryAgingByItemAggregate(purchaseRows, asOfDate);
|
|
||||||
const oldestPurchaseDate = agingItems[0]?.firstPurchasePeriod ?? summary.firstPeriod;
|
const oldestPurchaseDate = agingItems[0]?.firstPurchasePeriod ?? summary.firstPeriod;
|
||||||
const oldestPurchaseAgeDays = agingItems[0]?.ageDays ?? null;
|
const oldestPurchaseAgeDays = agingItems[0]?.ageDays ?? null;
|
||||||
|
const organizationLabel = agingItems.find((item) => item.organization)?.organization ?? null;
|
||||||
const oldestAnswerPreview = agingItems
|
const oldestAnswerPreview = agingItems
|
||||||
.slice(0, 3)
|
.slice(0, 3)
|
||||||
.map((item) => `${item.item} (${deps.inventoryTraceDateLabel(item.firstPurchasePeriod)})`)
|
.map((item) => `${item.item} (${deps.inventoryTraceDateLabel(item.firstPurchasePeriod)})`)
|
||||||
.join("; ");
|
.join("; ");
|
||||||
const directAnswerLine = agingItems.length > 0
|
const directAnswerLine = agingItems.length > 0
|
||||||
? `К самым старым закупкам в текущем подтвержденном срезе относятся позиции с самой ранней первой закупкой: ${oldestAnswerPreview}.`
|
? `Среди фактических положительных остатков есть давно закупавшиеся позиции: ${oldestAnswerPreview}.`
|
||||||
: "По доступному закупочному следу позиции со старыми закупками не материализованы.";
|
: "В фактическом положительном остатке не найдено позиций с подтвержденным старым закупочным следом.";
|
||||||
const lines = [directAnswerLine];
|
const lines = [directAnswerLine];
|
||||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Сводка:", [
|
const summaryLines = [
|
||||||
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||||
`Самая ранняя первая закупка среди позиций: ${deps.inventoryTraceDateLabel(oldestPurchaseDate)}.`,
|
`Самая ранняя первая закупка среди позиций: ${deps.inventoryTraceDateLabel(oldestPurchaseDate)}.`,
|
||||||
`Самая поздняя найденная закупка: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
`Самая поздняя найденная закупка: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
||||||
`Позиций в выборке: ${deps.formatNumberWithDots(agingItems.length)}.`,
|
`Позиций в выборке: ${deps.formatNumberWithDots(agingItems.length)}.`,
|
||||||
`Закупочных документов: ${deps.formatNumberWithDots(summary.documents.length)}.`,
|
`Закупочных документов: ${deps.formatNumberWithDots(summary.documents.length)}.`,
|
||||||
`Закупочных операций: ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
`Закупочных операций: ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
||||||
]);
|
];
|
||||||
|
if (organizationLabel) {
|
||||||
|
summaryLines.splice(1, 0, `Организация: ${organizationLabel}.`);
|
||||||
|
}
|
||||||
|
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Сводка:", summaryLines);
|
||||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Ограничения:", [
|
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Ограничения:", [
|
||||||
"Без партионного учета этот ответ показывает возраст закупочного следа по товарной позиции, а не возраст конкретного лота."
|
"Берем только позиции с положительным остатком на дату среза; без партионного учета это возраст закупочного следа по номенклатуре, а не доказанный возраст конкретной партии."
|
||||||
]);
|
]);
|
||||||
if (oldestPurchaseAgeDays !== null) {
|
if (oldestPurchaseAgeDays !== null) {
|
||||||
lines.push(`- Между самой ранней первой закупкой и датой среза прошло ${deps.formatNumberWithDots(oldestPurchaseAgeDays)} дн.`);
|
lines.push(`- Между самой ранней первой закупкой и датой среза прошло ${deps.formatNumberWithDots(oldestPurchaseAgeDays)} дн.`);
|
||||||
|
|
@ -411,11 +439,11 @@ function composeInventoryReply(intent, rows, options, deps) {
|
||||||
lines.push(`- Поставщики, встречающиеся в наблюдаемом закупочном следе: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
lines.push(`- Поставщики, встречающиеся в наблюдаемом закупочном следе: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||||
}
|
}
|
||||||
if (agingItems.length > 0) {
|
if (agingItems.length > 0) {
|
||||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Позиции от самых старых закупок:", deps.formatInventoryAgingRows(agingItems, asOfDate, 12));
|
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Позиции от самых старых закупок:", deps.formatInventoryAgingRows(agingItems, asOfDate, 5));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Позиции от самых старых закупок:", [
|
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Позиции от самых старых закупок:", [
|
||||||
"- В доступных данных не найдено закупочных движений для выбранного среза."
|
"- В доступных данных не найдено закупочных движений по позициям, которые есть в положительном остатке на дату среза."
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(agingItems.length > 0 ? "strong" : "medium", agingItems.length > 0));
|
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(agingItems.length > 0 ? "strong" : "medium", agingItems.length > 0));
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,17 @@ function cleanComparisonScopeCompanyLine(line, organization) {
|
||||||
}
|
}
|
||||||
return clean.trim();
|
return clean.trim();
|
||||||
}
|
}
|
||||||
|
function hasComparisonScopeProofCue(value) {
|
||||||
|
const text = toNullableString(value)?.toLocaleLowerCase("ru-RU").replace(/\s+/gu, " ").trim();
|
||||||
|
if (!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:\bсравни\b|собери\s+коротк\p{L}*\s+итог|коротк\p{L}*\s+сравн|что\s+.*подтверд\p{L}*.*(?:компан|контрагент|свк)|что\s+.*отдельно\s+по\s+(?:группа\s+свк|выбранн\p{L}*\s+контрагент)|какие\s+выводы\s+можно\s+делать\s+и\s+какие\s+нельзя)/iu.test(text);
|
||||||
|
}
|
||||||
function buildComparisonScopeProofReply(input) {
|
function buildComparisonScopeProofReply(input) {
|
||||||
|
if (!hasComparisonScopeProofCue(input.userMessage)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const entryPoint = toRecordObject(input.debug.assistant_mcp_discovery_entry_point_v1);
|
const entryPoint = toRecordObject(input.debug.assistant_mcp_discovery_entry_point_v1);
|
||||||
const turnInput = toRecordObject(entryPoint?.turn_input);
|
const turnInput = toRecordObject(entryPoint?.turn_input);
|
||||||
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
|
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ function hasCompactCashflowFollowupSignal(text) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return /(?:\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0440\u0435\u0437\p{L}*|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433|\u043f\u0440\u0438\u0448\p{L}*[\s\S]{0,80}\u0443\u0448\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u043f\u043e\u043b\u0443\u0447\p{L}*[\s\S]{0,80}\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*[\s\S]{0,80}\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b)/iu.test(value);
|
return /(?:\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0440\u0435\u0437\p{L}*|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433|\u043f\u0440\u0438\u0448\p{L}*[\s\S]{0,80}\u0443\u0448\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u043f\u043e\u043b\u0443\u0447\p{L}*[\s\S]{0,80}\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*[\s\S]{0,80}\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b|кто\s+больше\s+всего\s+зан[её]с|кому\s+больше\s+всего\s+ушл\p{L}*|банк\s+не\s+называй\s+обычн\p{L}*\s+клиент\p{L}*|главн\p{L}*\s+клиент|главн\p{L}*\s+поставщик)/iu.test(value);
|
||||||
}
|
}
|
||||||
function dateScopeToFilterWindow(dateScope) {
|
function dateScopeToFilterWindow(dateScope) {
|
||||||
if (!dateScope) {
|
if (!dateScope) {
|
||||||
|
|
@ -79,6 +79,35 @@ function dateScopeToFilterWindow(dateScope) {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
function filterWindowFromExtractedFilters(filters) {
|
||||||
|
if (!filters) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const asOfDate = typeof filters.as_of_date === "string" ? filters.as_of_date.trim() : "";
|
||||||
|
if (/^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(asOfDate)) {
|
||||||
|
return { as_of_date: asOfDate };
|
||||||
|
}
|
||||||
|
const periodFrom = typeof filters.period_from === "string" ? filters.period_from.trim() : "";
|
||||||
|
const periodTo = typeof filters.period_to === "string" ? filters.period_to.trim() : "";
|
||||||
|
if (/^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(periodFrom) && /^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(periodTo)) {
|
||||||
|
return {
|
||||||
|
period_from: periodFrom,
|
||||||
|
period_to: periodTo
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function hasBoundedFilterWindow(filters) {
|
||||||
|
if (!filters) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (typeof filters.as_of_date === "string" && /^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(filters.as_of_date.trim())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const periodFrom = typeof filters.period_from === "string" ? filters.period_from.trim() : "";
|
||||||
|
const periodTo = typeof filters.period_to === "string" ? filters.period_to.trim() : "";
|
||||||
|
return /^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(periodFrom) && /^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(periodTo);
|
||||||
|
}
|
||||||
function looksLikeReliableOrganizationScope(value) {
|
function looksLikeReliableOrganizationScope(value) {
|
||||||
const text = compactLower(value);
|
const text = compactLower(value);
|
||||||
if (!text) {
|
if (!text) {
|
||||||
|
|
@ -103,12 +132,15 @@ function inferBusinessOverviewDiscoveryContextFromSessionItems(sessionItems, toN
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const turnMeaningRef = toRecordObject(turnInput?.turn_meaning_ref);
|
const turnMeaningRef = toRecordObject(turnInput?.turn_meaning_ref);
|
||||||
const filterWindow = dateScopeToFilterWindow(toNonEmptyString(turnMeaningRef?.explicit_date_scope));
|
const debugFilters = toRecordObject(debug?.extracted_filters);
|
||||||
|
const filterWindow = dateScopeToFilterWindow(toNonEmptyString(turnMeaningRef?.explicit_date_scope)) ??
|
||||||
|
filterWindowFromExtractedFilters(debugFilters);
|
||||||
if (!filterWindow) {
|
if (!filterWindow) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const previousFilters = { ...filterWindow };
|
const previousFilters = { ...filterWindow };
|
||||||
const organization = toNonEmptyString(turnMeaningRef?.explicit_organization_scope);
|
const organization = toNonEmptyString(turnMeaningRef?.explicit_organization_scope) ??
|
||||||
|
toNonEmptyString(debugFilters?.organization);
|
||||||
if (looksLikeReliableOrganizationScope(organization)) {
|
if (looksLikeReliableOrganizationScope(organization)) {
|
||||||
previousFilters.organization = organization;
|
previousFilters.organization = organization;
|
||||||
}
|
}
|
||||||
|
|
@ -129,9 +161,7 @@ function mergeBusinessOverviewDateContextForCompactCashflow(input) {
|
||||||
return input.followupContext;
|
return input.followupContext;
|
||||||
}
|
}
|
||||||
const existingFilters = toRecordObject(input.followupContext?.previous_filters);
|
const existingFilters = toRecordObject(input.followupContext?.previous_filters);
|
||||||
if (input.toNonEmptyString(existingFilters?.period_from) ||
|
if (hasBoundedFilterWindow(existingFilters)) {
|
||||||
input.toNonEmptyString(existingFilters?.period_to) ||
|
|
||||||
input.toNonEmptyString(existingFilters?.as_of_date)) {
|
|
||||||
return input.followupContext;
|
return input.followupContext;
|
||||||
}
|
}
|
||||||
const inferred = inferBusinessOverviewDiscoveryContextFromSessionItems(input.sessionItems, input.toNonEmptyString);
|
const inferred = inferBusinessOverviewDiscoveryContextFromSessionItems(input.sessionItems, input.toNonEmptyString);
|
||||||
|
|
@ -142,12 +172,12 @@ function mergeBusinessOverviewDateContextForCompactCashflow(input) {
|
||||||
...inferred,
|
...inferred,
|
||||||
...(input.followupContext ?? {}),
|
...(input.followupContext ?? {}),
|
||||||
previous_filters: {
|
previous_filters: {
|
||||||
...(toRecordObject(inferred.previous_filters) ?? {}),
|
...(toRecordObject(input.followupContext?.previous_filters) ?? {}),
|
||||||
...(toRecordObject(input.followupContext?.previous_filters) ?? {})
|
...(toRecordObject(inferred.previous_filters) ?? {})
|
||||||
},
|
},
|
||||||
root_filters: {
|
root_filters: {
|
||||||
...(toRecordObject(inferred.root_filters) ?? {}),
|
...(toRecordObject(input.followupContext?.root_filters) ?? {}),
|
||||||
...(toRecordObject(input.followupContext?.root_filters) ?? {})
|
...(toRecordObject(inferred.root_filters) ?? {})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -172,6 +202,13 @@ function sameEntityHint(expected, actual) {
|
||||||
}
|
}
|
||||||
return left === right || left.includes(right) || right.includes(left);
|
return left === right || left.includes(right) || right.includes(left);
|
||||||
}
|
}
|
||||||
|
function hasComparisonScopeProofCue(text) {
|
||||||
|
const value = compactLower(text);
|
||||||
|
if (!value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:\bсравни\b|собери\s+коротк\p{L}*\s+итог|коротк\p{L}*\s+сравн|что\s+.*подтверд\p{L}*.*(?:компан|контрагент|свк)|что\s+.*отдельно\s+по\s+(?:группа\s+свк|выбранн\p{L}*\s+контрагент)|какие\s+выводы\s+можно\s+делать\s+и\s+какие\s+нельзя)/iu.test(value);
|
||||||
|
}
|
||||||
function isBusinessOverviewDiscoveryFollowup(followupContext, toNonEmptyString) {
|
function isBusinessOverviewDiscoveryFollowup(followupContext, toNonEmptyString) {
|
||||||
if (!followupContext) {
|
if (!followupContext) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -344,6 +381,9 @@ function mergeBusinessOverviewProofBundlesFromNavigationState(input) {
|
||||||
const entities = toRecordObject(input.predecomposeContract?.entities);
|
const entities = toRecordObject(input.predecomposeContract?.entities);
|
||||||
const hasCurrentOrganizationSelection = Boolean(input.toNonEmptyString(entities?.organization));
|
const hasCurrentOrganizationSelection = Boolean(input.toNonEmptyString(entities?.organization));
|
||||||
const businessOverviewFollowup = isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString);
|
const businessOverviewFollowup = isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString);
|
||||||
|
if (!hasComparisonScopeProofCue(input.userMessage)) {
|
||||||
|
return input.followupContext;
|
||||||
|
}
|
||||||
if (!businessOverviewFollowup && !hasCurrentOrganizationSelection) {
|
if (!businessOverviewFollowup && !hasCurrentOrganizationSelection) {
|
||||||
return input.followupContext;
|
return input.followupContext;
|
||||||
}
|
}
|
||||||
|
|
@ -376,6 +416,9 @@ function mergeBusinessOverviewProofBundlesFromNavigationState(input) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function mergeBusinessOverviewProofBundlesFromSessionItems(input) {
|
function mergeBusinessOverviewProofBundlesFromSessionItems(input) {
|
||||||
|
if (!hasComparisonScopeProofCue(input.userMessage)) {
|
||||||
|
return input.followupContext;
|
||||||
|
}
|
||||||
if (!isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString)) {
|
if (!isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString)) {
|
||||||
return input.followupContext;
|
return input.followupContext;
|
||||||
}
|
}
|
||||||
|
|
@ -576,6 +619,7 @@ async function buildAssistantAddressOrchestrationRuntime(input) {
|
||||||
});
|
});
|
||||||
const discoveryFollowupContextWithProofBundles = mergeBusinessOverviewProofBundlesFromSessionItems({
|
const discoveryFollowupContextWithProofBundles = mergeBusinessOverviewProofBundlesFromSessionItems({
|
||||||
followupContext: discoveryFollowupContext,
|
followupContext: discoveryFollowupContext,
|
||||||
|
userMessage: input.userMessage,
|
||||||
sessionItems: input.sessionItems,
|
sessionItems: input.sessionItems,
|
||||||
toNonEmptyString: input.toNonEmptyString
|
toNonEmptyString: input.toNonEmptyString
|
||||||
});
|
});
|
||||||
|
|
@ -583,6 +627,7 @@ async function buildAssistantAddressOrchestrationRuntime(input) {
|
||||||
followupContext: discoveryFollowupContextWithProofBundles,
|
followupContext: discoveryFollowupContextWithProofBundles,
|
||||||
sessionAddressNavigationState: input.sessionAddressNavigationState,
|
sessionAddressNavigationState: input.sessionAddressNavigationState,
|
||||||
predecomposeContract,
|
predecomposeContract,
|
||||||
|
userMessage: input.userMessage,
|
||||||
toNonEmptyString: input.toNonEmptyString
|
toNonEmptyString: input.toNonEmptyString
|
||||||
});
|
});
|
||||||
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(input.userMessage, addressInputMessage, carryover, addressPreDecompose);
|
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(input.userMessage, addressInputMessage, carryover, addressPreDecompose);
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,15 @@ function normalizeAddressRuntimeMetaForDeep(value) {
|
||||||
mcpDiscoveryRuntimeEntryPoint: source.mcpDiscoveryRuntimeEntryPoint ?? null
|
mcpDiscoveryRuntimeEntryPoint: source.mcpDiscoveryRuntimeEntryPoint ?? null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function stripRoutineShortAnswerPrefix(value) {
|
||||||
|
return String(value ?? "")
|
||||||
|
.split(/\r?\n/g)
|
||||||
|
.map((line) => line
|
||||||
|
.replace(/^((?:\s|\uFEFF)*(?:[-*]\s*)?)(?:\u041a\u043e\u0440\u043e\u0442\u043a\u043e:\s*)+/u, "$1")
|
||||||
|
.replace(/(:\s*)\u041a\u043e\u0440\u043e\u0442\u043a\u043e:\s*/u, "$1"))
|
||||||
|
.join("\n")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
function runAssistantDeepTurnResponseRuntime(input) {
|
function runAssistantDeepTurnResponseRuntime(input) {
|
||||||
const runPackagingRuntimeSafe = input.runPackagingRuntime ?? assistantDeepTurnPackagingRuntimeAdapter_1.runAssistantDeepTurnPackagingRuntime;
|
const runPackagingRuntimeSafe = input.runPackagingRuntime ?? assistantDeepTurnPackagingRuntimeAdapter_1.runAssistantDeepTurnPackagingRuntime;
|
||||||
const runFinalizeDeepTurnSafe = input.runFinalizeDeepTurn ?? assistantDeepTurnFinalizeRuntimeAdapter_1.finalizeAssistantDeepTurn;
|
const runFinalizeDeepTurnSafe = input.runFinalizeDeepTurn ?? assistantDeepTurnFinalizeRuntimeAdapter_1.finalizeAssistantDeepTurn;
|
||||||
|
|
@ -125,9 +134,9 @@ function runAssistantDeepTurnResponseRuntime(input) {
|
||||||
currentReplySource: "deep_analysis",
|
currentReplySource: "deep_analysis",
|
||||||
addressRuntimeMeta: addressRuntimeMetaForDeep
|
addressRuntimeMeta: addressRuntimeMetaForDeep
|
||||||
});
|
});
|
||||||
const assistantReply = mcpDiscoveryResponsePolicy.applied
|
const assistantReply = stripRoutineShortAnswerPrefix(mcpDiscoveryResponsePolicy.applied
|
||||||
? mcpDiscoveryResponsePolicy.reply_text
|
? input.sanitizeReply(mcpDiscoveryResponsePolicy.reply_text, packagingRuntime.safeAssistantReply)
|
||||||
: packagingRuntime.safeAssistantReply;
|
: packagingRuntime.safeAssistantReply);
|
||||||
const replyType = mcpDiscoveryResponsePolicy.applied ? "partial_coverage" : input.composition.reply_type;
|
const replyType = mcpDiscoveryResponsePolicy.applied ? "partial_coverage" : input.composition.reply_type;
|
||||||
const debug = {
|
const debug = {
|
||||||
...packagingRuntime.debug,
|
...packagingRuntime.debug,
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,15 @@ function hasInternalMechanics(value) {
|
||||||
text.includes("catalog_") ||
|
text.includes("catalog_") ||
|
||||||
text.includes("select "));
|
text.includes("select "));
|
||||||
}
|
}
|
||||||
|
function stripRoutineShortAnswerPrefix(value) {
|
||||||
|
return String(value ?? "")
|
||||||
|
.split(/\r?\n/g)
|
||||||
|
.map((line) => line
|
||||||
|
.replace(/^((?:\s|\uFEFF)*(?:[-*]\s*)?)(?:\u041a\u043e\u0440\u043e\u0442\u043a\u043e:\s*)+/u, "$1")
|
||||||
|
.replace(/(:\s*)\u041a\u043e\u0440\u043e\u0442\u043a\u043e:\s*/u, "$1"))
|
||||||
|
.join("\n")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
function isMcpDiscoveryEntryPointContract(value) {
|
function isMcpDiscoveryEntryPointContract(value) {
|
||||||
const record = toRecordObject(value);
|
const record = toRecordObject(value);
|
||||||
return (record?.schema_version === "assistant_mcp_discovery_runtime_entry_point_v1" &&
|
return (record?.schema_version === "assistant_mcp_discovery_runtime_entry_point_v1" &&
|
||||||
|
|
@ -797,12 +806,13 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_applied");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_applied");
|
||||||
|
const replyText = stripRoutineShortAnswerPrefix(String(candidate.reply_text));
|
||||||
return {
|
return {
|
||||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION,
|
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION,
|
||||||
policy_owner: "assistantMcpDiscoveryResponsePolicy",
|
policy_owner: "assistantMcpDiscoveryResponsePolicy",
|
||||||
decision: "apply_candidate",
|
decision: "apply_candidate",
|
||||||
applied: true,
|
applied: true,
|
||||||
reply_text: String(candidate.reply_text),
|
reply_text: replyText || String(candidate.reply_text),
|
||||||
reply_source: "mcp_discovery_response_candidate_guarded",
|
reply_source: "mcp_discovery_response_candidate_guarded",
|
||||||
candidate,
|
candidate,
|
||||||
reason_codes: reasonCodes
|
reason_codes: reasonCodes
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,7 @@ function isGarbageSemanticAnchorCandidate(value) {
|
||||||
}
|
}
|
||||||
if (/^(?:по\s+)?(?:этим|этими)\s+данн\p{L}*$/iu.test(text) ||
|
if (/^(?:по\s+)?(?:этим|этими)\s+данн\p{L}*$/iu.test(text) ||
|
||||||
/^(?:и\s+)?кто\s+(?:главн\p{L}*|основн\p{L}*|крупн\p{L}*)\s+(?:клиент|покупател|поставщик|контрагент)(?:\s+в)?$/iu.test(text) ||
|
/^(?:и\s+)?кто\s+(?:главн\p{L}*|основн\p{L}*|крупн\p{L}*)\s+(?:клиент|покупател|поставщик|контрагент)(?:\s+в)?$/iu.test(text) ||
|
||||||
|
/^(?:и\s+)?(?:главн\p{L}*|основн\p{L}*|крупн\p{L}*)\s+(?:клиент|покупател|поставщик|контрагент)(?:\s+в)?$/iu.test(text) ||
|
||||||
/^(?:или\s+)?(?:обычн\p{L}*\s+)?(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент)(?:\s+или\s+(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент))?$/iu.test(text) ||
|
/^(?:или\s+)?(?:обычн\p{L}*\s+)?(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент)(?:\s+или\s+(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент))?$/iu.test(text) ||
|
||||||
/^(?:что|чего)\s+(?:подтвержден\p{L}*|не\s+хватает)/iu.test(text) ||
|
/^(?:что|чего)\s+(?:подтвержден\p{L}*|не\s+хватает)/iu.test(text) ||
|
||||||
/^(?:можно\s+ли|если\s+нет|дай\s+proxy|дай\s+прокси)/iu.test(text)) {
|
/^(?:можно\s+ли|если\s+нет|дай\s+proxy|дай\s+прокси)/iu.test(text)) {
|
||||||
|
|
@ -1823,6 +1824,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
? null
|
? null
|
||||||
: explicitCurrentCounterpartyOverridesFollowupEntity
|
: explicitCurrentCounterpartyOverridesFollowupEntity
|
||||||
? null
|
? null
|
||||||
|
: businessOverviewSuppressesFollowupCounterparty
|
||||||
|
? rawMetadataScopeHint
|
||||||
: rawMetadataScopeHint ??
|
: rawMetadataScopeHint ??
|
||||||
followupSeed.metadataScopeHint ??
|
followupSeed.metadataScopeHint ??
|
||||||
followupSeed.discoveryEntity ??
|
followupSeed.discoveryEntity ??
|
||||||
|
|
@ -2013,7 +2016,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||||
/^\d{4}$/.test(rawDateScope) &&
|
/^\d{4}$/.test(rawDateScope) &&
|
||||||
normalizedPredecomposeDateScope &&
|
normalizedPredecomposeDateScope &&
|
||||||
normalizedPredecomposeDateScope.startsWith(`${rawDateScope}-`));
|
normalizedPredecomposeDateScope.startsWith(`${rawDateScope}-`));
|
||||||
const followupAllTimeScopeApplied = normalizedFollowupDateScope === "all_time_scope";
|
const followupAllTimeScopeApplied = normalizedFollowupDateScope === "all_time_scope" && !currentTurnCarriesExplicitPeriod;
|
||||||
const explicitDateScope = rawAllTimeScopeSignal || followupAllTimeScopeApplied
|
const explicitDateScope = rawAllTimeScopeSignal || followupAllTimeScopeApplied
|
||||||
? null
|
? null
|
||||||
: normalizedAssistantTurnMeaningDateScope ??
|
: normalizedAssistantTurnMeaningDateScope ??
|
||||||
|
|
|
||||||
|
|
@ -2025,7 +2025,11 @@ function repairAddressMojibake(value) {
|
||||||
}
|
}
|
||||||
function sanitizeOutgoingAssistantText(value, fallback = "Не смог сформировать читаемый ответ. Уточните запрос.") {
|
function sanitizeOutgoingAssistantText(value, fallback = "Не смог сформировать читаемый ответ. Уточните запрос.") {
|
||||||
const repaired = repairAddressMojibake(String(value ?? ""));
|
const repaired = repairAddressMojibake(String(value ?? ""));
|
||||||
const sanitized = String((0, answerComposer_1.sanitizeAssistantReplyForUserFacing)(repaired) ?? "").trim();
|
const sanitized = String((0, answerComposer_1.sanitizeAssistantReplyForUserFacing)(repaired) ?? "")
|
||||||
|
.split(/\r?\n/g)
|
||||||
|
.map((line) => line.replace(/^Коротко:\s*/u, "").replace(/(:\s*)Коротко:\s*/u, "$1"))
|
||||||
|
.join("\n")
|
||||||
|
.trim();
|
||||||
if (sanitized) {
|
if (sanitized) {
|
||||||
return sanitized;
|
return sanitized;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -182,7 +182,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
}
|
}
|
||||||
return `${match[3]}-${match[2]}-${match[1]}`;
|
return `${match[3]}-${match[2]}-${match[1]}`;
|
||||||
}
|
}
|
||||||
function computeMonthWindowFromIso(isoDate) {
|
function computeMonthWindowFromIso(isoDate, bridgeFacts = {}) {
|
||||||
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -199,10 +199,15 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
purchase_date: isoDate,
|
purchase_date: isoDate,
|
||||||
period_from: `${year}-${mm}-01`,
|
period_from: `${year}-${mm}-01`,
|
||||||
period_to: `${year}-${mm}-${dd}`,
|
period_to: `${year}-${mm}-${dd}`,
|
||||||
as_of_date: `${year}-${mm}-${dd}`
|
as_of_date: `${year}-${mm}-${dd}`,
|
||||||
|
purchase_date_bridge_selected: isoDate,
|
||||||
|
...bridgeFacts
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function extractEarliestDmyDateFromEntityRefs(entityRefs) {
|
function uniqueSortedIsoDates(dates) {
|
||||||
|
return Array.from(new Set(dates.filter(Boolean))).sort();
|
||||||
|
}
|
||||||
|
function extractDmyDatesFromEntityRefs(entityRefs) {
|
||||||
const dates = [];
|
const dates = [];
|
||||||
for (const entityRef of Array.isArray(entityRefs) ? entityRefs : []) {
|
for (const entityRef of Array.isArray(entityRefs) ? entityRefs : []) {
|
||||||
const value = deps.toNonEmptyString(entityRef?.value);
|
const value = deps.toNonEmptyString(entityRef?.value);
|
||||||
|
|
@ -220,21 +225,41 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (dates.length === 0) {
|
return uniqueSortedIsoDates(dates);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
return dates.sort()[0] ?? null;
|
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;
|
||||||
}
|
}
|
||||||
function extractPurchaseDateBridgeWindow(previousAddressItem, addressNavigationState) {
|
|
||||||
const replyText = deps.toNonEmptyString(previousAddressItem?.text);
|
const replyText = deps.toNonEmptyString(previousAddressItem?.text);
|
||||||
if (replyText) {
|
if (replyText && (!firstIsoDate || !lastIsoDate)) {
|
||||||
const repairedText = deps.repairAddressMojibake(replyText);
|
const repairedText = deps.repairAddressMojibake(replyText);
|
||||||
const explicitFirstDateMatch = repairedText.match(/первая\s+найденная\s+дата\s+закупки:\s*(\d{2}\.\d{2}\.\d{4})/iu);
|
const explicitFirstDateMatch = repairedText.match(/первая\s+(?:найденная|подтвержденная)\s+дата\s+закупки:\s*(\d{2}\.\d{2}\.\d{4})/iu);
|
||||||
const explicitFirstDateIso = explicitFirstDateMatch ? parseDmyDateToIso(explicitFirstDateMatch[1]) : null;
|
const explicitLastDateMatch = repairedText.match(/последняя\s+(?:найденная|подтвержденная)\s+дата\s+закупки:\s*(\d{2}\.\d{2}\.\d{4})/iu);
|
||||||
if (explicitFirstDateIso) {
|
firstIsoDate = explicitFirstDateMatch ? parseDmyDateToIso(explicitFirstDateMatch[1]) : null;
|
||||||
return computeMonthWindowFromIso(explicitFirstDateIso);
|
lastIsoDate = explicitLastDateMatch ? parseDmyDateToIso(explicitLastDateMatch[1]) : null;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (!firstIsoDate || !lastIsoDate) {
|
||||||
const navigationSessionState = (0, assistantContinuityPolicy_1.resolveNavigationSessionContextState)(addressNavigationState, deps.toNonEmptyString, deps.normalizeOrganizationScopeValue);
|
const navigationSessionState = (0, assistantContinuityPolicy_1.resolveNavigationSessionContextState)(addressNavigationState, deps.toNonEmptyString, deps.normalizeOrganizationScopeValue);
|
||||||
const focusObject = navigationSessionState.focusObject;
|
const focusObject = navigationSessionState.focusObject;
|
||||||
const preferredResultSetId = deps.toNonEmptyString(focusObject?.provenanceResultSetId) ?? navigationSessionState.activeResultSetId;
|
const preferredResultSetId = deps.toNonEmptyString(focusObject?.provenanceResultSetId) ?? navigationSessionState.activeResultSetId;
|
||||||
|
|
@ -244,8 +269,36 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
: null) ??
|
: null) ??
|
||||||
resultSets.find((item) => deps.toNonEmptyString(item?.intent) === "inventory_purchase_provenance_for_item") ??
|
resultSets.find((item) => deps.toNonEmptyString(item?.intent) === "inventory_purchase_provenance_for_item") ??
|
||||||
null;
|
null;
|
||||||
const earliestIsoDate = extractEarliestDmyDateFromEntityRefs(preferredResultSet?.entity_refs);
|
const datesFromResultSet = extractDmyDatesFromEntityRefs(preferredResultSet?.entity_refs);
|
||||||
return earliestIsoDate ? computeMonthWindowFromIso(earliestIsoDate) : null;
|
firstIsoDate = firstIsoDate ?? datesFromResultSet[0] ?? null;
|
||||||
|
lastIsoDate = lastIsoDate ?? datesFromResultSet[datesFromResultSet.length - 1] ?? null;
|
||||||
|
}
|
||||||
|
if (!firstIsoDate && !lastIsoDate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const normalizedFirst = firstIsoDate ?? lastIsoDate;
|
||||||
|
const normalizedLast = lastIsoDate ?? firstIsoDate;
|
||||||
|
const basis = detectPurchaseDateBridgeBasis(userMessage, alternateMessage);
|
||||||
|
const selectedIsoDate = basis === "last_confirmed_purchase" ? normalizedLast : normalizedFirst;
|
||||||
|
return {
|
||||||
|
selectedIsoDate,
|
||||||
|
firstIsoDate: normalizedFirst,
|
||||||
|
lastIsoDate: normalizedLast,
|
||||||
|
basis,
|
||||||
|
hasMultiplePurchaseDates: Boolean(normalizedFirst && normalizedLast && normalizedFirst !== normalizedLast)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function extractPurchaseDateBridgeWindow(previousAddressItem, addressNavigationState, userMessage, alternateMessage) {
|
||||||
|
const facts = extractPurchaseDateBridgeFacts(previousAddressItem, addressNavigationState, userMessage, alternateMessage);
|
||||||
|
if (!facts?.selectedIsoDate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return computeMonthWindowFromIso(facts.selectedIsoDate, {
|
||||||
|
purchase_date_bridge_first: facts.firstIsoDate,
|
||||||
|
purchase_date_bridge_last: facts.lastIsoDate,
|
||||||
|
purchase_date_bridge_basis: facts.basis,
|
||||||
|
purchase_date_bridge_has_multiple: facts.hasMultiplePurchaseDates
|
||||||
|
});
|
||||||
}
|
}
|
||||||
function isUsableFollowupSourceDebug(debug) {
|
function isUsableFollowupSourceDebug(debug) {
|
||||||
if (!debug || typeof debug !== "object") {
|
if (!debug || typeof debug !== "object") {
|
||||||
|
|
@ -460,10 +513,12 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
function hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasInventoryItemFocusHint) {
|
function hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasInventoryItemFocusHint, sourceHasPurchaseDateBridgeHint = false) {
|
||||||
|
const vatPurchaseBridgeContinuation = sourceIntentHint === "vat_liability_confirmed_for_tax_period" && sourceHasPurchaseDateBridgeHint;
|
||||||
if (sourceIntentHint !== "inventory_purchase_provenance_for_item" &&
|
if (sourceIntentHint !== "inventory_purchase_provenance_for_item" &&
|
||||||
!hasInventoryItemFocusHint &&
|
!hasInventoryItemFocusHint &&
|
||||||
!deps.isInventorySelectedObjectIntent(sourceIntentHint)) {
|
!deps.isInventorySelectedObjectIntent(sourceIntentHint) &&
|
||||||
|
!vatPurchaseBridgeContinuation) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const samples = [userMessage, alternateMessage]
|
const samples = [userMessage, alternateMessage]
|
||||||
|
|
@ -472,8 +527,11 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
if (samples.length === 0) {
|
if (samples.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return samples.some((sample) => /(?:ндс|vat)/iu.test(sample) &&
|
return samples.some((sample) => ((/(?:ндс|vat)/iu.test(sample) &&
|
||||||
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/iu.test(sample));
|
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/iu.test(sample)) ||
|
||||||
|
((vatPurchaseBridgeContinuation || sourceIntentHint === "vat_liability_confirmed_for_tax_period") &&
|
||||||
|
/(?:перв|последн|earliest|oldest|latest|last)/iu.test(sample) &&
|
||||||
|
/(?:покупк|закупк|purchase)/iu.test(sample))));
|
||||||
}
|
}
|
||||||
function hasInventoryRootRestatementLikeSignal(userMessage, sourceIntentHint, hasInventoryRootFrame) {
|
function hasInventoryRootRestatementLikeSignal(userMessage, sourceIntentHint, hasInventoryRootFrame) {
|
||||||
if (!hasInventoryRootFrame) {
|
if (!hasInventoryRootFrame) {
|
||||||
|
|
@ -701,12 +759,17 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
const hasBusinessOverviewCarryoverSourceHint = sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
|
const hasBusinessOverviewCarryoverSourceHint = sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
|
||||||
const navigationSessionState = earlyNavigationSessionState;
|
const navigationSessionState = earlyNavigationSessionState;
|
||||||
const navigationFocusObjectHint = navigationSessionState.focusObject;
|
const navigationFocusObjectHint = navigationSessionState.focusObject;
|
||||||
|
const sourceFiltersHint = (0, assistantContinuityPolicy_1.readAddressDebugFilters)(carryoverSourceDebug);
|
||||||
|
const sourceHasPurchaseDateBridgeHint = Boolean(deps.toNonEmptyString(sourceFiltersHint?.purchase_date_bridge_selected));
|
||||||
const hasNavigationInventoryItemFocusHint = Boolean(deps.toNonEmptyString(navigationFocusObjectHint?.label) &&
|
const hasNavigationInventoryItemFocusHint = Boolean(deps.toNonEmptyString(navigationFocusObjectHint?.label) &&
|
||||||
deps.toNonEmptyString(navigationFocusObjectHint?.objectType) === "item" &&
|
deps.toNonEmptyString(navigationFocusObjectHint?.objectType) === "item" &&
|
||||||
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||||
|
sourceIntentHint === "vat_liability_confirmed_for_tax_period" ||
|
||||||
|
sourceIntentHint === "vat_payable_forecast" ||
|
||||||
|
sourceIntentHint === "vat_payable_confirmed_as_of_date" ||
|
||||||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
deps.isInventorySelectedObjectIntent(sourceIntentHint)));
|
deps.isInventorySelectedObjectIntent(sourceIntentHint)));
|
||||||
const inventoryPurchaseDateVatBridge = hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasNavigationInventoryItemFocusHint);
|
const inventoryPurchaseDateVatBridge = hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasNavigationInventoryItemFocusHint, sourceHasPurchaseDateBridgeHint);
|
||||||
const inventoryMarginRankingFollowup = hasInventoryMarginRankingFollowupSignal(userMessage, alternateMessage, sourceIntentHint);
|
const inventoryMarginRankingFollowup = hasInventoryMarginRankingFollowupSignal(userMessage, alternateMessage, sourceIntentHint);
|
||||||
let inventoryShortFollowupPrimary = (deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
let inventoryShortFollowupPrimary = (deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
||||||
deps.hasShortInventoryObjectFollowupSignal(userMessage);
|
deps.hasShortInventoryObjectFollowupSignal(userMessage);
|
||||||
|
|
@ -1092,10 +1155,16 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
: findRecentInventoryPurchaseProvenanceItem(items, deps.toNonEmptyString(navigationFocusObjectLabel) ??
|
: findRecentInventoryPurchaseProvenanceItem(items, deps.toNonEmptyString(navigationFocusObjectLabel) ??
|
||||||
(0, assistantContinuityPolicy_1.readAddressDebugItem)(carryoverSourceDebug, deps.toNonEmptyString) ??
|
(0, assistantContinuityPolicy_1.readAddressDebugItem)(carryoverSourceDebug, deps.toNonEmptyString) ??
|
||||||
deps.toNonEmptyString(previousFilters.item)) ?? previousAddressItem;
|
deps.toNonEmptyString(previousFilters.item)) ?? previousAddressItem;
|
||||||
const purchaseBridgeWindow = extractPurchaseDateBridgeWindow(purchaseBridgeItem, addressNavigationState);
|
const purchaseBridgeWindow = extractPurchaseDateBridgeWindow(purchaseBridgeItem, addressNavigationState, userMessage, alternateMessage);
|
||||||
if (purchaseBridgeWindow) {
|
if (purchaseBridgeWindow) {
|
||||||
previousFilters.period_from = purchaseBridgeWindow.period_from;
|
previousFilters.period_from = purchaseBridgeWindow.period_from;
|
||||||
previousFilters.period_to = purchaseBridgeWindow.period_to;
|
previousFilters.period_to = purchaseBridgeWindow.period_to;
|
||||||
|
previousFilters.as_of_date = purchaseBridgeWindow.as_of_date;
|
||||||
|
previousFilters.purchase_date_bridge_selected = purchaseBridgeWindow.purchase_date_bridge_selected;
|
||||||
|
previousFilters.purchase_date_bridge_first = purchaseBridgeWindow.purchase_date_bridge_first;
|
||||||
|
previousFilters.purchase_date_bridge_last = purchaseBridgeWindow.purchase_date_bridge_last;
|
||||||
|
previousFilters.purchase_date_bridge_basis = purchaseBridgeWindow.purchase_date_bridge_basis;
|
||||||
|
previousFilters.purchase_date_bridge_has_multiple = purchaseBridgeWindow.purchase_date_bridge_has_multiple;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
previousFilters = (0, assistantContinuityPolicy_1.applyTemporalCarryoverFilters)(previousFilters, navigationDateScope, continuityTemporalScope, sourceIntentHint, deps.toNonEmptyString);
|
previousFilters = (0, assistantContinuityPolicy_1.applyTemporalCarryoverFilters)(previousFilters, navigationDateScope, continuityTemporalScope, sourceIntentHint, deps.toNonEmptyString);
|
||||||
|
|
|
||||||
|
|
@ -2777,106 +2777,11 @@ export function buildAutoRunsRouter(services: AppServices, openaiClient = new Op
|
||||||
|
|
||||||
router.post("/api/autoruns/autogen/generate", async (req, res, next) => {
|
router.post("/api/autoruns/autogen/generate", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const body = toRecord(req.body);
|
|
||||||
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(
|
throw new ApiError(
|
||||||
"AUTOGEN_MODE_NOT_SUPPORTED",
|
"AUTOGEN_QUESTION_GENERATION_DISABLED",
|
||||||
"Use `/api/autoruns/autogen/save-assistant-session` to save user sessions.",
|
"Автогенерация вопросов отключена: используйте сохранение живых пользовательских сессий.",
|
||||||
400
|
410
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
let questions: string[] = [];
|
|
||||||
if (mode === "qwen_seed") {
|
|
||||||
if (!llmConfig) {
|
|
||||||
throw new ApiError(
|
|
||||||
"AUTOGEN_LLM_CONFIG_REQUIRED",
|
|
||||||
"Для режима qwen_seed нужен активный LLM-контур (provider/model/baseUrl) из настроек подключения.",
|
|
||||||
400
|
|
||||||
);
|
|
||||||
}
|
|
||||||
questions = await generateQwenSeedQuestionsLive({
|
|
||||||
count,
|
|
||||||
domain,
|
|
||||||
personalityPrompt,
|
|
||||||
llmConfig,
|
|
||||||
client: openaiClient
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
questions = generateCodexCreativeQuestions(count, domain);
|
|
||||||
}
|
|
||||||
questions = Array.from(new Set(questions.map((item) => sanitizeGeneratedQuestion(item)).filter((item) => item.length > 0))).slice(
|
|
||||||
0,
|
|
||||||
count
|
|
||||||
);
|
|
||||||
const generationId = generateAutogenId();
|
|
||||||
|
|
||||||
let savedCaseSetFile: string | null = null;
|
|
||||||
if (persistCaseSet) {
|
|
||||||
if (!fs.existsSync(EVAL_CASES_DIR)) {
|
|
||||||
fs.mkdirSync(EVAL_CASES_DIR, { recursive: true });
|
|
||||||
}
|
|
||||||
const fileName = buildAutogenCaseSetFileName(mode, generationId);
|
|
||||||
const filePath = path.resolve(EVAL_CASES_DIR, fileName);
|
|
||||||
const payload = buildAutogenCaseSetPayload({
|
|
||||||
generationId,
|
|
||||||
mode,
|
|
||||||
domain,
|
|
||||||
questions
|
|
||||||
});
|
|
||||||
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), "utf-8");
|
|
||||||
savedCaseSetFile = fileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
const record: AutoGenHistoryRecord = {
|
|
||||||
generation_id: generationId,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
mode,
|
|
||||||
title: null,
|
|
||||||
count: questions.length,
|
|
||||||
domain,
|
|
||||||
questions,
|
|
||||||
generated_by: generatedBy,
|
|
||||||
saved_case_set_file: savedCaseSetFile,
|
|
||||||
context: context
|
|
||||||
? {
|
|
||||||
llm_provider: toStringSafe(context.llm_provider),
|
|
||||||
model: toStringSafe(context.model),
|
|
||||||
assistant_prompt_version: toStringSafe(context.assistant_prompt_version),
|
|
||||||
decomposition_prompt_version: toStringSafe(context.decomposition_prompt_version),
|
|
||||||
prompt_fingerprint: toStringSafe(context.prompt_fingerprint)
|
|
||||||
? repairAutogenMojibake(String(context.prompt_fingerprint))
|
|
||||||
: null,
|
|
||||||
autogen_personality_id: toStringSafe(context.autogen_personality_id),
|
|
||||||
autogen_personality_prompt: toStringSafe(context.autogen_personality_prompt)
|
|
||||||
? repairAutogenMojibake(String(context.autogen_personality_prompt))
|
|
||||||
: null,
|
|
||||||
source_session_id: null,
|
|
||||||
saved_session_file: null,
|
|
||||||
saved_case_set_kind: "single_turn_list"
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
};
|
|
||||||
const history = readAutoGenHistory();
|
|
||||||
history.unshift(record);
|
|
||||||
writeAutoGenHistory(history.slice(0, 500));
|
|
||||||
|
|
||||||
ok(res, {
|
|
||||||
ok: true,
|
|
||||||
generation: record
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3944,6 +3944,18 @@ export class AddressQueryService {
|
||||||
asOfDate: typeof filterSet.as_of_date === "string" ? filterSet.as_of_date : undefined,
|
asOfDate: typeof filterSet.as_of_date === "string" ? filterSet.as_of_date : undefined,
|
||||||
requestedResultMode,
|
requestedResultMode,
|
||||||
vatDirectSourceProbe: options.vatDirectSourceProbe ?? undefined,
|
vatDirectSourceProbe: options.vatDirectSourceProbe ?? undefined,
|
||||||
|
purchaseDateBridge:
|
||||||
|
typeof filterSet.purchase_date_bridge_selected === "string" ||
|
||||||
|
typeof filterSet.purchase_date_bridge_first === "string" ||
|
||||||
|
typeof filterSet.purchase_date_bridge_last === "string"
|
||||||
|
? {
|
||||||
|
selectedPurchaseDate: filterSet.purchase_date_bridge_selected,
|
||||||
|
firstPurchaseDate: filterSet.purchase_date_bridge_first,
|
||||||
|
lastPurchaseDate: filterSet.purchase_date_bridge_last,
|
||||||
|
basis: filterSet.purchase_date_bridge_basis,
|
||||||
|
hasMultiplePurchaseDates: filterSet.purchase_date_bridge_has_multiple === true
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
emphasizeNumbers: options.emphasizeNumbers ?? undefined,
|
emphasizeNumbers: options.emphasizeNumbers ?? undefined,
|
||||||
useRubCurrency: options.useRubCurrency ?? undefined
|
useRubCurrency: options.useRubCurrency ?? undefined
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1520,6 +1520,91 @@ function buildInventoryMovementQuery(
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildStaticWhereClause(conditions: Array<string | null | undefined>, indent = " "): string {
|
||||||
|
const cleaned = conditions.map((condition) => String(condition ?? "").trim()).filter((condition) => condition.length > 0);
|
||||||
|
return cleaned.length > 0 ? `ГДЕ\n${indent}${cleaned.join(`\n${indent}И `)}` : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveInventoryAgingAsOfExpr(filters: AddressFilterSet): string {
|
||||||
|
const now = new Date();
|
||||||
|
return (
|
||||||
|
(typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||||
|
? toDateTimeExpr(filters.as_of_date, true)
|
||||||
|
: null) ??
|
||||||
|
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||||
|
? toDateTimeExpr(filters.period_to, true)
|
||||||
|
: null) ??
|
||||||
|
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||||
|
? toDateTimeExpr(filters.period_from, true)
|
||||||
|
: null) ??
|
||||||
|
`ДАТАВРЕМЯ(${now.getFullYear()}, ${now.getMonth() + 1}, ${now.getDate()}, 23, 59, 59)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInventoryAgingByPurchaseDateQuery(filters: AddressFilterSet, resolvedLimit: number): string {
|
||||||
|
const asOfExpr = resolveInventoryAgingAsOfExpr(filters);
|
||||||
|
const inventoryAccountPredicate = buildAccountPrefixPredicate("Остатки.Счет", ["41.01"]);
|
||||||
|
const onHandScopeConditions = [
|
||||||
|
"Остатки.КоличествоРазвернутыйОстатокДт > 0",
|
||||||
|
inventoryAccountPredicate,
|
||||||
|
buildOrganizationReferenceCondition(filters, ["Остатки.Организация"]),
|
||||||
|
buildWarehouseReferenceCondition(filters, ["Остатки.Субконто3"]),
|
||||||
|
buildInventoryItemReferenceCondition(filters, ["Остатки.Субконто1"])
|
||||||
|
];
|
||||||
|
const onHandWhereClause = buildStaticWhereClause(onHandScopeConditions);
|
||||||
|
const currentStockItemSubquery = [
|
||||||
|
"(ВЫБРАТЬ РАЗЛИЧНЫЕ",
|
||||||
|
" Остатки.Субконто1",
|
||||||
|
" ИЗ",
|
||||||
|
` РегистрБухгалтерии.Хозрасчетный.Остатки(${asOfExpr}, , , ) КАК Остатки`,
|
||||||
|
buildStaticWhereClause(onHandScopeConditions, " "),
|
||||||
|
")"
|
||||||
|
].join("\n");
|
||||||
|
const purchaseWhereClause = buildStaticWhereClause([
|
||||||
|
"Товары.Ссылка.Проведен = ИСТИНА",
|
||||||
|
`Товары.Ссылка.Дата <= ${asOfExpr}`,
|
||||||
|
buildOrganizationReferenceCondition(filters, ["Товары.Ссылка.Организация"]),
|
||||||
|
buildInventoryItemReferenceCondition(filters, ["Товары.Номенклатура"]),
|
||||||
|
`Товары.Номенклатура В ${currentStockItemSubquery}`
|
||||||
|
]);
|
||||||
|
|
||||||
|
return `
|
||||||
|
ВЫБРАТЬ ПЕРВЫЕ ${resolvedLimit}
|
||||||
|
${asOfExpr} КАК Период,
|
||||||
|
"Остатки на дату" КАК Регистратор,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетДт,
|
||||||
|
"" КАК СчетКт,
|
||||||
|
Остатки.СуммаРазвернутыйОстатокДт КАК Сумма,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК Номенклатура,
|
||||||
|
"" КАК Контрагент,
|
||||||
|
"" КАК Договор,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК Склад,
|
||||||
|
Остатки.КоличествоРазвернутыйОстатокДт КАК Количество
|
||||||
|
ИЗ
|
||||||
|
РегистрБухгалтерии.Хозрасчетный.Остатки(${asOfExpr}, , , ) КАК Остатки
|
||||||
|
${onHandWhereClause}
|
||||||
|
ОБЪЕДИНИТЬ ВСЕ
|
||||||
|
ВЫБРАТЬ ПЕРВЫЕ ${resolvedLimit}
|
||||||
|
Товары.Ссылка.Дата КАК Период,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка) КАК Регистратор,
|
||||||
|
"41.01" КАК СчетДт,
|
||||||
|
"" КАК СчетКт,
|
||||||
|
Товары.Сумма КАК Сумма,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Товары.Номенклатура) КАК Номенклатура,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Контрагент) КАК Контрагент,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.ДоговорКонтрагента) КАК Договор,
|
||||||
|
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Организация) КАК Организация,
|
||||||
|
"" КАК Склад,
|
||||||
|
Товары.Количество КАК Количество
|
||||||
|
ИЗ
|
||||||
|
Документ.ПоступлениеТоваровУслуг.Товары КАК Товары
|
||||||
|
${purchaseWhereClause}
|
||||||
|
УПОРЯДОЧИТЬ ПО
|
||||||
|
Период ВОЗР
|
||||||
|
`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
function buildWarehouseReferenceCondition(filters: AddressFilterSet, fieldPaths: string[]): string | null {
|
function buildWarehouseReferenceCondition(filters: AddressFilterSet, fieldPaths: string[]): string | null {
|
||||||
const warehouse = typeof filters.warehouse === "string" ? filters.warehouse.trim() : "";
|
const warehouse = typeof filters.warehouse === "string" ? filters.warehouse.trim() : "";
|
||||||
if (!warehouse) {
|
if (!warehouse) {
|
||||||
|
|
@ -2199,7 +2284,7 @@ export function buildAddressRecipePlan(
|
||||||
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
||||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
||||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
? buildInventoryAgingByPurchaseDateQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "inventory_quality_events_profile"
|
: recipe.query_template === "inventory_quality_events_profile"
|
||||||
? buildInventoryQualityEventsQuery(filters, resolvedLimit)
|
? buildInventoryQualityEventsQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "contracts_by_counterparty_profile"
|
: recipe.query_template === "contracts_by_counterparty_profile"
|
||||||
|
|
|
||||||
|
|
@ -743,12 +743,42 @@ function needsVatPurchaseDateAnchorDisclosure(userMessage: string | null | undef
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildVatPurchaseDateAnchorDisclosureLine(
|
function vatPurchaseDateBasisLabel(basis: string | null | undefined, hasMultiplePurchaseDates: boolean): string {
|
||||||
|
if (!hasMultiplePurchaseDates) {
|
||||||
|
return "подтвержденную дату закупки";
|
||||||
|
}
|
||||||
|
if (basis === "last_confirmed_purchase") {
|
||||||
|
return "последнюю подтвержденную дату закупки";
|
||||||
|
}
|
||||||
|
return "первую подтвержденную дату закупки";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildVatPurchaseDateAnchorDisclosureLines(
|
||||||
options: ComposeFactualReplyOptions,
|
options: ComposeFactualReplyOptions,
|
||||||
periodWindowLabel: string | null
|
periodWindowLabel: string | null
|
||||||
): string | null {
|
): string[] {
|
||||||
if (!periodWindowLabel || !needsVatPurchaseDateAnchorDisclosure(options.userMessage)) {
|
const bridge = options.purchaseDateBridge ?? null;
|
||||||
return null;
|
const selectedPurchaseDate = normalizeIsoDateOnly(bridge?.selectedPurchaseDate);
|
||||||
|
const firstPurchaseDate = normalizeIsoDateOnly(bridge?.firstPurchaseDate);
|
||||||
|
const lastPurchaseDate = normalizeIsoDateOnly(bridge?.lastPurchaseDate);
|
||||||
|
const hasMultiplePurchaseDates = Boolean(
|
||||||
|
bridge?.hasMultiplePurchaseDates || (firstPurchaseDate && lastPurchaseDate && firstPurchaseDate !== lastPurchaseDate)
|
||||||
|
);
|
||||||
|
if (!periodWindowLabel || (!selectedPurchaseDate && !needsVatPurchaseDateAnchorDisclosure(options.userMessage))) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedPurchaseDate) {
|
||||||
|
const basisLabel = vatPurchaseDateBasisLabel(bridge?.basis, hasMultiplePurchaseDates);
|
||||||
|
const lines = [
|
||||||
|
`- Якорь периода: для расчета я использую ${basisLabel} ${formatDateRu(selectedPurchaseDate)}; налоговый период ${periodWindowLabel}; сам расчет ниже взят из книг продаж/покупок за это окно.`
|
||||||
|
];
|
||||||
|
if (hasMultiplePurchaseDates && firstPurchaseDate && lastPurchaseDate) {
|
||||||
|
lines.push(
|
||||||
|
`- Важно: у позиции несколько подтвержденных дат закупки (${formatDateRu(firstPurchaseDate)}..${formatDateRu(lastPurchaseDate)}); это расчет по выбранному якорю, а не единственная возможная дата.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
||||||
|
|
@ -765,10 +795,14 @@ function buildVatPurchaseDateAnchorDisclosureLine(
|
||||||
asOfTs >= fromTs &&
|
asOfTs >= fromTs &&
|
||||||
asOfTs <= toTs
|
asOfTs <= toTs
|
||||||
) {
|
) {
|
||||||
return `- Якорь периода: дата покупки ${formatDateRu(asOfDate)} попадает в налоговый период ${periodWindowLabel}; поэтому расчет ниже взят из книг продаж/покупок за это окно.`;
|
return [
|
||||||
|
`- Якорь периода: дата покупки ${formatDateRu(asOfDate)} попадает в налоговый период ${periodWindowLabel}; поэтому расчет ниже взят из книг продаж/покупок за это окно.`
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return `- Якорь периода: дата покупки из вопроса/контекста использована для выбора налогового периода ${periodWindowLabel}; сам расчет ниже взят из книг продаж/покупок за это окно.`;
|
return [
|
||||||
|
`- Якорь периода: дата покупки из вопроса/контекста использована для выбора налогового периода ${periodWindowLabel}; сам расчет ниже взят из книг продаж/покупок за это окно.`
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectRankingLimit(userMessage: string | null | undefined, fallback = 20): number {
|
function detectRankingLimit(userMessage: string | null | undefined, fallback = 20): number {
|
||||||
|
|
@ -1292,14 +1326,85 @@ function hasInventoryAccountPrefix(value: string | null | undefined, prefix: str
|
||||||
return normalized === prefix || normalized.startsWith(`${prefix}.`) || normalized.startsWith(prefix);
|
return normalized === prefix || normalized.startsWith(`${prefix}.`) || normalized.startsWith(prefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isInventoryOnHandSnapshotRow(row: ComposeStageRow): boolean {
|
||||||
|
const registrator = String(row.registrator ?? "").trim().toLowerCase();
|
||||||
|
return /(?:остатки\s+на\s+дату|stock\s*on\s*hand|balance\s+as\s+of)/iu.test(registrator);
|
||||||
|
}
|
||||||
|
|
||||||
function isInventoryPurchaseMovement(row: ComposeStageRow): boolean {
|
function isInventoryPurchaseMovement(row: ComposeStageRow): boolean {
|
||||||
return hasInventoryAccountPrefix(row.account_dt, "41.01");
|
return hasInventoryAccountPrefix(row.account_dt, "41.01") && !isInventoryOnHandSnapshotRow(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isInventorySaleMovement(row: ComposeStageRow): boolean {
|
function isInventorySaleMovement(row: ComposeStageRow): boolean {
|
||||||
return hasInventoryAccountPrefix(row.account_kt, "41.01");
|
return hasInventoryAccountPrefix(row.account_kt, "41.01");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface InventoryOnHandAgingScope {
|
||||||
|
itemKeys: Set<string>;
|
||||||
|
itemOrgKeys: Set<string>;
|
||||||
|
fullKeys: Set<string>;
|
||||||
|
orgKeys: Set<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inventoryAgingKeyPart(value: string | null | undefined): string {
|
||||||
|
return normalizeEntityToken(value) ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInventoryOnHandAgingScope(rows: ComposeStageRow[]): InventoryOnHandAgingScope {
|
||||||
|
const scope: InventoryOnHandAgingScope = {
|
||||||
|
itemKeys: new Set(),
|
||||||
|
itemOrgKeys: new Set(),
|
||||||
|
fullKeys: new Set(),
|
||||||
|
orgKeys: new Set()
|
||||||
|
};
|
||||||
|
for (const row of rows) {
|
||||||
|
if (!isInventoryOnHandSnapshotRow(row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const quantity = extractInventoryQuantity(row);
|
||||||
|
if (quantity === null || quantity <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const itemKey = inventoryAgingKeyPart(extractInventoryItemName(row));
|
||||||
|
if (!itemKey) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const organizationKey = inventoryAgingKeyPart(extractInventoryOrganizationName(row));
|
||||||
|
const warehouseKey = inventoryAgingKeyPart(extractInventoryWarehouseName(row));
|
||||||
|
scope.itemKeys.add(itemKey);
|
||||||
|
if (organizationKey) {
|
||||||
|
scope.orgKeys.add(organizationKey);
|
||||||
|
scope.itemOrgKeys.add(`${itemKey}|${organizationKey}`);
|
||||||
|
}
|
||||||
|
if (organizationKey || warehouseKey) {
|
||||||
|
scope.fullKeys.add(`${itemKey}|${warehouseKey}|${organizationKey}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inventoryPurchaseMatchesOnHandAgingScope(row: ComposeStageRow, scope: InventoryOnHandAgingScope): boolean {
|
||||||
|
if (scope.itemKeys.size === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const itemKey = inventoryAgingKeyPart(extractInventoryItemName(row));
|
||||||
|
if (!itemKey || !scope.itemKeys.has(itemKey)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const organizationKey = inventoryAgingKeyPart(extractInventoryOrganizationName(row));
|
||||||
|
const warehouseKey = inventoryAgingKeyPart(extractInventoryWarehouseName(row));
|
||||||
|
if (scope.fullKeys.has(`${itemKey}|${warehouseKey}|${organizationKey}`)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (organizationKey && scope.itemOrgKeys.has(`${itemKey}|${organizationKey}`)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (organizationKey && scope.orgKeys.size > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function looksLikeInventoryTraceDocumentToken(value: string): boolean {
|
function looksLikeInventoryTraceDocumentToken(value: string): boolean {
|
||||||
const normalized = String(value ?? "").trim();
|
const normalized = String(value ?? "").trim();
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
|
|
@ -1526,8 +1631,12 @@ function buildInventoryAgingByItemAggregate(
|
||||||
}
|
}
|
||||||
>();
|
>();
|
||||||
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
|
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
|
||||||
|
const onHandScope = buildInventoryOnHandAgingScope(rows);
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
|
if (!isInventoryPurchaseMovement(row) || !inventoryPurchaseMatchesOnHandAgingScope(row, onHandScope)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const item = extractInventoryItemName(row);
|
const item = extractInventoryItemName(row);
|
||||||
if (!item) {
|
if (!item) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -1613,19 +1722,11 @@ function formatInventoryAgingRows(items: InventoryAgingByItemAggregate[], asOfDa
|
||||||
const parts = [
|
const parts = [
|
||||||
`${index + 1}. ${item.item}`,
|
`${index + 1}. ${item.item}`,
|
||||||
`первая закупка: ${inventoryTraceDateLabel(item.firstPurchasePeriod)}`,
|
`первая закупка: ${inventoryTraceDateLabel(item.firstPurchasePeriod)}`,
|
||||||
`последняя закупка: ${inventoryTraceDateLabel(item.lastPurchasePeriod)}`,
|
`последняя закупка: ${inventoryTraceDateLabel(item.lastPurchasePeriod)}`
|
||||||
`документов: ${formatNumberWithDots(item.documentCount)}`,
|
|
||||||
`операций: ${formatNumberWithDots(item.operations)}`
|
|
||||||
];
|
];
|
||||||
if (item.ageDays !== null) {
|
if (item.ageDays !== null) {
|
||||||
parts.push(`возраст следа на ${formatDateRu(asOfDate)}: ${formatNumberWithDots(item.ageDays)} дн.`);
|
parts.push(`возраст следа на ${formatDateRu(asOfDate)}: ${formatNumberWithDots(item.ageDays)} дн.`);
|
||||||
}
|
}
|
||||||
if (item.warehouse) {
|
|
||||||
parts.push(`склад: ${item.warehouse}`);
|
|
||||||
}
|
|
||||||
if (item.organization) {
|
|
||||||
parts.push(`организация: ${item.organization}`);
|
|
||||||
}
|
|
||||||
if (item.counterparties.length > 0) {
|
if (item.counterparties.length > 0) {
|
||||||
parts.push(`поставщики: ${item.counterparties.slice(0, 3).join("; ")}`);
|
parts.push(`поставщики: ${item.counterparties.slice(0, 3).join("; ")}`);
|
||||||
}
|
}
|
||||||
|
|
@ -3934,16 +4035,30 @@ function composeFactualReplyBody(
|
||||||
const formatConfirmedMoney = (value: number): string => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
const formatConfirmedMoney = (value: number): string => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
||||||
const organizationLabel = normalizeOrganizationScopeValue(options.organizationHint);
|
const organizationLabel = normalizeOrganizationScopeValue(options.organizationHint);
|
||||||
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
|
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
|
||||||
const purchaseDateAnchorLine = buildVatPurchaseDateAnchorDisclosureLine(options, periodWindowLabel);
|
const purchaseDateAnchorLines = buildVatPurchaseDateAnchorDisclosureLines(options, periodWindowLabel);
|
||||||
|
const selectedPurchaseDate = normalizeIsoDateOnly(options.purchaseDateBridge?.selectedPurchaseDate);
|
||||||
|
const hasMultiplePurchaseDates = Boolean(
|
||||||
|
options.purchaseDateBridge?.hasMultiplePurchaseDates ||
|
||||||
|
(normalizeIsoDateOnly(options.purchaseDateBridge?.firstPurchaseDate) &&
|
||||||
|
normalizeIsoDateOnly(options.purchaseDateBridge?.lastPurchaseDate) &&
|
||||||
|
normalizeIsoDateOnly(options.purchaseDateBridge?.firstPurchaseDate) !==
|
||||||
|
normalizeIsoDateOnly(options.purchaseDateBridge?.lastPurchaseDate))
|
||||||
|
);
|
||||||
|
const purchaseBasisLabel = vatPurchaseDateBasisLabel(options.purchaseDateBridge?.basis, hasMultiplePurchaseDates);
|
||||||
|
const directVatLine = selectedPurchaseDate
|
||||||
|
? hasMultiplePurchaseDates
|
||||||
|
? `Коротко: если брать ${purchaseBasisLabel} ${formatDateRu(selectedPurchaseDate)}, подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`
|
||||||
|
: `Коротко: по дате покупки ${formatDateRu(selectedPurchaseDate)} подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`
|
||||||
|
: `Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`;
|
||||||
|
|
||||||
const lines = [
|
const lines = [
|
||||||
`Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`,
|
directVatLine,
|
||||||
"Расчет сделан по книгам продаж и покупок.",
|
"Расчет сделан по книгам продаж и покупок.",
|
||||||
"",
|
"",
|
||||||
"Что вошло в расчет:",
|
"Что вошло в расчет:",
|
||||||
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
|
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
|
||||||
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
|
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
|
||||||
...(purchaseDateAnchorLine ? [purchaseDateAnchorLine] : []),
|
...purchaseDateAnchorLines,
|
||||||
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
|
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
|
||||||
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
|
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
|
||||||
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`
|
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`
|
||||||
|
|
|
||||||
|
|
@ -574,6 +574,24 @@ function hasRelativeYearHint(text: string): boolean {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveMonthPeriodFromIsoDate(isoDate: string | null | undefined): { period_from: string; period_to: string; as_of_date: string } | null {
|
||||||
|
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const year = Number(match[1]);
|
||||||
|
const month = Number(match[2]);
|
||||||
|
if (!Number.isFinite(year) || !Number.isFinite(month) || month < 1 || month > 12) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
||||||
|
return {
|
||||||
|
period_from: `${match[1]}-${match[2]}-01`,
|
||||||
|
period_to: `${match[1]}-${match[2]}-${String(lastDay).padStart(2, "0")}`,
|
||||||
|
as_of_date: `${match[1]}-${match[2]}-${String(lastDay).padStart(2, "0")}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function resolveRelativeMonthPeriodFromInventoryRoot(
|
function resolveRelativeMonthPeriodFromInventoryRoot(
|
||||||
userMessage: string,
|
userMessage: string,
|
||||||
followupContext: AddressFollowupContext | null
|
followupContext: AddressFollowupContext | null
|
||||||
|
|
@ -801,6 +819,36 @@ export function hasInventoryPurchaseDateVatBridgeCue(text: string): boolean {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasInventoryPurchaseDateVatBridgeContinuationCue(text: string): boolean {
|
||||||
|
const normalized = textWithRepairedVariant(String(text ?? ""))
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ё/g, "е");
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const mentionsPurchase = /(?:покупк|закупк|purchase)/iu.test(normalized);
|
||||||
|
const mentionsFirstOrLast = /(?:перв|последн|earliest|oldest|latest|last)/iu.test(normalized);
|
||||||
|
return mentionsPurchase && mentionsFirstOrLast;
|
||||||
|
}
|
||||||
|
|
||||||
|
function purchaseDateBridgeBasisFromMessage(userMessage: string): string | null {
|
||||||
|
const normalized = textWithRepairedVariant(String(userMessage ?? ""))
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ё/g, "е");
|
||||||
|
if (!normalized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (/(?:последн|latest|last)/iu.test(normalized) && /(?:покупк|закупк|purchase)/iu.test(normalized)) {
|
||||||
|
return "last_confirmed_purchase";
|
||||||
|
}
|
||||||
|
if (/(?:перв|earliest|oldest)/iu.test(normalized) && /(?:покупк|закупк|purchase)/iu.test(normalized)) {
|
||||||
|
return "first_confirmed_purchase";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function hasInventoryMarginRankingFollowupCue(text: string): boolean {
|
export function hasInventoryMarginRankingFollowupCue(text: string): boolean {
|
||||||
const normalized = textWithRepairedVariant(String(text ?? ""))
|
const normalized = textWithRepairedVariant(String(text ?? ""))
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
|
@ -968,6 +1016,34 @@ function mergeFollowupFilters(
|
||||||
): { filters: AddressFilterSet; reasons: string[] } {
|
): { filters: AddressFilterSet; reasons: string[] } {
|
||||||
const merged: AddressFilterSet = { ...current };
|
const merged: AddressFilterSet = { ...current };
|
||||||
const reasons: string[] = [];
|
const reasons: string[] = [];
|
||||||
|
const clearInventoryAgingOrganizationAliasItem = (organizationHint: string | null): void => {
|
||||||
|
if (intent !== "inventory_aging_by_purchase_date") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const explicitItemMention = /(?:^|[\s,.;:!?()\-\u2014])(?:товар(?:у|а|ом)?|позици(?:и|я|ю)|item|row|line)(?=$|[\s,.;:!?()\-\u2014])/iu.test(
|
||||||
|
String(userMessage ?? "")
|
||||||
|
);
|
||||||
|
const agingItem = toNonEmptyString(merged.item);
|
||||||
|
const agingOrganization = toNonEmptyString(merged.organization) ?? organizationHint;
|
||||||
|
const normalizedAgingItem = normalizeOrganizationScopeSearchText(agingItem ?? "");
|
||||||
|
const normalizedAgingOrganization = normalizeOrganizationScopeSearchText(agingOrganization ?? "");
|
||||||
|
const itemLooksLikeOrganizationScope = Boolean(
|
||||||
|
agingItem &&
|
||||||
|
agingOrganization &&
|
||||||
|
(organizationsLikelySameEntity(agingItem, agingOrganization) ||
|
||||||
|
(normalizedAgingItem &&
|
||||||
|
normalizedAgingOrganization &&
|
||||||
|
(normalizedAgingOrganization.includes(normalizedAgingItem) ||
|
||||||
|
normalizedAgingItem.includes(normalizedAgingOrganization))))
|
||||||
|
);
|
||||||
|
if (agingItem && itemLooksLikeOrganizationScope) {
|
||||||
|
delete merged.item;
|
||||||
|
reasons.push("item_cleared_as_organization_scope_alias_for_stock_aging");
|
||||||
|
} else if (agingItem && !explicitItemMention) {
|
||||||
|
delete merged.item;
|
||||||
|
reasons.push("item_cleared_for_stock_slice_aging");
|
||||||
|
}
|
||||||
|
};
|
||||||
if (!followupContext) {
|
if (!followupContext) {
|
||||||
if (
|
if (
|
||||||
(intent === "list_open_contracts" ||
|
(intent === "list_open_contracts" ||
|
||||||
|
|
@ -987,6 +1063,7 @@ function mergeFollowupFilters(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
clearInventoryAgingOrganizationAliasItem(null);
|
||||||
return { filters: merged, reasons };
|
return { filters: merged, reasons };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1001,6 +1078,13 @@ function mergeFollowupFilters(
|
||||||
const previousAsOfDate = toNonEmptyString(previous.as_of_date);
|
const previousAsOfDate = toNonEmptyString(previous.as_of_date);
|
||||||
const previousPeriodFrom = toNonEmptyString(previous.period_from);
|
const previousPeriodFrom = toNonEmptyString(previous.period_from);
|
||||||
const previousPeriodTo = toNonEmptyString(previous.period_to);
|
const previousPeriodTo = toNonEmptyString(previous.period_to);
|
||||||
|
const previousPurchaseDateBridgeSelected = toNonEmptyString(previous.purchase_date_bridge_selected);
|
||||||
|
const previousPurchaseDateBridgeFirst = toNonEmptyString(previous.purchase_date_bridge_first);
|
||||||
|
const previousPurchaseDateBridgeLast = toNonEmptyString(previous.purchase_date_bridge_last);
|
||||||
|
const previousPurchaseDateBridgeBasis = toNonEmptyString(previous.purchase_date_bridge_basis);
|
||||||
|
const previousPurchaseDateBridgeHasMultiple =
|
||||||
|
previous.purchase_date_bridge_has_multiple === true ||
|
||||||
|
String(previous.purchase_date_bridge_has_multiple ?? "").trim().toLowerCase() === "true";
|
||||||
const relativeMonthFromInventoryRoot = resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext);
|
const relativeMonthFromInventoryRoot = resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext);
|
||||||
const relativeMonthFromFollowupYear = resolveRelativeMonthPeriodFromFollowupYear(userMessage, followupContext);
|
const relativeMonthFromFollowupYear = resolveRelativeMonthPeriodFromFollowupYear(userMessage, followupContext);
|
||||||
const allTimeRequested = hasAllTimeHint(userMessage);
|
const allTimeRequested = hasAllTimeHint(userMessage);
|
||||||
|
|
@ -1020,6 +1104,57 @@ function mergeFollowupFilters(
|
||||||
reasons.push("organization_from_followup_context");
|
reasons.push("organization_from_followup_context");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (intent === "vat_liability_confirmed_for_tax_period" && previousPurchaseDateBridgeSelected) {
|
||||||
|
const requestedBridgeBasis = purchaseDateBridgeBasisFromMessage(userMessage);
|
||||||
|
const shouldCarryPurchaseDateBridge =
|
||||||
|
!currentHasExplicitTemporalScope &&
|
||||||
|
(Boolean(requestedBridgeBasis) ||
|
||||||
|
hasInventoryPurchaseDateVatBridgeCue(userMessage) ||
|
||||||
|
hasInventoryPurchaseDateVatBridgeContinuationCue(userMessage));
|
||||||
|
if (!shouldCarryPurchaseDateBridge) {
|
||||||
|
delete merged.purchase_date_bridge_selected;
|
||||||
|
delete merged.purchase_date_bridge_first;
|
||||||
|
delete merged.purchase_date_bridge_last;
|
||||||
|
delete merged.purchase_date_bridge_basis;
|
||||||
|
delete merged.purchase_date_bridge_has_multiple;
|
||||||
|
reasons.push(
|
||||||
|
currentHasExplicitTemporalScope
|
||||||
|
? "purchase_date_bridge_suppressed_by_explicit_temporal_scope"
|
||||||
|
: "purchase_date_bridge_not_reused_without_bridge_cue"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const selectedBridgeDate =
|
||||||
|
requestedBridgeBasis === "last_confirmed_purchase"
|
||||||
|
? previousPurchaseDateBridgeLast ?? previousPurchaseDateBridgeSelected
|
||||||
|
: requestedBridgeBasis === "first_confirmed_purchase"
|
||||||
|
? previousPurchaseDateBridgeFirst ?? previousPurchaseDateBridgeSelected
|
||||||
|
: previousPurchaseDateBridgeSelected;
|
||||||
|
merged.purchase_date_bridge_selected = selectedBridgeDate;
|
||||||
|
const selectedBridgeWindow = resolveMonthPeriodFromIsoDate(selectedBridgeDate);
|
||||||
|
if (selectedBridgeWindow) {
|
||||||
|
merged.period_from = selectedBridgeWindow.period_from;
|
||||||
|
merged.period_to = selectedBridgeWindow.period_to;
|
||||||
|
merged.as_of_date = selectedBridgeWindow.as_of_date;
|
||||||
|
reasons.push("period_from_purchase_date_bridge_followup_context");
|
||||||
|
reasons.push("period_to_purchase_date_bridge_followup_context");
|
||||||
|
}
|
||||||
|
if (previousPurchaseDateBridgeFirst) {
|
||||||
|
merged.purchase_date_bridge_first = previousPurchaseDateBridgeFirst;
|
||||||
|
}
|
||||||
|
if (previousPurchaseDateBridgeLast) {
|
||||||
|
merged.purchase_date_bridge_last = previousPurchaseDateBridgeLast;
|
||||||
|
}
|
||||||
|
const effectiveBridgeBasis = requestedBridgeBasis ?? previousPurchaseDateBridgeBasis;
|
||||||
|
if (effectiveBridgeBasis) {
|
||||||
|
merged.purchase_date_bridge_basis = effectiveBridgeBasis;
|
||||||
|
}
|
||||||
|
if (previousPurchaseDateBridgeHasMultiple) {
|
||||||
|
merged.purchase_date_bridge_has_multiple = true;
|
||||||
|
}
|
||||||
|
reasons.push("purchase_date_bridge_from_followup_context");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
intent === "inventory_on_hand_as_of_date" &&
|
intent === "inventory_on_hand_as_of_date" &&
|
||||||
followupContext.previous_intent === "inventory_on_hand_as_of_date" &&
|
followupContext.previous_intent === "inventory_on_hand_as_of_date" &&
|
||||||
|
|
@ -1386,13 +1521,7 @@ function mergeFollowupFilters(
|
||||||
reasons.push("period_derived_from_inventory_root_frame_year");
|
reasons.push("period_derived_from_inventory_root_frame_year");
|
||||||
}
|
}
|
||||||
if (intent === "inventory_aging_by_purchase_date") {
|
if (intent === "inventory_aging_by_purchase_date") {
|
||||||
const explicitItemMention = /(?:^|[\s,.;:!?()\-\u2014])(?:товар(?:у|а|ом)?|позици(?:и|я|ю)|item|row|line)(?=$|[\s,.;:!?()\-\u2014])/iu.test(
|
clearInventoryAgingOrganizationAliasItem(previousOrganization);
|
||||||
String(userMessage ?? "")
|
|
||||||
);
|
|
||||||
if (toNonEmptyString(merged.item) && !explicitItemMention) {
|
|
||||||
delete merged.item;
|
|
||||||
reasons.push("item_cleared_for_stock_slice_aging");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!sameDateRequested &&
|
!sameDateRequested &&
|
||||||
|
|
@ -1646,7 +1775,13 @@ function deriveIntentWithFollowupContext(
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedMessage = String(userMessage ?? "");
|
const normalizedMessage = String(userMessage ?? "");
|
||||||
const hasFollowupSignal = hasAddressFollowupContextSignal(normalizedMessage);
|
const genericFollowupSignal = hasAddressFollowupContextSignal(normalizedMessage);
|
||||||
|
const previousFilters = followupContext.previous_filters ?? {};
|
||||||
|
const purchaseBridgeContinuationSignal =
|
||||||
|
followupContext.previous_intent === "vat_liability_confirmed_for_tax_period" &&
|
||||||
|
Boolean(toNonEmptyString(previousFilters.purchase_date_bridge_selected)) &&
|
||||||
|
hasInventoryPurchaseDateVatBridgeContinuationCue(normalizedMessage);
|
||||||
|
const hasFollowupSignal = genericFollowupSignal || purchaseBridgeContinuationSignal;
|
||||||
if (!hasFollowupSignal) {
|
if (!hasFollowupSignal) {
|
||||||
return detectedIntent;
|
return detectedIntent;
|
||||||
}
|
}
|
||||||
|
|
@ -1655,7 +1790,6 @@ function deriveIntentWithFollowupContext(
|
||||||
if (!sourceIntent && !fallbackIntent) {
|
if (!sourceIntent && !fallbackIntent) {
|
||||||
return detectedIntent;
|
return detectedIntent;
|
||||||
}
|
}
|
||||||
const previousFilters = followupContext.previous_filters ?? {};
|
|
||||||
const previousPeriodFrom = toNonEmptyString(previousFilters.period_from);
|
const previousPeriodFrom = toNonEmptyString(previousFilters.period_from);
|
||||||
const previousPeriodTo = toNonEmptyString(previousFilters.period_to);
|
const previousPeriodTo = toNonEmptyString(previousFilters.period_to);
|
||||||
const previousContract = toNonEmptyString(previousFilters.contract);
|
const previousContract = toNonEmptyString(previousFilters.contract);
|
||||||
|
|
@ -1672,6 +1806,7 @@ function deriveIntentWithFollowupContext(
|
||||||
isVatFollowup &&
|
isVatFollowup &&
|
||||||
hasSamePeriodHint(normalizedMessage) &&
|
hasSamePeriodHint(normalizedMessage) &&
|
||||||
Boolean(previousPeriodFrom || previousPeriodTo);
|
Boolean(previousPeriodFrom || previousPeriodTo);
|
||||||
|
const previousPurchaseDateBridgeSelected = toNonEmptyString(previousFilters.purchase_date_bridge_selected);
|
||||||
const previousIsInventoryFamily = isInventoryIntent(sourceIntent ?? undefined);
|
const previousIsInventoryFamily = isInventoryIntent(sourceIntent ?? undefined);
|
||||||
const rootIsInventoryFamily = isInventoryIntent(followupContext.root_intent ?? undefined);
|
const rootIsInventoryFamily = isInventoryIntent(followupContext.root_intent ?? undefined);
|
||||||
const inventoryLineageActive =
|
const inventoryLineageActive =
|
||||||
|
|
@ -1700,8 +1835,13 @@ function deriveIntentWithFollowupContext(
|
||||||
) || hasSelectedObjectInlineSnapshotMetadata(normalizedMessage);
|
) || hasSelectedObjectInlineSnapshotMetadata(normalizedMessage);
|
||||||
const staleInventoryLineageCanYieldToCounterparty =
|
const staleInventoryLineageCanYieldToCounterparty =
|
||||||
previousCounterpartyLaneActive && !hasExplicitInventoryItemReference;
|
previousCounterpartyLaneActive && !hasExplicitInventoryItemReference;
|
||||||
|
const vatPurchaseDateBridgeContinuation =
|
||||||
|
sourceIntent === "vat_liability_confirmed_for_tax_period" &&
|
||||||
|
Boolean(previousPurchaseDateBridgeSelected) &&
|
||||||
|
hasInventoryPurchaseDateVatBridgeContinuationCue(normalizedMessage);
|
||||||
const inventoryPurchaseDateVatBridge =
|
const inventoryPurchaseDateVatBridge =
|
||||||
inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage);
|
(inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage)) ||
|
||||||
|
vatPurchaseDateBridgeContinuation;
|
||||||
const marginRankingLineageActive =
|
const marginRankingLineageActive =
|
||||||
sourceIntent === "inventory_margin_ranking_for_nomenclature" ||
|
sourceIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
fallbackIntent === "inventory_margin_ranking_for_nomenclature" ||
|
fallbackIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
|
|
@ -1728,6 +1868,7 @@ function deriveIntentWithFollowupContext(
|
||||||
inventoryPurchaseDateVatBridge &&
|
inventoryPurchaseDateVatBridge &&
|
||||||
(detectedIntent.intent === "unknown" ||
|
(detectedIntent.intent === "unknown" ||
|
||||||
detectedIntent.intent === sourceIntent ||
|
detectedIntent.intent === sourceIntent ||
|
||||||
|
detectedIntent.intent === "account_balance_snapshot" ||
|
||||||
detectedIntent.intent === "vat_payable_confirmed_as_of_date" ||
|
detectedIntent.intent === "vat_payable_confirmed_as_of_date" ||
|
||||||
detectedIntent.intent === "vat_payable_forecast")
|
detectedIntent.intent === "vat_payable_forecast")
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,17 @@ function inventoryPartyListOrUnknown(parties: string[]): string {
|
||||||
return parties.length > 0 ? parties.slice(0, 4).join("; ") : "не выделен отдельным полем";
|
return parties.length > 0 ? parties.slice(0, 4).join("; ") : "не выделен отдельным полем";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeInventoryReplyEntityToken(value: string | null | undefined): string | null {
|
||||||
|
const normalized = String(value ?? "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ё/gu, "е")
|
||||||
|
.replace(/[^a-zа-я0-9]+/giu, " ")
|
||||||
|
.replace(/\s+/gu, " ")
|
||||||
|
.trim();
|
||||||
|
return normalized || null;
|
||||||
|
}
|
||||||
|
|
||||||
function sumInventoryRowAmount(rows: ComposeStageRow[]): number {
|
function sumInventoryRowAmount(rows: ComposeStageRow[]): number {
|
||||||
return rows.reduce((sum, row) => sum + (typeof row.amount === "number" && Number.isFinite(row.amount) ? row.amount : 0), 0);
|
return rows.reduce((sum, row) => sum + (typeof row.amount === "number" && Number.isFinite(row.amount) ? row.amount : 0), 0);
|
||||||
}
|
}
|
||||||
|
|
@ -550,30 +561,50 @@ export function composeInventoryReply(
|
||||||
|
|
||||||
if (intent === "inventory_aging_by_purchase_date") {
|
if (intent === "inventory_aging_by_purchase_date") {
|
||||||
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
||||||
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
const agingItems = deps.buildInventoryAgingByItemAggregate(rows, asOfDate);
|
||||||
|
const agingItemTokens = new Set(
|
||||||
|
agingItems
|
||||||
|
.map((item) => normalizeInventoryReplyEntityToken(item.item))
|
||||||
|
.filter((item): item is string => Boolean(item))
|
||||||
|
);
|
||||||
|
const purchaseRows = rows.filter((row) => {
|
||||||
|
if (!deps.isInventoryPurchaseMovement(row)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (agingItemTokens.size === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const item = deps.summarizeInventoryTraceRows([row]).item;
|
||||||
|
const itemToken = normalizeInventoryReplyEntityToken(item);
|
||||||
|
return Boolean(itemToken && agingItemTokens.has(itemToken));
|
||||||
|
});
|
||||||
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
|
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
|
||||||
const agingItems = deps.buildInventoryAgingByItemAggregate(purchaseRows, asOfDate);
|
|
||||||
const oldestPurchaseDate = agingItems[0]?.firstPurchasePeriod ?? summary.firstPeriod;
|
const oldestPurchaseDate = agingItems[0]?.firstPurchasePeriod ?? summary.firstPeriod;
|
||||||
const oldestPurchaseAgeDays = agingItems[0]?.ageDays ?? null;
|
const oldestPurchaseAgeDays = agingItems[0]?.ageDays ?? null;
|
||||||
|
const organizationLabel = agingItems.find((item) => item.organization)?.organization ?? null;
|
||||||
const oldestAnswerPreview = agingItems
|
const oldestAnswerPreview = agingItems
|
||||||
.slice(0, 3)
|
.slice(0, 3)
|
||||||
.map((item) => `${item.item} (${deps.inventoryTraceDateLabel(item.firstPurchasePeriod)})`)
|
.map((item) => `${item.item} (${deps.inventoryTraceDateLabel(item.firstPurchasePeriod)})`)
|
||||||
.join("; ");
|
.join("; ");
|
||||||
const directAnswerLine =
|
const directAnswerLine =
|
||||||
agingItems.length > 0
|
agingItems.length > 0
|
||||||
? `К самым старым закупкам в текущем подтвержденном срезе относятся позиции с самой ранней первой закупкой: ${oldestAnswerPreview}.`
|
? `Среди фактических положительных остатков есть давно закупавшиеся позиции: ${oldestAnswerPreview}.`
|
||||||
: "По доступному закупочному следу позиции со старыми закупками не материализованы.";
|
: "В фактическом положительном остатке не найдено позиций с подтвержденным старым закупочным следом.";
|
||||||
const lines: string[] = [directAnswerLine];
|
const lines: string[] = [directAnswerLine];
|
||||||
appendInventoryBulletSection(lines, "Сводка:", [
|
const summaryLines = [
|
||||||
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||||
`Самая ранняя первая закупка среди позиций: ${deps.inventoryTraceDateLabel(oldestPurchaseDate)}.`,
|
`Самая ранняя первая закупка среди позиций: ${deps.inventoryTraceDateLabel(oldestPurchaseDate)}.`,
|
||||||
`Самая поздняя найденная закупка: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
`Самая поздняя найденная закупка: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
||||||
`Позиций в выборке: ${deps.formatNumberWithDots(agingItems.length)}.`,
|
`Позиций в выборке: ${deps.formatNumberWithDots(agingItems.length)}.`,
|
||||||
`Закупочных документов: ${deps.formatNumberWithDots(summary.documents.length)}.`,
|
`Закупочных документов: ${deps.formatNumberWithDots(summary.documents.length)}.`,
|
||||||
`Закупочных операций: ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
`Закупочных операций: ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
||||||
]);
|
];
|
||||||
|
if (organizationLabel) {
|
||||||
|
summaryLines.splice(1, 0, `Организация: ${organizationLabel}.`);
|
||||||
|
}
|
||||||
|
appendInventoryBulletSection(lines, "Сводка:", summaryLines);
|
||||||
appendInventoryBulletSection(lines, "Ограничения:", [
|
appendInventoryBulletSection(lines, "Ограничения:", [
|
||||||
"Без партионного учета этот ответ показывает возраст закупочного следа по товарной позиции, а не возраст конкретного лота."
|
"Берем только позиции с положительным остатком на дату среза; без партионного учета это возраст закупочного следа по номенклатуре, а не доказанный возраст конкретной партии."
|
||||||
]);
|
]);
|
||||||
if (oldestPurchaseAgeDays !== null) {
|
if (oldestPurchaseAgeDays !== null) {
|
||||||
lines.push(`- Между самой ранней первой закупкой и датой среза прошло ${deps.formatNumberWithDots(oldestPurchaseAgeDays)} дн.`);
|
lines.push(`- Между самой ранней первой закупкой и датой среза прошло ${deps.formatNumberWithDots(oldestPurchaseAgeDays)} дн.`);
|
||||||
|
|
@ -582,10 +613,10 @@ export function composeInventoryReply(
|
||||||
lines.push(`- Поставщики, встречающиеся в наблюдаемом закупочном следе: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
lines.push(`- Поставщики, встречающиеся в наблюдаемом закупочном следе: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||||
}
|
}
|
||||||
if (agingItems.length > 0) {
|
if (agingItems.length > 0) {
|
||||||
appendInventorySection(lines, "Позиции от самых старых закупок:", deps.formatInventoryAgingRows(agingItems, asOfDate, 12));
|
appendInventorySection(lines, "Позиции от самых старых закупок:", deps.formatInventoryAgingRows(agingItems, asOfDate, 5));
|
||||||
} else {
|
} else {
|
||||||
appendInventorySection(lines, "Позиции от самых старых закупок:", [
|
appendInventorySection(lines, "Позиции от самых старых закупок:", [
|
||||||
"- В доступных данных не найдено закупочных движений для выбранного среза."
|
"- В доступных данных не найдено закупочных движений по позициям, которые есть в положительном остатке на дату среза."
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
return buildFactualSummaryReply(
|
return buildFactualSummaryReply(
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,13 @@ export interface ComposeFactualReplyOptions<TVatDirectSourceProbe = unknown> {
|
||||||
asOfDate?: string;
|
asOfDate?: string;
|
||||||
requestedResultMode?: AddressResultMode;
|
requestedResultMode?: AddressResultMode;
|
||||||
vatDirectSourceProbe?: TVatDirectSourceProbe | null;
|
vatDirectSourceProbe?: TVatDirectSourceProbe | null;
|
||||||
|
purchaseDateBridge?: {
|
||||||
|
selectedPurchaseDate?: string | null;
|
||||||
|
firstPurchaseDate?: string | null;
|
||||||
|
lastPurchaseDate?: string | null;
|
||||||
|
basis?: string | null;
|
||||||
|
hasMultiplePurchaseDates?: boolean;
|
||||||
|
} | null;
|
||||||
emphasizeNumbers?: boolean;
|
emphasizeNumbers?: boolean;
|
||||||
useRubCurrency?: boolean;
|
useRubCurrency?: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -120,12 +120,25 @@ function cleanComparisonScopeCompanyLine(line: string, organization: string | nu
|
||||||
return clean.trim();
|
return clean.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasComparisonScopeProofCue(value: unknown): boolean {
|
||||||
|
const text = toNullableString(value)?.toLocaleLowerCase("ru-RU").replace(/\s+/gu, " ").trim();
|
||||||
|
if (!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:\bсравни\b|собери\s+коротк\p{L}*\s+итог|коротк\p{L}*\s+сравн|что\s+.*подтверд\p{L}*.*(?:компан|контрагент|свк)|что\s+.*отдельно\s+по\s+(?:группа\s+свк|выбранн\p{L}*\s+контрагент)|какие\s+выводы\s+можно\s+делать\s+и\s+какие\s+нельзя)/iu.test(
|
||||||
|
text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function buildComparisonScopeProofReply(input: {
|
function buildComparisonScopeProofReply(input: {
|
||||||
baseReply: string;
|
baseReply: string;
|
||||||
debug: Record<string, unknown>;
|
debug: Record<string, unknown>;
|
||||||
session: unknown;
|
session: unknown;
|
||||||
userMessage?: unknown;
|
userMessage?: unknown;
|
||||||
}): { reply: string; audit: Record<string, unknown> } | null {
|
}): { reply: string; audit: Record<string, unknown> } | null {
|
||||||
|
if (!hasComparisonScopeProofCue(input.userMessage)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const entryPoint = toRecordObject(input.debug.assistant_mcp_discovery_entry_point_v1);
|
const entryPoint = toRecordObject(input.debug.assistant_mcp_discovery_entry_point_v1);
|
||||||
const turnInput = toRecordObject(entryPoint?.turn_input);
|
const turnInput = toRecordObject(entryPoint?.turn_input);
|
||||||
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
|
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ function hasCompactCashflowFollowupSignal(text: string | null): boolean {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return /(?:\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0440\u0435\u0437\p{L}*|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433|\u043f\u0440\u0438\u0448\p{L}*[\s\S]{0,80}\u0443\u0448\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u043f\u043e\u043b\u0443\u0447\p{L}*[\s\S]{0,80}\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*[\s\S]{0,80}\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b)/iu.test(value);
|
return /(?:\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0440\u0435\u0437\p{L}*|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433|\u043f\u0440\u0438\u0448\p{L}*[\s\S]{0,80}\u0443\u0448\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u043f\u043e\u043b\u0443\u0447\p{L}*[\s\S]{0,80}\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*[\s\S]{0,80}\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b|кто\s+больше\s+всего\s+зан[её]с|кому\s+больше\s+всего\s+ушл\p{L}*|банк\s+не\s+называй\s+обычн\p{L}*\s+клиент\p{L}*|главн\p{L}*\s+клиент|главн\p{L}*\s+поставщик)/iu.test(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function dateScopeToFilterWindow(dateScope: string | null): Record<string, string> | null {
|
function dateScopeToFilterWindow(dateScope: string | null): Record<string, string> | null {
|
||||||
|
|
@ -169,6 +169,37 @@ function dateScopeToFilterWindow(dateScope: string | null): Record<string, strin
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filterWindowFromExtractedFilters(filters: Record<string, unknown> | null): Record<string, string> | null {
|
||||||
|
if (!filters) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const asOfDate = typeof filters.as_of_date === "string" ? filters.as_of_date.trim() : "";
|
||||||
|
if (/^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(asOfDate)) {
|
||||||
|
return { as_of_date: asOfDate };
|
||||||
|
}
|
||||||
|
const periodFrom = typeof filters.period_from === "string" ? filters.period_from.trim() : "";
|
||||||
|
const periodTo = typeof filters.period_to === "string" ? filters.period_to.trim() : "";
|
||||||
|
if (/^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(periodFrom) && /^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(periodTo)) {
|
||||||
|
return {
|
||||||
|
period_from: periodFrom,
|
||||||
|
period_to: periodTo
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasBoundedFilterWindow(filters: Record<string, unknown> | null): boolean {
|
||||||
|
if (!filters) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (typeof filters.as_of_date === "string" && /^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(filters.as_of_date.trim())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const periodFrom = typeof filters.period_from === "string" ? filters.period_from.trim() : "";
|
||||||
|
const periodTo = typeof filters.period_to === "string" ? filters.period_to.trim() : "";
|
||||||
|
return /^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(periodFrom) && /^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(periodTo);
|
||||||
|
}
|
||||||
|
|
||||||
function looksLikeReliableOrganizationScope(value: string | null): boolean {
|
function looksLikeReliableOrganizationScope(value: string | null): boolean {
|
||||||
const text = compactLower(value);
|
const text = compactLower(value);
|
||||||
if (!text) {
|
if (!text) {
|
||||||
|
|
@ -197,12 +228,17 @@ function inferBusinessOverviewDiscoveryContextFromSessionItems(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const turnMeaningRef = toRecordObject(turnInput?.turn_meaning_ref);
|
const turnMeaningRef = toRecordObject(turnInput?.turn_meaning_ref);
|
||||||
const filterWindow = dateScopeToFilterWindow(toNonEmptyString(turnMeaningRef?.explicit_date_scope));
|
const debugFilters = toRecordObject(debug?.extracted_filters);
|
||||||
|
const filterWindow =
|
||||||
|
dateScopeToFilterWindow(toNonEmptyString(turnMeaningRef?.explicit_date_scope)) ??
|
||||||
|
filterWindowFromExtractedFilters(debugFilters);
|
||||||
if (!filterWindow) {
|
if (!filterWindow) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const previousFilters: Record<string, unknown> = { ...filterWindow };
|
const previousFilters: Record<string, unknown> = { ...filterWindow };
|
||||||
const organization = toNonEmptyString(turnMeaningRef?.explicit_organization_scope);
|
const organization =
|
||||||
|
toNonEmptyString(turnMeaningRef?.explicit_organization_scope) ??
|
||||||
|
toNonEmptyString(debugFilters?.organization);
|
||||||
if (looksLikeReliableOrganizationScope(organization)) {
|
if (looksLikeReliableOrganizationScope(organization)) {
|
||||||
previousFilters.organization = organization;
|
previousFilters.organization = organization;
|
||||||
}
|
}
|
||||||
|
|
@ -229,11 +265,7 @@ function mergeBusinessOverviewDateContextForCompactCashflow(input: {
|
||||||
return input.followupContext;
|
return input.followupContext;
|
||||||
}
|
}
|
||||||
const existingFilters = toRecordObject(input.followupContext?.previous_filters);
|
const existingFilters = toRecordObject(input.followupContext?.previous_filters);
|
||||||
if (
|
if (hasBoundedFilterWindow(existingFilters)) {
|
||||||
input.toNonEmptyString(existingFilters?.period_from) ||
|
|
||||||
input.toNonEmptyString(existingFilters?.period_to) ||
|
|
||||||
input.toNonEmptyString(existingFilters?.as_of_date)
|
|
||||||
) {
|
|
||||||
return input.followupContext;
|
return input.followupContext;
|
||||||
}
|
}
|
||||||
const inferred = inferBusinessOverviewDiscoveryContextFromSessionItems(input.sessionItems, input.toNonEmptyString);
|
const inferred = inferBusinessOverviewDiscoveryContextFromSessionItems(input.sessionItems, input.toNonEmptyString);
|
||||||
|
|
@ -244,12 +276,12 @@ function mergeBusinessOverviewDateContextForCompactCashflow(input: {
|
||||||
...inferred,
|
...inferred,
|
||||||
...(input.followupContext ?? {}),
|
...(input.followupContext ?? {}),
|
||||||
previous_filters: {
|
previous_filters: {
|
||||||
...(toRecordObject(inferred.previous_filters) ?? {}),
|
...(toRecordObject(input.followupContext?.previous_filters) ?? {}),
|
||||||
...(toRecordObject(input.followupContext?.previous_filters) ?? {})
|
...(toRecordObject(inferred.previous_filters) ?? {})
|
||||||
},
|
},
|
||||||
root_filters: {
|
root_filters: {
|
||||||
...(toRecordObject(inferred.root_filters) ?? {}),
|
...(toRecordObject(input.followupContext?.root_filters) ?? {}),
|
||||||
...(toRecordObject(input.followupContext?.root_filters) ?? {})
|
...(toRecordObject(inferred.root_filters) ?? {})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -281,6 +313,16 @@ function sameEntityHint(expected: string | null, actual: string | null): boolean
|
||||||
return left === right || left.includes(right) || right.includes(left);
|
return left === right || left.includes(right) || right.includes(left);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasComparisonScopeProofCue(text: string | null): boolean {
|
||||||
|
const value = compactLower(text);
|
||||||
|
if (!value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:\bсравни\b|собери\s+коротк\p{L}*\s+итог|коротк\p{L}*\s+сравн|что\s+.*подтверд\p{L}*.*(?:компан|контрагент|свк)|что\s+.*отдельно\s+по\s+(?:группа\s+свк|выбранн\p{L}*\s+контрагент)|какие\s+выводы\s+можно\s+делать\s+и\s+какие\s+нельзя)/iu.test(
|
||||||
|
value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function isBusinessOverviewDiscoveryFollowup(
|
function isBusinessOverviewDiscoveryFollowup(
|
||||||
followupContext: Record<string, unknown> | null,
|
followupContext: Record<string, unknown> | null,
|
||||||
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
|
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
|
||||||
|
|
@ -489,6 +531,7 @@ function mergeBusinessOverviewProofBundlesFromNavigationState(input: {
|
||||||
followupContext: Record<string, unknown> | null;
|
followupContext: Record<string, unknown> | null;
|
||||||
sessionAddressNavigationState: unknown;
|
sessionAddressNavigationState: unknown;
|
||||||
predecomposeContract: Record<string, unknown> | null;
|
predecomposeContract: Record<string, unknown> | null;
|
||||||
|
userMessage: string;
|
||||||
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"];
|
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"];
|
||||||
}): Record<string, unknown> | null {
|
}): Record<string, unknown> | null {
|
||||||
const currentValueBundle = toRecordObject(input.followupContext?.previous_discovery_bidirectional_value_flow);
|
const currentValueBundle = toRecordObject(input.followupContext?.previous_discovery_bidirectional_value_flow);
|
||||||
|
|
@ -508,6 +551,9 @@ function mergeBusinessOverviewProofBundlesFromNavigationState(input: {
|
||||||
const entities = toRecordObject(input.predecomposeContract?.entities);
|
const entities = toRecordObject(input.predecomposeContract?.entities);
|
||||||
const hasCurrentOrganizationSelection = Boolean(input.toNonEmptyString(entities?.organization));
|
const hasCurrentOrganizationSelection = Boolean(input.toNonEmptyString(entities?.organization));
|
||||||
const businessOverviewFollowup = isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString);
|
const businessOverviewFollowup = isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString);
|
||||||
|
if (!hasComparisonScopeProofCue(input.userMessage)) {
|
||||||
|
return input.followupContext;
|
||||||
|
}
|
||||||
if (!businessOverviewFollowup && !hasCurrentOrganizationSelection) {
|
if (!businessOverviewFollowup && !hasCurrentOrganizationSelection) {
|
||||||
return input.followupContext;
|
return input.followupContext;
|
||||||
}
|
}
|
||||||
|
|
@ -550,9 +596,13 @@ function mergeBusinessOverviewProofBundlesFromNavigationState(input: {
|
||||||
|
|
||||||
function mergeBusinessOverviewProofBundlesFromSessionItems(input: {
|
function mergeBusinessOverviewProofBundlesFromSessionItems(input: {
|
||||||
followupContext: Record<string, unknown> | null;
|
followupContext: Record<string, unknown> | null;
|
||||||
|
userMessage: string;
|
||||||
sessionItems: unknown[];
|
sessionItems: unknown[];
|
||||||
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"];
|
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"];
|
||||||
}): Record<string, unknown> | null {
|
}): Record<string, unknown> | null {
|
||||||
|
if (!hasComparisonScopeProofCue(input.userMessage)) {
|
||||||
|
return input.followupContext;
|
||||||
|
}
|
||||||
if (!isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString)) {
|
if (!isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString)) {
|
||||||
return input.followupContext;
|
return input.followupContext;
|
||||||
}
|
}
|
||||||
|
|
@ -853,6 +903,7 @@ export async function buildAssistantAddressOrchestrationRuntime(
|
||||||
});
|
});
|
||||||
const discoveryFollowupContextWithProofBundles = mergeBusinessOverviewProofBundlesFromSessionItems({
|
const discoveryFollowupContextWithProofBundles = mergeBusinessOverviewProofBundlesFromSessionItems({
|
||||||
followupContext: discoveryFollowupContext,
|
followupContext: discoveryFollowupContext,
|
||||||
|
userMessage: input.userMessage,
|
||||||
sessionItems: input.sessionItems,
|
sessionItems: input.sessionItems,
|
||||||
toNonEmptyString: input.toNonEmptyString
|
toNonEmptyString: input.toNonEmptyString
|
||||||
});
|
});
|
||||||
|
|
@ -860,6 +911,7 @@ export async function buildAssistantAddressOrchestrationRuntime(
|
||||||
followupContext: discoveryFollowupContextWithProofBundles,
|
followupContext: discoveryFollowupContextWithProofBundles,
|
||||||
sessionAddressNavigationState: input.sessionAddressNavigationState,
|
sessionAddressNavigationState: input.sessionAddressNavigationState,
|
||||||
predecomposeContract,
|
predecomposeContract,
|
||||||
|
userMessage: input.userMessage,
|
||||||
toNonEmptyString: input.toNonEmptyString
|
toNonEmptyString: input.toNonEmptyString
|
||||||
});
|
});
|
||||||
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(
|
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,21 @@ function normalizeAddressRuntimeMetaForDeep(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripRoutineShortAnswerPrefix(value: string): string {
|
||||||
|
return String(value ?? "")
|
||||||
|
.split(/\r?\n/g)
|
||||||
|
.map((line) =>
|
||||||
|
line
|
||||||
|
.replace(
|
||||||
|
/^((?:\s|\uFEFF)*(?:[-*]\s*)?)(?:\u041a\u043e\u0440\u043e\u0442\u043a\u043e:\s*)+/u,
|
||||||
|
"$1"
|
||||||
|
)
|
||||||
|
.replace(/(:\s*)\u041a\u043e\u0440\u043e\u0442\u043a\u043e:\s*/u, "$1")
|
||||||
|
)
|
||||||
|
.join("\n")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
export function runAssistantDeepTurnResponseRuntime(
|
export function runAssistantDeepTurnResponseRuntime(
|
||||||
input: RunAssistantDeepTurnResponseRuntimeInput
|
input: RunAssistantDeepTurnResponseRuntimeInput
|
||||||
): RunAssistantDeepTurnResponseRuntimeOutput {
|
): RunAssistantDeepTurnResponseRuntimeOutput {
|
||||||
|
|
@ -243,9 +258,11 @@ export function runAssistantDeepTurnResponseRuntime(
|
||||||
currentReplySource: "deep_analysis",
|
currentReplySource: "deep_analysis",
|
||||||
addressRuntimeMeta: addressRuntimeMetaForDeep as unknown as Record<string, unknown> | null
|
addressRuntimeMeta: addressRuntimeMetaForDeep as unknown as Record<string, unknown> | null
|
||||||
});
|
});
|
||||||
const assistantReply = mcpDiscoveryResponsePolicy.applied
|
const assistantReply = stripRoutineShortAnswerPrefix(
|
||||||
? mcpDiscoveryResponsePolicy.reply_text
|
mcpDiscoveryResponsePolicy.applied
|
||||||
: packagingRuntime.safeAssistantReply;
|
? input.sanitizeReply(mcpDiscoveryResponsePolicy.reply_text, packagingRuntime.safeAssistantReply)
|
||||||
|
: packagingRuntime.safeAssistantReply
|
||||||
|
);
|
||||||
const replyType = mcpDiscoveryResponsePolicy.applied ? "partial_coverage" : input.composition.reply_type;
|
const replyType = mcpDiscoveryResponsePolicy.applied ? "partial_coverage" : input.composition.reply_type;
|
||||||
const debug = {
|
const debug = {
|
||||||
...packagingRuntime.debug,
|
...packagingRuntime.debug,
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,21 @@ function hasInternalMechanics(value: string): boolean {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripRoutineShortAnswerPrefix(value: string): string {
|
||||||
|
return String(value ?? "")
|
||||||
|
.split(/\r?\n/g)
|
||||||
|
.map((line) =>
|
||||||
|
line
|
||||||
|
.replace(
|
||||||
|
/^((?:\s|\uFEFF)*(?:[-*]\s*)?)(?:\u041a\u043e\u0440\u043e\u0442\u043a\u043e:\s*)+/u,
|
||||||
|
"$1"
|
||||||
|
)
|
||||||
|
.replace(/(:\s*)\u041a\u043e\u0440\u043e\u0442\u043a\u043e:\s*/u, "$1")
|
||||||
|
)
|
||||||
|
.join("\n")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
function isMcpDiscoveryEntryPointContract(value: unknown): value is AssistantMcpDiscoveryRuntimeEntryPointContract {
|
function isMcpDiscoveryEntryPointContract(value: unknown): value is AssistantMcpDiscoveryRuntimeEntryPointContract {
|
||||||
const record = toRecordObject(value);
|
const record = toRecordObject(value);
|
||||||
return (
|
return (
|
||||||
|
|
@ -1052,12 +1067,13 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||||
}
|
}
|
||||||
|
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_applied");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_applied");
|
||||||
|
const replyText = stripRoutineShortAnswerPrefix(String(candidate.reply_text));
|
||||||
return {
|
return {
|
||||||
schema_version: ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION,
|
schema_version: ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION,
|
||||||
policy_owner: "assistantMcpDiscoveryResponsePolicy",
|
policy_owner: "assistantMcpDiscoveryResponsePolicy",
|
||||||
decision: "apply_candidate",
|
decision: "apply_candidate",
|
||||||
applied: true,
|
applied: true,
|
||||||
reply_text: String(candidate.reply_text),
|
reply_text: replyText || String(candidate.reply_text),
|
||||||
reply_source: "mcp_discovery_response_candidate_guarded",
|
reply_source: "mcp_discovery_response_candidate_guarded",
|
||||||
candidate,
|
candidate,
|
||||||
reason_codes: reasonCodes
|
reason_codes: reasonCodes
|
||||||
|
|
|
||||||
|
|
@ -225,6 +225,9 @@ function isGarbageSemanticAnchorCandidate(value: string | null): boolean {
|
||||||
/^(?:и\s+)?кто\s+(?:главн\p{L}*|основн\p{L}*|крупн\p{L}*)\s+(?:клиент|покупател|поставщик|контрагент)(?:\s+в)?$/iu.test(
|
/^(?:и\s+)?кто\s+(?:главн\p{L}*|основн\p{L}*|крупн\p{L}*)\s+(?:клиент|покупател|поставщик|контрагент)(?:\s+в)?$/iu.test(
|
||||||
text
|
text
|
||||||
) ||
|
) ||
|
||||||
|
/^(?:и\s+)?(?:главн\p{L}*|основн\p{L}*|крупн\p{L}*)\s+(?:клиент|покупател|поставщик|контрагент)(?:\s+в)?$/iu.test(
|
||||||
|
text
|
||||||
|
) ||
|
||||||
/^(?:или\s+)?(?:обычн\p{L}*\s+)?(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент)(?:\s+или\s+(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент))?$/iu.test(
|
/^(?:или\s+)?(?:обычн\p{L}*\s+)?(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент)(?:\s+или\s+(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент))?$/iu.test(
|
||||||
text
|
text
|
||||||
) ||
|
) ||
|
||||||
|
|
@ -2458,6 +2461,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
? null
|
? null
|
||||||
: explicitCurrentCounterpartyOverridesFollowupEntity
|
: explicitCurrentCounterpartyOverridesFollowupEntity
|
||||||
? null
|
? null
|
||||||
|
: businessOverviewSuppressesFollowupCounterparty
|
||||||
|
? rawMetadataScopeHint
|
||||||
: rawMetadataScopeHint ??
|
: rawMetadataScopeHint ??
|
||||||
followupSeed.metadataScopeHint ??
|
followupSeed.metadataScopeHint ??
|
||||||
followupSeed.discoveryEntity ??
|
followupSeed.discoveryEntity ??
|
||||||
|
|
@ -2685,7 +2690,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||||
normalizedPredecomposeDateScope &&
|
normalizedPredecomposeDateScope &&
|
||||||
normalizedPredecomposeDateScope.startsWith(`${rawDateScope}-`)
|
normalizedPredecomposeDateScope.startsWith(`${rawDateScope}-`)
|
||||||
);
|
);
|
||||||
const followupAllTimeScopeApplied = normalizedFollowupDateScope === "all_time_scope";
|
const followupAllTimeScopeApplied = normalizedFollowupDateScope === "all_time_scope" && !currentTurnCarriesExplicitPeriod;
|
||||||
const explicitDateScope =
|
const explicitDateScope =
|
||||||
rawAllTimeScopeSignal || followupAllTimeScopeApplied
|
rawAllTimeScopeSignal || followupAllTimeScopeApplied
|
||||||
? null
|
? null
|
||||||
|
|
|
||||||
|
|
@ -1982,7 +1982,11 @@ function repairAddressMojibake(value) {
|
||||||
}
|
}
|
||||||
function sanitizeOutgoingAssistantText(value, fallback = "Не смог сформировать читаемый ответ. Уточните запрос.") {
|
function sanitizeOutgoingAssistantText(value, fallback = "Не смог сформировать читаемый ответ. Уточните запрос.") {
|
||||||
const repaired = repairAddressMojibake(String(value ?? ""));
|
const repaired = repairAddressMojibake(String(value ?? ""));
|
||||||
const sanitized = String((0, answerComposer_1.sanitizeAssistantReplyForUserFacing)(repaired) ?? "").trim();
|
const sanitized = String((0, answerComposer_1.sanitizeAssistantReplyForUserFacing)(repaired) ?? "")
|
||||||
|
.split(/\r?\n/g)
|
||||||
|
.map((line) => line.replace(/^Коротко:\s*/u, "").replace(/(:\s*)Коротко:\s*/u, "$1"))
|
||||||
|
.join("\n")
|
||||||
|
.trim();
|
||||||
if (sanitized) {
|
if (sanitized) {
|
||||||
return sanitized;
|
return sanitized;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -301,7 +301,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
return `${match[3]}-${match[2]}-${match[1]}`;
|
return `${match[3]}-${match[2]}-${match[1]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeMonthWindowFromIso(isoDate) {
|
function computeMonthWindowFromIso(isoDate, bridgeFacts = {}) {
|
||||||
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -318,11 +318,17 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
purchase_date: isoDate,
|
purchase_date: isoDate,
|
||||||
period_from: `${year}-${mm}-01`,
|
period_from: `${year}-${mm}-01`,
|
||||||
period_to: `${year}-${mm}-${dd}`,
|
period_to: `${year}-${mm}-${dd}`,
|
||||||
as_of_date: `${year}-${mm}-${dd}`
|
as_of_date: `${year}-${mm}-${dd}`,
|
||||||
|
purchase_date_bridge_selected: isoDate,
|
||||||
|
...bridgeFacts
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractEarliestDmyDateFromEntityRefs(entityRefs) {
|
function uniqueSortedIsoDates(dates) {
|
||||||
|
return Array.from(new Set(dates.filter(Boolean))).sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractDmyDatesFromEntityRefs(entityRefs) {
|
||||||
const dates = [];
|
const dates = [];
|
||||||
for (const entityRef of Array.isArray(entityRefs) ? entityRefs : []) {
|
for (const entityRef of Array.isArray(entityRefs) ? entityRefs : []) {
|
||||||
const value = deps.toNonEmptyString(entityRef?.value);
|
const value = deps.toNonEmptyString(entityRef?.value);
|
||||||
|
|
@ -340,23 +346,48 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (dates.length === 0) {
|
return uniqueSortedIsoDates(dates);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return dates.sort()[0] ?? null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractPurchaseDateBridgeWindow(previousAddressItem, addressNavigationState) {
|
function detectPurchaseDateBridgeBasis(userMessage, alternateMessage) {
|
||||||
|
const text = [userMessage, alternateMessage]
|
||||||
|
.map((item) => deps.compactWhitespace(deps.repairAddressMojibake(String(item ?? "")).toLowerCase()).replace(/ё/g, "е"))
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
if (/(?:последн|latest|last|сам[а-я\s-]*поздн)/iu.test(text)) {
|
||||||
|
return "last_confirmed_purchase";
|
||||||
|
}
|
||||||
|
if (/(?:перв|earliest|oldest|ранн|сам[а-я\s-]*стар)/iu.test(text)) {
|
||||||
|
return "first_confirmed_purchase";
|
||||||
|
}
|
||||||
|
return "first_confirmed_purchase_default";
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPurchaseDateBridgeFacts(previousAddressItem, addressNavigationState, userMessage, alternateMessage) {
|
||||||
|
const previousFilters = readAddressDebugFilters(previousAddressItem?.debug ?? null);
|
||||||
|
let firstIsoDate = deps.toNonEmptyString(previousFilters?.purchase_date_bridge_first);
|
||||||
|
let lastIsoDate = deps.toNonEmptyString(previousFilters?.purchase_date_bridge_last);
|
||||||
|
const selectedIsoDateFromFilters = deps.toNonEmptyString(previousFilters?.purchase_date_bridge_selected);
|
||||||
|
if (!firstIsoDate && selectedIsoDateFromFilters) {
|
||||||
|
firstIsoDate = selectedIsoDateFromFilters;
|
||||||
|
}
|
||||||
|
if (!lastIsoDate && selectedIsoDateFromFilters) {
|
||||||
|
lastIsoDate = selectedIsoDateFromFilters;
|
||||||
|
}
|
||||||
const replyText = deps.toNonEmptyString(previousAddressItem?.text);
|
const replyText = deps.toNonEmptyString(previousAddressItem?.text);
|
||||||
if (replyText) {
|
if (replyText && (!firstIsoDate || !lastIsoDate)) {
|
||||||
const repairedText = deps.repairAddressMojibake(replyText);
|
const repairedText = deps.repairAddressMojibake(replyText);
|
||||||
const explicitFirstDateMatch = repairedText.match(/первая\s+найденная\s+дата\s+закупки:\s*(\d{2}\.\d{2}\.\d{4})/iu);
|
const explicitFirstDateMatch = repairedText.match(
|
||||||
const explicitFirstDateIso = explicitFirstDateMatch ? parseDmyDateToIso(explicitFirstDateMatch[1]) : null;
|
/первая\s+(?:найденная|подтвержденная)\s+дата\s+закупки:\s*(\d{2}\.\d{2}\.\d{4})/iu
|
||||||
if (explicitFirstDateIso) {
|
);
|
||||||
return computeMonthWindowFromIso(explicitFirstDateIso);
|
const explicitLastDateMatch = repairedText.match(
|
||||||
}
|
/последняя\s+(?:найденная|подтвержденная)\s+дата\s+закупки:\s*(\d{2}\.\d{2}\.\d{4})/iu
|
||||||
|
);
|
||||||
|
firstIsoDate = explicitFirstDateMatch ? parseDmyDateToIso(explicitFirstDateMatch[1]) : null;
|
||||||
|
lastIsoDate = explicitLastDateMatch ? parseDmyDateToIso(explicitLastDateMatch[1]) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!firstIsoDate || !lastIsoDate) {
|
||||||
const navigationSessionState = resolveNavigationSessionContextState(
|
const navigationSessionState = resolveNavigationSessionContextState(
|
||||||
addressNavigationState,
|
addressNavigationState,
|
||||||
deps.toNonEmptyString,
|
deps.toNonEmptyString,
|
||||||
|
|
@ -372,8 +403,39 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
: null) ??
|
: null) ??
|
||||||
resultSets.find((item) => deps.toNonEmptyString(item?.intent) === "inventory_purchase_provenance_for_item") ??
|
resultSets.find((item) => deps.toNonEmptyString(item?.intent) === "inventory_purchase_provenance_for_item") ??
|
||||||
null;
|
null;
|
||||||
const earliestIsoDate = extractEarliestDmyDateFromEntityRefs(preferredResultSet?.entity_refs);
|
const datesFromResultSet = extractDmyDatesFromEntityRefs(preferredResultSet?.entity_refs);
|
||||||
return earliestIsoDate ? computeMonthWindowFromIso(earliestIsoDate) : null;
|
firstIsoDate = firstIsoDate ?? datesFromResultSet[0] ?? null;
|
||||||
|
lastIsoDate = lastIsoDate ?? datesFromResultSet[datesFromResultSet.length - 1] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!firstIsoDate && !lastIsoDate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedFirst = firstIsoDate ?? lastIsoDate;
|
||||||
|
const normalizedLast = lastIsoDate ?? firstIsoDate;
|
||||||
|
const basis = detectPurchaseDateBridgeBasis(userMessage, alternateMessage);
|
||||||
|
const selectedIsoDate = basis === "last_confirmed_purchase" ? normalizedLast : normalizedFirst;
|
||||||
|
return {
|
||||||
|
selectedIsoDate,
|
||||||
|
firstIsoDate: normalizedFirst,
|
||||||
|
lastIsoDate: normalizedLast,
|
||||||
|
basis,
|
||||||
|
hasMultiplePurchaseDates: Boolean(normalizedFirst && normalizedLast && normalizedFirst !== normalizedLast)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPurchaseDateBridgeWindow(previousAddressItem, addressNavigationState, userMessage, alternateMessage) {
|
||||||
|
const facts = extractPurchaseDateBridgeFacts(previousAddressItem, addressNavigationState, userMessage, alternateMessage);
|
||||||
|
if (!facts?.selectedIsoDate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return computeMonthWindowFromIso(facts.selectedIsoDate, {
|
||||||
|
purchase_date_bridge_first: facts.firstIsoDate,
|
||||||
|
purchase_date_bridge_last: facts.lastIsoDate,
|
||||||
|
purchase_date_bridge_basis: facts.basis,
|
||||||
|
purchase_date_bridge_has_multiple: facts.hasMultiplePurchaseDates
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function isUsableFollowupSourceDebug(debug) {
|
function isUsableFollowupSourceDebug(debug) {
|
||||||
|
|
@ -605,11 +667,20 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasInventoryItemFocusHint) {
|
function hasInventoryPurchaseDateVatBridgeSignal(
|
||||||
|
userMessage,
|
||||||
|
alternateMessage,
|
||||||
|
sourceIntentHint,
|
||||||
|
hasInventoryItemFocusHint,
|
||||||
|
sourceHasPurchaseDateBridgeHint = false
|
||||||
|
) {
|
||||||
|
const vatPurchaseBridgeContinuation =
|
||||||
|
sourceIntentHint === "vat_liability_confirmed_for_tax_period" && sourceHasPurchaseDateBridgeHint;
|
||||||
if (
|
if (
|
||||||
sourceIntentHint !== "inventory_purchase_provenance_for_item" &&
|
sourceIntentHint !== "inventory_purchase_provenance_for_item" &&
|
||||||
!hasInventoryItemFocusHint &&
|
!hasInventoryItemFocusHint &&
|
||||||
!deps.isInventorySelectedObjectIntent(sourceIntentHint)
|
!deps.isInventorySelectedObjectIntent(sourceIntentHint) &&
|
||||||
|
!vatPurchaseBridgeContinuation
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -621,10 +692,13 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
}
|
}
|
||||||
return samples.some(
|
return samples.some(
|
||||||
(sample) =>
|
(sample) =>
|
||||||
/(?:ндс|vat)/iu.test(sample) &&
|
((/(?:ндс|vat)/iu.test(sample) &&
|
||||||
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/iu.test(
|
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/iu.test(
|
||||||
sample
|
sample
|
||||||
)
|
)) ||
|
||||||
|
((vatPurchaseBridgeContinuation || sourceIntentHint === "vat_liability_confirmed_for_tax_period") &&
|
||||||
|
/(?:перв|последн|earliest|oldest|latest|last)/iu.test(sample) &&
|
||||||
|
/(?:покупк|закупк|purchase)/iu.test(sample)))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -932,10 +1006,17 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
|
sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
|
||||||
const navigationSessionState = earlyNavigationSessionState;
|
const navigationSessionState = earlyNavigationSessionState;
|
||||||
const navigationFocusObjectHint = navigationSessionState.focusObject;
|
const navigationFocusObjectHint = navigationSessionState.focusObject;
|
||||||
|
const sourceFiltersHint = readAddressDebugFilters(carryoverSourceDebug);
|
||||||
|
const sourceHasPurchaseDateBridgeHint = Boolean(
|
||||||
|
deps.toNonEmptyString(sourceFiltersHint?.purchase_date_bridge_selected)
|
||||||
|
);
|
||||||
const hasNavigationInventoryItemFocusHint = Boolean(
|
const hasNavigationInventoryItemFocusHint = Boolean(
|
||||||
deps.toNonEmptyString(navigationFocusObjectHint?.label) &&
|
deps.toNonEmptyString(navigationFocusObjectHint?.label) &&
|
||||||
deps.toNonEmptyString(navigationFocusObjectHint?.objectType) === "item" &&
|
deps.toNonEmptyString(navigationFocusObjectHint?.objectType) === "item" &&
|
||||||
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||||
|
sourceIntentHint === "vat_liability_confirmed_for_tax_period" ||
|
||||||
|
sourceIntentHint === "vat_payable_forecast" ||
|
||||||
|
sourceIntentHint === "vat_payable_confirmed_as_of_date" ||
|
||||||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
deps.isInventorySelectedObjectIntent(sourceIntentHint))
|
deps.isInventorySelectedObjectIntent(sourceIntentHint))
|
||||||
);
|
);
|
||||||
|
|
@ -943,7 +1024,8 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
userMessage,
|
userMessage,
|
||||||
alternateMessage,
|
alternateMessage,
|
||||||
sourceIntentHint,
|
sourceIntentHint,
|
||||||
hasNavigationInventoryItemFocusHint
|
hasNavigationInventoryItemFocusHint,
|
||||||
|
sourceHasPurchaseDateBridgeHint
|
||||||
);
|
);
|
||||||
const inventoryMarginRankingFollowup = hasInventoryMarginRankingFollowupSignal(
|
const inventoryMarginRankingFollowup = hasInventoryMarginRankingFollowupSignal(
|
||||||
userMessage,
|
userMessage,
|
||||||
|
|
@ -1457,10 +1539,21 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
readAddressDebugItem(carryoverSourceDebug, deps.toNonEmptyString) ??
|
readAddressDebugItem(carryoverSourceDebug, deps.toNonEmptyString) ??
|
||||||
deps.toNonEmptyString(previousFilters.item)
|
deps.toNonEmptyString(previousFilters.item)
|
||||||
) ?? previousAddressItem;
|
) ?? previousAddressItem;
|
||||||
const purchaseBridgeWindow = extractPurchaseDateBridgeWindow(purchaseBridgeItem, addressNavigationState);
|
const purchaseBridgeWindow = extractPurchaseDateBridgeWindow(
|
||||||
|
purchaseBridgeItem,
|
||||||
|
addressNavigationState,
|
||||||
|
userMessage,
|
||||||
|
alternateMessage
|
||||||
|
);
|
||||||
if (purchaseBridgeWindow) {
|
if (purchaseBridgeWindow) {
|
||||||
previousFilters.period_from = purchaseBridgeWindow.period_from;
|
previousFilters.period_from = purchaseBridgeWindow.period_from;
|
||||||
previousFilters.period_to = purchaseBridgeWindow.period_to;
|
previousFilters.period_to = purchaseBridgeWindow.period_to;
|
||||||
|
previousFilters.as_of_date = purchaseBridgeWindow.as_of_date;
|
||||||
|
previousFilters.purchase_date_bridge_selected = purchaseBridgeWindow.purchase_date_bridge_selected;
|
||||||
|
previousFilters.purchase_date_bridge_first = purchaseBridgeWindow.purchase_date_bridge_first;
|
||||||
|
previousFilters.purchase_date_bridge_last = purchaseBridgeWindow.purchase_date_bridge_last;
|
||||||
|
previousFilters.purchase_date_bridge_basis = purchaseBridgeWindow.purchase_date_bridge_basis;
|
||||||
|
previousFilters.purchase_date_bridge_has_multiple = purchaseBridgeWindow.purchase_date_bridge_has_multiple;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
previousFilters = applyTemporalCarryoverFilters(
|
previousFilters = applyTemporalCarryoverFilters(
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,11 @@ export interface AddressFilterSet {
|
||||||
warehouse?: string;
|
warehouse?: string;
|
||||||
document_type?: string;
|
document_type?: string;
|
||||||
document_ref?: string;
|
document_ref?: string;
|
||||||
|
purchase_date_bridge_selected?: string;
|
||||||
|
purchase_date_bridge_first?: string;
|
||||||
|
purchase_date_bridge_last?: string;
|
||||||
|
purchase_date_bridge_basis?: string;
|
||||||
|
purchase_date_bridge_has_multiple?: boolean;
|
||||||
status?: string;
|
status?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
sort?: "period_desc" | "period_asc";
|
sort?: "period_desc" | "period_asc";
|
||||||
|
|
|
||||||
|
|
@ -244,7 +244,12 @@ describe("address follow-up temporal regressions", () => {
|
||||||
organization: 'ООО "Альтернатива Плюс"',
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
period_from: "2015-02-01",
|
period_from: "2015-02-01",
|
||||||
period_to: "2015-02-28",
|
period_to: "2015-02-28",
|
||||||
as_of_date: "2016-03-31"
|
as_of_date: "2015-02-28",
|
||||||
|
purchase_date_bridge_selected: "2015-02-05",
|
||||||
|
purchase_date_bridge_first: "2015-02-05",
|
||||||
|
purchase_date_bridge_last: "2015-07-22",
|
||||||
|
purchase_date_bridge_basis: "first_confirmed_purchase_default",
|
||||||
|
purchase_date_bridge_has_multiple: true
|
||||||
},
|
},
|
||||||
previous_anchor_type: "item",
|
previous_anchor_type: "item",
|
||||||
previous_anchor_value: "Рабочая станция универсального специалиста"
|
previous_anchor_value: "Рабочая станция универсального специалиста"
|
||||||
|
|
@ -254,7 +259,98 @@ describe("address follow-up temporal regressions", () => {
|
||||||
expect(result?.intent.intent).toBe("vat_liability_confirmed_for_tax_period");
|
expect(result?.intent.intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||||
expect(result?.filters.extracted_filters.period_from).toBe("2015-02-01");
|
expect(result?.filters.extracted_filters.period_from).toBe("2015-02-01");
|
||||||
expect(result?.filters.extracted_filters.period_to).toBe("2015-02-28");
|
expect(result?.filters.extracted_filters.period_to).toBe("2015-02-28");
|
||||||
expect(result?.baseReasons).toContain("period_from_followup_context");
|
expect(result?.filters.extracted_filters.as_of_date).toBe("2015-02-28");
|
||||||
|
expect(result?.filters.extracted_filters.purchase_date_bridge_selected).toBe("2015-02-05");
|
||||||
|
expect(result?.filters.extracted_filters.purchase_date_bridge_has_multiple).toBe(true);
|
||||||
|
expect(result?.baseReasons).toContain("period_from_purchase_date_bridge_followup_context");
|
||||||
|
expect(result?.baseReasons).toContain("purchase_date_bridge_from_followup_context");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not reuse purchase-date VAT bridge when the next VAT question has an explicit period", () => {
|
||||||
|
const result = runAddressDecomposeStage("а какой ндс мы должны сгрузить на март 2020?", {
|
||||||
|
previous_intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
target_intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
previous_filters: {
|
||||||
|
item: "Рабочая станция универсального специалиста",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
period_from: "2015-02-01",
|
||||||
|
period_to: "2015-02-28",
|
||||||
|
as_of_date: "2015-02-28",
|
||||||
|
purchase_date_bridge_selected: "2015-02-05",
|
||||||
|
purchase_date_bridge_first: "2015-02-05",
|
||||||
|
purchase_date_bridge_last: "2015-07-22",
|
||||||
|
purchase_date_bridge_basis: "first_confirmed_purchase_default",
|
||||||
|
purchase_date_bridge_has_multiple: true
|
||||||
|
},
|
||||||
|
previous_anchor_type: "organization",
|
||||||
|
previous_anchor_value: 'ООО "Альтернатива Плюс"'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.intent.intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||||
|
expect(result?.filters.extracted_filters.period_from).toBe("2020-03-01");
|
||||||
|
expect(result?.filters.extracted_filters.period_to).toBe("2020-03-31");
|
||||||
|
expect(result?.filters.extracted_filters.as_of_date).toBeUndefined();
|
||||||
|
expect(result?.filters.extracted_filters.purchase_date_bridge_selected).toBeUndefined();
|
||||||
|
expect(result?.baseReasons).toContain("purchase_date_bridge_suppressed_by_explicit_temporal_scope");
|
||||||
|
expect(result?.baseReasons).not.toContain("period_from_purchase_date_bridge_followup_context");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps first confirmed purchase VAT recalculation in the VAT lane without repeating VAT wording", () => {
|
||||||
|
const result = runAddressDecomposeStage("если у нее несколько закупок, посчитай по первой подтвержденной покупке", {
|
||||||
|
previous_intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
target_intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
previous_filters: {
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
period_from: "2015-02-01",
|
||||||
|
period_to: "2015-02-28",
|
||||||
|
as_of_date: "2015-02-28",
|
||||||
|
purchase_date_bridge_selected: "2015-02-05",
|
||||||
|
purchase_date_bridge_first: "2015-02-05",
|
||||||
|
purchase_date_bridge_last: "2015-07-22",
|
||||||
|
purchase_date_bridge_basis: "first_confirmed_purchase_default",
|
||||||
|
purchase_date_bridge_has_multiple: true
|
||||||
|
},
|
||||||
|
previous_anchor_type: "organization",
|
||||||
|
previous_anchor_value: 'ООО "Альтернатива Плюс"'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.intent.intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||||
|
expect(result?.filters.extracted_filters.purchase_date_bridge_selected).toBe("2015-02-05");
|
||||||
|
expect(result?.filters.extracted_filters.purchase_date_bridge_basis).toBe("first_confirmed_purchase");
|
||||||
|
expect(result?.filters.extracted_filters.period_from).toBe("2015-02-01");
|
||||||
|
expect(result?.filters.extracted_filters.period_to).toBe("2015-02-28");
|
||||||
|
expect(result?.baseReasons).toContain("intent_adjusted_to_inventory_purchase_date_vat_bridge");
|
||||||
|
expect(result?.baseReasons).toContain("purchase_date_bridge_from_followup_context");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps last confirmed purchase VAT recalculation and moves the tax period to the last purchase month", () => {
|
||||||
|
const result = runAddressDecomposeStage("а теперь по последней подтвержденной покупке", {
|
||||||
|
previous_intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
target_intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
previous_filters: {
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
period_from: "2015-02-01",
|
||||||
|
period_to: "2015-02-28",
|
||||||
|
as_of_date: "2015-02-28",
|
||||||
|
purchase_date_bridge_selected: "2015-02-05",
|
||||||
|
purchase_date_bridge_first: "2015-02-05",
|
||||||
|
purchase_date_bridge_last: "2015-07-22",
|
||||||
|
purchase_date_bridge_basis: "first_confirmed_purchase",
|
||||||
|
purchase_date_bridge_has_multiple: true
|
||||||
|
},
|
||||||
|
previous_anchor_type: "organization",
|
||||||
|
previous_anchor_value: 'ООО "Альтернатива Плюс"'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.intent.intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||||
|
expect(result?.filters.extracted_filters.purchase_date_bridge_selected).toBe("2015-07-22");
|
||||||
|
expect(result?.filters.extracted_filters.purchase_date_bridge_basis).toBe("last_confirmed_purchase");
|
||||||
|
expect(result?.filters.extracted_filters.period_from).toBe("2015-07-01");
|
||||||
|
expect(result?.filters.extracted_filters.period_to).toBe("2015-07-31");
|
||||||
|
expect(result?.filters.extracted_filters.as_of_date).toBe("2015-07-31");
|
||||||
});
|
});
|
||||||
it("clears counterparty noise from bare organization clarification selections that resume inventory", () => {
|
it("clears counterparty noise from bare organization clarification selections that resume inventory", () => {
|
||||||
const result = runAddressDecomposeStage("АЛЬТЕРНАТИВА", {
|
const result = runAddressDecomposeStage("АЛЬТЕРНАТИВА", {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,44 @@ import { runAddressDecomposeStage } from "../src/services/address_runtime/decomp
|
||||||
import { composeFactualReply } from "../src/services/address_runtime/composeStage";
|
import { composeFactualReply } from "../src/services/address_runtime/composeStage";
|
||||||
|
|
||||||
describe("inventory aging follow-up", () => {
|
describe("inventory aging follow-up", () => {
|
||||||
|
it("clears organization alias item on root old-stock questions with LLM organization semantics", () => {
|
||||||
|
const result = runAddressDecomposeStage(
|
||||||
|
'Есть ли у ООО "Альтернатива Плюс" остатки товара, которые закупались очень давно',
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
scope_target_kind: "organization",
|
||||||
|
scope_target_text: 'ООО "Альтернатива Плюс"',
|
||||||
|
date_scope_kind: "implicit_current",
|
||||||
|
self_scope_detected: false,
|
||||||
|
selected_object_scope_detected: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.intent.intent).toBe("inventory_aging_by_purchase_date");
|
||||||
|
expect(result?.filters.extracted_filters.organization).toBe('ООО "Альтернатива Плюс"');
|
||||||
|
expect(result?.filters.extracted_filters.item).toBeUndefined();
|
||||||
|
expect(result?.baseReasons).toContain("item_cleared_as_organization_scope_alias_for_stock_aging");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not mistake an explicit organization name for an inventory item in old-stock questions", () => {
|
||||||
|
const result = runAddressDecomposeStage('Есть ли у ООО "Альтернатива Плюс" остатки товара, которые закупались очень давно', {
|
||||||
|
previous_intent: "inventory_aging_by_purchase_date",
|
||||||
|
previous_filters: {
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
as_of_date: "2026-05-25"
|
||||||
|
},
|
||||||
|
previous_anchor_type: "organization",
|
||||||
|
previous_anchor_value: 'ООО "Альтернатива Плюс"'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.intent.intent).toBe("inventory_aging_by_purchase_date");
|
||||||
|
expect(result?.filters.extracted_filters.organization).toBe('ООО "Альтернатива Плюс"');
|
||||||
|
expect(result?.filters.extracted_filters.item).toBeUndefined();
|
||||||
|
expect(result?.baseReasons).toContain("item_cleared_as_organization_scope_alias_for_stock_aging");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps carried date window for 'на эту дату' aging follow-up", () => {
|
it("keeps carried date window for 'на эту дату' aging follow-up", () => {
|
||||||
const result = runAddressDecomposeStage("Какие остатки по товарам на эту дату относятся к старым закупкам", {
|
const result = runAddressDecomposeStage("Какие остатки по товарам на эту дату относятся к старым закупкам", {
|
||||||
previous_intent: "inventory_purchase_provenance_for_item",
|
previous_intent: "inventory_purchase_provenance_for_item",
|
||||||
|
|
|
||||||
|
|
@ -2182,6 +2182,49 @@ describe("address compose stage utf8 headers", () => {
|
||||||
expect(reply.text).toContain("книг продаж/покупок");
|
expect(reply.text).toContain("книг продаж/покупок");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("discloses ambiguous inventory purchase-date VAT bridge instead of hiding the selected date", () => {
|
||||||
|
const reply = composeFactualReply(
|
||||||
|
"vat_liability_confirmed_for_tax_period",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
period: "2015-02-28T23:59:59Z",
|
||||||
|
registrator: "VAT_BOOK_SALES",
|
||||||
|
account_dt: "68.02",
|
||||||
|
account_kt: "",
|
||||||
|
amount: 3500000,
|
||||||
|
analytics: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
period: "2015-02-28T23:59:59Z",
|
||||||
|
registrator: "VAT_BOOK_PURCHASES",
|
||||||
|
account_dt: "19",
|
||||||
|
account_kt: "",
|
||||||
|
amount: 868612,
|
||||||
|
analytics: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
userMessage: "ндс можешь прикинуть на дату покупки рабочей станции?",
|
||||||
|
periodFrom: "2015-02-01",
|
||||||
|
periodTo: "2015-02-28",
|
||||||
|
asOfDate: "2015-02-28",
|
||||||
|
organizationHint: "ООО Альтернатива Плюс",
|
||||||
|
purchaseDateBridge: {
|
||||||
|
selectedPurchaseDate: "2015-02-05",
|
||||||
|
firstPurchaseDate: "2015-02-05",
|
||||||
|
lastPurchaseDate: "2015-07-22",
|
||||||
|
basis: "first_confirmed_purchase_default",
|
||||||
|
hasMultiplePurchaseDates: true
|
||||||
|
},
|
||||||
|
useRubCurrency: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(reply.text).toContain("если брать первую подтвержденную дату закупки 05.02.2015");
|
||||||
|
expect(reply.text).toContain("у позиции несколько подтвержденных дат закупки");
|
||||||
|
expect(reply.text).toContain("05.02.2015..22.07.2015");
|
||||||
|
});
|
||||||
|
|
||||||
it("formats VAT forecast amounts in rubles and emphasizes numbers when requested", () => {
|
it("formats VAT forecast amounts in rubles and emphasizes numbers when requested", () => {
|
||||||
const reply = composeFactualReply(
|
const reply = composeFactualReply(
|
||||||
"vat_payable_forecast",
|
"vat_payable_forecast",
|
||||||
|
|
@ -5741,6 +5784,24 @@ it("routes old purchase residue questions to aging-by-purchase-date", () => {
|
||||||
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Остатки.Счет.Код, \"\"), 1, 5) = \"41.01\"");
|
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Остатки.Счет.Код, \"\"), 1, 5) = \"41.01\"");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("builds old-stock aging query from positive stock snapshot plus purchase history", () => {
|
||||||
|
const selected = selectAddressRecipe("inventory_aging_by_purchase_date", {
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
as_of_date: "2026-04-18"
|
||||||
|
});
|
||||||
|
expect(selected.selected_recipe?.recipe_id).toBe("address_inventory_aging_by_purchase_date_v1");
|
||||||
|
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
as_of_date: "2026-04-18"
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(plan.query).toContain("РегистрБухгалтерии.Хозрасчетный.Остатки(");
|
||||||
|
expect(plan.query).toContain("Документ.ПоступлениеТоваровУслуг.Товары");
|
||||||
|
expect(plan.query).toContain("Остатки.КоличествоРазвернутыйОстатокДт > 0");
|
||||||
|
expect(plan.query).toContain("Товары.Номенклатура В");
|
||||||
|
expect(plan.query).toContain("Товары.Ссылка.Дата <= ДАТАВРЕМЯ(2026, 4, 18, 23, 59, 59)");
|
||||||
|
});
|
||||||
|
|
||||||
it("renders confirmed inventory-on-hand snapshot from normalized rows", () => {
|
it("renders confirmed inventory-on-hand snapshot from normalized rows", () => {
|
||||||
const reply = composeFactualReply(
|
const reply = composeFactualReply(
|
||||||
"inventory_on_hand_as_of_date",
|
"inventory_on_hand_as_of_date",
|
||||||
|
|
@ -5776,6 +5837,59 @@ it("routes old purchase residue questions to aging-by-purchase-date", () => {
|
||||||
expect(reply.semantics?.balance_confirmed).toBe(true);
|
expect(reply.semantics?.balance_confirmed).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders old-stock aging only for items that are still in positive stock", () => {
|
||||||
|
const reply = composeFactualReply(
|
||||||
|
"inventory_aging_by_purchase_date",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
period: "2015-02-05T00:00:00Z",
|
||||||
|
registrator: "Поступление товаров и услуг 00000000001 от 05.02.2015",
|
||||||
|
account_dt: "41.01",
|
||||||
|
account_kt: "",
|
||||||
|
amount: 1000,
|
||||||
|
analytics: ["Старый проданный товар", 'ООО "Альтернатива Плюс"', "Поставщик А"],
|
||||||
|
item: "Старый проданный товар",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
counterparty: "Поставщик А",
|
||||||
|
quantity: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
period: "2017-03-10T00:00:00Z",
|
||||||
|
registrator: "Поступление товаров и услуг 00000000002 от 10.03.2017",
|
||||||
|
account_dt: "41.01",
|
||||||
|
account_kt: "",
|
||||||
|
amount: 2500,
|
||||||
|
analytics: ["Товар в остатке", 'ООО "Альтернатива Плюс"', "Поставщик Б"],
|
||||||
|
item: "Товар в остатке",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
counterparty: "Поставщик Б",
|
||||||
|
quantity: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
period: "2026-04-18T23:59:59Z",
|
||||||
|
registrator: "Остатки на дату",
|
||||||
|
account_dt: "41.01",
|
||||||
|
account_kt: "",
|
||||||
|
amount: 2500,
|
||||||
|
analytics: ["Товар в остатке", "Основной склад", 'ООО "Альтернатива Плюс"'],
|
||||||
|
item: "Товар в остатке",
|
||||||
|
warehouse: "Основной склад",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
quantity: 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
asOfDate: "2026-04-18",
|
||||||
|
useRubCurrency: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
||||||
|
expect(reply.text.split("\n")[0]).toContain("Товар в остатке");
|
||||||
|
expect(reply.text).toContain("положительным остатком");
|
||||||
|
expect(reply.text).not.toContain("Старый проданный товар");
|
||||||
|
});
|
||||||
|
|
||||||
it("sorts inventory-on-hand positions by amount before rendering the top list", () => {
|
it("sorts inventory-on-hand positions by amount before rendering the top list", () => {
|
||||||
const reply = composeFactualReply(
|
const reply = composeFactualReply(
|
||||||
"inventory_on_hand_as_of_date",
|
"inventory_on_hand_as_of_date",
|
||||||
|
|
|
||||||
|
|
@ -270,7 +270,7 @@ describe("address reply builders regressions", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const firstLine = result?.text.split("\n")[0] ?? "";
|
const firstLine = result?.text.split("\n")[0] ?? "";
|
||||||
expect(firstLine).toContain("К самым старым закупкам");
|
expect(firstLine).toContain("фактических положительных остатков");
|
||||||
expect(firstLine).not.toContain("2026-04-18");
|
expect(firstLine).not.toContain("2026-04-18");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -245,4 +245,90 @@ describe("assistant deep turn response runtime adapter", () => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sanitizes guarded MCP discovery candidates before finalizing the visible reply", () => {
|
||||||
|
const runPackagingRuntime = vi.fn(() => ({
|
||||||
|
messageId: "msg-a1",
|
||||||
|
investigationStateSnapshot: null,
|
||||||
|
droppedIntentSegments: [],
|
||||||
|
analysisContextForContract: null,
|
||||||
|
routesForDebug: [],
|
||||||
|
resolvedExecutionState: [],
|
||||||
|
safeAssistantReplyBase: "bad-base",
|
||||||
|
safeAssistantReply: "bad deep partial answer",
|
||||||
|
debug: { trace_id: "trace-1" },
|
||||||
|
assistantItem: {
|
||||||
|
message_id: "msg-a1",
|
||||||
|
session_id: "asst-1",
|
||||||
|
role: "assistant",
|
||||||
|
text: "bad deep partial answer",
|
||||||
|
reply_type: "partial_coverage",
|
||||||
|
created_at: "2026-04-10T00:00:00.000Z",
|
||||||
|
trace_id: "trace-1",
|
||||||
|
debug: null
|
||||||
|
},
|
||||||
|
deepAnalysisLogDetails: {}
|
||||||
|
}));
|
||||||
|
const sanitizeReply = vi.fn((value: string) =>
|
||||||
|
value.replace(/^\u041a\u043e\u0440\u043e\u0442\u043a\u043e:\s*/u, "")
|
||||||
|
);
|
||||||
|
const runFinalizeDeepTurn = vi.fn((input) => ({
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
session_id: "asst-1",
|
||||||
|
assistant_reply: input.assistantReply,
|
||||||
|
reply_type: input.replyType,
|
||||||
|
conversation_item: input.assistantItem,
|
||||||
|
conversation: [],
|
||||||
|
debug: input.debug
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const runtime = runAssistantDeepTurnResponseRuntime(
|
||||||
|
buildBaseInput({
|
||||||
|
composition: { reply_type: "partial_coverage" },
|
||||||
|
sanitizeReply,
|
||||||
|
addressRuntimeMetaForDeep: {
|
||||||
|
mcpDiscoveryRuntimeEntryPoint: {
|
||||||
|
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||||
|
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
|
||||||
|
entry_status: "bridge_executed",
|
||||||
|
hot_runtime_wired: false,
|
||||||
|
discovery_attempted: true,
|
||||||
|
turn_input: { should_run_discovery: true },
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
user_facing_response_allowed: true,
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
requires_user_clarification: false,
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference",
|
||||||
|
headline: "Коротко: Discovery answer",
|
||||||
|
confirmed_lines: ["Confirmed fact"],
|
||||||
|
inference_lines: [],
|
||||||
|
unknown_lines: [],
|
||||||
|
limitation_lines: [],
|
||||||
|
next_step_line: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reason_codes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
runPackagingRuntime,
|
||||||
|
runFinalizeDeepTurn
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(sanitizeReply).toHaveBeenCalledWith(expect.not.stringContaining("Коротко:"), "bad deep partial answer");
|
||||||
|
expect(runtime.response.assistant_reply).toContain("Discovery answer");
|
||||||
|
expect(runtime.response.assistant_reply).not.toContain("Коротко:");
|
||||||
|
expect(runFinalizeDeepTurn).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
assistantReply: expect.not.stringContaining("Коротко:"),
|
||||||
|
assistantItem: expect.objectContaining({
|
||||||
|
text: expect.not.stringContaining("Коротко:")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,37 @@ describe("assistant MCP discovery response policy", () => {
|
||||||
expect(result.reason_codes).toContain("mcp_discovery_response_policy_candidate_applied");
|
expect(result.reason_codes).toContain("mcp_discovery_response_policy_candidate_applied");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("removes routine short-answer prefix from applied guarded candidates", () => {
|
||||||
|
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||||
|
currentReply: "route is not wired",
|
||||||
|
currentReplySource: "deterministic_unsupported_current_turn_boundary",
|
||||||
|
modeDecisionReason: "unsupported_current_turn_meaning_boundary",
|
||||||
|
addressRuntimeMeta: {
|
||||||
|
mcpDiscoveryRuntimeEntryPoint: entryPoint({
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
user_facing_response_allowed: true,
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
requires_user_clarification: false,
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference",
|
||||||
|
headline: "Коротко: Confirmed scoped answer.",
|
||||||
|
confirmed_lines: ["Коротко: Confirmed fact"],
|
||||||
|
inference_lines: [],
|
||||||
|
unknown_lines: [],
|
||||||
|
limitation_lines: [],
|
||||||
|
next_step_line: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.applied).toBe(true);
|
||||||
|
expect(result.reply_text).toContain("Confirmed scoped answer.");
|
||||||
|
expect(result.reply_text).not.toContain("Коротко:");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps the current reply when the turn is not an unsupported boundary", () => {
|
it("keeps the current reply when the turn is not an unsupported boundary", () => {
|
||||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||||
currentReply: "regular chat",
|
currentReply: "regular chat",
|
||||||
|
|
|
||||||
|
|
@ -214,6 +214,152 @@ describe("assistantTransitionPolicy", () => {
|
||||||
expect(contract.decision).toBe("continue_previous");
|
expect(contract.decision).toBe("continue_previous");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("carries explicit purchase-date VAT bridge facts without silently losing ambiguity", () => {
|
||||||
|
const policy = buildPolicy({
|
||||||
|
findLastAddressAssistantItem: () => ({
|
||||||
|
role: "assistant",
|
||||||
|
text: [
|
||||||
|
"По позиции Рабочая станция до 31.03.2016 однозначный поставщик не подтвержден.",
|
||||||
|
"- Первая найденная дата закупки: 05.02.2015.",
|
||||||
|
"- Последняя найденная дата закупки: 22.07.2015."
|
||||||
|
].join("\n"),
|
||||||
|
debug: {
|
||||||
|
detected_intent: "inventory_purchase_provenance_for_item",
|
||||||
|
extracted_filters: {
|
||||||
|
item: "Рабочая станция",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
as_of_date: "2016-03-31"
|
||||||
|
},
|
||||||
|
anchor_type: "item",
|
||||||
|
anchor_value_resolved: "Рабочая станция"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||||
|
"ндс можешь прикинуть на дату покупки рабочей станции?",
|
||||||
|
[],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(carryover?.followupContext?.target_intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||||
|
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||||||
|
period_from: "2015-02-01",
|
||||||
|
period_to: "2015-02-28",
|
||||||
|
as_of_date: "2015-02-28",
|
||||||
|
purchase_date_bridge_selected: "2015-02-05",
|
||||||
|
purchase_date_bridge_first: "2015-02-05",
|
||||||
|
purchase_date_bridge_last: "2015-07-22",
|
||||||
|
purchase_date_bridge_basis: "first_confirmed_purchase_default",
|
||||||
|
purchase_date_bridge_has_multiple: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can retarget a follow-up VAT recalculation to the last confirmed purchase date", () => {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
text: [
|
||||||
|
"По позиции Рабочая станция до 31.03.2016 однозначный поставщик не подтвержден.",
|
||||||
|
"- Первая найденная дата закупки: 05.02.2015.",
|
||||||
|
"- Последняя найденная дата закупки: 22.07.2015."
|
||||||
|
].join("\n"),
|
||||||
|
debug: {
|
||||||
|
detected_intent: "inventory_purchase_provenance_for_item",
|
||||||
|
extracted_filters: {
|
||||||
|
item: "Рабочая станция",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
text: "Коротко: если брать первую подтвержденную дату закупки 05.02.2015, НДС к уплате 100.",
|
||||||
|
debug: {
|
||||||
|
detected_intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
extracted_filters: {
|
||||||
|
item: "Рабочая станция",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
purchase_date_bridge_selected: "2015-02-05"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const policy = buildPolicy({
|
||||||
|
findLastAddressAssistantItem: (sourceItems: unknown[]) => {
|
||||||
|
const list = sourceItems as Record<string, unknown>[];
|
||||||
|
return list[list.length - 1];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||||
|
"а теперь по последней подтвержденной покупке",
|
||||||
|
items,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
session_context: {
|
||||||
|
active_focus_object: {
|
||||||
|
object_type: "item",
|
||||||
|
label: "Рабочая станция"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(carryover?.followupContext?.target_intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||||
|
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||||||
|
period_from: "2015-07-01",
|
||||||
|
period_to: "2015-07-31",
|
||||||
|
as_of_date: "2015-07-31",
|
||||||
|
purchase_date_bridge_selected: "2015-07-22",
|
||||||
|
purchase_date_bridge_basis: "last_confirmed_purchase"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("continues purchase-date VAT recalculation from prior VAT bridge filters without relying on active item focus", () => {
|
||||||
|
const policy = buildPolicy({
|
||||||
|
findLastAddressAssistantItem: () => ({
|
||||||
|
role: "assistant",
|
||||||
|
text: "Коротко: если брать первую подтвержденную дату закупки 05.02.2015, НДС к уплате 100.",
|
||||||
|
debug: {
|
||||||
|
detected_intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
extracted_filters: {
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
period_from: "2015-02-01",
|
||||||
|
period_to: "2015-02-28",
|
||||||
|
as_of_date: "2015-02-28",
|
||||||
|
purchase_date_bridge_selected: "2015-02-05",
|
||||||
|
purchase_date_bridge_first: "2015-02-05",
|
||||||
|
purchase_date_bridge_last: "2015-07-22",
|
||||||
|
purchase_date_bridge_basis: "first_confirmed_purchase_default",
|
||||||
|
purchase_date_bridge_has_multiple: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||||
|
"если у нее несколько закупок, посчитай по первой подтвержденной покупке",
|
||||||
|
[],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(carryover?.followupContext?.target_intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||||
|
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||||||
|
period_from: "2015-02-01",
|
||||||
|
period_to: "2015-02-28",
|
||||||
|
as_of_date: "2015-02-28",
|
||||||
|
purchase_date_bridge_selected: "2015-02-05",
|
||||||
|
purchase_date_bridge_basis: "first_confirmed_purchase",
|
||||||
|
purchase_date_bridge_has_multiple: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("retargets displayed counterparty follow-up through shared referenced-entity carryover", () => {
|
it("retargets displayed counterparty follow-up through shared referenced-entity carryover", () => {
|
||||||
const policy = buildPolicy({
|
const policy = buildPolicy({
|
||||||
findLastAddressAssistantItem: () => ({
|
findLastAddressAssistantItem: () => ({
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue