ARCH: удержать counterparty scope после contracts pivot в payments
This commit is contained in:
parent
0528745e39
commit
7e1a2edadb
|
|
@ -0,0 +1,65 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase75_contracts_to_payments_year_switch",
|
||||
"domain": "address_phase75_contracts_to_payments_year_switch",
|
||||
"title": "Phase 75 contracts to payments year-switch continuity",
|
||||
"description": "Replay for a human chain where the user opens documents by counterparty, pivots to contracts, then pivots again to payments via pronoun follow-up, and finally switches the year without renaming the counterparty.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_documents_by_counterparty",
|
||||
"title": "Open documents for the counterparty",
|
||||
"question": "Покажи документы по Жуковке 51.",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк",
|
||||
"(?i)документ|сч[её]т|акт|накладн|строк"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["documents_by_counterparty", "pivot_seed", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_contracts_by_pronoun_followup",
|
||||
"title": "Pivot to contracts",
|
||||
"question": "А по нему договоры?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)договор|контракт|соглаш"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["contracts_followup", "counterparty_pronoun_resolution", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_payments_after_contracts_pivot",
|
||||
"title": "Pivot again to payments",
|
||||
"question": "А по нему платежи?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)платеж|операц|банк|поступлен|списан"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["payments_followup", "second_pivot", "integrity_guard"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_year_switch_after_second_pivot",
|
||||
"title": "Switch the year after the second pivot",
|
||||
"question": "А за 2021?",
|
||||
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2021",
|
||||
"(?i)жуковк|контрагент",
|
||||
"(?i)платеж|операц|банк|поступлен|списан"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните .* контрагент",
|
||||
"(?i)метадан",
|
||||
"(?i)схем",
|
||||
"(?i)объект[а-я]* 1с"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": ["year_switch_after_second_pivot", "payments_followup", "integrity_guard"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -2483,11 +2483,19 @@ function findRecentInventoryRootFrame(items) {
|
|||
const ADDRESS_FOLLOWUP_OFFER_BY_INTENT = {
|
||||
list_documents_by_counterparty: ["bank_operations_by_counterparty", "list_contracts_by_counterparty"],
|
||||
bank_operations_by_counterparty: ["list_documents_by_counterparty", "list_contracts_by_counterparty"],
|
||||
list_contracts_by_counterparty: ["list_documents_by_contract", "bank_operations_by_contract"],
|
||||
list_documents_by_contract: ["bank_operations_by_contract"],
|
||||
bank_operations_by_contract: ["list_documents_by_contract"],
|
||||
open_items_by_counterparty_or_contract: ["list_documents_by_counterparty", "bank_operations_by_counterparty"]
|
||||
};
|
||||
function resolveAddressFollowupSuggestedIntents(intent, anchorType) {
|
||||
if (intent === "list_contracts_by_counterparty") {
|
||||
if (anchorType === "contract") {
|
||||
return ["list_documents_by_contract", "bank_operations_by_contract"];
|
||||
}
|
||||
return ["list_documents_by_counterparty", "bank_operations_by_counterparty"];
|
||||
}
|
||||
return ADDRESS_FOLLOWUP_OFFER_BY_INTENT[intent] ?? null;
|
||||
}
|
||||
function buildAddressFollowupOffer(addressDebug) {
|
||||
if (!isAddressLaneDebugPayload(addressDebug)) {
|
||||
return null;
|
||||
|
|
@ -2496,11 +2504,11 @@ function buildAddressFollowupOffer(addressDebug) {
|
|||
if (!intent) {
|
||||
return null;
|
||||
}
|
||||
const suggestedIntents = ADDRESS_FOLLOWUP_OFFER_BY_INTENT[intent];
|
||||
const anchorContext = (0, assistantContinuityPolicy_1.resolveAddressDebugAnchorContext)(addressDebug, toNonEmptyString);
|
||||
const suggestedIntents = resolveAddressFollowupSuggestedIntents(intent, anchorContext.anchorType);
|
||||
if (!Array.isArray(suggestedIntents) || suggestedIntents.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const anchorContext = (0, assistantContinuityPolicy_1.resolveAddressDebugAnchorContext)(addressDebug, toNonEmptyString);
|
||||
return {
|
||||
enabled: true,
|
||||
source_intent: intent,
|
||||
|
|
|
|||
|
|
@ -2439,11 +2439,19 @@ function findRecentInventoryRootFrame(items) {
|
|||
const ADDRESS_FOLLOWUP_OFFER_BY_INTENT = {
|
||||
list_documents_by_counterparty: ["bank_operations_by_counterparty", "list_contracts_by_counterparty"],
|
||||
bank_operations_by_counterparty: ["list_documents_by_counterparty", "list_contracts_by_counterparty"],
|
||||
list_contracts_by_counterparty: ["list_documents_by_contract", "bank_operations_by_contract"],
|
||||
list_documents_by_contract: ["bank_operations_by_contract"],
|
||||
bank_operations_by_contract: ["list_documents_by_contract"],
|
||||
open_items_by_counterparty_or_contract: ["list_documents_by_counterparty", "bank_operations_by_counterparty"]
|
||||
} as Record<string, string[]>;
|
||||
function resolveAddressFollowupSuggestedIntents(intent, anchorType) {
|
||||
if (intent === "list_contracts_by_counterparty") {
|
||||
if (anchorType === "contract") {
|
||||
return ["list_documents_by_contract", "bank_operations_by_contract"];
|
||||
}
|
||||
return ["list_documents_by_counterparty", "bank_operations_by_counterparty"];
|
||||
}
|
||||
return ADDRESS_FOLLOWUP_OFFER_BY_INTENT[intent] ?? null;
|
||||
}
|
||||
function buildAddressFollowupOffer(addressDebug) {
|
||||
if (!isAddressLaneDebugPayload(addressDebug)) {
|
||||
return null;
|
||||
|
|
@ -2452,11 +2460,11 @@ function buildAddressFollowupOffer(addressDebug) {
|
|||
if (!intent) {
|
||||
return null;
|
||||
}
|
||||
const suggestedIntents = ADDRESS_FOLLOWUP_OFFER_BY_INTENT[intent];
|
||||
const anchorContext = (0, assistantContinuityPolicy_1.resolveAddressDebugAnchorContext)(addressDebug, toNonEmptyString);
|
||||
const suggestedIntents = resolveAddressFollowupSuggestedIntents(intent, anchorContext.anchorType);
|
||||
if (!Array.isArray(suggestedIntents) || suggestedIntents.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const anchorContext = (0, assistantContinuityPolicy_1.resolveAddressDebugAnchorContext)(addressDebug, toNonEmptyString);
|
||||
return {
|
||||
enabled: true,
|
||||
source_intent: intent,
|
||||
|
|
|
|||
|
|
@ -1496,6 +1496,125 @@ describe("assistant address follow-up carryover", () => {
|
|||
expect(String(calls[0].message).toLowerCase()).toContain("свк");
|
||||
expect(chatClient.chat).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
it("keeps counterparty scope when pivoting from contracts list to payments by pronoun", async () => {
|
||||
const calls: Array<{ message: string; options?: any }> = [];
|
||||
const firstMessage = "Покажи документы по Жуковке 51.";
|
||||
const secondMessage = "А по нему договоры?";
|
||||
const thirdMessage = "А по нему платежи?";
|
||||
|
||||
const addressQueryService = {
|
||||
tryHandle: vi.fn(async (message: string, options?: any) => {
|
||||
calls.push({ message, options });
|
||||
const normalizedMessage = String(message ?? "").toLowerCase();
|
||||
if (normalizedMessage.includes("документ") && normalizedMessage.includes("жуков")) {
|
||||
return buildAddressLaneResult({
|
||||
reply_text: "Собран список документов по контрагенту ТСЖ \\Жуковка 51\\.",
|
||||
debug: {
|
||||
...buildAddressLaneResult().debug,
|
||||
detected_intent: "list_documents_by_counterparty",
|
||||
selected_recipe: "address_documents_by_counterparty_v1",
|
||||
extracted_filters: {
|
||||
counterparty: "ТСЖ \\Жуковка 51\\"
|
||||
},
|
||||
anchor_type: "counterparty",
|
||||
anchor_value_raw: "Жуковке 51",
|
||||
anchor_value_resolved: "ТСЖ \\Жуковка 51\\",
|
||||
reasons: ["documents_by_counterparty_signal_detected"]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (normalizedMessage.includes("договор")) {
|
||||
return buildAddressLaneResult({
|
||||
reply_text: [
|
||||
"Коротко: найдено 1 договоров по контрагенту.",
|
||||
"Контрагент: ТСЖ \\Жуковка 51\\.",
|
||||
"1. Счет № 5 от 05.04.2017"
|
||||
].join("\n"),
|
||||
debug: {
|
||||
...buildAddressLaneResult().debug,
|
||||
detected_intent: "list_contracts_by_counterparty",
|
||||
selected_recipe: "address_contracts_by_counterparty_v1",
|
||||
extracted_filters: {
|
||||
counterparty: "ТСЖ \\Жуковка 51\\"
|
||||
},
|
||||
anchor_type: "counterparty",
|
||||
anchor_value_raw: "нему",
|
||||
anchor_value_resolved: "ТСЖ \\Жуковка 51\\",
|
||||
reasons: ["contracts_by_counterparty_signal_detected", "address_followup_context_applied"]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (normalizedMessage.includes("плат") || normalizedMessage.includes("банк")) {
|
||||
return buildAddressLaneResult({
|
||||
reply_text: "Собран список банковских операций по контрагенту ТСЖ \\Жуковка 51\\.",
|
||||
debug: {
|
||||
...buildAddressLaneResult().debug,
|
||||
detected_intent: "bank_operations_by_counterparty",
|
||||
selected_recipe: "address_bank_operations_by_counterparty_v1",
|
||||
extracted_filters: {
|
||||
counterparty: "ТСЖ \\Жуковка 51\\"
|
||||
},
|
||||
anchor_type: "counterparty",
|
||||
anchor_value_raw: "нему",
|
||||
anchor_value_resolved: "ТСЖ \\Жуковка 51\\",
|
||||
reasons: ["bank_operations_by_counterparty_signal_detected", "address_followup_context_applied"]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
} as any;
|
||||
|
||||
const normalizerService = {
|
||||
normalize: vi.fn(async () => ({
|
||||
assistant_reply: "normalizer_fallback_should_not_be_used",
|
||||
reply_type: "partial_coverage",
|
||||
debug: {}
|
||||
}))
|
||||
} as any;
|
||||
|
||||
const sessions = new AssistantSessionStore();
|
||||
const service = new AssistantService(
|
||||
normalizerService,
|
||||
sessions as any,
|
||||
{} as any,
|
||||
{ persistSession: vi.fn() } as any,
|
||||
addressQueryService
|
||||
);
|
||||
|
||||
const sessionId = `asst-address-followup-contracts-payments-${Date.now()}`;
|
||||
const first = await service.handleMessage({
|
||||
session_id: sessionId,
|
||||
user_message: firstMessage,
|
||||
useMock: true
|
||||
} as any);
|
||||
expect(first.ok).toBe(true);
|
||||
expect(first.reply_type).toBe("factual");
|
||||
|
||||
const second = await service.handleMessage({
|
||||
session_id: sessionId,
|
||||
user_message: secondMessage,
|
||||
useMock: true
|
||||
} as any);
|
||||
expect(second.ok).toBe(true);
|
||||
expect(second.reply_type).toBe("factual");
|
||||
expect(second.debug?.detected_intent).toBe("list_contracts_by_counterparty");
|
||||
|
||||
const third = await service.handleMessage({
|
||||
session_id: sessionId,
|
||||
user_message: thirdMessage,
|
||||
useMock: true
|
||||
} as any);
|
||||
expect(third.ok).toBe(true);
|
||||
expect(third.reply_type).toBe("factual");
|
||||
expect(third.debug?.detected_intent).toBe("bank_operations_by_counterparty");
|
||||
expect(third.debug?.dialog_continuation_contract_v2?.target_intent).toBe("bank_operations_by_counterparty");
|
||||
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps debt lifecycle follow-up context for 'а нам кто должен?.' after payables as-of answer", async () => {
|
||||
const calls: Array<{ message: string; options?: any }> = [];
|
||||
const firstMessage =
|
||||
|
|
|
|||
Loading…
Reference in New Issue