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 = {
|
const ADDRESS_FOLLOWUP_OFFER_BY_INTENT = {
|
||||||
list_documents_by_counterparty: ["bank_operations_by_counterparty", "list_contracts_by_counterparty"],
|
list_documents_by_counterparty: ["bank_operations_by_counterparty", "list_contracts_by_counterparty"],
|
||||||
bank_operations_by_counterparty: ["list_documents_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"],
|
list_documents_by_contract: ["bank_operations_by_contract"],
|
||||||
bank_operations_by_contract: ["list_documents_by_contract"],
|
bank_operations_by_contract: ["list_documents_by_contract"],
|
||||||
open_items_by_counterparty_or_contract: ["list_documents_by_counterparty", "bank_operations_by_counterparty"]
|
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) {
|
function buildAddressFollowupOffer(addressDebug) {
|
||||||
if (!isAddressLaneDebugPayload(addressDebug)) {
|
if (!isAddressLaneDebugPayload(addressDebug)) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -2496,11 +2504,11 @@ function buildAddressFollowupOffer(addressDebug) {
|
||||||
if (!intent) {
|
if (!intent) {
|
||||||
return null;
|
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) {
|
if (!Array.isArray(suggestedIntents) || suggestedIntents.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const anchorContext = (0, assistantContinuityPolicy_1.resolveAddressDebugAnchorContext)(addressDebug, toNonEmptyString);
|
|
||||||
return {
|
return {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
source_intent: intent,
|
source_intent: intent,
|
||||||
|
|
|
||||||
|
|
@ -2439,11 +2439,19 @@ function findRecentInventoryRootFrame(items) {
|
||||||
const ADDRESS_FOLLOWUP_OFFER_BY_INTENT = {
|
const ADDRESS_FOLLOWUP_OFFER_BY_INTENT = {
|
||||||
list_documents_by_counterparty: ["bank_operations_by_counterparty", "list_contracts_by_counterparty"],
|
list_documents_by_counterparty: ["bank_operations_by_counterparty", "list_contracts_by_counterparty"],
|
||||||
bank_operations_by_counterparty: ["list_documents_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"],
|
list_documents_by_contract: ["bank_operations_by_contract"],
|
||||||
bank_operations_by_contract: ["list_documents_by_contract"],
|
bank_operations_by_contract: ["list_documents_by_contract"],
|
||||||
open_items_by_counterparty_or_contract: ["list_documents_by_counterparty", "bank_operations_by_counterparty"]
|
open_items_by_counterparty_or_contract: ["list_documents_by_counterparty", "bank_operations_by_counterparty"]
|
||||||
} as Record<string, string[]>;
|
} 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) {
|
function buildAddressFollowupOffer(addressDebug) {
|
||||||
if (!isAddressLaneDebugPayload(addressDebug)) {
|
if (!isAddressLaneDebugPayload(addressDebug)) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -2452,11 +2460,11 @@ function buildAddressFollowupOffer(addressDebug) {
|
||||||
if (!intent) {
|
if (!intent) {
|
||||||
return null;
|
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) {
|
if (!Array.isArray(suggestedIntents) || suggestedIntents.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const anchorContext = (0, assistantContinuityPolicy_1.resolveAddressDebugAnchorContext)(addressDebug, toNonEmptyString);
|
|
||||||
return {
|
return {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
source_intent: intent,
|
source_intent: intent,
|
||||||
|
|
|
||||||
|
|
@ -1496,6 +1496,125 @@ describe("assistant address follow-up carryover", () => {
|
||||||
expect(String(calls[0].message).toLowerCase()).toContain("свк");
|
expect(String(calls[0].message).toLowerCase()).toContain("свк");
|
||||||
expect(chatClient.chat).toHaveBeenCalledTimes(0);
|
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 () => {
|
it("keeps debt lifecycle follow-up context for 'а нам кто должен?.' after payables as-of answer", async () => {
|
||||||
const calls: Array<{ message: string; options?: any }> = [];
|
const calls: Array<{ message: string; options?: any }> = [];
|
||||||
const firstMessage =
|
const firstMessage =
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue