ДОМЕНЫ - ВОПРОСЫ - Исправить обработку коротких debt follow-up и защиту от диагностических LLM rewrite
This commit is contained in:
@@ -24,6 +24,15 @@ describe("address capability policy", () => {
|
||||
expect(isCapabilityRouteBlocked(decision)).toBe(false);
|
||||
});
|
||||
|
||||
it("maps confirmed VAT payable intent to compute exact capability", () => {
|
||||
const decision = resolveAddressCapabilityRouteDecision("vat_payable_confirmed_as_of_date");
|
||||
expect(decision.capability_id).toBe("confirmed_vat_payable_as_of_date");
|
||||
expect(decision.capability_layer).toBe("compute");
|
||||
expect(decision.capability_route_mode).toBe("exact");
|
||||
expect(decision.capability_route_enabled).toBe(true);
|
||||
expect(isCapabilityRouteBlocked(decision)).toBe(false);
|
||||
});
|
||||
|
||||
it("maps document drilldown intent to navigation capability", () => {
|
||||
const decision = resolveAddressCapabilityRouteDecision("list_documents_by_contract");
|
||||
expect(decision.capability_id).toBe("documents_drilldown");
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveAddressIntent } from "../src/services/addressIntentResolver";
|
||||
|
||||
describe("debt lifecycle typo tolerance", () => {
|
||||
it("routes payables typo phrasing to exact confirmed payables intent", () => {
|
||||
const result = resolveAddressIntent("кому мы должэны на май 2021");
|
||||
expect(result.intent).toBe("payables_confirmed_as_of_date");
|
||||
expect(result.reasons).toContain("payables_debt_lifecycle_signal_detected");
|
||||
});
|
||||
|
||||
it("routes receivables typo phrasing to exact confirmed receivables intent", () => {
|
||||
const result = resolveAddressIntent("кто нам должэны на июль 2020");
|
||||
expect(result.intent).toBe("receivables_confirmed_as_of_date");
|
||||
expect(result.reasons).toContain("receivables_debt_lifecycle_signal_detected");
|
||||
});
|
||||
});
|
||||
@@ -1855,6 +1855,12 @@ describe("address intent resolver expansion (M2.3a)", () => {
|
||||
expect(result.reasons).toContain("payables_debt_lifecycle_signal_detected");
|
||||
});
|
||||
|
||||
it("resolves repair phrasing 'кто нам в целом должен' as receivables debt lifecycle intent", () => {
|
||||
const result = resolveAddressIntent("нет вопрос кто нам в целом должен на денег на эту дату");
|
||||
expect(result.intent).toBe("receivables_confirmed_as_of_date");
|
||||
expect(result.reasons).toContain("receivables_debt_lifecycle_signal_detected");
|
||||
});
|
||||
|
||||
it("keeps out-of-scope supplier control wording as unknown intent", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"Какие поставщики у нас уже пару месяцев сдают акты без приходок. Может, их надо проконтролировать отдельно чтоб не засорять бухгалтерию дальше?"
|
||||
@@ -2022,6 +2028,15 @@ describe("address filter extraction for balance drilldown", () => {
|
||||
expect(extracted.warnings).toContain("counterparty_anchor_dropped_low_quality");
|
||||
});
|
||||
|
||||
it("drops pseudo-counterparty 'деньги на данную дату' from diagnostic rewrite phrase", () => {
|
||||
const extracted = extractAddressFilters(
|
||||
"Неясно, кто должен компании деньги на данную дату.",
|
||||
"unknown"
|
||||
);
|
||||
expect(extracted.extracted_filters.counterparty).toBeUndefined();
|
||||
expect(extracted.warnings).toContain("counterparty_anchor_dropped_low_quality");
|
||||
});
|
||||
|
||||
it("does not capture narrative filler as counterparty in broad docs-vs-money question", () => {
|
||||
const extracted = extractAddressFilters(
|
||||
"В каких случаях мы видим ситуацию, когда документы есть, а денег нет и пока не предвидится?",
|
||||
@@ -3243,6 +3258,26 @@ describe("address decompose stage follow-up carryover", () => {
|
||||
expect(result?.baseReasons).toContain("address_followup_context_applied");
|
||||
});
|
||||
|
||||
it("inherits as_of_date for receivables follow-up without explicit period", () => {
|
||||
const result = runAddressDecomposeStage("\u0430 \u043d\u0430\u043c \u043a\u0442\u043e \u0434\u043e\u043b\u0436\u0435\u043d?.", {
|
||||
previous_intent: "receivables_confirmed_as_of_date",
|
||||
previous_filters: {
|
||||
period_from: "2017-09-01",
|
||||
period_to: "2017-09-30",
|
||||
as_of_date: "2017-09-30"
|
||||
},
|
||||
previous_anchor_type: "unknown",
|
||||
previous_anchor_value: null
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.intent.intent).toBe("receivables_confirmed_as_of_date");
|
||||
expect(result?.filters.extracted_filters.period_from).toBe("2017-09-01");
|
||||
expect(result?.filters.extracted_filters.period_to).toBe("2017-09-30");
|
||||
expect(result?.filters.extracted_filters.as_of_date).toBe("2017-09-30");
|
||||
expect(result?.baseReasons).toContain("as_of_date_from_followup_context");
|
||||
expect(result?.baseReasons).toContain("address_followup_context_applied");
|
||||
});
|
||||
|
||||
it("keeps contract scope when follow-up asks for bank operations without explicit anchor", () => {
|
||||
const result = runAddressDecomposeStage("а теперь банковские операции", {
|
||||
previous_intent: "list_documents_by_contract",
|
||||
@@ -3415,6 +3450,27 @@ describe("address decompose stage follow-up carryover", () => {
|
||||
result?.baseReasons?.includes("intent_from_followup_context")
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("promotes short 'а ндс?' follow-up to confirmed VAT intent with inherited as-of date", () => {
|
||||
const result = runAddressDecomposeStage("\u0430 \u043d\u0434\u0441?", {
|
||||
previous_intent: "payables_confirmed_as_of_date",
|
||||
previous_filters: {
|
||||
period_from: "2017-09-01",
|
||||
period_to: "2017-09-30",
|
||||
as_of_date: "2017-09-30"
|
||||
},
|
||||
previous_anchor_type: "unknown",
|
||||
previous_anchor_value: null
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.mode.mode).toBe("address_query");
|
||||
expect(result?.intent.intent).toBe("vat_payable_confirmed_as_of_date");
|
||||
expect(result?.filters.extracted_filters.as_of_date).toBe("2017-09-30");
|
||||
expect(result?.filters.extracted_filters.period_from).toBe("2017-09-01");
|
||||
expect(result?.filters.extracted_filters.period_to).toBe("2017-09-30");
|
||||
expect(result?.baseReasons).toContain("intent_adjusted_to_vat_followup_context");
|
||||
expect(result?.baseReasons).toContain("as_of_date_from_followup_context");
|
||||
});
|
||||
});
|
||||
|
||||
describe("address recipe catalog counterparty filtering", () => {
|
||||
|
||||
@@ -7,12 +7,36 @@ import { evaluateAddressRouteExpectation } from "../src/services/addressRouteExp
|
||||
import { AddressQueryService } from "../src/services/addressQueryService";
|
||||
|
||||
describe("receivables confirmed as-of route", () => {
|
||||
it("routes canonical debtor phrasing into exact receivables intent", () => {
|
||||
const result = resolveAddressIntent("кто является дебитором компании по состоянию на июль 2020 года");
|
||||
expect(result.intent).toBe("receivables_confirmed_as_of_date");
|
||||
expect(result.reasons).toContain("receivables_debt_lifecycle_signal_detected");
|
||||
});
|
||||
|
||||
it("keeps exact receivables route for canonical debtor phrasing in runtime", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle("кто является дебитором компании по состоянию на июль 2020 года");
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.debug.detected_intent).toBe("receivables_confirmed_as_of_date");
|
||||
expect(result?.debug.selected_recipe).toBe("address_receivables_confirmed_as_of_date_v1");
|
||||
expect(result?.debug.requested_result_mode).toBe("confirmed_balance");
|
||||
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
|
||||
});
|
||||
|
||||
it("routes 'кто нам должен' wording into exact receivables intent", () => {
|
||||
const result = resolveAddressIntent("кто нам должен на июль 2020");
|
||||
expect(result.intent).toBe("receivables_confirmed_as_of_date");
|
||||
expect(result.reasons).toContain("receivables_debt_lifecycle_signal_detected");
|
||||
});
|
||||
|
||||
it("drops low-quality counterparty anchor from as-of debtor phrasing", () => {
|
||||
const extracted = extractAddressFilters(
|
||||
"кто является дебитором компании по состоянию на июль 2020 года",
|
||||
"receivables_confirmed_as_of_date"
|
||||
);
|
||||
expect(extracted.extracted_filters.counterparty).toBeUndefined();
|
||||
});
|
||||
|
||||
it("selects confirmed receivables recipe and builds balance query", () => {
|
||||
const filters = extractAddressFilters("кто нам должен на июль 2020", "receivables_confirmed_as_of_date").extracted_filters;
|
||||
const selected = selectAddressRecipe("receivables_confirmed_as_of_date", filters);
|
||||
|
||||
@@ -34,6 +34,17 @@ describe("address route expectations contract", () => {
|
||||
expect(audit.reason).toBe("route_expectation_matched");
|
||||
});
|
||||
|
||||
it("matches expected recipe and result mode for exact VAT payable route", () => {
|
||||
const audit = evaluateAddressRouteExpectation({
|
||||
intent: "vat_payable_confirmed_as_of_date",
|
||||
selectedRecipe: "address_vat_payable_confirmed_as_of_date_v1",
|
||||
requestedResultMode: "confirmed_balance",
|
||||
resultMode: "confirmed_balance"
|
||||
});
|
||||
expect(audit.status).toBe("matched");
|
||||
expect(audit.reason).toBe("route_expectation_matched");
|
||||
});
|
||||
|
||||
it("detects selected recipe mismatch", () => {
|
||||
const audit = evaluateAddressRouteExpectation({
|
||||
intent: "payables_confirmed_as_of_date",
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveAddressIntent } from "../src/services/addressIntentResolver";
|
||||
import { extractAddressFilters } from "../src/services/addressFilterExtractor";
|
||||
import { buildAddressRecipePlan, selectAddressRecipe } from "../src/services/addressRecipeCatalog";
|
||||
import { resolveAddressCapabilityRouteDecision } from "../src/services/addressCapabilityPolicy";
|
||||
import { evaluateAddressRouteExpectation } from "../src/services/addressRouteExpectations";
|
||||
import { AddressQueryService } from "../src/services/addressQueryService";
|
||||
|
||||
describe("vat payable confirmed as-of route", () => {
|
||||
it("routes VAT payable question into exact confirmed intent", () => {
|
||||
const result = resolveAddressIntent("сколько НДС к уплате на март 2020");
|
||||
expect(result.intent).toBe("vat_payable_confirmed_as_of_date");
|
||||
expect(result.reasons).toContain("vat_payable_confirmed_signal_detected");
|
||||
});
|
||||
|
||||
it("keeps VAT forecast intent when explicit forecast wording is used", () => {
|
||||
const result = resolveAddressIntent("какой прогноз оплаты ндс на март 2020");
|
||||
expect(result.intent).toBe("vat_payable_forecast");
|
||||
expect(result.reasons).toContain("forecast_tax_signal_detected");
|
||||
});
|
||||
|
||||
it("derives as_of_date for confirmed VAT route from period boundary", () => {
|
||||
const extracted = extractAddressFilters("сколько НДС к уплате на март 2020", "vat_payable_confirmed_as_of_date");
|
||||
expect(extracted.extracted_filters.period_from).toBe("2020-03-01");
|
||||
expect(extracted.extracted_filters.period_to).toBe("2020-03-31");
|
||||
expect(extracted.extracted_filters.as_of_date).toBe("2020-03-31");
|
||||
});
|
||||
|
||||
it("selects confirmed VAT recipe and builds balance query", () => {
|
||||
const filters = extractAddressFilters("сколько НДС к уплате на март 2020", "vat_payable_confirmed_as_of_date").extracted_filters;
|
||||
const selected = selectAddressRecipe("vat_payable_confirmed_as_of_date", filters);
|
||||
expect(selected.selected_recipe?.recipe_id).toBe("address_vat_payable_confirmed_as_of_date_v1");
|
||||
|
||||
const plan = buildAddressRecipePlan(selected.selected_recipe!, filters);
|
||||
expect(plan.query).toContain("РегистрБухгалтерии.Хозрасчетный.Остатки");
|
||||
expect(plan.query).toContain("СуммаРазвернутыйОстатокКт");
|
||||
expect(plan.query).toContain("Остатки.Счет");
|
||||
expect(plan.query).toContain("68.02");
|
||||
});
|
||||
|
||||
it("exposes compute exact capability and route expectation for confirmed VAT route", () => {
|
||||
const capability = resolveAddressCapabilityRouteDecision("vat_payable_confirmed_as_of_date");
|
||||
expect(capability.capability_id).toBe("confirmed_vat_payable_as_of_date");
|
||||
expect(capability.capability_layer).toBe("compute");
|
||||
expect(capability.capability_route_mode).toBe("exact");
|
||||
|
||||
const expectation = evaluateAddressRouteExpectation({
|
||||
intent: "vat_payable_confirmed_as_of_date",
|
||||
selectedRecipe: "address_vat_payable_confirmed_as_of_date_v1",
|
||||
requestedResultMode: "confirmed_balance",
|
||||
resultMode: "confirmed_balance"
|
||||
});
|
||||
expect(expectation.status).toBe("matched");
|
||||
});
|
||||
|
||||
it("uses exact VAT route in runtime for monthly as-of query", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle("сколько НДС к уплате на март 2020");
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.debug.detected_intent).toBe("vat_payable_confirmed_as_of_date");
|
||||
expect(result?.debug.selected_recipe).toBe("address_vat_payable_confirmed_as_of_date_v1");
|
||||
expect(result?.debug.requested_result_mode).toBe("confirmed_balance");
|
||||
expect(result?.debug.route_expectation_status).toBe("matched");
|
||||
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
||||
});
|
||||
});
|
||||
@@ -1081,6 +1081,340 @@ describe("assistant address follow-up carryover", () => {
|
||||
expect(String(calls[0].message).toLowerCase()).toContain("свк");
|
||||
expect(chatClient.chat).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
it("keeps debt lifecycle follow-up context for 'а нам кто должен?.' after payables as-of answer", async () => {
|
||||
const calls: Array<{ message: string; options?: any }> = [];
|
||||
const firstMessage =
|
||||
"\u043a\u043e\u043c\u0443 \u043c\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430 \u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c 2017";
|
||||
const followupMessage =
|
||||
"\u0430 \u043d\u0430\u043c \u043a\u0442\u043e \u0434\u043e\u043b\u0436\u0435\u043d?.";
|
||||
|
||||
const payablesResult = buildAddressLaneResult({
|
||||
reply_text:
|
||||
"\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043d\u044b\u0439 \u0441\u0440\u0435\u0437 \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u0441\u0442\u0432 \u043a \u043e\u043f\u043b\u0430\u0442\u0435 \u043d\u0430 30.09.2017",
|
||||
debug: {
|
||||
...buildAddressLaneResult().debug,
|
||||
query_shape: "UNKNOWN",
|
||||
query_shape_confidence: "low",
|
||||
detected_intent: "payables_confirmed_as_of_date",
|
||||
detected_intent_confidence: "high",
|
||||
extracted_filters: {
|
||||
sort: "period_desc",
|
||||
limit: 20,
|
||||
period_from: "2017-09-01",
|
||||
period_to: "2017-09-30",
|
||||
as_of_date: "2017-09-30"
|
||||
},
|
||||
selected_recipe: "address_payables_confirmed_as_of_date_v1",
|
||||
response_type: "FACTUAL_LIST",
|
||||
requested_result_mode: "confirmed_balance",
|
||||
result_mode: "confirmed_balance",
|
||||
balance_confirmed: true,
|
||||
reasons: ["payables_debt_lifecycle_signal_detected", "confirmed_balance_exact_payables_intent"]
|
||||
}
|
||||
});
|
||||
|
||||
const receivablesResult = buildAddressLaneResult({
|
||||
reply_text:
|
||||
"\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043d\u044b\u0439 \u0441\u0440\u0435\u0437 \u0434\u0435\u0431\u0438\u0442\u043e\u0440\u0441\u043a\u043e\u0439 \u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u0438 \u043d\u0430 30.09.2017",
|
||||
debug: {
|
||||
...buildAddressLaneResult().debug,
|
||||
query_shape: "UNKNOWN",
|
||||
query_shape_confidence: "low",
|
||||
detected_intent: "receivables_confirmed_as_of_date",
|
||||
detected_intent_confidence: "high",
|
||||
extracted_filters: {
|
||||
sort: "period_desc",
|
||||
limit: 20,
|
||||
period_from: "2017-09-01",
|
||||
period_to: "2017-09-30",
|
||||
as_of_date: "2017-09-30"
|
||||
},
|
||||
selected_recipe: "address_receivables_confirmed_as_of_date_v1",
|
||||
response_type: "FACTUAL_LIST",
|
||||
requested_result_mode: "confirmed_balance",
|
||||
result_mode: "confirmed_balance",
|
||||
balance_confirmed: true,
|
||||
reasons: ["receivables_debt_lifecycle_signal_detected", "confirmed_balance_exact_receivables_intent"]
|
||||
}
|
||||
});
|
||||
|
||||
const addressQueryService = {
|
||||
tryHandle: vi.fn(async (message: string, options?: any) => {
|
||||
calls.push({ message, options });
|
||||
if (message === firstMessage) {
|
||||
return payablesResult;
|
||||
}
|
||||
if (message === followupMessage) {
|
||||
if (!options?.followupContext) {
|
||||
return null;
|
||||
}
|
||||
return receivablesResult;
|
||||
}
|
||||
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-debt-${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: followupMessage,
|
||||
useMock: true
|
||||
} as any);
|
||||
|
||||
expect(second.ok).toBe(true);
|
||||
expect(second.reply_type).toBe("factual");
|
||||
expect(second.debug?.detected_intent).toBe("receivables_confirmed_as_of_date");
|
||||
expect(second.debug?.selected_recipe).toBe("address_receivables_confirmed_as_of_date_v1");
|
||||
|
||||
expect(calls).toHaveLength(2);
|
||||
expect(calls[1].message).toBe(followupMessage);
|
||||
expect(calls[1].options?.followupContext?.previous_intent).toBe("receivables_confirmed_as_of_date");
|
||||
expect(calls[1].options?.followupContext?.previous_filters?.as_of_date).toBe("2017-09-30");
|
||||
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("mirrors receivables->payables for short follow-up 'a мы кому' and keeps as-of date", async () => {
|
||||
const calls: Array<{ message: string; options?: any }> = [];
|
||||
const firstMessage = "кто нам должен на сентябрь 2017";
|
||||
const followupMessage = "a мы кому";
|
||||
|
||||
const receivablesResult = buildAddressLaneResult({
|
||||
reply_text: "Подтвержденный срез дебиторской задолженности на 30.09.2017",
|
||||
debug: {
|
||||
...buildAddressLaneResult().debug,
|
||||
query_shape: "UNKNOWN",
|
||||
query_shape_confidence: "low",
|
||||
detected_intent: "receivables_confirmed_as_of_date",
|
||||
detected_intent_confidence: "high",
|
||||
extracted_filters: {
|
||||
sort: "period_desc",
|
||||
limit: 20,
|
||||
period_from: "2017-09-01",
|
||||
period_to: "2017-09-30",
|
||||
as_of_date: "2017-09-30"
|
||||
},
|
||||
selected_recipe: "address_receivables_confirmed_as_of_date_v1",
|
||||
response_type: "FACTUAL_LIST",
|
||||
requested_result_mode: "confirmed_balance",
|
||||
result_mode: "confirmed_balance",
|
||||
balance_confirmed: true,
|
||||
reasons: ["receivables_debt_lifecycle_signal_detected", "confirmed_balance_exact_receivables_intent"]
|
||||
}
|
||||
});
|
||||
|
||||
const payablesResult = buildAddressLaneResult({
|
||||
reply_text: "Подтвержденный срез обязательств к оплате на 30.09.2017",
|
||||
debug: {
|
||||
...buildAddressLaneResult().debug,
|
||||
query_shape: "UNKNOWN",
|
||||
query_shape_confidence: "low",
|
||||
detected_intent: "payables_confirmed_as_of_date",
|
||||
detected_intent_confidence: "high",
|
||||
extracted_filters: {
|
||||
sort: "period_desc",
|
||||
limit: 20,
|
||||
period_from: "2017-09-01",
|
||||
period_to: "2017-09-30",
|
||||
as_of_date: "2017-09-30"
|
||||
},
|
||||
selected_recipe: "address_payables_confirmed_as_of_date_v1",
|
||||
response_type: "FACTUAL_LIST",
|
||||
requested_result_mode: "confirmed_balance",
|
||||
result_mode: "confirmed_balance",
|
||||
balance_confirmed: true,
|
||||
reasons: ["payables_debt_lifecycle_signal_detected", "confirmed_balance_exact_payables_intent"]
|
||||
}
|
||||
});
|
||||
|
||||
const addressQueryService = {
|
||||
tryHandle: vi.fn(async (message: string, options?: any) => {
|
||||
calls.push({ message, options });
|
||||
if (message === firstMessage) {
|
||||
return receivablesResult;
|
||||
}
|
||||
if (message === followupMessage) {
|
||||
if (!options?.followupContext) {
|
||||
return null;
|
||||
}
|
||||
if (options?.followupContext?.previous_intent !== "payables_confirmed_as_of_date") {
|
||||
return null;
|
||||
}
|
||||
return payablesResult;
|
||||
}
|
||||
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-debt-mirror-${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: followupMessage,
|
||||
useMock: true
|
||||
} as any);
|
||||
|
||||
expect(second.ok).toBe(true);
|
||||
expect(second.reply_type).toBe("factual");
|
||||
expect(second.debug?.detected_intent).toBe("payables_confirmed_as_of_date");
|
||||
expect(second.debug?.selected_recipe).toBe("address_payables_confirmed_as_of_date_v1");
|
||||
|
||||
expect(calls).toHaveLength(2);
|
||||
expect(calls[1].message).toBe(followupMessage);
|
||||
expect(calls[1].options?.followupContext?.previous_intent).toBe("payables_confirmed_as_of_date");
|
||||
expect(calls[1].options?.followupContext?.previous_filters?.as_of_date).toBe("2017-09-30");
|
||||
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps short VAT follow-up in address lane after debt as-of answer", async () => {
|
||||
const calls: Array<{ message: string; options?: any }> = [];
|
||||
const firstMessage =
|
||||
"\u043a\u043e\u043c\u0443 \u043c\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430 \u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c 2017";
|
||||
const followupMessage = "\u0430 \u043d\u0434\u0441?";
|
||||
|
||||
const payablesResult = buildAddressLaneResult({
|
||||
debug: {
|
||||
...buildAddressLaneResult().debug,
|
||||
detected_intent: "payables_confirmed_as_of_date",
|
||||
extracted_filters: {
|
||||
sort: "period_desc",
|
||||
limit: 20,
|
||||
period_from: "2017-09-01",
|
||||
period_to: "2017-09-30",
|
||||
as_of_date: "2017-09-30"
|
||||
},
|
||||
selected_recipe: "address_payables_confirmed_as_of_date_v1",
|
||||
response_type: "FACTUAL_LIST",
|
||||
requested_result_mode: "confirmed_balance",
|
||||
result_mode: "confirmed_balance",
|
||||
balance_confirmed: true
|
||||
}
|
||||
});
|
||||
|
||||
const vatResult = buildAddressLaneResult({
|
||||
debug: {
|
||||
...buildAddressLaneResult().debug,
|
||||
detected_intent: "vat_payable_confirmed_as_of_date",
|
||||
extracted_filters: {
|
||||
sort: "period_desc",
|
||||
limit: 20,
|
||||
period_from: "2017-09-01",
|
||||
period_to: "2017-09-30",
|
||||
as_of_date: "2017-09-30"
|
||||
},
|
||||
selected_recipe: "address_vat_payable_confirmed_as_of_date_v1",
|
||||
response_type: "FACTUAL_LIST",
|
||||
requested_result_mode: "confirmed_balance",
|
||||
result_mode: "confirmed_balance",
|
||||
balance_confirmed: true
|
||||
}
|
||||
});
|
||||
|
||||
const addressQueryService = {
|
||||
tryHandle: vi.fn(async (message: string, options?: any) => {
|
||||
calls.push({ message, options });
|
||||
if (message === firstMessage) {
|
||||
return payablesResult;
|
||||
}
|
||||
if (!options?.followupContext) {
|
||||
return null;
|
||||
}
|
||||
return vatResult;
|
||||
})
|
||||
} 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-vat-${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: followupMessage,
|
||||
useMock: true
|
||||
} as any);
|
||||
|
||||
expect(second.ok).toBe(true);
|
||||
expect(second.reply_type).toBe("factual");
|
||||
expect(second.debug?.detected_intent).toBe("vat_payable_confirmed_as_of_date");
|
||||
expect(second.debug?.selected_recipe).toBe("address_vat_payable_confirmed_as_of_date_v1");
|
||||
|
||||
expect(calls).toHaveLength(2);
|
||||
expect(typeof calls[1].message).toBe("string");
|
||||
expect(String(calls[1].message).length).toBeGreaterThan(0);
|
||||
expect(calls[1].options?.followupContext?.previous_filters?.as_of_date).toBe("2017-09-30");
|
||||
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes active organization scope into address lane follow-up context", async () => {
|
||||
const calls: Array<{ message: string; options?: any }> = [];
|
||||
const addressQueryService = {
|
||||
|
||||
@@ -442,6 +442,105 @@ describe("assistant address llm pre-decompose candidate preference", () => {
|
||||
expect(response.debug?.llm_decomposition_reason).toBe("normalized_fragment_rejected_anchor_substitution");
|
||||
});
|
||||
|
||||
it("rejects diagnostic canonical rewrite like 'Неясно...' for debt-intent repair message", async () => {
|
||||
const calls: Array<{ message: string }> = [];
|
||||
const addressQueryService = {
|
||||
tryHandle: vi.fn(async (message: string) => {
|
||||
calls.push({ message });
|
||||
return buildAddressLaneResult(message);
|
||||
})
|
||||
} as any;
|
||||
|
||||
const sourceMessage = "нет вопрос кто нам в целом должен на денег на эту дату";
|
||||
const candidateMessage = "Неясно, кто должен компании деньги на данную дату.";
|
||||
|
||||
const normalizerService = {
|
||||
normalize: vi.fn(async () => ({
|
||||
trace_id: "norm-predecompose-diagnostic-rewrite",
|
||||
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: candidateMessage,
|
||||
domain_relevance: "in_scope",
|
||||
business_scope: "company_specific_accounting",
|
||||
entity_hints: [],
|
||||
account_hints: [],
|
||||
document_hints: [],
|
||||
register_hints: [],
|
||||
time_scope: {
|
||||
type: "implicit",
|
||||
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: false,
|
||||
asks_for_evidence: false,
|
||||
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-diagnostic-rewrite-${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(calls[0].message).not.toBe(candidateMessage);
|
||||
expect(response.debug?.llm_decomposition_reason).toBe("normalized_fragment_rejected_diagnostic_rewrite");
|
||||
expect(String(response.debug?.llm_decomposition_effective_message ?? "")).toBe(sourceMessage);
|
||||
});
|
||||
|
||||
it("rejects follow-up intent injection when llm adds documents to same-date account prompt", async () => {
|
||||
const calls: Array<{ message: string }> = [];
|
||||
const addressQueryService = {
|
||||
@@ -1054,7 +1153,8 @@ describe("assistant address llm pre-decompose candidate preference", () => {
|
||||
[
|
||||
"llm_predecompose_semantic_guard_rejected",
|
||||
"llm_predecompose_unsupported_mode",
|
||||
"address_signal_unsupported_intent_fallback_to_deep"
|
||||
"address_signal_unsupported_intent_fallback_to_deep",
|
||||
"non_domain_query_indexed"
|
||||
]
|
||||
).toContain(response.debug?.address_tool_gate_reason);
|
||||
});
|
||||
|
||||
@@ -315,6 +315,36 @@ describe("assistant orchestration contract", () => {
|
||||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||||
});
|
||||
|
||||
it("keeps short mirror follow-up 'a мы кому' in address lane instead of non-domain chat", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "a мы кому",
|
||||
effectiveAddressUserMessage: "a мы кому",
|
||||
followupContext: null,
|
||||
llmPreDecomposeMeta: {
|
||||
applied: false,
|
||||
reason: "normalized_fragment_rejected_semantic_guard",
|
||||
llmCanonicalCandidateDetected: true,
|
||||
predecomposeContract: {
|
||||
mode: "unsupported",
|
||||
mode_confidence: "low",
|
||||
intent: "unknown",
|
||||
intent_confidence: "low"
|
||||
},
|
||||
semanticExtractionContract: {
|
||||
valid: false,
|
||||
apply_canonical_recommended: false,
|
||||
reason_codes: ["unsupported_low_confidence_contract"]
|
||||
}
|
||||
} as any,
|
||||
useMock: false
|
||||
});
|
||||
|
||||
expect(decision.runAddressLane).toBe(true);
|
||||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||||
expect(decision.livingMode).toBe("address_data");
|
||||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||||
});
|
||||
|
||||
it("routes unsupported turnover-by-organization query to deep analysis", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "\u043a\u0430\u043a\u0438\u0435 \u043e\u0431\u043e\u0440\u043e\u0442\u044b \u043f\u043e \u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435 \u0437\u0430 2020 \u0433\u043e\u0434",
|
||||
|
||||
Reference in New Issue
Block a user