Сжать supplier-overlap ответы и detector utility gap
This commit is contained in:
parent
fdd04abbde
commit
ed44c6428a
|
|
@ -134,7 +134,7 @@
|
|||
"kind": "answer_text_shape",
|
||||
"automation_level": "semi_automatic",
|
||||
"description": "The first meaningful line is not a direct business answer for a direct user question.",
|
||||
"issue_codes": ["business_direct_answer_missing"],
|
||||
"issue_codes": ["business_direct_answer_missing", "business_utility_gap"],
|
||||
"inputs": ["steps/<step_id>/output.md"],
|
||||
"check": {
|
||||
"first_line_should_be": "business_answer_or_honest_boundary"
|
||||
|
|
@ -144,7 +144,7 @@
|
|||
"kind": "answer_text_regex_forbidden_in_prefix",
|
||||
"automation_level": "automatic",
|
||||
"description": "Answer starts with scaffold/service narration before the user-facing business conclusion.",
|
||||
"issue_codes": ["business_direct_answer_missing"],
|
||||
"issue_codes": ["business_direct_answer_missing", "business_utility_gap"],
|
||||
"inputs": ["steps/<step_id>/output.md"],
|
||||
"check": {
|
||||
"prefix_line_count": 3,
|
||||
|
|
@ -155,7 +155,7 @@
|
|||
"kind": "answer_text_regex_forbidden",
|
||||
"automation_level": "automatic",
|
||||
"description": "Final user-facing answer contains runtime/debug/service tokens.",
|
||||
"issue_codes": ["technical_garbage_in_answer"],
|
||||
"issue_codes": ["technical_garbage_in_answer", "business_utility_gap"],
|
||||
"inputs": ["steps/<step_id>/output.md"],
|
||||
"check": {
|
||||
"forbidden_patterns": ["(?i)(route_id|capability_id|runtime_|snapshot_items|debug|answer_object|selected_object)"]
|
||||
|
|
@ -165,7 +165,7 @@
|
|||
"kind": "answer_text_regex_forbidden",
|
||||
"automation_level": "automatic",
|
||||
"description": "Final user-facing answer contains capability ids or route ids.",
|
||||
"issue_codes": ["technical_garbage_in_answer"],
|
||||
"issue_codes": ["technical_garbage_in_answer", "business_utility_gap"],
|
||||
"inputs": ["steps/<step_id>/output.md"],
|
||||
"check": {
|
||||
"forbidden_patterns": ["(?i)(capability[_ -]?id|route[_ -]?id|address\\.[a-z0-9_\\.]+)"]
|
||||
|
|
@ -185,13 +185,23 @@
|
|||
"kind": "answer_text_required_when_limited",
|
||||
"automation_level": "semi_automatic",
|
||||
"description": "Answer states a limitation but gives no concrete next action for recovering or narrowing the answer.",
|
||||
"issue_codes": ["business_next_step_missing"],
|
||||
"issue_codes": ["business_next_step_missing", "business_utility_gap"],
|
||||
"inputs": ["steps/<step_id>/output.md"],
|
||||
"check": {
|
||||
"limited_patterns": ["(?i)(не могу|не хватает|не подтвержден|нет данных|недостаточно)"],
|
||||
"required_next_action_patterns_any": ["(?i)(можно|следующ|уточн|перезапусти|проверь|выбери|нужен|добавь)"]
|
||||
}
|
||||
},
|
||||
"business_answer_too_verbose_output": {
|
||||
"kind": "answer_text_regex_forbidden",
|
||||
"automation_level": "automatic",
|
||||
"description": "A scoped business follow-up answer still exposes verbose proof/scaffold sections that should be collapsed into a compact proof line.",
|
||||
"issue_codes": ["business_utility_gap", "business_answer_too_verbose"],
|
||||
"inputs": ["steps/<step_id>/output.md"],
|
||||
"check": {
|
||||
"forbidden_patterns": ["(?im)^\\s*(Что проверили|Опорные документы):"]
|
||||
}
|
||||
},
|
||||
"route_candidate_needs_enablement": {
|
||||
"kind": "stage_review_signal",
|
||||
"automation_level": "semi_automatic",
|
||||
|
|
|
|||
|
|
@ -237,6 +237,48 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"business_utility_gap": {
|
||||
"severity": "P2",
|
||||
"business_meaning": "Ответ технически заземлен, но для узкого бизнес follow-up остается слишком тяжелым или недостаточно практичным.",
|
||||
"root_layers": ["answer_surface", "business_utility"],
|
||||
"expected_answer_contract": "compact_business_followup_surface_v1",
|
||||
"detectors": [
|
||||
"business_answer_too_verbose_output",
|
||||
"top_level_scaffold_before_answer",
|
||||
"runtime_tokens_in_user_answer",
|
||||
"capability_ids_in_user_answer",
|
||||
"limited_answer_without_next_action"
|
||||
],
|
||||
"allowed_patch_targets": [
|
||||
"llm_normalizer/backend/src/services/address_runtime/composeStage.ts",
|
||||
"llm_normalizer/backend/src/services/address_runtime/inventoryReplyBuilders.ts",
|
||||
"llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts",
|
||||
"llm_normalizer/backend/src/services/assistantService.ts"
|
||||
],
|
||||
"forbidden_patch_targets": [
|
||||
"route masking",
|
||||
"fake evidence",
|
||||
"evidence deletion from artifacts"
|
||||
],
|
||||
"rerun_matrix": [
|
||||
"failed_scenario",
|
||||
"answer_surface_pack",
|
||||
"accepted_smoke_pack"
|
||||
],
|
||||
"acceptance": {
|
||||
"must_have": [
|
||||
"direct_answer_first",
|
||||
"compact_business_surface",
|
||||
"short_evidence_basis",
|
||||
"honest_limitation_when_needed"
|
||||
],
|
||||
"must_not_have": [
|
||||
"verbose proof packet by default",
|
||||
"technical ids",
|
||||
"duplicated answer blocks"
|
||||
]
|
||||
}
|
||||
},
|
||||
"route_candidate_enablement_gap": {
|
||||
"severity": "P1",
|
||||
"business_meaning": "Планировщик понял бизнес-запрос, но route candidate ещё не исполняется как точная возможность.",
|
||||
|
|
@ -427,7 +469,7 @@
|
|||
"severity": "P1",
|
||||
"business_meaning": "Ответ слишком длинный для прямого бизнес-вопроса и снижает практическую полезность.",
|
||||
"root_layers": ["answer_surface", "business_utility"],
|
||||
"detectors": ["business_answer_too_verbose_signal"],
|
||||
"detectors": ["business_answer_too_verbose_output", "business_answer_too_verbose_signal"],
|
||||
"allowed_patch_targets": [
|
||||
"llm_normalizer/backend/src/services/address_runtime/composeStage.ts",
|
||||
"llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts",
|
||||
|
|
|
|||
|
|
@ -543,7 +543,7 @@ function composeInventoryReply(intent, rows, options, deps) {
|
|||
if (supplierScopedItemQuestion) {
|
||||
const requestedSupplierLabel = summary.counterparties[0] ?? String(options.counterpartyHint ?? "").trim();
|
||||
const directAnswerLine = supplierScopedItems.length > 0
|
||||
? `По поставщику ${requestedSupplierLabel} в складском срезе ${warehouseLabel} на ${deps.formatDateRu(asOfDate)} подтверждены позиции: ${inventoryItemLeadList(supplierScopedItems)}.`
|
||||
? `По поставщику ${requestedSupplierLabel} в складском срезе ${warehouseLabel} на ${deps.formatDateRu(asOfDate)} подтверждено позиций: ${deps.formatNumberWithDots(supplierScopedItems.length)}.`
|
||||
: `По поставщику ${requestedSupplierLabel} в складском срезе ${warehouseLabel} на ${deps.formatDateRu(asOfDate)} позиции не подтверждены в доступном закупочном следе.`;
|
||||
const lines = [directAnswerLine];
|
||||
if (supplierScopedItems.length > 0) {
|
||||
|
|
@ -552,24 +552,11 @@ function composeInventoryReply(intent, rows, options, deps) {
|
|||
lines.push(`- Показаны первые 8 из ${deps.formatNumberWithDots(supplierScopedItems.length)} позиций.`);
|
||||
}
|
||||
}
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Что проверили:", [
|
||||
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
`Период найденного закупочного следа: ${purchasePeriodLabel}.`,
|
||||
`Закупочных документов / операций в выборке: ${deps.formatNumberWithDots(summary.documents.length)} / ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
||||
]);
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Ограничения:", [
|
||||
"Без партионного учета этот ответ показывает закупочный след текущего остатка, но не доказывает владельца каждой конкретной партии.",
|
||||
INVENTORY_SUPPLIER_ATTRIBUTION_NEXT_ACTION
|
||||
]);
|
||||
lines.push("", `Проверка: срез ${deps.formatDateRu(asOfDate)}; закупочный след ${purchasePeriodLabel}; документов/операций: ${deps.formatNumberWithDots(summary.documents.length)} / ${deps.formatNumberWithDots(purchaseRows.length)}.`);
|
||||
if (observedCounterparties.length > 0) {
|
||||
lines.push(`- Найденные поставщики: ${inventoryPartyLeadList(observedCounterparties)}.`);
|
||||
}
|
||||
if (purchaseRows.length > 0) {
|
||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Опорные документы:", deps.formatInventoryTraceRows(purchaseRows, INVENTORY_TRACE_EVIDENCE_ROW_LIMIT));
|
||||
if (purchaseRows.length > INVENTORY_TRACE_EVIDENCE_ROW_LIMIT) {
|
||||
lines.push(`- Показаны первые ${INVENTORY_TRACE_EVIDENCE_ROW_LIMIT} из ${deps.formatNumberWithDots(purchaseRows.length)} найденных строк; полный след остается в подтвержденном срезе.`);
|
||||
}
|
||||
lines.push(`Поставщик в найденном следе: ${inventoryPartyLeadList(observedCounterparties)}.`);
|
||||
}
|
||||
lines.push("Ограничение: без партионного учета это закупочный след остатка, а не доказательство владельца каждой конкретной партии.", INVENTORY_SUPPLIER_ATTRIBUTION_NEXT_ACTION);
|
||||
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(purchaseRows.length > 0 ? (supplierScopedItems.length > 0 ? "strong" : "medium") : "medium", purchaseRows.length > 0));
|
||||
}
|
||||
const directAnswerLine = observedCounterparties.length === 1
|
||||
|
|
|
|||
|
|
@ -747,7 +747,7 @@ export function composeInventoryReply(
|
|||
const requestedSupplierLabel = summary.counterparties[0] ?? String(options.counterpartyHint ?? "").trim();
|
||||
const directAnswerLine =
|
||||
supplierScopedItems.length > 0
|
||||
? `По поставщику ${requestedSupplierLabel} в складском срезе ${warehouseLabel} на ${deps.formatDateRu(asOfDate)} подтверждены позиции: ${inventoryItemLeadList(supplierScopedItems)}.`
|
||||
? `По поставщику ${requestedSupplierLabel} в складском срезе ${warehouseLabel} на ${deps.formatDateRu(asOfDate)} подтверждено позиций: ${deps.formatNumberWithDots(supplierScopedItems.length)}.`
|
||||
: `По поставщику ${requestedSupplierLabel} в складском срезе ${warehouseLabel} на ${deps.formatDateRu(asOfDate)} позиции не подтверждены в доступном закупочном следе.`;
|
||||
const lines: string[] = [directAnswerLine];
|
||||
if (supplierScopedItems.length > 0) {
|
||||
|
|
@ -760,30 +760,17 @@ export function composeInventoryReply(
|
|||
lines.push(`- Показаны первые 8 из ${deps.formatNumberWithDots(supplierScopedItems.length)} позиций.`);
|
||||
}
|
||||
}
|
||||
appendInventoryBulletSection(lines, "Что проверили:", [
|
||||
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
`Период найденного закупочного следа: ${purchasePeriodLabel}.`,
|
||||
`Закупочных документов / операций в выборке: ${deps.formatNumberWithDots(summary.documents.length)} / ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
||||
]);
|
||||
appendInventoryBulletSection(lines, "Ограничения:", [
|
||||
"Без партионного учета этот ответ показывает закупочный след текущего остатка, но не доказывает владельца каждой конкретной партии.",
|
||||
INVENTORY_SUPPLIER_ATTRIBUTION_NEXT_ACTION
|
||||
]);
|
||||
lines.push(
|
||||
"",
|
||||
`Проверка: срез ${deps.formatDateRu(asOfDate)}; закупочный след ${purchasePeriodLabel}; документов/операций: ${deps.formatNumberWithDots(summary.documents.length)} / ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
||||
);
|
||||
if (observedCounterparties.length > 0) {
|
||||
lines.push(`- Найденные поставщики: ${inventoryPartyLeadList(observedCounterparties)}.`);
|
||||
}
|
||||
if (purchaseRows.length > 0) {
|
||||
appendInventorySection(
|
||||
lines,
|
||||
"Опорные документы:",
|
||||
deps.formatInventoryTraceRows(purchaseRows, INVENTORY_TRACE_EVIDENCE_ROW_LIMIT)
|
||||
);
|
||||
if (purchaseRows.length > INVENTORY_TRACE_EVIDENCE_ROW_LIMIT) {
|
||||
lines.push(
|
||||
`- Показаны первые ${INVENTORY_TRACE_EVIDENCE_ROW_LIMIT} из ${deps.formatNumberWithDots(purchaseRows.length)} найденных строк; полный след остается в подтвержденном срезе.`
|
||||
);
|
||||
}
|
||||
lines.push(`Поставщик в найденном следе: ${inventoryPartyLeadList(observedCounterparties)}.`);
|
||||
}
|
||||
lines.push(
|
||||
"Ограничение: без партионного учета это закупочный след остатка, а не доказательство владельца каждой конкретной партии.",
|
||||
INVENTORY_SUPPLIER_ATTRIBUTION_NEXT_ACTION
|
||||
);
|
||||
return buildFactualSummaryReply(
|
||||
lines,
|
||||
buildConfirmedBalanceSemantics(
|
||||
|
|
|
|||
|
|
@ -6308,11 +6308,18 @@ it("routes old purchase residue questions to aging-by-purchase-date", () => {
|
|||
);
|
||||
|
||||
const firstLine = reply.text.split("\n")[0] ?? "";
|
||||
expect(firstLine).toContain("подтверждены позиции");
|
||||
expect(firstLine).toContain("Столешница 600*3050*26 альмандин");
|
||||
expect(firstLine).toContain("Столешница 600*3050*26 дуб ниагара");
|
||||
expect(firstLine).toContain("подтверждено позиций: 2");
|
||||
expect(firstLine).not.toContain("выявлен поставщик закупочного следа");
|
||||
expect(reply.text).toContain("Позиции:");
|
||||
expect(reply.text.indexOf("Позиции:")).toBeLessThan(reply.text.indexOf("Проверка:"));
|
||||
expect(reply.text).toContain("Столешница 600*3050*26 альмандин");
|
||||
expect(reply.text).toContain("Столешница 600*3050*26 дуб ниагара");
|
||||
expect(reply.text).toContain("Проверка:");
|
||||
expect(reply.text).toContain("Ограничение:");
|
||||
expect(reply.text).toContain("Следующий шаг:");
|
||||
expect(reply.text).not.toContain("Что проверили:");
|
||||
expect(reply.text).not.toContain("Опорные документы:");
|
||||
expect(reply.text).not.toContain("Поступление товаров и услуг");
|
||||
});
|
||||
|
||||
it("routes inventory provenance questions to a dedicated intent", () => {
|
||||
|
|
|
|||
|
|
@ -211,6 +211,33 @@ class DomainCaseLoopLeadHandoffTests(unittest.TestCase):
|
|||
self.assertIn("first_line_not_direct_answer", detector_names)
|
||||
self.assertNotIn("runtime_tokens_in_user_answer", detector_names)
|
||||
|
||||
def test_business_utility_gap_uses_known_surface_detectors(self) -> None:
|
||||
candidates = dcl.build_detector_candidates(
|
||||
{
|
||||
"target_count": 1,
|
||||
"acceptance_status": "partial",
|
||||
"final_status": "partial",
|
||||
"targets": [
|
||||
{
|
||||
"target_id": "inventory_supplier_overlap:step_04_supplier_items_on_stock",
|
||||
"issue_code": "business_utility_gap",
|
||||
"severity": "P2",
|
||||
"evidence_paths": [
|
||||
"artifacts/domain_runs/pack/scenarios/inventory_supplier_overlap/steps/step_04_supplier_items_on_stock/output.md"
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
detector_names = [item["detector"] for item in candidates["candidates"]]
|
||||
|
||||
self.assertIn("business_answer_too_verbose_output", detector_names)
|
||||
self.assertIn("top_level_scaffold_before_answer", detector_names)
|
||||
self.assertIn("runtime_tokens_in_user_answer", detector_names)
|
||||
self.assertIn("limited_answer_without_next_action", detector_names)
|
||||
self.assertNotIn("business_utility_gap_detector", detector_names)
|
||||
|
||||
def test_detector_results_gate_allows_pass_and_blocks_fail_or_review(self) -> None:
|
||||
pass_ok, pass_reason = dcl.evaluate_detector_results_gate({"status": "pass"})
|
||||
fail_ok, fail_reason = dcl.evaluate_detector_results_gate(
|
||||
|
|
|
|||
Loading…
Reference in New Issue