diff --git a/docs/orchestration/address_truth_harness_inventory_supplier_overlap_next_action.json b/docs/orchestration/address_truth_harness_inventory_supplier_overlap_next_action.json new file mode 100644 index 0000000..d78bbe5 --- /dev/null +++ b/docs/orchestration/address_truth_harness_inventory_supplier_overlap_next_action.json @@ -0,0 +1,81 @@ +{ + "schema_version": "domain_truth_harness_spec_v1", + "scenario_id": "address_truth_harness_inventory_supplier_overlap_next_action", + "domain": "inventory_supplier_overlap_next_action", + "title": "Supplier-overlap limited answer includes a concrete next action", + "description": "Targeted replay for the inventory supplier-overlap surface: when supplier attribution is limited by missing party-level proof, the answer must still give a concrete next action instead of stopping at a limitation.", + "bindings": {}, + "steps": [ + { + "step_id": "step_01_current_stock_anchor", + "title": "Current stock anchor", + "question": "какие остатки на складе на сентябрь 2021", + "allowed_reply_types": [ + "factual" + ], + "expected_intents": [ + "inventory_on_hand_as_of_date" + ], + "required_filters": { + "as_of_date": "2021-09-30", + "period_from": "2021-09-01", + "period_to": "2021-09-30" + }, + "required_answer_patterns_all": [ + "(?i)30\\.09\\.2021|сентябр", + "(?i)склад|остат" + ], + "forbidden_answer_patterns": [ + "(?i)route_id|capability_id|runtime_|snapshot_items|answer_object|debug" + ] + }, + { + "step_id": "step_02_supplier_overlap_now", + "title": "Supplier overlap now", + "question": "У какого поставщика были куплены товары, которые лежали на складе Основной склад на 30.09.2021", + "allowed_reply_types": [ + "factual" + ], + "expected_intents": [ + "inventory_supplier_stock_overlap_as_of_date" + ], + "required_filters": { + "as_of_date": "2021-09-30" + }, + "required_answer_patterns_all": [ + "(?i)30\\.09\\.2021", + "(?i)поставщик|поставщиков|закупочн", + "(?i)не подтвержден|ограничен|партионн", + "(?i)следующий шаг|проверьте|детализац" + ], + "forbidden_answer_patterns": [ + "(?i)03\\.06\\.2026", + "(?i)route_id|capability_id|runtime_|snapshot_items|answer_object|debug" + ] + }, + { + "step_id": "step_03_supplier_residue_now", + "title": "Supplier residue wording", + "question": "По какому поставщику проходит товарный остаток на складе Основной склад на 30.09.2021", + "allowed_reply_types": [ + "factual" + ], + "expected_intents": [ + "inventory_supplier_stock_overlap_as_of_date" + ], + "required_filters": { + "as_of_date": "2021-09-30" + }, + "required_answer_patterns_all": [ + "(?i)30\\.09\\.2021", + "(?i)поставщик|поставщиков|закупочн", + "(?i)не подтвержден|ограничен|партионн", + "(?i)следующий шаг|проверьте|детализац" + ], + "forbidden_answer_patterns": [ + "(?i)03\\.06\\.2026", + "(?i)route_id|capability_id|runtime_|snapshot_items|answer_object|debug" + ] + } + ] +} diff --git a/llm_normalizer/backend/dist/services/address_runtime/inventoryReplyBuilders.js b/llm_normalizer/backend/dist/services/address_runtime/inventoryReplyBuilders.js index 5f6a6d8..2dc17d7 100644 --- a/llm_normalizer/backend/dist/services/address_runtime/inventoryReplyBuilders.js +++ b/llm_normalizer/backend/dist/services/address_runtime/inventoryReplyBuilders.js @@ -4,6 +4,7 @@ exports.composeInventoryReply = composeInventoryReply; const replyContracts_1 = require("./replyContracts"); const inventoryReplyPresentation_1 = require("./inventoryReplyPresentation"); const INVENTORY_TRACE_EVIDENCE_ROW_LIMIT = 3; +const INVENTORY_SUPPLIER_ATTRIBUTION_NEXT_ACTION = "Следующий шаг: чтобы сузить доказательство до партии, проверьте партионный/серийный учет или детализацию по выбранной номенклатуре, поступлению и последующему движению."; function cleanupInventoryRequestedParty(value) { const cleaned = String(value ?? "") .replace(/\s*(?:->|=>|→)\s*(?:товар|позици|номенклатур|покупател|buyer|customer|item|product|sku)[\s\S]*$/iu, "") @@ -519,7 +520,8 @@ function composeInventoryReply(intent, rows, options, deps) { `Поставщиков, выделенных в остальных операциях: ${deps.formatNumberWithDots(observedCounterparties.length)}.` ]); (0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Ограничения:", [ - "Без партионного учета это проверка доступного закупочного следа по складскому срезу, а не юридическое доказательство владельца каждой партии." + "Без партионного учета это проверка доступного закупочного следа по складскому срезу, а не юридическое доказательство владельца каждой партии.", + INVENTORY_SUPPLIER_ATTRIBUTION_NEXT_ACTION ]); if (unresolvedRows.length > 0) { (0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Опорные документы:", deps.formatInventoryTraceRows(unresolvedRows, unresolvedDocumentPreviewLimit)); @@ -556,7 +558,8 @@ function composeInventoryReply(intent, rows, options, deps) { `Закупочных документов / операций в выборке: ${deps.formatNumberWithDots(summary.documents.length)} / ${deps.formatNumberWithDots(purchaseRows.length)}.` ]); (0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Ограничения:", [ - "Без партионного учета этот ответ показывает закупочный след текущего остатка, но не доказывает владельца каждой конкретной партии." + "Без партионного учета этот ответ показывает закупочный след текущего остатка, но не доказывает владельца каждой конкретной партии.", + INVENTORY_SUPPLIER_ATTRIBUTION_NEXT_ACTION ]); if (observedCounterparties.length > 0) { lines.push(`- Найденные поставщики: ${inventoryPartyLeadList(observedCounterparties)}.`); @@ -586,7 +589,8 @@ function composeInventoryReply(intent, rows, options, deps) { `Закупочных документов / операций в выборке: ${deps.formatNumberWithDots(summary.documents.length)} / ${deps.formatNumberWithDots(purchaseRows.length)}.` ]); (0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Ограничения:", [ - "Без партионного учета этот ответ показывает закупочный след текущего остатка, но не доказывает владельца каждой конкретной партии." + "Без партионного учета этот ответ показывает закупочный след текущего остатка, но не доказывает владельца каждой конкретной партии.", + INVENTORY_SUPPLIER_ATTRIBUTION_NEXT_ACTION ]); if (observedCounterparties.length > 0) { lines.push(`- Найденные поставщики: ${inventoryPartyLeadList(observedCounterparties)}.`); diff --git a/llm_normalizer/backend/src/services/address_runtime/inventoryReplyBuilders.ts b/llm_normalizer/backend/src/services/address_runtime/inventoryReplyBuilders.ts index 76ad5be..2ab23ca 100644 --- a/llm_normalizer/backend/src/services/address_runtime/inventoryReplyBuilders.ts +++ b/llm_normalizer/backend/src/services/address_runtime/inventoryReplyBuilders.ts @@ -48,6 +48,8 @@ interface InventoryTraceSummary { } const INVENTORY_TRACE_EVIDENCE_ROW_LIMIT = 3; +const INVENTORY_SUPPLIER_ATTRIBUTION_NEXT_ACTION = + "Следующий шаг: чтобы сузить доказательство до партии, проверьте партионный/серийный учет или детализацию по выбранной номенклатуре, поступлению и последующему движению."; interface InventoryAgingByItemAggregate { item: string; @@ -712,7 +714,8 @@ export function composeInventoryReply( `Поставщиков, выделенных в остальных операциях: ${deps.formatNumberWithDots(observedCounterparties.length)}.` ]); appendInventoryBulletSection(lines, "Ограничения:", [ - "Без партионного учета это проверка доступного закупочного следа по складскому срезу, а не юридическое доказательство владельца каждой партии." + "Без партионного учета это проверка доступного закупочного следа по складскому срезу, а не юридическое доказательство владельца каждой партии.", + INVENTORY_SUPPLIER_ATTRIBUTION_NEXT_ACTION ]); if (unresolvedRows.length > 0) { appendInventorySection( @@ -763,7 +766,8 @@ export function composeInventoryReply( `Закупочных документов / операций в выборке: ${deps.formatNumberWithDots(summary.documents.length)} / ${deps.formatNumberWithDots(purchaseRows.length)}.` ]); appendInventoryBulletSection(lines, "Ограничения:", [ - "Без партионного учета этот ответ показывает закупочный след текущего остатка, но не доказывает владельца каждой конкретной партии." + "Без партионного учета этот ответ показывает закупочный след текущего остатка, но не доказывает владельца каждой конкретной партии.", + INVENTORY_SUPPLIER_ATTRIBUTION_NEXT_ACTION ]); if (observedCounterparties.length > 0) { lines.push(`- Найденные поставщики: ${inventoryPartyLeadList(observedCounterparties)}.`); @@ -808,7 +812,8 @@ export function composeInventoryReply( `Закупочных документов / операций в выборке: ${deps.formatNumberWithDots(summary.documents.length)} / ${deps.formatNumberWithDots(purchaseRows.length)}.` ]); appendInventoryBulletSection(lines, "Ограничения:", [ - "Без партионного учета этот ответ показывает закупочный след текущего остатка, но не доказывает владельца каждой конкретной партии." + "Без партионного учета этот ответ показывает закупочный след текущего остатка, но не доказывает владельца каждой конкретной партии.", + INVENTORY_SUPPLIER_ATTRIBUTION_NEXT_ACTION ]); if (observedCounterparties.length > 0) { lines.push(`- Найденные поставщики: ${inventoryPartyLeadList(observedCounterparties)}.`); diff --git a/llm_normalizer/backend/tests/addressReplyBuildersRegression.test.ts b/llm_normalizer/backend/tests/addressReplyBuildersRegression.test.ts index 4b814dc..ea359a9 100644 --- a/llm_normalizer/backend/tests/addressReplyBuildersRegression.test.ts +++ b/llm_normalizer/backend/tests/addressReplyBuildersRegression.test.ts @@ -900,6 +900,8 @@ describe("address reply builders regressions", () => { expect(firstLine).toContain("ВИЗАНТИЯ"); expect(text.indexOf(firstLine)).toBeLessThan(text.indexOf("Что проверили:")); expect(text).toContain("однозначная атрибуция части текущего остатка не подтверждена: 1 закупочных операций без явно выделенного поставщика."); + expect(text).toContain("Следующий шаг:"); + expect(text).toContain("проверьте партионный/серийный учет"); expect(text).not.toContain("Часть закупочных операций"); expect(text.match(/^\d+\./gmu)?.length ?? 0).toBeLessThanOrEqual(3); }); diff --git a/llm_normalizer/data/autorun_generators/history.json b/llm_normalizer/data/autorun_generators/history.json index e0da456..88257f8 100644 --- a/llm_normalizer/data/autorun_generators/history.json +++ b/llm_normalizer/data/autorun_generators/history.json @@ -1,4 +1,40 @@ [ + { + "generation_id": "gen-ag06030841-d61275", + "created_at": "2026-06-03T08:41:17+00:00", + "mode": "saved_user_sessions", + "title": "AGENT | Supplier-overlap limited answer includes a concrete next action", + "count": 3, + "domain": "inventory_supplier_overlap_next_action", + "questions": [ + "какие остатки на складе на сентябрь 2021", + "У какого поставщика были куплены товары, которые лежали на складе Основной склад на 30.09.2021", + "По какому поставщику проходит товарный остаток на складе Основной склад на 30.09.2021" + ], + "generated_by": "codex_agent", + "saved_case_set_file": "assistant_autogen_saved_user_sessions_20260603084117_gen-ag06030841-d61275.json", + "context": { + "llm_provider": null, + "model": null, + "assistant_prompt_version": null, + "decomposition_prompt_version": null, + "prompt_fingerprint": null, + "autogen_personality_id": null, + "autogen_personality_prompt": null, + "source_session_id": null, + "saved_session_file": "assistant_saved_session_20260603084117_gen-ag06030841-d61275.json", + "saved_case_set_kind": "agent_semantic_scenario", + "agent_run": true, + "agent_focus": "Targeted replay for the inventory supplier-overlap surface: when supplier attribution is limited by missing party-level proof, the answer must still give a concrete next action instead of stopping at a limitation.", + "architecture_phase": "turnaround_11", + "source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_inventory_supplier_overlap_next_action.json", + "scenario_id": "address_truth_harness_inventory_supplier_overlap_next_action", + "semantic_tags": [], + "validation_status": "accepted_live_replay", + "validated_run_dir": "artifacts\\domain_runs\\inventory_supplier_overlap_next_action_p2", + "saved_after_validated_replay": true + } + }, { "generation_id": "gen-ag06030440-256da0", "created_at": "2026-06-03T04:40:01+00:00", diff --git a/llm_normalizer/data/autorun_generators/saved_sessions/assistant_saved_session_20260603084117_gen-ag06030841-d61275.json b/llm_normalizer/data/autorun_generators/saved_sessions/assistant_saved_session_20260603084117_gen-ag06030841-d61275.json new file mode 100644 index 0000000..c483d79 --- /dev/null +++ b/llm_normalizer/data/autorun_generators/saved_sessions/assistant_saved_session_20260603084117_gen-ag06030841-d61275.json @@ -0,0 +1,133 @@ +{ + "saved_at": "2026-06-03T08:41:17+00:00", + "generation_id": "gen-ag06030841-d61275", + "mode": "saved_user_sessions", + "title": "AGENT | Supplier-overlap limited answer includes a concrete next action", + "agent_run": true, + "questions": [ + "какие остатки на складе на сентябрь 2021", + "У какого поставщика были куплены товары, которые лежали на складе Основной склад на 30.09.2021", + "По какому поставщику проходит товарный остаток на складе Основной склад на 30.09.2021" + ], + "metadata": { + "assistant_prompt_version": null, + "decomposition_prompt_version": null, + "prompt_fingerprint": null, + "agent_focus": "Targeted replay for the inventory supplier-overlap surface: when supplier attribution is limited by missing party-level proof, the answer must still give a concrete next action instead of stopping at a limitation.", + "architecture_phase": "turnaround_11", + "source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_inventory_supplier_overlap_next_action.json", + "scenario_id": "address_truth_harness_inventory_supplier_overlap_next_action", + "semantic_tags": [], + "validation_status": "accepted_live_replay", + "validated_run_dir": "artifacts\\domain_runs\\inventory_supplier_overlap_next_action_p2", + "saved_after_validated_replay": true, + "save_gate": { + "schema_version": "agent_semantic_save_gate_v1", + "validation_status": "accepted_live_replay", + "validated_run_dir": "artifacts\\domain_runs\\inventory_supplier_overlap_next_action_p2", + "final_status": "accepted", + "review_overall_status": "pass", + "business_overall_status": "pass", + "steps_total": 3, + "steps_passed": 3, + "steps_failed": 0, + "steps_with_business_failures": 0, + "steps_with_business_warnings": 0, + "acceptance_gate_passed": true, + "effective_runtime": { + "manifest_path": "artifacts\\domain_runs\\inventory_supplier_overlap_next_action_p2\\effective_runtime.json", + "runner": "domain_truth_harness.run-live", + "git_sha": "c8a58f6163423ac334dc293aeb8542d19e676d71", + "backend_url": "http://127.0.0.1:8787", + "mcp_proxy_url": "http://127.0.0.1:6003", + "llm_provider": "local", + "llm_model": "unsloth/qwen3-30b-a3b-instruct-2507", + "temperature": 0.8, + "max_output_tokens": 900, + "prompt_version": "normalizer_v2_0_2", + "prompt_source": "file", + "prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1", + "prompt_registry_status": "pass" + }, + "saved_after_validated_replay": true + } + }, + "source_session_id": null, + "session": { + "session_id": null, + "mode": "agent_semantic_run", + "items": [ + { + "message_id": "agent-user-001", + "role": "user", + "text": "какие остатки на складе на сентябрь 2021", + "created_at": "2026-06-03T08:41:17+00:00", + "reply_type": null, + "trace_id": null, + "debug": null + }, + { + "message_id": "agent-user-002", + "role": "user", + "text": "У какого поставщика были куплены товары, которые лежали на складе Основной склад на 30.09.2021", + "created_at": "2026-06-03T08:41:17+00:00", + "reply_type": null, + "trace_id": null, + "debug": null + }, + { + "message_id": "agent-user-003", + "role": "user", + "text": "По какому поставщику проходит товарный остаток на складе Основной склад на 30.09.2021", + "created_at": "2026-06-03T08:41:17+00:00", + "reply_type": null, + "trace_id": null, + "debug": null + } + ], + "agent_run": true, + "metadata": { + "assistant_prompt_version": null, + "decomposition_prompt_version": null, + "prompt_fingerprint": null, + "agent_focus": "Targeted replay for the inventory supplier-overlap surface: when supplier attribution is limited by missing party-level proof, the answer must still give a concrete next action instead of stopping at a limitation.", + "architecture_phase": "turnaround_11", + "source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_inventory_supplier_overlap_next_action.json", + "scenario_id": "address_truth_harness_inventory_supplier_overlap_next_action", + "semantic_tags": [], + "validation_status": "accepted_live_replay", + "validated_run_dir": "artifacts\\domain_runs\\inventory_supplier_overlap_next_action_p2", + "saved_after_validated_replay": true, + "save_gate": { + "schema_version": "agent_semantic_save_gate_v1", + "validation_status": "accepted_live_replay", + "validated_run_dir": "artifacts\\domain_runs\\inventory_supplier_overlap_next_action_p2", + "final_status": "accepted", + "review_overall_status": "pass", + "business_overall_status": "pass", + "steps_total": 3, + "steps_passed": 3, + "steps_failed": 0, + "steps_with_business_failures": 0, + "steps_with_business_warnings": 0, + "acceptance_gate_passed": true, + "effective_runtime": { + "manifest_path": "artifacts\\domain_runs\\inventory_supplier_overlap_next_action_p2\\effective_runtime.json", + "runner": "domain_truth_harness.run-live", + "git_sha": "c8a58f6163423ac334dc293aeb8542d19e676d71", + "backend_url": "http://127.0.0.1:8787", + "mcp_proxy_url": "http://127.0.0.1:6003", + "llm_provider": "local", + "llm_model": "unsloth/qwen3-30b-a3b-instruct-2507", + "temperature": 0.8, + "max_output_tokens": 900, + "prompt_version": "normalizer_v2_0_2", + "prompt_source": "file", + "prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1", + "prompt_registry_status": "pass" + }, + "saved_after_validated_replay": true + } + } + } +} diff --git a/llm_normalizer/data/eval_cases/assistant_autogen_saved_user_sessions_20260603084117_gen-ag06030841-d61275.json b/llm_normalizer/data/eval_cases/assistant_autogen_saved_user_sessions_20260603084117_gen-ag06030841-d61275.json new file mode 100644 index 0000000..a2cf935 --- /dev/null +++ b/llm_normalizer/data/eval_cases/assistant_autogen_saved_user_sessions_20260603084117_gen-ag06030841-d61275.json @@ -0,0 +1,34 @@ +{ + "suite_id": "assistant_saved_session_gen-ag06030841-d61275", + "suite_version": "0.1.0", + "schema_version": "assistant_saved_session_suite_v0_1", + "generated_at": "2026-06-03T08:41:17+00:00", + "generation_id": "gen-ag06030841-d61275", + "mode": "saved_user_sessions", + "title": "AGENT | Supplier-overlap limited answer includes a concrete next action", + "domain": "inventory_supplier_overlap_next_action", + "scenario_count": 1, + "case_ids": [ + "SAVED-001" + ], + "cases": [ + { + "case_id": "SAVED-001", + "scenario_tag": "agent_saved_user_sessions", + "title": "AGENT | Supplier-overlap limited answer includes a concrete next action", + "question_type": "followup", + "broadness_level": "medium", + "turns": [ + { + "user_message": "какие остатки на складе на сентябрь 2021" + }, + { + "user_message": "У какого поставщика были куплены товары, которые лежали на складе Основной склад на 30.09.2021" + }, + { + "user_message": "По какому поставщику проходит товарный остаток на складе Основной склад на 30.09.2021" + } + ] + } + ] +}