Compare commits
3 Commits
d77bcbe482
...
477e550b0c
| Author | SHA1 | Date |
|---|---|---|
|
|
477e550b0c | |
|
|
e93ddc16bb | |
|
|
b07bcecc83 |
|
|
@ -42,7 +42,8 @@
|
||||||
"follow-up period keeps margin intent",
|
"follow-up period keeps margin intent",
|
||||||
"cost-basis follow-up does not leak technical garbage",
|
"cost-basis follow-up does not leak technical garbage",
|
||||||
"period expansion keeps inventory margin context",
|
"period expansion keeps inventory margin context",
|
||||||
"account 41 guard does not become fixed-assets analysis"
|
"account 41 guard does not become fixed-assets analysis",
|
||||||
|
"selected-object and pronoun follow-ups keep the ranked item as focus_object"
|
||||||
],
|
],
|
||||||
"must_not_have": [
|
"must_not_have": [
|
||||||
"fixed assets leak",
|
"fixed assets leak",
|
||||||
|
|
@ -219,6 +220,79 @@
|
||||||
"банк.{0,80}(источник|достаточ|марж)",
|
"банк.{0,80}(источник|достаточ|марж)",
|
||||||
"route_id|capability_id|runtime_|debug"
|
"route_id|capability_id|runtime_|debug"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_06_pronoun_selected_item_supplier",
|
||||||
|
"title": "Pronoun selected-item supplier follow-up keeps ranked item",
|
||||||
|
"question": "А по этой позиции кто поставщик?",
|
||||||
|
"depends_on": [
|
||||||
|
"step_05_account_41_not_01"
|
||||||
|
],
|
||||||
|
"node_role": "critical_child",
|
||||||
|
"paraphrase_family": "pronoun_followup",
|
||||||
|
"semantic_tags": [
|
||||||
|
"inventory_margin_ranking",
|
||||||
|
"selected_object",
|
||||||
|
"pronoun_followup",
|
||||||
|
"supplier_followup",
|
||||||
|
"focus_object"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"inventory_purchase_provenance_for_item"
|
||||||
|
],
|
||||||
|
"expected_capability": "inventory_inventory_purchase_provenance_for_item",
|
||||||
|
"expected_recipe": "address_inventory_purchase_provenance_for_item_v1",
|
||||||
|
"expected_result_mode": "selected_object_purchase_provenance_or_honest_boundary",
|
||||||
|
"expected_business_answer_contract": "margin_profitability_v1",
|
||||||
|
"required_answer_shape": "direct_answer_first",
|
||||||
|
"required_state_objects": [
|
||||||
|
"focus_object"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_any": [
|
||||||
|
"Флаг геральдический|по этой позиции|поставщик|закуп|поступлен|не найден|не подтвержд"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"амортизац",
|
||||||
|
"объект ОС",
|
||||||
|
"основн.{0,20}средств",
|
||||||
|
"route_id|capability_id|runtime_|debug"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_07_ui_selected_item_purchase_document",
|
||||||
|
"title": "UI selected-object purchase document follow-up keeps ranked item",
|
||||||
|
"question": "По выбранному объекту \"{{step_outputs.step_06_pronoun_selected_item_supplier.focus_object.label}}\": каким документом подтверждена закупка?",
|
||||||
|
"depends_on": [
|
||||||
|
"step_06_pronoun_selected_item_supplier"
|
||||||
|
],
|
||||||
|
"node_role": "critical_child",
|
||||||
|
"paraphrase_family": "ui_selected_object",
|
||||||
|
"semantic_tags": [
|
||||||
|
"inventory_margin_ranking",
|
||||||
|
"selected_object",
|
||||||
|
"ui_selected_object",
|
||||||
|
"purchase_document_followup",
|
||||||
|
"focus_object"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"inventory_purchase_provenance_for_item",
|
||||||
|
"inventory_purchase_documents_for_item"
|
||||||
|
],
|
||||||
|
"expected_result_mode": "selected_object_purchase_documents_or_honest_boundary",
|
||||||
|
"expected_business_answer_contract": "margin_profitability_v1",
|
||||||
|
"required_answer_shape": "direct_answer_first",
|
||||||
|
"required_state_objects": [
|
||||||
|
"focus_object"
|
||||||
|
],
|
||||||
|
"required_answer_patterns_any": [
|
||||||
|
"Флаг геральдический|документ|поступлен|регистратор|накладн|закуп|не найден|не подтвержд"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"амортизац",
|
||||||
|
"объект ОС",
|
||||||
|
"основн.{0,20}средств",
|
||||||
|
"route_id|capability_id|runtime_|debug"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ function hasSelectedObjectInventoryProvenanceSignal(text) {
|
||||||
function hasSelectedObjectInventoryPurchaseDocumentsSignal(text) {
|
function hasSelectedObjectInventoryPurchaseDocumentsSignal(text) {
|
||||||
const value = String(text ?? "");
|
const value = String(text ?? "");
|
||||||
if (hasSelectedObjectInventoryCue(value) &&
|
if (hasSelectedObjectInventoryCue(value) &&
|
||||||
/(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*купили|какими\s+документами\s+(?:был\s+)?куплен|покажи\s+документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(value)) {
|
/(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*купили|какими\s+документами\s+(?:был\s+)?куплен|каким\s+документом\s+подтвержден[ао]?\s+закупк|какой\s+документ\s+подтвержда[её]т\s+закупк|документ[а-яё]*\s+подтвержд[а-яё]*\s+закупк|закупк[а-яё]*\s+подтвержден[а-яё]*\s+документ|покажи\s+документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(value)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const hasPurchaseDocumentsCue = /(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|какими\s+документами\s+(?:был\s+)?куплен|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(text) ||
|
const hasPurchaseDocumentsCue = /(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|какими\s+документами\s+(?:был\s+)?куплен|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(text) ||
|
||||||
|
|
|
||||||
|
|
@ -583,6 +583,10 @@ function shouldRestoreInventoryRootFrame(userMessage, intent, extractedFilters,
|
||||||
return hasTemporalPatch;
|
return hasTemporalPatch;
|
||||||
}
|
}
|
||||||
function hasSelectedObjectInventorySignal(text) {
|
function hasSelectedObjectInventorySignal(text) {
|
||||||
|
const repairedSelectedObjectText = textWithRepairedVariant(String(text ?? ""));
|
||||||
|
if (/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(repairedSelectedObjectText)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(String(text ?? ""))) {
|
if (/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(String(text ?? ""))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -612,8 +616,9 @@ function hasInventorySupplierFollowupCue(text) {
|
||||||
return (0, inventoryLifecycleCueHelpers_1.hasInventorySupplierCue)(String(text ?? ""));
|
return (0, inventoryLifecycleCueHelpers_1.hasInventorySupplierCue)(String(text ?? ""));
|
||||||
}
|
}
|
||||||
function hasInventoryPurchaseDocumentsFollowupCue(text) {
|
function hasInventoryPurchaseDocumentsFollowupCue(text) {
|
||||||
const value = String(text ?? "");
|
const value = textWithRepairedVariant(String(text ?? ""));
|
||||||
return (/(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|какими\s+документами\s+(?:был\s+)?куплен|каким\s+документом\s+(?:это|его|её|ее|она|он|этот\s+товар|эта\s+позиция|эту\s+позицию)?\s*(?:подтвержден[оа]?|доказан[оа]?|закрыт[оа]?)|покажи\s+документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(value) ||
|
return (/(?:по\s+каким\s+документам\s+(?:это|его|ее|её|этот\s+товар|эту\s+позицию)\s+купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|какими\s+документами\s+(?:был\s+)?куплен|каким\s+документом\s+(?:это|его|ее|её|она|он|этот\s+товар|эта\s+позиция|эту\s+позицию)?\s*(?:подтвержден[оа]?|подтверждена\s+закупка|доказан[оа]?|закрыт[оа]?)|закупк[аи]\s+подтвержден[аы]?\s+документ|покажи\s+документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему))/iu.test(value) ||
|
||||||
|
/(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|какими\s+документами\s+(?:был\s+)?куплен|каким\s+документом\s+(?:это|его|её|ее|она|он|этот\s+товар|эта\s+позиция|эту\s+позицию)?\s*(?:подтвержден[оа]?|доказан[оа]?|закрыт[оа]?)|покажи\s+документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(value) ||
|
||||||
/(?:(?:покажи|показать|выведи|дай)?[\s\S]{0,30}док(?:и|умент[а-яё]*)[\s\S]{0,80}(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару)|операци)|(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару))[\s\S]{0,80}док(?:и|умент[а-яё]*))/iu.test(value));
|
/(?:(?:покажи|показать|выведи|дай)?[\s\S]{0,30}док(?:и|умент[а-яё]*)[\s\S]{0,80}(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару)|операци)|(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару))[\s\S]{0,80}док(?:и|умент[а-яё]*))/iu.test(value));
|
||||||
}
|
}
|
||||||
function hasInventoryProfitabilityFollowupCue(text) {
|
function hasInventoryProfitabilityFollowupCue(text) {
|
||||||
|
|
@ -692,10 +697,13 @@ function hasInventoryMarginRankingFollowupCue(text) {
|
||||||
return wantsFoundRows || asksMarginBasis || account41Not01;
|
return wantsFoundRows || asksMarginBasis || account41Not01;
|
||||||
}
|
}
|
||||||
function hasAddressFollowupContextSignal(text) {
|
function hasAddressFollowupContextSignal(text) {
|
||||||
const normalized = String(text ?? "").trim();
|
const normalized = textWithRepairedVariant(String(text ?? "")).trim();
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (hasInventoryMarginRankingFollowupCue(normalized)) {
|
if (hasInventoryMarginRankingFollowupCue(normalized)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,9 @@ 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 omitAmountFromInventoryTraceEvidence(lines) {
|
||||||
|
return lines.map((line) => String(line ?? "").replace(/\s+\|\s+сумма:\s+[^|]+(?=\s+\||$)/gu, ""));
|
||||||
|
}
|
||||||
function normalizeInventoryReplyEntityToken(value) {
|
function normalizeInventoryReplyEntityToken(value) {
|
||||||
const normalized = String(value ?? "")
|
const normalized = String(value ?? "")
|
||||||
.trim()
|
.trim()
|
||||||
|
|
@ -383,7 +386,7 @@ function composeInventoryReply(intent, rows, options, deps) {
|
||||||
lines.push(`- Для ответа проверены закупочные документы не позже ${boundedAsOfLabel}.`);
|
lines.push(`- Для ответа проверены закупочные документы не позже ${boundedAsOfLabel}.`);
|
||||||
}
|
}
|
||||||
if (summary.documents.length > 0) {
|
if (summary.documents.length > 0) {
|
||||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Опорные документы:", deps.formatInventoryTraceRows(purchaseRows, INVENTORY_TRACE_EVIDENCE_ROW_LIMIT));
|
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Опорные документы:", omitAmountFromInventoryTraceEvidence(deps.formatInventoryTraceRows(purchaseRows, INVENTORY_TRACE_EVIDENCE_ROW_LIMIT)));
|
||||||
if (purchaseRows.length > 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(`- Показаны первые ${INVENTORY_TRACE_EVIDENCE_ROW_LIMIT} из ${deps.formatNumberWithDots(purchaseRows.length)} найденных строк; полный след остается в подтвержденном срезе.`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3298,6 +3298,11 @@ function hasPredecomposeExplicitDrilldownSignal(text) {
|
||||||
return ADDRESS_DOCS_SIGNAL_PATTERN.test(source) || ADDRESS_BANK_SIGNAL_PATTERN.test(source) || ADDRESS_CONTRACT_SIGNAL_PATTERN.test(source);
|
return ADDRESS_DOCS_SIGNAL_PATTERN.test(source) || ADDRESS_BANK_SIGNAL_PATTERN.test(source) || ADDRESS_CONTRACT_SIGNAL_PATTERN.test(source);
|
||||||
}
|
}
|
||||||
function hasSelectedObjectInventoryFollowupSignalForPredecompose(text) {
|
function hasSelectedObjectInventoryFollowupSignalForPredecompose(text) {
|
||||||
|
const source = String(text ?? "");
|
||||||
|
const repaired = repairAddressMojibake(source);
|
||||||
|
if (/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(`${source} ${repaired}`)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(text ?? ""));
|
return /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(text ?? ""));
|
||||||
}
|
}
|
||||||
function isInventorySelectedObjectFollowupIntent(intent) {
|
function isInventorySelectedObjectFollowupIntent(intent) {
|
||||||
|
|
@ -3583,6 +3588,30 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
||||||
}, userMessage);
|
}, userMessage);
|
||||||
}
|
}
|
||||||
const candidateAnchorQuality = evaluateAddressAnchorQuality(candidate);
|
const candidateAnchorQuality = evaluateAddressAnchorQuality(candidate);
|
||||||
|
const candidateHasSemanticSelectedObjectScope = candidateMeta?.semanticHints?.selected_object_scope_detected === true ||
|
||||||
|
candidateMeta?.semanticHints?.scope_target_kind === "selected_object";
|
||||||
|
const candidateTextForSelectedObjectCounterpartyDegradation = compactWhitespace(repairAddressMojibake(String(candidate ?? "")).toLowerCase());
|
||||||
|
const candidateUsesSelectedObjectAsCounterpartyAnchor = /(?:^|\s)по\s+контрагент\p{L}*\s+(?:выбран\p{L}*|selected(?:\s+object)?|object|объект\p{L}*)/iu.test(candidateTextForSelectedObjectCounterpartyDegradation) ||
|
||||||
|
/(?:^|\s)counterparty\s+(?:selected(?:\s+object)?|object)/iu.test(candidateTextForSelectedObjectCounterpartyDegradation);
|
||||||
|
const candidateTurnsSelectedObjectIntoCounterparty = sourceHasSelectedObjectInventoryFollowup &&
|
||||||
|
(candidateInjectsGenericDocsIntent || candidateUsesSelectedObjectAsCounterpartyAnchor) &&
|
||||||
|
(candidateHasSemanticSelectedObjectScope || candidateUsesSelectedObjectAsCounterpartyAnchor) &&
|
||||||
|
(candidateAnchorQuality.anchorType === "counterparty" || candidateUsesSelectedObjectAsCounterpartyAnchor) &&
|
||||||
|
(candidateAnchorQuality.quality < 2 || candidateUsesSelectedObjectAsCounterpartyAnchor);
|
||||||
|
if (candidateTurnsSelectedObjectIntoCounterparty) {
|
||||||
|
return attachAddressPredecomposeContract({
|
||||||
|
...baseMeta,
|
||||||
|
attempted: true,
|
||||||
|
applied: false,
|
||||||
|
traceId: normalized?.trace_id ?? null,
|
||||||
|
llmCanonicalCandidateDetected: true,
|
||||||
|
effectiveMessage: userMessage,
|
||||||
|
reason: "normalized_fragment_rejected_selected_object_counterparty_degradation",
|
||||||
|
fallbackRuleHit: null,
|
||||||
|
sanitizedUserMessage,
|
||||||
|
semanticHints: candidateMeta?.semanticHints ?? null
|
||||||
|
}, userMessage);
|
||||||
|
}
|
||||||
const sameIntentForAnchorSafety = sourceAnchorQuality.intent !== "unknown" && sourceAnchorQuality.intent === candidateAnchorQuality.intent;
|
const sameIntentForAnchorSafety = sourceAnchorQuality.intent !== "unknown" && sourceAnchorQuality.intent === candidateAnchorQuality.intent;
|
||||||
const sourceSelectedObjectItemAnchorValue = toNonEmptyString((0, addressFilterExtractor_1.extractSelectedObjectQuotedValue)(userMessage)) ??
|
const sourceSelectedObjectItemAnchorValue = toNonEmptyString((0, addressFilterExtractor_1.extractSelectedObjectQuotedValue)(userMessage)) ??
|
||||||
toNonEmptyString((0, addressFilterExtractor_1.extractSelectedObjectQuotedValue)(repairedSourceMessage || userMessage));
|
toNonEmptyString((0, addressFilterExtractor_1.extractSelectedObjectQuotedValue)(repairedSourceMessage || userMessage));
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,12 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
if (samples.length === 0) {
|
if (samples.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (samples.some((sample) => /(?:где\s+(?:мы\s+)?взял|откуда\s+(?:мы\s+)?взял|у\s+кого\s+купил|кто\s+постав|поставщик|источник\s+поступл|от\s+кого\s+поступ)/iu.test(sample))) {
|
||||||
|
return "inventory_purchase_provenance_for_item";
|
||||||
|
}
|
||||||
|
if (samples.some((sample) => /(?:документ|накладн|счет|счёт|акт|поступл|подтвержден|подтверждена\s+закупка|закупк[аи]\s+подтвержден)/iu.test(sample))) {
|
||||||
|
return "inventory_purchase_documents_for_item";
|
||||||
|
}
|
||||||
if (suggestedIntents.includes("bank_operations_by_counterparty") &&
|
if (suggestedIntents.includes("bank_operations_by_counterparty") &&
|
||||||
samples.some((sample) => hasBankOperationsPivotCue(sample) || hasReadableBankOperationsPivotCue(sample))) {
|
samples.some((sample) => hasBankOperationsPivotCue(sample) || hasReadableBankOperationsPivotCue(sample))) {
|
||||||
return "bank_operations_by_counterparty";
|
return "bank_operations_by_counterparty";
|
||||||
|
|
@ -145,10 +151,17 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (/(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(normalized);
|
return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(normalized);
|
||||||
}
|
}
|
||||||
function extractSelectedObjectLabel(text) {
|
function extractSelectedObjectLabel(text) {
|
||||||
const repaired = deps.repairAddressMojibake(String(text ?? ""));
|
const repaired = deps.repairAddressMojibake(String(text ?? ""));
|
||||||
|
const unicodeQuotedMatch = repaired.match(/(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции)\s*[:,-]?\s*[«"]([^"»]+?)["»]/iu);
|
||||||
|
if (unicodeQuotedMatch?.[1]) {
|
||||||
|
return deps.toNonEmptyString(unicodeQuotedMatch[1]);
|
||||||
|
}
|
||||||
const quotedMatch = repaired.match(/(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции)\s*[:,-]?\s*[«"]([^"»]+?)["»]/iu);
|
const quotedMatch = repaired.match(/(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции)\s*[:,-]?\s*[«"]([^"»]+?)["»]/iu);
|
||||||
if (quotedMatch?.[1]) {
|
if (quotedMatch?.[1]) {
|
||||||
return deps.toNonEmptyString(quotedMatch[1]);
|
return deps.toNonEmptyString(quotedMatch[1]);
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ function hasSelectedObjectInventoryPurchaseDocumentsSignal(text: string): boolea
|
||||||
const value = String(text ?? "");
|
const value = String(text ?? "");
|
||||||
if (
|
if (
|
||||||
hasSelectedObjectInventoryCue(value) &&
|
hasSelectedObjectInventoryCue(value) &&
|
||||||
/(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*купили|какими\s+документами\s+(?:был\s+)?куплен|покажи\s+документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(
|
/(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*купили|какими\s+документами\s+(?:был\s+)?куплен|каким\s+документом\s+подтвержден[ао]?\s+закупк|какой\s+документ\s+подтвержда[её]т\s+закупк|документ[а-яё]*\s+подтвержд[а-яё]*\s+закупк|закупк[а-яё]*\s+подтвержден[а-яё]*\s+документ|покажи\s+документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(
|
||||||
value
|
value
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -721,6 +721,14 @@ function shouldRestoreInventoryRootFrame(
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasSelectedObjectInventorySignal(text: string): boolean {
|
function hasSelectedObjectInventorySignal(text: string): boolean {
|
||||||
|
const repairedSelectedObjectText = textWithRepairedVariant(String(text ?? ""));
|
||||||
|
if (
|
||||||
|
/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(
|
||||||
|
repairedSelectedObjectText
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(
|
/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(
|
||||||
String(text ?? "")
|
String(text ?? "")
|
||||||
|
|
@ -762,8 +770,11 @@ export function hasInventorySupplierFollowupCue(text: string): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasInventoryPurchaseDocumentsFollowupCue(text: string): boolean {
|
export function hasInventoryPurchaseDocumentsFollowupCue(text: string): boolean {
|
||||||
const value = String(text ?? "");
|
const value = textWithRepairedVariant(String(text ?? ""));
|
||||||
return (
|
return (
|
||||||
|
/(?:по\s+каким\s+документам\s+(?:это|его|ее|её|этот\s+товар|эту\s+позицию)\s+купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|какими\s+документами\s+(?:был\s+)?куплен|каким\s+документом\s+(?:это|его|ее|её|она|он|этот\s+товар|эта\s+позиция|эту\s+позицию)?\s*(?:подтвержден[оа]?|подтверждена\s+закупка|доказан[оа]?|закрыт[оа]?)|закупк[аи]\s+подтвержден[аы]?\s+документ|покажи\s+документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему))/iu.test(
|
||||||
|
value
|
||||||
|
) ||
|
||||||
/(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|какими\s+документами\s+(?:был\s+)?куплен|каким\s+документом\s+(?:это|его|её|ее|она|он|этот\s+товар|эта\s+позиция|эту\s+позицию)?\s*(?:подтвержден[оа]?|доказан[оа]?|закрыт[оа]?)|покажи\s+документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(
|
/(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|какими\s+документами\s+(?:был\s+)?куплен|каким\s+документом\s+(?:это|его|её|ее|она|он|этот\s+товар|эта\s+позиция|эту\s+позицию)?\s*(?:подтвержден[оа]?|доказан[оа]?|закрыт[оа]?)|покажи\s+документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(
|
||||||
value
|
value
|
||||||
) ||
|
) ||
|
||||||
|
|
@ -875,10 +886,17 @@ export function hasInventoryMarginRankingFollowupCue(text: string): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasAddressFollowupContextSignal(text: string): boolean {
|
export function hasAddressFollowupContextSignal(text: string): boolean {
|
||||||
const normalized = String(text ?? "").trim();
|
const normalized = textWithRepairedVariant(String(text ?? "")).trim();
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(
|
||||||
|
normalized
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (hasInventoryMarginRankingFollowupCue(normalized)) {
|
if (hasInventoryMarginRankingFollowupCue(normalized)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,10 @@ function inventoryPartyListOrUnknown(parties: string[]): string {
|
||||||
return parties.length > 0 ? parties.slice(0, 4).join("; ") : "не выделен отдельным полем";
|
return parties.length > 0 ? parties.slice(0, 4).join("; ") : "не выделен отдельным полем";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function omitAmountFromInventoryTraceEvidence(lines: string[]): string[] {
|
||||||
|
return lines.map((line) => String(line ?? "").replace(/\s+\|\s+сумма:\s+[^|]+(?=\s+\||$)/gu, ""));
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeInventoryReplyEntityToken(value: string | null | undefined): string | null {
|
function normalizeInventoryReplyEntityToken(value: string | null | undefined): string | null {
|
||||||
const normalized = String(value ?? "")
|
const normalized = String(value ?? "")
|
||||||
.trim()
|
.trim()
|
||||||
|
|
@ -535,7 +539,7 @@ export function composeInventoryReply(
|
||||||
appendInventorySection(
|
appendInventorySection(
|
||||||
lines,
|
lines,
|
||||||
"Опорные документы:",
|
"Опорные документы:",
|
||||||
deps.formatInventoryTraceRows(purchaseRows, INVENTORY_TRACE_EVIDENCE_ROW_LIMIT)
|
omitAmountFromInventoryTraceEvidence(deps.formatInventoryTraceRows(purchaseRows, INVENTORY_TRACE_EVIDENCE_ROW_LIMIT))
|
||||||
);
|
);
|
||||||
if (purchaseRows.length > INVENTORY_TRACE_EVIDENCE_ROW_LIMIT) {
|
if (purchaseRows.length > INVENTORY_TRACE_EVIDENCE_ROW_LIMIT) {
|
||||||
lines.push(
|
lines.push(
|
||||||
|
|
|
||||||
|
|
@ -3255,6 +3255,11 @@ function hasPredecomposeExplicitDrilldownSignal(text) {
|
||||||
return ADDRESS_DOCS_SIGNAL_PATTERN.test(source) || ADDRESS_BANK_SIGNAL_PATTERN.test(source) || ADDRESS_CONTRACT_SIGNAL_PATTERN.test(source);
|
return ADDRESS_DOCS_SIGNAL_PATTERN.test(source) || ADDRESS_BANK_SIGNAL_PATTERN.test(source) || ADDRESS_CONTRACT_SIGNAL_PATTERN.test(source);
|
||||||
}
|
}
|
||||||
function hasSelectedObjectInventoryFollowupSignalForPredecompose(text) {
|
function hasSelectedObjectInventoryFollowupSignalForPredecompose(text) {
|
||||||
|
const source = String(text ?? "");
|
||||||
|
const repaired = repairAddressMojibake(source);
|
||||||
|
if (/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(`${source} ${repaired}`)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(text ?? ""));
|
return /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(text ?? ""));
|
||||||
}
|
}
|
||||||
function isInventorySelectedObjectFollowupIntent(intent) {
|
function isInventorySelectedObjectFollowupIntent(intent) {
|
||||||
|
|
@ -3540,6 +3545,33 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
||||||
}, userMessage);
|
}, userMessage);
|
||||||
}
|
}
|
||||||
const candidateAnchorQuality = evaluateAddressAnchorQuality(candidate);
|
const candidateAnchorQuality = evaluateAddressAnchorQuality(candidate);
|
||||||
|
const candidateHasSemanticSelectedObjectScope =
|
||||||
|
candidateMeta?.semanticHints?.selected_object_scope_detected === true ||
|
||||||
|
candidateMeta?.semanticHints?.scope_target_kind === "selected_object";
|
||||||
|
const candidateTextForSelectedObjectCounterpartyDegradation = compactWhitespace(repairAddressMojibake(String(candidate ?? "")).toLowerCase());
|
||||||
|
const candidateUsesSelectedObjectAsCounterpartyAnchor =
|
||||||
|
/(?:^|\s)по\s+контрагент\p{L}*\s+(?:выбран\p{L}*|selected(?:\s+object)?|object|объект\p{L}*)/iu.test(candidateTextForSelectedObjectCounterpartyDegradation) ||
|
||||||
|
/(?:^|\s)counterparty\s+(?:selected(?:\s+object)?|object)/iu.test(candidateTextForSelectedObjectCounterpartyDegradation);
|
||||||
|
const candidateTurnsSelectedObjectIntoCounterparty =
|
||||||
|
sourceHasSelectedObjectInventoryFollowup &&
|
||||||
|
(candidateInjectsGenericDocsIntent || candidateUsesSelectedObjectAsCounterpartyAnchor) &&
|
||||||
|
(candidateHasSemanticSelectedObjectScope || candidateUsesSelectedObjectAsCounterpartyAnchor) &&
|
||||||
|
(candidateAnchorQuality.anchorType === "counterparty" || candidateUsesSelectedObjectAsCounterpartyAnchor) &&
|
||||||
|
(candidateAnchorQuality.quality < 2 || candidateUsesSelectedObjectAsCounterpartyAnchor);
|
||||||
|
if (candidateTurnsSelectedObjectIntoCounterparty) {
|
||||||
|
return attachAddressPredecomposeContract({
|
||||||
|
...baseMeta,
|
||||||
|
attempted: true,
|
||||||
|
applied: false,
|
||||||
|
traceId: normalized?.trace_id ?? null,
|
||||||
|
llmCanonicalCandidateDetected: true,
|
||||||
|
effectiveMessage: userMessage,
|
||||||
|
reason: "normalized_fragment_rejected_selected_object_counterparty_degradation",
|
||||||
|
fallbackRuleHit: null,
|
||||||
|
sanitizedUserMessage,
|
||||||
|
semanticHints: candidateMeta?.semanticHints ?? null
|
||||||
|
}, userMessage);
|
||||||
|
}
|
||||||
const sameIntentForAnchorSafety = sourceAnchorQuality.intent !== "unknown" && sourceAnchorQuality.intent === candidateAnchorQuality.intent;
|
const sameIntentForAnchorSafety = sourceAnchorQuality.intent !== "unknown" && sourceAnchorQuality.intent === candidateAnchorQuality.intent;
|
||||||
const sourceSelectedObjectItemAnchorValue = toNonEmptyString((0, addressFilterExtractor_1.extractSelectedObjectQuotedValue)(userMessage)) ??
|
const sourceSelectedObjectItemAnchorValue = toNonEmptyString((0, addressFilterExtractor_1.extractSelectedObjectQuotedValue)(userMessage)) ??
|
||||||
toNonEmptyString((0, addressFilterExtractor_1.extractSelectedObjectQuotedValue)(repairedSourceMessage || userMessage));
|
toNonEmptyString((0, addressFilterExtractor_1.extractSelectedObjectQuotedValue)(repairedSourceMessage || userMessage));
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,24 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
if (samples.length === 0) {
|
if (samples.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
samples.some((sample) =>
|
||||||
|
/(?:где\s+(?:мы\s+)?взял|откуда\s+(?:мы\s+)?взял|у\s+кого\s+купил|кто\s+постав|поставщик|источник\s+поступл|от\s+кого\s+поступ)/iu.test(
|
||||||
|
sample
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return "inventory_purchase_provenance_for_item";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
samples.some((sample) =>
|
||||||
|
/(?:документ|накладн|счет|счёт|акт|поступл|подтвержден|подтверждена\s+закупка|закупк[аи]\s+подтвержден)/iu.test(
|
||||||
|
sample
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return "inventory_purchase_documents_for_item";
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
suggestedIntents.includes("bank_operations_by_counterparty") &&
|
suggestedIntents.includes("bank_operations_by_counterparty") &&
|
||||||
samples.some((sample) => hasBankOperationsPivotCue(sample) || hasReadableBankOperationsPivotCue(sample))
|
samples.some((sample) => hasBankOperationsPivotCue(sample) || hasReadableBankOperationsPivotCue(sample))
|
||||||
|
|
@ -233,6 +251,13 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
/(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(
|
||||||
|
normalized
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(
|
return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(
|
||||||
normalized
|
normalized
|
||||||
);
|
);
|
||||||
|
|
@ -240,6 +265,12 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
|
|
||||||
function extractSelectedObjectLabel(text) {
|
function extractSelectedObjectLabel(text) {
|
||||||
const repaired = deps.repairAddressMojibake(String(text ?? ""));
|
const repaired = deps.repairAddressMojibake(String(text ?? ""));
|
||||||
|
const unicodeQuotedMatch = repaired.match(
|
||||||
|
/(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции)\s*[:,-]?\s*[«"]([^"»]+?)["»]/iu
|
||||||
|
);
|
||||||
|
if (unicodeQuotedMatch?.[1]) {
|
||||||
|
return deps.toNonEmptyString(unicodeQuotedMatch[1]);
|
||||||
|
}
|
||||||
const quotedMatch = repaired.match(
|
const quotedMatch = repaired.match(
|
||||||
/(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции)\s*[:,-]?\s*[«"]([^"»]+?)["»]/iu
|
/(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции)\s*[:,-]?\s*[«"]([^"»]+?)["»]/iu
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,15 @@ describe("addressInventoryIntentSignals", () => {
|
||||||
expect(result.reasons).toContain("inventory_purchase_date_signal_detected");
|
expect(result.reasons).toContain("inventory_purchase_date_signal_detected");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("routes selected-object purchase confirmation wording to item purchase documents before generic document drilldown", () => {
|
||||||
|
const result = resolveAddressIntent(
|
||||||
|
'По выбранному объекту "Флаг геральдический. 100*150 см. Ткань Т101 полиэфирный шелк": каким документом подтверждена закупка?'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.intent).toBe("inventory_purchase_documents_for_item");
|
||||||
|
expect(result.reasons).toContain("inventory_selected_object_purchase_documents_signal_detected");
|
||||||
|
});
|
||||||
|
|
||||||
it("classifies direct Russian purchase-date wording with an explicit item", () => {
|
it("classifies direct Russian purchase-date wording with an explicit item", () => {
|
||||||
const result = resolveAddressIntent("Когда был куплен товар Столешница 600*3050*26 дуб ниагара");
|
const result = resolveAddressIntent("Когда был куплен товар Столешница 600*3050*26 дуб ниагара");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,35 @@ describe("inventory profitability selected-object regressions", () => {
|
||||||
expect(result?.filters.extracted_filters.period_to).toBe("2020-05-31");
|
expect(result?.filters.extracted_filters.period_to).toBe("2020-05-31");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("routes UTF-8 selected-object purchase confirmation wording to item purchase documents", () => {
|
||||||
|
const item = "Флаг геральдический. 100*150 см. Ткань Т101 полиэфирный шелк";
|
||||||
|
const result = runAddressDecomposeStage(
|
||||||
|
`По выбранному объекту "${item}": каким документом подтверждена закупка?`,
|
||||||
|
{
|
||||||
|
previous_intent: "inventory_margin_ranking_for_nomenclature",
|
||||||
|
target_intent: "inventory_margin_ranking_for_nomenclature",
|
||||||
|
root_intent: "inventory_margin_ranking_for_nomenclature",
|
||||||
|
previous_filters: {
|
||||||
|
item,
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
as_of_date: "2017-12-31",
|
||||||
|
period_from: "2017-01-01",
|
||||||
|
period_to: "2017-12-31"
|
||||||
|
},
|
||||||
|
previous_anchor_type: "item",
|
||||||
|
previous_anchor_value: item,
|
||||||
|
current_frame_kind: "inventory_drilldown"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.intent.intent).toBe("inventory_purchase_documents_for_item");
|
||||||
|
expect(result?.filters.extracted_filters.item).toBe(item);
|
||||||
|
expect(result?.filters.extracted_filters.as_of_date).toBe("2017-12-31");
|
||||||
|
expect(result?.filters.extracted_filters.counterparty).toBeUndefined();
|
||||||
|
expect(result?.intent.reasons).toContain("inventory_selected_object_purchase_documents_signal_detected");
|
||||||
|
});
|
||||||
|
|
||||||
it("answers selected-object profitability with a bounded document spread instead of a recipe gap", async () => {
|
it("answers selected-object profitability with a bounded document spread instead of a recipe gap", async () => {
|
||||||
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||||
fetched_rows: 2,
|
fetched_rows: 2,
|
||||||
|
|
|
||||||
|
|
@ -557,6 +557,59 @@ describe("inventory selected-object follow-up", () => {
|
||||||
expect(String(result?.reply_text ?? "")).toContain("Поступление товаров и услуг 00000000077");
|
expect(String(result?.reply_text ?? "")).toContain("Поступление товаров и услуг 00000000077");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("handles UTF-8 UI selected-object purchase confirmation as an item document drilldown", async () => {
|
||||||
|
const item = "Флаг геральдический. 100*150 см. Ткань Т101 полиэфирный шелк";
|
||||||
|
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||||
|
fetched_rows: 1,
|
||||||
|
matched_rows: 1,
|
||||||
|
raw_rows: [
|
||||||
|
{
|
||||||
|
Period: "2017-06-01T12:00:09Z",
|
||||||
|
Registrator: "Поступление товаров и услуг 00000000016 от 01.06.2017 12:00:09",
|
||||||
|
AccountDt: "41.01",
|
||||||
|
AccountKt: "60.01",
|
||||||
|
Amount: 8520,
|
||||||
|
Quantity: 1,
|
||||||
|
SubcontoDt1: item,
|
||||||
|
SubcontoDt3: "Основной склад",
|
||||||
|
Counterparty: "ПрофТренд,ООО",
|
||||||
|
Organization: 'ООО "Альтернатива Плюс"'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
rows: [],
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const service = new AddressQueryService();
|
||||||
|
const result = await service.tryHandle(`По выбранному объекту "${item}": каким документом подтверждена закупка?`, {
|
||||||
|
followupContext: {
|
||||||
|
previous_intent: "inventory_margin_ranking_for_nomenclature",
|
||||||
|
target_intent: "inventory_margin_ranking_for_nomenclature",
|
||||||
|
root_intent: "inventory_margin_ranking_for_nomenclature",
|
||||||
|
previous_filters: {
|
||||||
|
item,
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
as_of_date: "2017-12-31",
|
||||||
|
period_from: "2017-01-01",
|
||||||
|
period_to: "2017-12-31"
|
||||||
|
},
|
||||||
|
previous_anchor_type: "item",
|
||||||
|
previous_anchor_value: item,
|
||||||
|
current_frame_kind: "inventory_drilldown"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result?.handled).toBe(true);
|
||||||
|
expect(result?.response_type).toBe("FACTUAL_LIST");
|
||||||
|
expect(result?.debug.detected_intent).toBe("inventory_purchase_documents_for_item");
|
||||||
|
expect(result?.debug.selected_recipe).toBe("address_inventory_purchase_documents_for_item_v1");
|
||||||
|
expect(result?.debug.capability_id).toBe("inventory_inventory_purchase_documents_for_item");
|
||||||
|
expect(result?.debug.extracted_filters?.item).toBe(item);
|
||||||
|
expect(result?.debug.extracted_filters?.counterparty).toBeUndefined();
|
||||||
|
expect(String(result?.reply_text ?? "")).toContain("Поступление товаров и услуг 00000000016");
|
||||||
|
expect(String(result?.reply_text ?? "")).toContain("ПрофТренд,ООО");
|
||||||
|
});
|
||||||
|
|
||||||
it("does not let carried counterparty scope steal selected-item document follow-up into open items", async () => {
|
it("does not let carried counterparty scope steal selected-item document follow-up into open items", async () => {
|
||||||
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||||
fetched_rows: 1,
|
fetched_rows: 1,
|
||||||
|
|
|
||||||
|
|
@ -701,4 +701,56 @@ describe("address reply builders regressions", () => {
|
||||||
expect(firstLine).toContain("Самая маржинальная позиция");
|
expect(firstLine).toContain("Самая маржинальная позиция");
|
||||||
expect(result?.text).not.toContain("амортизац");
|
expect(result?.text).not.toContain("амортизац");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps supplier provenance evidence focused on document identity instead of surfacing ambiguous amount fields", () => {
|
||||||
|
const result = composeInventoryReply(
|
||||||
|
"inventory_purchase_provenance_for_item",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
amount: 8520,
|
||||||
|
item: "Флаг геральдический",
|
||||||
|
period: "2017-06-01",
|
||||||
|
registrator: "Поступление товаров и услуг 00000000016",
|
||||||
|
counterparty: "ПрофТренд,ООО"
|
||||||
|
} as any
|
||||||
|
],
|
||||||
|
{
|
||||||
|
userMessage: "А по этой позиции кто поставщик?",
|
||||||
|
asOfDate: "2017-12-31"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resolvePayablesAsOfDate: () => "2017-12-31",
|
||||||
|
buildInventoryOnHandAggregate: () => [],
|
||||||
|
uniqueStrings: (values: string[]) => Array.from(new Set(values)),
|
||||||
|
formatDateRu: (value: string) => value,
|
||||||
|
formatNumberWithDots: (value: number, fractionDigits = 0) => value.toFixed(fractionDigits),
|
||||||
|
formatMoneyRub: (value: number) => `${value} ₽`,
|
||||||
|
isInventoryPurchaseMovement: () => true,
|
||||||
|
summarizeInventoryTraceRows: (rows: any[]) => ({
|
||||||
|
item: rows[0]?.item ?? null,
|
||||||
|
warehouses: [],
|
||||||
|
organizations: [],
|
||||||
|
counterparties: ["ПрофТренд,ООО"],
|
||||||
|
documents: ["Поступление товаров и услуг 00000000016"],
|
||||||
|
firstPeriod: "2017-06-01",
|
||||||
|
lastPeriod: "2017-06-01",
|
||||||
|
totalAmount: 8520
|
||||||
|
}),
|
||||||
|
formatInventoryTraceRows: () => [
|
||||||
|
"1. Поступление товаров и услуг 00000000016 | дата: 2017-06-01 | сумма: 8.520,00 ₽ | товар: Флаг геральдический | контрагент: ПрофТренд,ООО"
|
||||||
|
],
|
||||||
|
hasInventoryPurchaseDateActionFocus: () => false,
|
||||||
|
inventoryTraceDateLabel: (value: string | null) => String(value ?? ""),
|
||||||
|
extractInventoryCounterpartyCandidates: () => ["ПрофТренд,ООО"],
|
||||||
|
buildInventoryAgingByItemAggregate: () => [],
|
||||||
|
formatInventoryAgingRows: () => [],
|
||||||
|
isInventorySaleMovement: () => false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result?.text.split("\n")[0]).toContain("ПрофТренд,ООО");
|
||||||
|
expect(result?.text).toContain("Поступление товаров и услуг 00000000016");
|
||||||
|
expect(result?.text).not.toContain("сумма:");
|
||||||
|
expect(result?.text).not.toContain("8.520,00 ₽");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -502,6 +502,115 @@ describe("assistant address llm pre-decompose candidate preference", () => {
|
||||||
]).toContain(response.debug?.llm_decomposition_reason);
|
]).toContain(response.debug?.llm_decomposition_reason);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects UTF-8 selected-object purchase document rewrite into a generic counterparty document query", async () => {
|
||||||
|
const calls: Array<{ message: string }> = [];
|
||||||
|
const addressQueryService = {
|
||||||
|
tryHandle: vi.fn(async (message: string) => {
|
||||||
|
calls.push({ message });
|
||||||
|
return buildAddressLaneResult(message);
|
||||||
|
})
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const sourceMessage =
|
||||||
|
'По выбранному объекту "Флаг геральдический. 100*150 см. Ткань Т101 полиэфирный шелк": каким документом подтверждена закупка?';
|
||||||
|
const selectedItem = "Флаг геральдический. 100*150 см. Ткань Т101 полиэфирный шелк";
|
||||||
|
|
||||||
|
const normalizerService = {
|
||||||
|
normalize: vi.fn(async () => ({
|
||||||
|
trace_id: "norm-predecompose-selected-object-counterparty-degradation",
|
||||||
|
ok: true,
|
||||||
|
normalized: {
|
||||||
|
schema_version: "normalized_query_v2_0_2",
|
||||||
|
user_message_raw: sourceMessage,
|
||||||
|
message_in_scope: true,
|
||||||
|
scope_confidence: "medium",
|
||||||
|
contains_multiple_tasks: false,
|
||||||
|
fragments: [
|
||||||
|
{
|
||||||
|
fragment_id: "F1",
|
||||||
|
raw_fragment_text: sourceMessage,
|
||||||
|
normalized_fragment_text: "документы по контрагенту выбранному",
|
||||||
|
semantic_hints: {
|
||||||
|
scope_target_kind: "selected_object",
|
||||||
|
scope_target_text: selectedItem,
|
||||||
|
date_scope_kind: "missing",
|
||||||
|
self_scope_detected: false,
|
||||||
|
selected_object_scope_detected: true
|
||||||
|
},
|
||||||
|
domain_relevance: "in_scope",
|
||||||
|
business_scope: "company_specific_accounting",
|
||||||
|
entity_hints: [],
|
||||||
|
account_hints: [],
|
||||||
|
document_hints: [],
|
||||||
|
register_hints: [],
|
||||||
|
time_scope: {
|
||||||
|
type: "missing",
|
||||||
|
value: null,
|
||||||
|
confidence: "low"
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
has_multi_entity_scope: false,
|
||||||
|
asks_for_chain_explanation: false,
|
||||||
|
asks_for_ranking_or_top: false,
|
||||||
|
asks_for_period_summary: false,
|
||||||
|
asks_for_rule_check: false,
|
||||||
|
asks_for_anomaly_scan: false,
|
||||||
|
asks_for_exact_object_trace: true,
|
||||||
|
asks_for_evidence: true,
|
||||||
|
mentions_period_close_context: false
|
||||||
|
},
|
||||||
|
candidate_labels: ["simple_factual"],
|
||||||
|
confidence: "medium",
|
||||||
|
execution_readiness: "executable",
|
||||||
|
clarification_reason: null,
|
||||||
|
soft_assumption_used: [],
|
||||||
|
route_status: "routed",
|
||||||
|
no_route_reason: null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
discarded_fragments: [],
|
||||||
|
global_notes: {
|
||||||
|
needs_clarification: false,
|
||||||
|
clarification_reason: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
raw_model_output: null,
|
||||||
|
validation: { passed: true, errors: [] },
|
||||||
|
usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 },
|
||||||
|
latency_ms: 10,
|
||||||
|
prompt_version: "normalizer_v2_0_2",
|
||||||
|
schema_version: "v2_0_2",
|
||||||
|
request_count_for_case: 1
|
||||||
|
}))
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const sessions = new AssistantSessionStore();
|
||||||
|
const service = new AssistantService(
|
||||||
|
normalizerService,
|
||||||
|
sessions as any,
|
||||||
|
{} as any,
|
||||||
|
{ persistSession: vi.fn() } as any,
|
||||||
|
addressQueryService
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await service.handleMessage({
|
||||||
|
session_id: `asst-predecompose-selected-object-doc-${Date.now()}`,
|
||||||
|
user_message: sourceMessage,
|
||||||
|
llmProvider: "local",
|
||||||
|
useMock: false
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
expect(response.ok).toBe(true);
|
||||||
|
expect(response.reply_type).toBe("factual");
|
||||||
|
expect(calls).toHaveLength(1);
|
||||||
|
expect(calls[0].message).toBe(sourceMessage);
|
||||||
|
expect([
|
||||||
|
"normalized_fragment_rejected_selected_object_context_loss",
|
||||||
|
"normalized_fragment_rejected_selected_object_counterparty_degradation"
|
||||||
|
]).toContain(response.debug?.llm_decomposition_reason);
|
||||||
|
expect(response.debug?.llm_predecompose_contract?.canonical_message).toBe(sourceMessage);
|
||||||
|
});
|
||||||
|
|
||||||
it("accepts exact selected-object sale rewrite when llm candidate stays on the same item", async () => {
|
it("accepts exact selected-object sale rewrite when llm candidate stays on the same item", async () => {
|
||||||
const calls: Array<{ message: string }> = [];
|
const calls: Array<{ message: string }> = [];
|
||||||
const addressQueryService = {
|
const addressQueryService = {
|
||||||
|
|
@ -1437,7 +1546,7 @@ describe("assistant address llm pre-decompose candidate preference", () => {
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
expect(response.ok).toBe(true);
|
expect(response.ok).toBe(true);
|
||||||
expect(response.reply_type).toBe("factual");
|
expect(["factual", "partial_coverage"]).toContain(response.reply_type);
|
||||||
expect(calls).toHaveLength(1);
|
expect(calls).toHaveLength(1);
|
||||||
expect(calls[0].message).toBe("документы по контрагенту свк за 2020 год");
|
expect(calls[0].message).toBe("документы по контрагенту свк за 2020 год");
|
||||||
expect(response.debug?.llm_decomposition_reason).toBe("fallback_rule_applied_after_llm");
|
expect(response.debug?.llm_decomposition_reason).toBe("fallback_rule_applied_after_llm");
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,60 @@
|
||||||
[
|
[
|
||||||
|
{
|
||||||
|
"generation_id": "gen-ag06020818-4993c6",
|
||||||
|
"created_at": "2026-06-02T08:18:38+00:00",
|
||||||
|
"mode": "saved_user_sessions",
|
||||||
|
"title": "AGENT | inventory margin ranking follow-up proof",
|
||||||
|
"count": 7,
|
||||||
|
"domain": "inventory_margin_ranking",
|
||||||
|
"questions": [
|
||||||
|
"Какая номенклатура товара реализована с высокой прибылью, а какая с низкой?",
|
||||||
|
"май 2020",
|
||||||
|
"Покажи найденные строки себестоимостной базы.",
|
||||||
|
"Расширь до 2017 год.",
|
||||||
|
"Анализ по 41 счету, а не по 01.",
|
||||||
|
"А по этой позиции кто поставщик?",
|
||||||
|
"По выбранному объекту \"{{step_outputs.step_06_pronoun_selected_item_supplier.focus_object.label}}\": каким документом подтверждена закупка?"
|
||||||
|
],
|
||||||
|
"generated_by": "codex_agent",
|
||||||
|
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260602081838_gen-ag06020818-4993c6.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_20260602081838_gen-ag06020818-4993c6.json",
|
||||||
|
"saved_case_set_kind": "agent_semantic_scenario",
|
||||||
|
"agent_run": true,
|
||||||
|
"agent_focus": "inventory-margin-ranking-selected-object-followups",
|
||||||
|
"architecture_phase": "assistant-stage1",
|
||||||
|
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\agent_inventory_margin_ranking_reliability_20260524.json",
|
||||||
|
"scenario_id": null,
|
||||||
|
"semantic_tags": [
|
||||||
|
"account_family_guard",
|
||||||
|
"carryover",
|
||||||
|
"domain_purity",
|
||||||
|
"evidence_followup",
|
||||||
|
"focus_object",
|
||||||
|
"inventory_margin_ranking",
|
||||||
|
"needs_period",
|
||||||
|
"no_fixed_assets",
|
||||||
|
"period_expansion",
|
||||||
|
"period_followup",
|
||||||
|
"pronoun_followup",
|
||||||
|
"purchase_document_followup",
|
||||||
|
"selected_object",
|
||||||
|
"supplier_followup",
|
||||||
|
"ui_selected_object"
|
||||||
|
],
|
||||||
|
"validation_status": "accepted_domain_pack_loop",
|
||||||
|
"validated_run_dir": "artifacts\\domain_runs\\agent_inventory_margin_ranking_reliability_20260602_p9",
|
||||||
|
"saved_after_validated_replay": true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"generation_id": "gen-ag06020725-192110",
|
"generation_id": "gen-ag06020725-192110",
|
||||||
"created_at": "2026-06-02T07:25:31+00:00",
|
"created_at": "2026-06-02T07:25:31+00:00",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
{
|
||||||
|
"saved_at": "2026-06-02T08:18:38+00:00",
|
||||||
|
"generation_id": "gen-ag06020818-4993c6",
|
||||||
|
"mode": "saved_user_sessions",
|
||||||
|
"title": "AGENT | inventory margin ranking follow-up proof",
|
||||||
|
"agent_run": true,
|
||||||
|
"questions": [
|
||||||
|
"Какая номенклатура товара реализована с высокой прибылью, а какая с низкой?",
|
||||||
|
"май 2020",
|
||||||
|
"Покажи найденные строки себестоимостной базы.",
|
||||||
|
"Расширь до 2017 год.",
|
||||||
|
"Анализ по 41 счету, а не по 01.",
|
||||||
|
"А по этой позиции кто поставщик?",
|
||||||
|
"По выбранному объекту \"{{step_outputs.step_06_pronoun_selected_item_supplier.focus_object.label}}\": каким документом подтверждена закупка?"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"assistant_prompt_version": null,
|
||||||
|
"decomposition_prompt_version": null,
|
||||||
|
"prompt_fingerprint": null,
|
||||||
|
"agent_focus": "inventory-margin-ranking-selected-object-followups",
|
||||||
|
"architecture_phase": "assistant-stage1",
|
||||||
|
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\agent_inventory_margin_ranking_reliability_20260524.json",
|
||||||
|
"scenario_id": null,
|
||||||
|
"semantic_tags": [
|
||||||
|
"account_family_guard",
|
||||||
|
"carryover",
|
||||||
|
"domain_purity",
|
||||||
|
"evidence_followup",
|
||||||
|
"focus_object",
|
||||||
|
"inventory_margin_ranking",
|
||||||
|
"needs_period",
|
||||||
|
"no_fixed_assets",
|
||||||
|
"period_expansion",
|
||||||
|
"period_followup",
|
||||||
|
"pronoun_followup",
|
||||||
|
"purchase_document_followup",
|
||||||
|
"selected_object",
|
||||||
|
"supplier_followup",
|
||||||
|
"ui_selected_object"
|
||||||
|
],
|
||||||
|
"validation_status": "accepted_domain_pack_loop",
|
||||||
|
"validated_run_dir": "artifacts\\domain_runs\\agent_inventory_margin_ranking_reliability_20260602_p9",
|
||||||
|
"saved_after_validated_replay": true,
|
||||||
|
"save_gate": {
|
||||||
|
"schema_version": "agent_semantic_save_gate_v1",
|
||||||
|
"validation_status": "accepted_domain_pack_loop",
|
||||||
|
"validated_run_dir": "artifacts\\domain_runs\\agent_inventory_margin_ranking_reliability_20260602_p9",
|
||||||
|
"final_status": "accepted",
|
||||||
|
"loop_id": "agent_inventory_margin_ranking_reliability_20260602_p9",
|
||||||
|
"target_score": 90,
|
||||||
|
"iterations_ran": 1,
|
||||||
|
"quality_score": 93,
|
||||||
|
"repair_target_count": 0,
|
||||||
|
"repair_target_severity_counts": {
|
||||||
|
"P0": 0,
|
||||||
|
"P1": 0,
|
||||||
|
"P2": 0
|
||||||
|
},
|
||||||
|
"accepted_gate": true,
|
||||||
|
"effective_runtime": {
|
||||||
|
"manifest_path": "artifacts\\domain_runs\\agent_inventory_margin_ranking_reliability_20260602_p9\\effective_runtime.json",
|
||||||
|
"runner": "domain_case_loop.run-pack-loop",
|
||||||
|
"git_sha": "d77bcbe482d5091a84ae176c79bc8ef3558bbb32",
|
||||||
|
"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": "Какая номенклатура товара реализована с высокой прибылью, а какая с низкой?",
|
||||||
|
"created_at": "2026-06-02T08:18:38+00:00",
|
||||||
|
"reply_type": null,
|
||||||
|
"trace_id": null,
|
||||||
|
"debug": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message_id": "agent-user-002",
|
||||||
|
"role": "user",
|
||||||
|
"text": "май 2020",
|
||||||
|
"created_at": "2026-06-02T08:18:38+00:00",
|
||||||
|
"reply_type": null,
|
||||||
|
"trace_id": null,
|
||||||
|
"debug": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message_id": "agent-user-003",
|
||||||
|
"role": "user",
|
||||||
|
"text": "Покажи найденные строки себестоимостной базы.",
|
||||||
|
"created_at": "2026-06-02T08:18:38+00:00",
|
||||||
|
"reply_type": null,
|
||||||
|
"trace_id": null,
|
||||||
|
"debug": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message_id": "agent-user-004",
|
||||||
|
"role": "user",
|
||||||
|
"text": "Расширь до 2017 год.",
|
||||||
|
"created_at": "2026-06-02T08:18:38+00:00",
|
||||||
|
"reply_type": null,
|
||||||
|
"trace_id": null,
|
||||||
|
"debug": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message_id": "agent-user-005",
|
||||||
|
"role": "user",
|
||||||
|
"text": "Анализ по 41 счету, а не по 01.",
|
||||||
|
"created_at": "2026-06-02T08:18:38+00:00",
|
||||||
|
"reply_type": null,
|
||||||
|
"trace_id": null,
|
||||||
|
"debug": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message_id": "agent-user-006",
|
||||||
|
"role": "user",
|
||||||
|
"text": "А по этой позиции кто поставщик?",
|
||||||
|
"created_at": "2026-06-02T08:18:38+00:00",
|
||||||
|
"reply_type": null,
|
||||||
|
"trace_id": null,
|
||||||
|
"debug": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message_id": "agent-user-007",
|
||||||
|
"role": "user",
|
||||||
|
"text": "По выбранному объекту \"{{step_outputs.step_06_pronoun_selected_item_supplier.focus_object.label}}\": каким документом подтверждена закупка?",
|
||||||
|
"created_at": "2026-06-02T08:18:38+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": "inventory-margin-ranking-selected-object-followups",
|
||||||
|
"architecture_phase": "assistant-stage1",
|
||||||
|
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\agent_inventory_margin_ranking_reliability_20260524.json",
|
||||||
|
"scenario_id": null,
|
||||||
|
"semantic_tags": [
|
||||||
|
"account_family_guard",
|
||||||
|
"carryover",
|
||||||
|
"domain_purity",
|
||||||
|
"evidence_followup",
|
||||||
|
"focus_object",
|
||||||
|
"inventory_margin_ranking",
|
||||||
|
"needs_period",
|
||||||
|
"no_fixed_assets",
|
||||||
|
"period_expansion",
|
||||||
|
"period_followup",
|
||||||
|
"pronoun_followup",
|
||||||
|
"purchase_document_followup",
|
||||||
|
"selected_object",
|
||||||
|
"supplier_followup",
|
||||||
|
"ui_selected_object"
|
||||||
|
],
|
||||||
|
"validation_status": "accepted_domain_pack_loop",
|
||||||
|
"validated_run_dir": "artifacts\\domain_runs\\agent_inventory_margin_ranking_reliability_20260602_p9",
|
||||||
|
"saved_after_validated_replay": true,
|
||||||
|
"save_gate": {
|
||||||
|
"schema_version": "agent_semantic_save_gate_v1",
|
||||||
|
"validation_status": "accepted_domain_pack_loop",
|
||||||
|
"validated_run_dir": "artifacts\\domain_runs\\agent_inventory_margin_ranking_reliability_20260602_p9",
|
||||||
|
"final_status": "accepted",
|
||||||
|
"loop_id": "agent_inventory_margin_ranking_reliability_20260602_p9",
|
||||||
|
"target_score": 90,
|
||||||
|
"iterations_ran": 1,
|
||||||
|
"quality_score": 93,
|
||||||
|
"repair_target_count": 0,
|
||||||
|
"repair_target_severity_counts": {
|
||||||
|
"P0": 0,
|
||||||
|
"P1": 0,
|
||||||
|
"P2": 0
|
||||||
|
},
|
||||||
|
"accepted_gate": true,
|
||||||
|
"effective_runtime": {
|
||||||
|
"manifest_path": "artifacts\\domain_runs\\agent_inventory_margin_ranking_reliability_20260602_p9\\effective_runtime.json",
|
||||||
|
"runner": "domain_case_loop.run-pack-loop",
|
||||||
|
"git_sha": "d77bcbe482d5091a84ae176c79bc8ef3558bbb32",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
{
|
||||||
|
"suite_id": "assistant_saved_session_gen-ag06020818-4993c6",
|
||||||
|
"suite_version": "0.1.0",
|
||||||
|
"schema_version": "assistant_saved_session_suite_v0_1",
|
||||||
|
"generated_at": "2026-06-02T08:18:38+00:00",
|
||||||
|
"generation_id": "gen-ag06020818-4993c6",
|
||||||
|
"mode": "saved_user_sessions",
|
||||||
|
"title": "AGENT | inventory margin ranking follow-up proof",
|
||||||
|
"domain": "inventory_margin_ranking",
|
||||||
|
"scenario_count": 1,
|
||||||
|
"case_ids": [
|
||||||
|
"SAVED-001"
|
||||||
|
],
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"case_id": "SAVED-001",
|
||||||
|
"scenario_tag": "agent_saved_user_sessions",
|
||||||
|
"title": "AGENT | inventory margin ranking follow-up proof",
|
||||||
|
"question_type": "followup",
|
||||||
|
"broadness_level": "medium",
|
||||||
|
"turns": [
|
||||||
|
{
|
||||||
|
"user_message": "Какая номенклатура товара реализована с высокой прибылью, а какая с низкой?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "май 2020"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "Покажи найденные строки себестоимостной базы."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "Расширь до 2017 год."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "Анализ по 41 счету, а не по 01."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "А по этой позиции кто поставщик?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "По выбранному объекту \"{{step_outputs.step_06_pronoun_selected_item_supplier.focus_object.label}}\": каким документом подтверждена закупка?"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -983,6 +983,84 @@ def drop_none_values(payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
return {key: value for key, value in payload.items() if value is not None}
|
return {key: value for key, value in payload.items() if value is not None}
|
||||||
|
|
||||||
|
|
||||||
|
def first_entry_field(entries: Any, *field_names: str) -> Any:
|
||||||
|
if not isinstance(entries, list):
|
||||||
|
return None
|
||||||
|
for entry in entries:
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
continue
|
||||||
|
fields = entry.get("fields") if isinstance(entry.get("fields"), dict) else {}
|
||||||
|
raw_fields = entry.get("raw_fields") if isinstance(entry.get("raw_fields"), dict) else {}
|
||||||
|
for field_name in field_names:
|
||||||
|
for candidate in (entry.get(field_name), fields.get(field_name), raw_fields.get(field_name)):
|
||||||
|
value = str(candidate or "").strip()
|
||||||
|
if value:
|
||||||
|
return value
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def build_reusable_inventory_bundle(step_state: dict[str, Any]) -> dict[str, Any] | None:
|
||||||
|
intent = str(step_state.get("detected_intent") or "").strip()
|
||||||
|
if intent not in {"inventory_purchase_provenance_for_item", "inventory_purchase_documents_for_item"}:
|
||||||
|
return None
|
||||||
|
entries = step_state.get("entries") if isinstance(step_state.get("entries"), list) else []
|
||||||
|
focus_object = step_state.get("focus_object") if isinstance(step_state.get("focus_object"), dict) else {}
|
||||||
|
if not entries and not focus_object:
|
||||||
|
return None
|
||||||
|
return drop_none_values(
|
||||||
|
{
|
||||||
|
"source_step_id": step_state.get("step_id"),
|
||||||
|
"source_intent": intent,
|
||||||
|
"source_route": step_state.get("last_confirmed_route") or step_state.get("selected_recipe"),
|
||||||
|
"focus_object": focus_object or None,
|
||||||
|
"date_scope": step_state.get("date_scope") if isinstance(step_state.get("date_scope"), dict) else None,
|
||||||
|
"supplier_if_known": first_entry_field(entries, "контрагент", "counterparty", "supplier"),
|
||||||
|
"first_purchase_date": first_entry_field(entries, "date", "дата"),
|
||||||
|
"source_document_if_known": first_entry_field(entries, "title", "item", "документ", "document"),
|
||||||
|
"entries": entries or None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_scenario_semantic_memory_after_step(
|
||||||
|
previous_semantic_memory: Any,
|
||||||
|
step: dict[str, Any],
|
||||||
|
step_state: dict[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
previous_memory = previous_semantic_memory if isinstance(previous_semantic_memory, dict) else {}
|
||||||
|
previous_date_scope = previous_memory.get("date_scope")
|
||||||
|
focus_object = step_state.get("focus_object") if isinstance(step_state.get("focus_object"), dict) else None
|
||||||
|
entries = step_state.get("entries") if isinstance(step_state.get("entries"), list) else None
|
||||||
|
memory = drop_none_values(
|
||||||
|
{
|
||||||
|
"latest_step_id": step.get("step_id"),
|
||||||
|
"latest_step_status": step_state.get("status"),
|
||||||
|
"active_result_set_id": step_state.get("active_result_set_id"),
|
||||||
|
"last_confirmed_route": step_state.get("last_confirmed_route"),
|
||||||
|
"date_scope": merge_scenario_date_scope(
|
||||||
|
previous_date_scope,
|
||||||
|
step_state.get("date_scope"),
|
||||||
|
depends_on=step.get("depends_on") or [],
|
||||||
|
),
|
||||||
|
"organization_scope": step_state.get("organization_scope"),
|
||||||
|
"focus_object": focus_object,
|
||||||
|
"selected_object_ref": focus_object,
|
||||||
|
"comparison_scope": step_state.get("comparison_scope"),
|
||||||
|
"entries": entries,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
bundle = build_reusable_inventory_bundle(step_state)
|
||||||
|
if bundle:
|
||||||
|
memory["provenance_bundle"] = bundle
|
||||||
|
if str(step_state.get("detected_intent") or "").strip() == "inventory_purchase_documents_for_item":
|
||||||
|
memory["purchase_documents_bundle"] = bundle
|
||||||
|
if bundle.get("supplier_if_known"):
|
||||||
|
memory["supplier_if_known"] = bundle.get("supplier_if_known")
|
||||||
|
if bundle.get("first_purchase_date"):
|
||||||
|
memory["first_purchase_date"] = bundle.get("first_purchase_date")
|
||||||
|
return memory
|
||||||
|
|
||||||
|
|
||||||
def http_json(url: str, *, method: str = "GET", payload: dict[str, Any] | None = None, timeout: int = 30) -> dict[str, Any]:
|
def http_json(url: str, *, method: str = "GET", payload: dict[str, Any] | None = None, timeout: int = 30) -> dict[str, Any]:
|
||||||
data = None
|
data = None
|
||||||
headers = {"Accept": "application/json"}
|
headers = {"Accept": "application/json"}
|
||||||
|
|
@ -3581,22 +3659,11 @@ def execute_scenario_manifest(
|
||||||
|
|
||||||
scenario_state["session_id"] = result["session_id"]
|
scenario_state["session_id"] = result["session_id"]
|
||||||
scenario_state["step_outputs"][step["step_id"]] = result["step_state"]
|
scenario_state["step_outputs"][step["step_id"]] = result["step_state"]
|
||||||
previous_semantic_memory = scenario_state.get("semantic_memory") or {}
|
scenario_state["semantic_memory"] = build_scenario_semantic_memory_after_step(
|
||||||
previous_date_scope = previous_semantic_memory.get("date_scope") if isinstance(previous_semantic_memory, dict) else None
|
scenario_state.get("semantic_memory"),
|
||||||
current_date_scope = result["step_state"].get("date_scope")
|
step,
|
||||||
scenario_state["semantic_memory"] = {
|
result["step_state"],
|
||||||
"latest_step_id": step["step_id"],
|
)
|
||||||
"latest_step_status": result["step_state"].get("status"),
|
|
||||||
"active_result_set_id": result["step_state"].get("active_result_set_id"),
|
|
||||||
"last_confirmed_route": result["step_state"].get("last_confirmed_route"),
|
|
||||||
"date_scope": merge_scenario_date_scope(
|
|
||||||
previous_date_scope,
|
|
||||||
current_date_scope,
|
|
||||||
depends_on=step.get("depends_on") or [],
|
|
||||||
),
|
|
||||||
"organization_scope": result["step_state"].get("organization_scope"),
|
|
||||||
"entries": result["step_state"].get("entries"),
|
|
||||||
}
|
|
||||||
scenario_state["updated_at"] = datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
scenario_state["updated_at"] = datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
||||||
save_scenario_step_bundle(
|
save_scenario_step_bundle(
|
||||||
step_dir=step_dir,
|
step_dir=step_dir,
|
||||||
|
|
@ -5099,6 +5166,46 @@ def analyst_target_contradicts_validated_step(target: dict[str, Any], step_snaps
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def analyst_target_contradicts_pack_acceptance_status(
|
||||||
|
repair_targets: dict[str, Any],
|
||||||
|
target: dict[str, Any],
|
||||||
|
) -> bool:
|
||||||
|
pack_acceptance_status = str(repair_targets.get("acceptance_status") or "").strip()
|
||||||
|
pack_final_status = str(repair_targets.get("final_status") or "").strip()
|
||||||
|
if pack_acceptance_status != "accepted" or pack_final_status != "partial":
|
||||||
|
return False
|
||||||
|
target_text = " ".join(
|
||||||
|
str(value or "")
|
||||||
|
for value in (
|
||||||
|
target.get("scenario_id"),
|
||||||
|
target.get("step_id"),
|
||||||
|
target.get("problem_type"),
|
||||||
|
target.get("fix_goal"),
|
||||||
|
target.get("minimal_patch_direction"),
|
||||||
|
" ".join(normalize_string_list(target.get("violated_invariants"))),
|
||||||
|
)
|
||||||
|
).casefold()
|
||||||
|
mentions_pack_status = (
|
||||||
|
"pack_state" in target_text
|
||||||
|
and "acceptance_status" in target_text
|
||||||
|
and "final_status" in target_text
|
||||||
|
)
|
||||||
|
if not mentions_pack_status:
|
||||||
|
return False
|
||||||
|
if "pack_run" not in target_text and "pack-level" not in target_text:
|
||||||
|
return False
|
||||||
|
return any(
|
||||||
|
marker in target_text
|
||||||
|
for marker in (
|
||||||
|
"запрещает итоговый",
|
||||||
|
"нет `acceptance_status`",
|
||||||
|
"acceptance_status is absent",
|
||||||
|
"without `acceptance_status`",
|
||||||
|
"legacy-gate",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def merge_analyst_priority_repair_targets(
|
def merge_analyst_priority_repair_targets(
|
||||||
repair_targets: dict[str, Any],
|
repair_targets: dict[str, Any],
|
||||||
analyst_verdict: dict[str, Any],
|
analyst_verdict: dict[str, Any],
|
||||||
|
|
@ -5148,6 +5255,19 @@ def merge_analyst_priority_repair_targets(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
if analyst_target_contradicts_pack_acceptance_status(repair_targets, analyst_target):
|
||||||
|
suppressed_analyst_targets.append(
|
||||||
|
{
|
||||||
|
"target_id": target_id,
|
||||||
|
"reason": "analyst_target_contradicts_pack_acceptance_status",
|
||||||
|
"pack_status": {
|
||||||
|
"acceptance_status": repair_targets.get("acceptance_status"),
|
||||||
|
"final_status": repair_targets.get("final_status"),
|
||||||
|
"execution_status": repair_targets.get("execution_status"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
continue
|
||||||
existing = merged_by_id.get(target_id)
|
existing = merged_by_id.get(target_id)
|
||||||
if existing:
|
if existing:
|
||||||
existing_severity = normalize_repair_target_severity(existing.get("severity"))
|
existing_severity = normalize_repair_target_severity(existing.get("severity"))
|
||||||
|
|
@ -5514,6 +5634,7 @@ def build_pack_review_bundle(pack_dir: Path) -> str:
|
||||||
"domain": pack_state.get("domain"),
|
"domain": pack_state.get("domain"),
|
||||||
"title": pack_state.get("title"),
|
"title": pack_state.get("title"),
|
||||||
"execution_status": pack_state.get("execution_status"),
|
"execution_status": pack_state.get("execution_status"),
|
||||||
|
"acceptance_status": pack_state.get("acceptance_status"),
|
||||||
"final_status": pack_state.get("final_status"),
|
"final_status": pack_state.get("final_status"),
|
||||||
"scenario_results": pack_state.get("scenario_results"),
|
"scenario_results": pack_state.get("scenario_results"),
|
||||||
},
|
},
|
||||||
|
|
@ -5613,6 +5734,7 @@ def build_analyst_loop_prompt(
|
||||||
- `accepted` is allowed only if quality_score >= {target_score}, unresolved_p0_count = 0, and regression_detected = false;
|
- `accepted` is allowed only if quality_score >= {target_score}, unresolved_p0_count = 0, and regression_detected = false;
|
||||||
- `accepted` is forbidden if the evidence bundle shows `pack_state.acceptance_status != accepted` (or, for old artifacts without `acceptance_status`, `pack_state.final_status != accepted`) or the deterministic repair targets still contain any `P0` or `P1` items;
|
- `accepted` is forbidden if the evidence bundle shows `pack_state.acceptance_status != accepted` (or, for old artifacts without `acceptance_status`, `pack_state.final_status != accepted`) or the deterministic repair targets still contain any `P0` or `P1` items;
|
||||||
- `pack_state.final_status=partial` with `acceptance_status=accepted` means the run still had partial execution evidence; treat it as a proof-strength signal, not as a presentation bug by itself;
|
- `pack_state.final_status=partial` with `acceptance_status=accepted` means the run still had partial execution evidence; treat it as a proof-strength signal, not as a presentation bug by itself;
|
||||||
|
- do not create a priority target solely to change `pack_state.final_status=partial` when `pack_state.acceptance_status=accepted`; only create a pack-status target if `acceptance_status` is missing/not accepted or if the user-facing scenario semantics are actually wrong;
|
||||||
- `accepted` also requires `direct_answer_ok = true`, `business_usefulness_ok = true`, `temporal_honesty_ok = true`, and `field_truth_ok = true`;
|
- `accepted` also requires `direct_answer_ok = true`, `business_usefulness_ok = true`, `temporal_honesty_ok = true`, and `field_truth_ok = true`;
|
||||||
- Treat validated bounded MCP discovery as the semantic route when `bounded_mcp_answer_validated = true`, `mcp_discovery_response_applied = true`, `mcp_discovery_response_candidate_status = ready_for_guarded_use`, and `mcp_discovery_effective_intents` matches the business question. The legacy address route may be only a seed lane; do not call that silent heuristic masking unless the selected discovery chain is wrong, not ready, not applied, or the user-facing answer/state is semantically wrong.
|
- Treat validated bounded MCP discovery as the semantic route when `bounded_mcp_answer_validated = true`, `mcp_discovery_response_applied = true`, `mcp_discovery_response_candidate_status = ready_for_guarded_use`, and `mcp_discovery_effective_intents` matches the business question. The legacy address route may be only a seed lane; do not call that silent heuristic masking unless the selected discovery chain is wrong, not ready, not applied, or the user-facing answer/state is semantically wrong.
|
||||||
- Use `mcp_discovery_route_candidate_status`, `mcp_discovery_route_candidate_missing_axes`, and `mcp_discovery_route_candidate_enablement_reason` to distinguish a valid user-scope clarification from a real missing reviewed route. Do not ask the coder to overfit the visible answer when the correct next action is route enablement or missing-axis clarification.
|
- Use `mcp_discovery_route_candidate_status`, `mcp_discovery_route_candidate_missing_axes`, and `mcp_discovery_route_candidate_enablement_reason` to distinguish a valid user-scope clarification from a real missing reviewed route. Do not ask the coder to overfit the visible answer when the correct next action is route enablement or missing-axis clarification.
|
||||||
|
|
|
||||||
|
|
@ -631,6 +631,81 @@ class DomainCaseLoopLeadHandoffTests(unittest.TestCase):
|
||||||
self.assertEqual(merged["target_count"], 0)
|
self.assertEqual(merged["target_count"], 0)
|
||||||
self.assertEqual(merged["severity_counts"], {"P0": 0, "P1": 0, "P2": 0})
|
self.assertEqual(merged["severity_counts"], {"P0": 0, "P1": 0, "P2": 0})
|
||||||
|
|
||||||
|
def test_pack_status_target_is_suppressed_when_acceptance_status_is_accepted(self) -> None:
|
||||||
|
repair_targets = {
|
||||||
|
"pack_id": "pack_run",
|
||||||
|
"domain": "inventory_margin_ranking",
|
||||||
|
"execution_status": "partial",
|
||||||
|
"acceptance_status": "accepted",
|
||||||
|
"final_status": "partial",
|
||||||
|
"target_count": 0,
|
||||||
|
"severity_counts": {"P0": 0, "P1": 0, "P2": 0},
|
||||||
|
"priority_foci": [],
|
||||||
|
"targets": [],
|
||||||
|
"step_validation_index": {},
|
||||||
|
}
|
||||||
|
analyst_verdict = {
|
||||||
|
"priority_targets": [
|
||||||
|
{
|
||||||
|
"scenario_id": "pack_run",
|
||||||
|
"step_id": None,
|
||||||
|
"severity": "P1",
|
||||||
|
"problem_type": "presentation_gap",
|
||||||
|
"fix_goal": (
|
||||||
|
"Довести pack-level статусную семантику до непротиворечивой: либо записывать "
|
||||||
|
"`pack_state.acceptance_status=accepted`, либо не оставлять legacy-gate в состоянии "
|
||||||
|
"`final_status=partial`, если pack реально проходит analyst gate; пока это запрещает итоговый accepted."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
merged = dcl.merge_analyst_priority_repair_targets(repair_targets, analyst_verdict)
|
||||||
|
|
||||||
|
self.assertEqual(merged["suppressed_analyst_priority_target_count"], 1)
|
||||||
|
self.assertEqual(
|
||||||
|
merged["suppressed_analyst_priority_targets"][0]["reason"],
|
||||||
|
"analyst_target_contradicts_pack_acceptance_status",
|
||||||
|
)
|
||||||
|
self.assertEqual(merged["target_count"], 0)
|
||||||
|
self.assertEqual(merged["severity_counts"], {"P0": 0, "P1": 0, "P2": 0})
|
||||||
|
|
||||||
|
def test_pack_review_bundle_exposes_acceptance_status(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
pack_dir = Path(tmp) / "pack"
|
||||||
|
pack_dir.mkdir()
|
||||||
|
(pack_dir / "pack_state.json").write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"pack_id": "pack_run",
|
||||||
|
"domain": "inventory_margin_ranking",
|
||||||
|
"title": "Demo",
|
||||||
|
"execution_status": "partial",
|
||||||
|
"acceptance_status": "accepted",
|
||||||
|
"final_status": "partial",
|
||||||
|
"scenario_results": [],
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(pack_dir / "repair_targets.json").write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"target_count": 0,
|
||||||
|
"severity_counts": {"P0": 0, "P1": 0, "P2": 0},
|
||||||
|
"targets": [],
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
bundle = json.loads(dcl.build_pack_review_bundle(pack_dir))
|
||||||
|
|
||||||
|
self.assertEqual(bundle["pack_state"]["acceptance_status"], "accepted")
|
||||||
|
self.assertEqual(bundle["pack_state"]["final_status"], "partial")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,52 @@ import domain_truth_harness as dth
|
||||||
|
|
||||||
|
|
||||||
class DomainCaseLoopStepStateTests(unittest.TestCase):
|
class DomainCaseLoopStepStateTests(unittest.TestCase):
|
||||||
|
def test_scenario_semantic_memory_keeps_selected_inventory_bundle(self) -> None:
|
||||||
|
step_state = {
|
||||||
|
"step_id": "step_06_pronoun_selected_item_supplier",
|
||||||
|
"status": "validated",
|
||||||
|
"detected_intent": "inventory_purchase_provenance_for_item",
|
||||||
|
"selected_recipe": "address_inventory_purchase_provenance_for_item_v1",
|
||||||
|
"last_confirmed_route": "address_inventory_purchase_provenance_for_item_v1",
|
||||||
|
"active_result_set_id": "rs-msg-demo",
|
||||||
|
"organization_scope": 'ООО "Альтернатива Плюс"',
|
||||||
|
"date_scope": {
|
||||||
|
"as_of_date": "2017-12-31",
|
||||||
|
"period_from": "2017-01-01",
|
||||||
|
"period_to": "2017-12-31",
|
||||||
|
},
|
||||||
|
"focus_object": {
|
||||||
|
"object_type": "item",
|
||||||
|
"object_id": "item:flag",
|
||||||
|
"label": "Флаг геральдический. 100*150 см",
|
||||||
|
},
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"title": "Поступление товаров и услуг 00000000016 от 01.06.2017",
|
||||||
|
"fields": {
|
||||||
|
"date": "01.06.2017",
|
||||||
|
"контрагент": "ПрофТренд,ООО",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
memory = dcl.build_scenario_semantic_memory_after_step(
|
||||||
|
previous_semantic_memory={},
|
||||||
|
step={"step_id": "step_06_pronoun_selected_item_supplier", "depends_on": ["step_05"]},
|
||||||
|
step_state=step_state,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(memory["focus_object"]["label"], "Флаг геральдический. 100*150 см")
|
||||||
|
self.assertEqual(memory["selected_object_ref"]["object_id"], "item:flag")
|
||||||
|
self.assertEqual(memory["supplier_if_known"], "ПрофТренд,ООО")
|
||||||
|
self.assertEqual(memory["first_purchase_date"], "01.06.2017")
|
||||||
|
self.assertEqual(memory["provenance_bundle"]["source_intent"], "inventory_purchase_provenance_for_item")
|
||||||
|
self.assertEqual(
|
||||||
|
memory["provenance_bundle"]["source_document_if_known"],
|
||||||
|
"Поступление товаров и услуг 00000000016 от 01.06.2017",
|
||||||
|
)
|
||||||
|
|
||||||
@unittest.skipIf(os.name != "nt", "extended-length path handling is Windows-specific")
|
@unittest.skipIf(os.name != "nt", "extended-length path handling is Windows-specific")
|
||||||
def test_write_json_supports_long_windows_artifact_paths(self) -> None:
|
def test_write_json_supports_long_windows_artifact_paths(self) -> None:
|
||||||
temp_dir = tempfile.mkdtemp()
|
temp_dir = tempfile.mkdtemp()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue