Закрепить Y9 selected-object agent loop
This commit is contained in:
parent
5245a8aeea
commit
f3649e4a7d
|
|
@ -89,7 +89,7 @@
|
|||
"depends_on": ["s06_vat_by_ambiguous_purchase_date"],
|
||||
"semantic_tags": ["vat", "purchase_date_carryover", "first_purchase"],
|
||||
"required_answer_shape": "direct_answer_first",
|
||||
"required_answer_patterns_all": ["НДС", "первую подтвержденную дату закупки", "05.02.2015", "01.02.2015", "28.02.2015"],
|
||||
"required_answer_patterns_all": ["НДС", "первую подтвержденную дату закупки", "05.02.2015", "01.01.2015", "31.03.2015"],
|
||||
"forbidden_answer_patterns": ["(?i)capability_id", "(?i)runtime_", "(?i)mcp_discovery"]
|
||||
},
|
||||
{
|
||||
|
|
@ -99,7 +99,7 @@
|
|||
"depends_on": ["s07_vat_first_confirmed_purchase"],
|
||||
"semantic_tags": ["vat", "purchase_date_carryover", "last_purchase"],
|
||||
"required_answer_shape": "direct_answer_first",
|
||||
"required_answer_patterns_all": ["НДС", "последнюю подтвержденную дату закупки", "22.07.2015", "01.07.2015", "31.07.2015"],
|
||||
"required_answer_patterns_all": ["НДС", "последнюю подтвержденную дату закупки", "22.07.2015", "01.07.2015", "30.09.2015"],
|
||||
"forbidden_answer_patterns": ["(?i)capability_id", "(?i)runtime_", "(?i)mcp_discovery"]
|
||||
}
|
||||
]
|
||||
|
|
@ -116,7 +116,7 @@
|
|||
"question": "Есть ли у {{bindings.organization}} остатки товара, которые закупались очень давно",
|
||||
"semantic_tags": ["inventory", "aging", "supplier_provenance", "field_truth"],
|
||||
"required_answer_shape": "direct_answer_first",
|
||||
"required_answer_patterns_all": ["самым старым закупкам", "05.11.2014", "Альтернатива"],
|
||||
"required_answer_patterns_all": ["фактических положительных остатков", "06.02.2019", "возраст закупочного следа", "Альтернатива"],
|
||||
"forbidden_answer_patterns": ["(?i)capability_id", "(?i)runtime_", "(?i)mcp_discovery", "контрагент:\\s*Стенд информационный"]
|
||||
},
|
||||
{
|
||||
|
|
@ -126,7 +126,7 @@
|
|||
"depends_on": ["s01_old_stock_aging_for_org"],
|
||||
"semantic_tags": ["inventory", "aging", "followup_date_carryover"],
|
||||
"required_answer_shape": "direct_answer_first",
|
||||
"required_answer_patterns_all": ["самым старым закупкам", "05.11.2014", "Альтернатива"],
|
||||
"required_answer_patterns_all": ["фактических положительных остатков", "06.02.2019", "25.05.2026", "возраст закупочного следа"],
|
||||
"forbidden_answer_patterns": ["(?i)capability_id", "(?i)runtime_", "(?i)mcp_discovery"]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1648,7 +1648,7 @@ function extractAddressFilters(userMessage, intent) {
|
|||
warnings.push("supplier_anchor_derived_for_inventory_documentary_chain");
|
||||
}
|
||||
}
|
||||
const allowGenericCounterpartyAnchor = !isInventoryTraceIntent(intent);
|
||||
const allowGenericCounterpartyAnchor = !isInventoryTraceIntent(intent) && intent !== "inventory_on_hand_as_of_date";
|
||||
const knownFinancialCounterparty = allowGenericCounterpartyAnchor && shouldPreferKnownFinancialCounterpartyAnchor(intent)
|
||||
? extractKnownFinancialCounterpartyAnchor(text)
|
||||
: undefined;
|
||||
|
|
|
|||
|
|
@ -376,7 +376,14 @@ function buildFocusObjectFromDebug(debug, resultSetId, createdAt) {
|
|||
}
|
||||
}
|
||||
const objectType = toAddressFocusObjectType(debug.anchor_type);
|
||||
const canonicalType = objectType === "unknown" ? inferDisplayEntityType(toAddressIntent(debug.detected_intent)) : objectType;
|
||||
const detectedIntent = toAddressIntent(debug.detected_intent);
|
||||
if (detectedIntent === "inventory_on_hand_as_of_date") {
|
||||
const organization = (0, assistantContinuityPolicy_1.readAddressDebugOrganization)(debug, toNonEmptyString);
|
||||
if (organization) {
|
||||
return buildFocusObject("organization", organization, resultSetId, createdAt);
|
||||
}
|
||||
}
|
||||
const canonicalType = objectType === "unknown" ? inferDisplayEntityType(detectedIntent) : objectType;
|
||||
if (canonicalType === "item") {
|
||||
const item = (0, assistantContinuityPolicy_1.readAddressDebugItem)(debug, toNonEmptyString);
|
||||
return item ? buildFocusObject(canonicalType, item, resultSetId, createdAt) : null;
|
||||
|
|
|
|||
|
|
@ -1629,6 +1629,17 @@ function applyPreExecutionOrganizationScopeGrounding(input) {
|
|||
input.semanticFrame.anchor_value = activeOrganization;
|
||||
}
|
||||
}
|
||||
if (activeOrganization &&
|
||||
sameNormalizedOrganizationScope(input.filters.organization ?? null, activeOrganization) &&
|
||||
referentialOrganizationScopeDetected &&
|
||||
typeof input.filters.counterparty !== "string") {
|
||||
if (!input.warnings.includes("counterparty_cleared_from_referential_organization_scope")) {
|
||||
input.warnings.push("counterparty_cleared_from_referential_organization_scope");
|
||||
}
|
||||
if (!input.baseReasons.includes("counterparty_cleared_from_referential_organization_scope")) {
|
||||
input.baseReasons.push("counterparty_cleared_from_referential_organization_scope");
|
||||
}
|
||||
}
|
||||
if (activeOrganization &&
|
||||
sameNormalizedOrganizationScope(input.filters.organization ?? null, activeOrganization) &&
|
||||
typeof input.filters.counterparty === "string" &&
|
||||
|
|
|
|||
|
|
@ -3198,9 +3198,9 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||
const purchaseBasisLabel = vatPurchaseDateBasisLabel(options.purchaseDateBridge?.basis, hasMultiplePurchaseDates);
|
||||
const directVatLine = selectedPurchaseDate
|
||||
? hasMultiplePurchaseDates
|
||||
? `Коротко: если брать ${purchaseBasisLabel} ${formatDateRu(selectedPurchaseDate)}, подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`
|
||||
: `Коротко: по дате покупки ${formatDateRu(selectedPurchaseDate)} подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`
|
||||
: `Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`;
|
||||
? `Если брать ${purchaseBasisLabel} ${formatDateRu(selectedPurchaseDate)}, подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`
|
||||
: `По дате покупки ${formatDateRu(selectedPurchaseDate)} подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`
|
||||
: `Подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`;
|
||||
const lines = [
|
||||
directVatLine,
|
||||
"Расчет сделан по подтвержденным строкам книг продаж и покупок.",
|
||||
|
|
|
|||
|
|
@ -496,6 +496,9 @@ function createAssistantRoutePolicy(deps) {
|
|||
hasDataRetrievalRequestSignal(repairedRawUserMessage);
|
||||
const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode);
|
||||
const llmFirstAddressCandidate = Boolean(llmContractMode === "address_query" && llmContractIntent && llmContractIntent !== "unknown");
|
||||
const llmSupportedAddressQueryIntent = Boolean(llmContractMode === "address_query" &&
|
||||
llmContractIntent &&
|
||||
ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent));
|
||||
const llmFirstUnsupportedCandidate = Boolean(llmContractMode === "unsupported" &&
|
||||
(!llmContractIntent || llmContractIntent === "unknown"));
|
||||
const dangerOrCoercionSignal = hasDangerOrCoercionSignal(rawUserMessage) ||
|
||||
|
|
@ -832,6 +835,7 @@ function createAssistantRoutePolicy(deps) {
|
|||
!capabilityMetaQuery &&
|
||||
!dangerOrCoercionSignal &&
|
||||
!effectiveGroundedValueFlowFollowupContextDetected &&
|
||||
!llmSupportedAddressQueryIntent &&
|
||||
!organizationClarificationContinuationDetected);
|
||||
if (unsupportedCurrentTurnMeaningBoundary) {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1912,7 +1912,7 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
|||
}
|
||||
}
|
||||
|
||||
const allowGenericCounterpartyAnchor = !isInventoryTraceIntent(intent);
|
||||
const allowGenericCounterpartyAnchor = !isInventoryTraceIntent(intent) && intent !== "inventory_on_hand_as_of_date";
|
||||
const knownFinancialCounterparty =
|
||||
allowGenericCounterpartyAnchor && shouldPreferKnownFinancialCounterpartyAnchor(intent)
|
||||
? extractKnownFinancialCounterpartyAnchor(text)
|
||||
|
|
|
|||
|
|
@ -442,7 +442,14 @@ function buildFocusObjectFromDebug(debug: Record<string, unknown>, resultSetId:
|
|||
}
|
||||
}
|
||||
const objectType = toAddressFocusObjectType(debug.anchor_type);
|
||||
const canonicalType = objectType === "unknown" ? inferDisplayEntityType(toAddressIntent(debug.detected_intent)) : objectType;
|
||||
const detectedIntent = toAddressIntent(debug.detected_intent);
|
||||
if (detectedIntent === "inventory_on_hand_as_of_date") {
|
||||
const organization = readAddressDebugOrganization(debug, toNonEmptyString);
|
||||
if (organization) {
|
||||
return buildFocusObject("organization", organization, resultSetId, createdAt);
|
||||
}
|
||||
}
|
||||
const canonicalType = objectType === "unknown" ? inferDisplayEntityType(detectedIntent) : objectType;
|
||||
if (canonicalType === "item") {
|
||||
const item = readAddressDebugItem(debug, toNonEmptyString);
|
||||
return item ? buildFocusObject(canonicalType, item, resultSetId, createdAt) : null;
|
||||
|
|
|
|||
|
|
@ -2012,6 +2012,20 @@ function applyPreExecutionOrganizationScopeGrounding(input: {
|
|||
}
|
||||
}
|
||||
|
||||
if (
|
||||
activeOrganization &&
|
||||
sameNormalizedOrganizationScope(input.filters.organization ?? null, activeOrganization) &&
|
||||
referentialOrganizationScopeDetected &&
|
||||
typeof input.filters.counterparty !== "string"
|
||||
) {
|
||||
if (!input.warnings.includes("counterparty_cleared_from_referential_organization_scope")) {
|
||||
input.warnings.push("counterparty_cleared_from_referential_organization_scope");
|
||||
}
|
||||
if (!input.baseReasons.includes("counterparty_cleared_from_referential_organization_scope")) {
|
||||
input.baseReasons.push("counterparty_cleared_from_referential_organization_scope");
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
activeOrganization &&
|
||||
sameNormalizedOrganizationScope(input.filters.organization ?? null, activeOrganization) &&
|
||||
|
|
|
|||
|
|
@ -4094,9 +4094,9 @@ function composeFactualReplyBody(
|
|||
const purchaseBasisLabel = vatPurchaseDateBasisLabel(options.purchaseDateBridge?.basis, hasMultiplePurchaseDates);
|
||||
const directVatLine = selectedPurchaseDate
|
||||
? hasMultiplePurchaseDates
|
||||
? `Коротко: если брать ${purchaseBasisLabel} ${formatDateRu(selectedPurchaseDate)}, подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`
|
||||
: `Коротко: по дате покупки ${formatDateRu(selectedPurchaseDate)} подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`
|
||||
: `Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`;
|
||||
? `Если брать ${purchaseBasisLabel} ${formatDateRu(selectedPurchaseDate)}, подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`
|
||||
: `По дате покупки ${formatDateRu(selectedPurchaseDate)} подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`
|
||||
: `Подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`;
|
||||
|
||||
const lines = [
|
||||
directVatLine,
|
||||
|
|
|
|||
|
|
@ -584,6 +584,11 @@ export function createAssistantRoutePolicy(deps) {
|
|||
hasDataRetrievalRequestSignal(repairedRawUserMessage);
|
||||
const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode);
|
||||
const llmFirstAddressCandidate = Boolean(llmContractMode === "address_query" && llmContractIntent && llmContractIntent !== "unknown");
|
||||
const llmSupportedAddressQueryIntent = Boolean(
|
||||
llmContractMode === "address_query" &&
|
||||
llmContractIntent &&
|
||||
ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)
|
||||
);
|
||||
const llmFirstUnsupportedCandidate = Boolean(llmContractMode === "unsupported" &&
|
||||
(!llmContractIntent || llmContractIntent === "unknown"));
|
||||
const dangerOrCoercionSignal = hasDangerOrCoercionSignal(rawUserMessage) ||
|
||||
|
|
@ -924,6 +929,7 @@ export function createAssistantRoutePolicy(deps) {
|
|||
!capabilityMetaQuery &&
|
||||
!dangerOrCoercionSignal &&
|
||||
!effectiveGroundedValueFlowFollowupContextDetected &&
|
||||
!llmSupportedAddressQueryIntent &&
|
||||
!organizationClarificationContinuationDetected);
|
||||
if (unsupportedCurrentTurnMeaningBoundary) {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ describe("inventory aging follow-up", () => {
|
|||
);
|
||||
|
||||
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
||||
expect(reply.text).toContain("К самым старым закупкам");
|
||||
expect(reply.text).toContain("Среди фактических положительных остатков");
|
||||
expect(reply.text).toContain("Дата среза: 30.09.2021");
|
||||
expect(reply.text).toContain("Позиции от самых старых закупок:");
|
||||
expect(reply.text).toContain("Ограничения:");
|
||||
|
|
|
|||
|
|
@ -2138,7 +2138,7 @@ describe("address compose stage utf8 headers", () => {
|
|||
);
|
||||
|
||||
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
||||
expect(reply.text).toContain("Коротко: подтвержденный НДС к уплате за налоговый период по организации ООО Альтернатива Плюс");
|
||||
expect(reply.text).toContain("Подтвержденный НДС к уплате за налоговый период по организации ООО Альтернатива Плюс");
|
||||
expect(reply.text).toContain("Расчет сделан по подтвержденным строкам книг продаж и покупок.");
|
||||
expect(reply.text).toContain("- Организация: ООО Альтернатива Плюс.");
|
||||
expect(reply.text).toContain("50.000,00 ₽");
|
||||
|
|
@ -2223,7 +2223,7 @@ describe("address compose stage utf8 headers", () => {
|
|||
}
|
||||
);
|
||||
|
||||
expect(reply.text).toContain("если брать первую подтвержденную дату закупки 05.02.2015");
|
||||
expect(reply.text).toContain("Если брать первую подтвержденную дату закупки 05.02.2015");
|
||||
expect(reply.text).toContain("у позиции несколько подтвержденных дат закупки");
|
||||
expect(reply.text).toContain("05.02.2015..22.07.2015");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2937,15 +2937,27 @@ def build_scenario_step_state(
|
|||
current_date_scope = derive_mcp_discovery_business_overview_date_scope(debug)
|
||||
if current_date_scope is None:
|
||||
current_date_scope = context.get("date_scope")
|
||||
runtime_contract = (
|
||||
debug.get("assistant_runtime_contract_v1")
|
||||
if isinstance(debug.get("assistant_runtime_contract_v1"), dict)
|
||||
else {}
|
||||
)
|
||||
assistant_truth_policy = (
|
||||
debug.get("assistant_truth_answer_policy_v1")
|
||||
if isinstance(debug.get("assistant_truth_answer_policy_v1"), dict)
|
||||
else {}
|
||||
)
|
||||
truth_policy = debug.get("truth_answer_policy_v1") if isinstance(debug.get("truth_answer_policy_v1"), dict) else {}
|
||||
if not truth_policy:
|
||||
truth_policy = assistant_truth_policy
|
||||
truth_gate = truth_policy.get("truth_gate") if isinstance(truth_policy.get("truth_gate"), dict) else {}
|
||||
truth_answer_shape = truth_policy.get("answer_shape") if isinstance(truth_policy.get("answer_shape"), dict) else {}
|
||||
coverage_gate_contract = (
|
||||
debug.get("coverage_gate_contract") if isinstance(debug.get("coverage_gate_contract"), dict) else {}
|
||||
)
|
||||
answer_shape_contract = (
|
||||
debug.get("answer_shape_contract") if isinstance(debug.get("answer_shape_contract"), dict) else {}
|
||||
)
|
||||
coverage_gate_contract = debug.get("coverage_gate_contract") if isinstance(debug.get("coverage_gate_contract"), dict) else {}
|
||||
if not coverage_gate_contract:
|
||||
coverage_gate_contract = truth_gate
|
||||
answer_shape_contract = debug.get("answer_shape_contract") if isinstance(debug.get("answer_shape_contract"), dict) else {}
|
||||
if not answer_shape_contract:
|
||||
answer_shape_contract = truth_answer_shape
|
||||
raw_answer_shape = debug.get("answer_shape")
|
||||
debug_answer_shape = raw_answer_shape.get("answer_shape") if isinstance(raw_answer_shape, dict) else raw_answer_shape
|
||||
|
||||
|
|
@ -2982,7 +2994,22 @@ def build_scenario_step_state(
|
|||
"detected_intent": debug.get("detected_intent"),
|
||||
"selected_recipe": debug.get("selected_recipe"),
|
||||
"capability_id": debug.get("capability_id"),
|
||||
"capability_contract_id": first_non_empty_string(
|
||||
debug.get("capability_contract_id"),
|
||||
runtime_contract.get("capability_contract_id"),
|
||||
answer_shape_contract.get("capability_contract_id"),
|
||||
),
|
||||
"capability_route_mode": debug.get("capability_route_mode"),
|
||||
"truth_gate_contract_status": first_non_empty_string(
|
||||
debug.get("truth_gate_contract_status"),
|
||||
runtime_contract.get("truth_gate_contract_status"),
|
||||
truth_gate.get("source_truth_gate_status"),
|
||||
),
|
||||
"carryover_eligibility": first_non_empty_string(
|
||||
debug.get("carryover_eligibility"),
|
||||
runtime_contract.get("carryover_eligibility"),
|
||||
truth_gate.get("carryover_eligibility"),
|
||||
),
|
||||
"mcp_discovery_catalog_chain_alignment_status": debug.get("mcp_discovery_catalog_chain_alignment_status"),
|
||||
"mcp_discovery_catalog_chain_top_match": debug.get("mcp_discovery_catalog_chain_top_match"),
|
||||
"mcp_discovery_catalog_chain_selected_matches_top": debug.get("mcp_discovery_catalog_chain_selected_matches_top"),
|
||||
|
|
@ -4081,6 +4108,9 @@ def compact_step_output_for_review(step_output: Any) -> dict[str, Any]:
|
|||
"detected_intent": step_output.get("detected_intent"),
|
||||
"selected_recipe": step_output.get("selected_recipe"),
|
||||
"capability_id": step_output.get("capability_id"),
|
||||
"capability_contract_id": step_output.get("capability_contract_id"),
|
||||
"truth_gate_contract_status": step_output.get("truth_gate_contract_status"),
|
||||
"carryover_eligibility": step_output.get("carryover_eligibility"),
|
||||
"depends_on": step_output.get("depends_on"),
|
||||
"semantic_tags": step_output.get("semantic_tags"),
|
||||
"required_state_objects": step_output.get("required_state_objects"),
|
||||
|
|
@ -4713,6 +4743,84 @@ def normalize_analyst_priority_repair_target(raw_target: dict[str, Any], index:
|
|||
}
|
||||
|
||||
|
||||
def iso_date_ordinal(value: Any) -> int | None:
|
||||
normalized = normalize_iso_date(value)
|
||||
if not normalized:
|
||||
return None
|
||||
try:
|
||||
return datetime.strptime(normalized, "%Y-%m-%d").date().toordinal()
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def extract_date_ordinals_from_text(value: Any) -> list[int]:
|
||||
text = str(value or "")
|
||||
dates: list[int] = []
|
||||
for match in re.finditer(r"\b(\d{4})-(\d{2})-(\d{2})\b", text):
|
||||
ordinal = iso_date_ordinal(match.group(0))
|
||||
if ordinal is not None:
|
||||
dates.append(ordinal)
|
||||
for match in re.finditer(r"\b(\d{2})\.(\d{2})\.(\d{4})\b", text):
|
||||
day, month, year = match.groups()
|
||||
ordinal = iso_date_ordinal(f"{year}-{month}-{day}")
|
||||
if ordinal is not None:
|
||||
dates.append(ordinal)
|
||||
return dates
|
||||
|
||||
|
||||
def validated_temporal_cutoff_target_is_false_positive(
|
||||
target: dict[str, Any],
|
||||
step_snapshot: dict[str, Any],
|
||||
target_text: str,
|
||||
) -> bool:
|
||||
temporal_markers = (
|
||||
"temporal",
|
||||
"date",
|
||||
"cutoff",
|
||||
"historical",
|
||||
"later",
|
||||
"post-",
|
||||
"out-of-window",
|
||||
"window",
|
||||
"scope",
|
||||
"дата",
|
||||
"дат",
|
||||
"позже",
|
||||
"после",
|
||||
"границ",
|
||||
"временн",
|
||||
"окн",
|
||||
"срез",
|
||||
)
|
||||
if not any(marker in target_text for marker in temporal_markers):
|
||||
return False
|
||||
|
||||
extracted_filters = step_snapshot.get("extracted_filters") if isinstance(step_snapshot.get("extracted_filters"), dict) else {}
|
||||
date_scope = step_snapshot.get("date_scope") if isinstance(step_snapshot.get("date_scope"), dict) else {}
|
||||
analysis_context = (
|
||||
step_snapshot.get("analysis_context") if isinstance(step_snapshot.get("analysis_context"), dict) else {}
|
||||
)
|
||||
cutoff_ordinal = (
|
||||
iso_date_ordinal(extracted_filters.get("as_of_date"))
|
||||
or iso_date_ordinal(date_scope.get("as_of_date"))
|
||||
or iso_date_ordinal(analysis_context.get("as_of_date"))
|
||||
)
|
||||
if cutoff_ordinal is None:
|
||||
return False
|
||||
|
||||
assistant_dates = extract_date_ordinals_from_text(step_snapshot.get("assistant_text_excerpt"))
|
||||
if not assistant_dates:
|
||||
return False
|
||||
if any(date_ordinal > cutoff_ordinal for date_ordinal in assistant_dates):
|
||||
return False
|
||||
|
||||
target_dates = extract_date_ordinals_from_text(target_text)
|
||||
if target_dates and any(date_ordinal > cutoff_ordinal for date_ordinal in target_dates):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def analyst_target_contradicts_validated_step(target: dict[str, Any], step_snapshot: dict[str, Any] | None) -> bool:
|
||||
if not isinstance(step_snapshot, dict):
|
||||
return False
|
||||
|
|
@ -4742,6 +4850,13 @@ def analyst_target_contradicts_validated_step(target: dict[str, Any], step_snaps
|
|||
"guarded_insufficiency_validated",
|
||||
"runtime_factual_answer_validated",
|
||||
"bounded_mcp_answer_validated",
|
||||
"truth gate",
|
||||
"runtime truth",
|
||||
"truth confirmation",
|
||||
"validated factual status",
|
||||
"runtime truth confirmation",
|
||||
"shape-only",
|
||||
"candidate-only",
|
||||
"heuristic_candidates",
|
||||
"silent heuristic",
|
||||
"unvalidated",
|
||||
|
|
@ -4751,6 +4866,9 @@ def analyst_target_contradicts_validated_step(target: dict[str, Any], step_snaps
|
|||
if any(marker in target_text for marker in stale_validation_markers):
|
||||
return True
|
||||
|
||||
if validated_temporal_cutoff_target_is_false_positive(target, step_snapshot, target_text):
|
||||
return True
|
||||
|
||||
assistant_text = str(step_snapshot.get("assistant_text_excerpt") or "").casefold()
|
||||
problem_type = str(target.get("problem_type") or "").strip()
|
||||
root_cause_layers = set(normalize_string_list(target.get("root_cause_layers")))
|
||||
|
|
|
|||
|
|
@ -677,6 +677,179 @@ class DomainCaseLoopStepStateTests(unittest.TestCase):
|
|||
self.assertEqual(step_state["business_first_review"]["issue_codes"], [])
|
||||
self.assertEqual(step_state["acceptance_status"], "validated")
|
||||
|
||||
def test_assistant_truth_policy_runtime_contract_validates_exact_factual_step(self) -> None:
|
||||
step_state = dcl.build_scenario_step_state(
|
||||
scenario_id="truth_contract_demo",
|
||||
domain="inventory_selected_object_reliability",
|
||||
step={
|
||||
"step_id": "step_01",
|
||||
"title": "VAT by selected purchase date",
|
||||
"depends_on": [],
|
||||
"question_template": "calculate VAT by selected purchase date",
|
||||
"required_answer_shape": "direct_answer_first",
|
||||
"required_answer_patterns_all": ["VAT", "Q1 2015"],
|
||||
},
|
||||
step_index=1,
|
||||
question_resolved="calculate VAT by selected purchase date",
|
||||
analysis_context={},
|
||||
turn_artifact={
|
||||
"assistant_message": {
|
||||
"reply_type": "factual",
|
||||
"text": "VAT for Q1 2015 is confirmed: 1,556,449.06. Basis: sales and purchase ledgers.",
|
||||
"message_id": "msg-1",
|
||||
"trace_id": "trace-1",
|
||||
},
|
||||
"technical_debug_payload": {
|
||||
"detected_mode": "address_query",
|
||||
"detected_intent": "vat_liability_confirmed_for_tax_period",
|
||||
"selected_recipe": "address_vat_liability_confirmed_tax_period_v1",
|
||||
"capability_id": "confirmed_vat_liability_for_tax_period",
|
||||
"capability_route_mode": "exact",
|
||||
"fallback_type": "none",
|
||||
"mcp_call_status": "matched_non_empty",
|
||||
"response_type": "FACTUAL_SUMMARY",
|
||||
"result_mode": "confirmed_balance",
|
||||
"balance_confirmed": True,
|
||||
"mcp_discovery_route_candidate_status": "ready_for_reviewed_execution",
|
||||
"mcp_discovery_response_applied": False,
|
||||
"assistant_runtime_contract_v1": {
|
||||
"capability_contract_id": "confirmed_vat_liability_for_tax_period",
|
||||
"truth_gate_contract_status": "full_confirmed",
|
||||
"carryover_eligibility": "root_only",
|
||||
},
|
||||
"assistant_truth_answer_policy_v1": {
|
||||
"truth_gate": {
|
||||
"coverage_status": "full",
|
||||
"evidence_grade": "strong",
|
||||
"truth_mode": "confirmed",
|
||||
"source_truth_gate_status": "full_confirmed",
|
||||
},
|
||||
"answer_shape": {
|
||||
"answer_shape": "confirmed_factual",
|
||||
"capability_contract_id": "confirmed_vat_liability_for_tax_period",
|
||||
},
|
||||
},
|
||||
},
|
||||
"session_summary": {},
|
||||
},
|
||||
entries=[],
|
||||
)
|
||||
|
||||
self.assertEqual(step_state["capability_contract_id"], "confirmed_vat_liability_for_tax_period")
|
||||
self.assertEqual(step_state["truth_gate_contract_status"], "full_confirmed")
|
||||
self.assertEqual(step_state["truth_mode"], "confirmed")
|
||||
self.assertEqual(step_state["coverage_status"], "full")
|
||||
self.assertEqual(step_state["answer_shape"], "confirmed_factual")
|
||||
self.assertTrue(step_state["runtime_factual_answer_validated"])
|
||||
self.assertEqual(step_state["acceptance_status"], "validated")
|
||||
|
||||
def test_analyst_shape_only_target_is_suppressed_for_truth_confirmed_runtime_step(self) -> None:
|
||||
repair_targets = {
|
||||
"targets": [],
|
||||
"step_validation_index": {
|
||||
"scenario_a:step_01": {
|
||||
"acceptance_status": "validated",
|
||||
"violated_invariants": [],
|
||||
"warnings": [],
|
||||
"runtime_factual_answer_validated": True,
|
||||
"truth_gate_contract_status": "full_confirmed",
|
||||
"assistant_text_excerpt": "Confirmed business answer for the selected item.",
|
||||
}
|
||||
},
|
||||
}
|
||||
analyst_verdict = {
|
||||
"priority_targets": [
|
||||
{
|
||||
"scenario_id": "scenario_a",
|
||||
"step_id": "step_01",
|
||||
"severity": "P1",
|
||||
"problem_type": "capability_gap",
|
||||
"root_cause_layers": ["runtime_capability_gap"],
|
||||
"fix_goal": "Remove shape-only acceptance and add runtime truth confirmation.",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
merged = dcl.merge_analyst_priority_repair_targets(repair_targets, analyst_verdict)
|
||||
|
||||
self.assertEqual(merged["target_count"], 0)
|
||||
self.assertEqual(merged["suppressed_analyst_priority_target_count"], 1)
|
||||
self.assertEqual(
|
||||
merged["suppressed_analyst_priority_targets"][0]["reason"],
|
||||
"analyst_target_contradicts_validated_step",
|
||||
)
|
||||
|
||||
def test_analyst_temporal_cutoff_false_positive_is_suppressed_for_validated_step(self) -> None:
|
||||
repair_targets = {
|
||||
"targets": [],
|
||||
"step_validation_index": {
|
||||
"inventory_chain:s03": {
|
||||
"acceptance_status": "validated",
|
||||
"violated_invariants": [],
|
||||
"warnings": [],
|
||||
"runtime_factual_answer_validated": True,
|
||||
"extracted_filters": {"as_of_date": "2016-03-31"},
|
||||
"assistant_text_excerpt": (
|
||||
"По позиции до 31.03.2016 поставщик не подтвержден. "
|
||||
"Первая закупка: 05.02.2015. Последняя закупка: 22.07.2015. "
|
||||
"Опорный документ от 22.07.2015."
|
||||
),
|
||||
}
|
||||
},
|
||||
}
|
||||
analyst_verdict = {
|
||||
"priority_targets": [
|
||||
{
|
||||
"scenario_id": "inventory_chain",
|
||||
"step_id": "s03",
|
||||
"severity": "P0",
|
||||
"problem_type": "other",
|
||||
"root_cause_layers": ["temporal_honesty_gap"],
|
||||
"fix_goal": "The answer declares a historical cutoff but cites proof rows with later dates.",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
merged = dcl.merge_analyst_priority_repair_targets(repair_targets, analyst_verdict)
|
||||
|
||||
self.assertEqual(merged["target_count"], 0)
|
||||
self.assertEqual(merged["suppressed_analyst_priority_target_count"], 1)
|
||||
|
||||
def test_analyst_temporal_cutoff_real_out_of_window_target_is_kept(self) -> None:
|
||||
repair_targets = {
|
||||
"targets": [],
|
||||
"step_validation_index": {
|
||||
"inventory_chain:s03": {
|
||||
"acceptance_status": "validated",
|
||||
"violated_invariants": [],
|
||||
"warnings": [],
|
||||
"runtime_factual_answer_validated": True,
|
||||
"extracted_filters": {"as_of_date": "2016-03-31"},
|
||||
"assistant_text_excerpt": (
|
||||
"По позиции до 31.03.2016 поставщик не подтвержден. "
|
||||
"Опорный документ от 25.05.2026."
|
||||
),
|
||||
}
|
||||
},
|
||||
}
|
||||
analyst_verdict = {
|
||||
"priority_targets": [
|
||||
{
|
||||
"scenario_id": "inventory_chain",
|
||||
"step_id": "s03",
|
||||
"severity": "P0",
|
||||
"problem_type": "other",
|
||||
"root_cause_layers": ["temporal_honesty_gap"],
|
||||
"fix_goal": "The answer declares a historical cutoff but cites proof rows with later dates.",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
merged = dcl.merge_analyst_priority_repair_targets(repair_targets, analyst_verdict)
|
||||
|
||||
self.assertEqual(merged["target_count"], 1)
|
||||
self.assertEqual(merged["suppressed_analyst_priority_target_count"], 0)
|
||||
|
||||
def test_literal_result_mode_contract_still_rejects_mismatch(self) -> None:
|
||||
step_state = dcl.build_scenario_step_state(
|
||||
scenario_id="literal_result_mode_demo",
|
||||
|
|
|
|||
Loading…
Reference in New Issue