277 lines
16 KiB
TypeScript
277 lines
16 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import { createAssistantTurnMeaningPolicy } from "../src/services/assistantTurnMeaningPolicy";
|
||
import { resolveAddressIntent } from "../src/services/addressIntentResolver";
|
||
|
||
function buildPolicy(overrides: Record<string, unknown> = {}) {
|
||
return createAssistantTurnMeaningPolicy({
|
||
compactWhitespace: (value: string) => String(value ?? "").replace(/\s+/g, " ").trim(),
|
||
repairAddressMojibake: (value: string) => value,
|
||
resolveAddressIntent,
|
||
toNonEmptyString: (value: unknown) => {
|
||
if (value === null || value === undefined) {
|
||
return null;
|
||
}
|
||
const text = String(value).trim();
|
||
return text.length > 0 ? text : null;
|
||
},
|
||
...overrides
|
||
});
|
||
}
|
||
|
||
describe("assistantTurnMeaningPolicy", () => {
|
||
it("recovers a supported receivables intent from light current-turn typo noise", () => {
|
||
const policy = buildPolicy();
|
||
|
||
const meaning = policy.resolveAssistantTurnMeaning({
|
||
rawUserMessage:
|
||
"\u043a\u0442\u043e \u043d\u0430\u043c\u0441 \u0434\u043e\u043b\u0436\u0435\u043d \u0434\u0435\u043d\u0435\u0433 \u043d\u0430 \u0441\u0435\u0433\u043e\u0434\u043d\u044f"
|
||
});
|
||
|
||
expect(meaning.schema_version).toBe("assistant_turn_meaning_v1");
|
||
expect(meaning.explicit_intent_candidate).toBe("receivables_confirmed_as_of_date");
|
||
expect(meaning.asked_domain_family).toBe("receivables");
|
||
expect(meaning.carryover_budget).toBe("matching_family_only");
|
||
expect(meaning.stale_replay_forbidden).toBe(false);
|
||
});
|
||
|
||
it("promotes specific counterparty turnover to the supported revenue intent", () => {
|
||
const policy = buildPolicy();
|
||
|
||
const meaning = policy.resolveAssistantTurnMeaning({
|
||
rawUserMessage:
|
||
"\u043a\u0430\u043a\u043e\u0439 \u043e\u0431\u043e\u0440\u043e\u0442 \u0431\u044b\u043b \u0441\u0432\u043a"
|
||
});
|
||
|
||
expect(meaning.explicit_intent_candidate).toBe("customer_revenue_and_payments");
|
||
expect(meaning.asked_domain_family).toBe("counterparty");
|
||
expect(meaning.asked_action_family).toBe("counterparty_value_or_turnover");
|
||
expect(meaning.unsupported_but_understood_family).toBeNull();
|
||
expect(meaning.stale_replay_forbidden).toBe(false);
|
||
expect(meaning.carryover_budget).toBe("matching_family_only");
|
||
expect(meaning.explicit_entity_candidates).toEqual([
|
||
{
|
||
type: "counterparty",
|
||
value: "\u0441\u0432\u043a",
|
||
source: "current_turn_loose_entity_tail"
|
||
}
|
||
]);
|
||
});
|
||
|
||
it("ignores temporal tail words in all-time revenue ranking questions", () => {
|
||
const policy = buildPolicy({
|
||
resolveAddressIntent: (text: string) =>
|
||
text.includes("\u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0439")
|
||
? { intent: "customer_revenue_and_payments", confidence: "high" }
|
||
: resolveAddressIntent(text)
|
||
});
|
||
|
||
const meaning = policy.resolveAssistantTurnMeaning({
|
||
rawUserMessage:
|
||
"\u043a\u0442\u043e \u0443 \u043d\u0430\u0441 \u0441\u0430\u043c\u044b\u0439 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043b\u0438\u0435\u043d\u0442 \u0437\u0430 \u0432\u0441\u0435 \u0432\u0440\u0435\u043c\u044f"
|
||
});
|
||
|
||
expect(meaning.explicit_intent_candidate).toBe("customer_revenue_and_payments");
|
||
expect(meaning.explicit_entity_candidates).toEqual([]);
|
||
expect(meaning.stale_replay_forbidden).toBe(false);
|
||
});
|
||
|
||
it("treats VAT period questions as supported current-turn intent", () => {
|
||
const policy = buildPolicy({
|
||
resolveAddressIntent: (text: string) =>
|
||
text.includes("\u043d\u0434\u0441")
|
||
? { intent: "vat_liability_confirmed_for_tax_period", confidence: "high" }
|
||
: resolveAddressIntent(text)
|
||
});
|
||
|
||
const meaning = policy.resolveAssistantTurnMeaning({
|
||
rawUserMessage:
|
||
"\u0430 \u043a\u0430\u043a\u043e\u0439 \u043d\u0434\u0441 \u043c\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c \u0437\u0430 \u044d\u0442\u043e\u0442 \u043f\u0435\u0440\u0438\u043e\u0434"
|
||
});
|
||
|
||
expect(meaning.explicit_intent_candidate).toBe("vat_liability_confirmed_for_tax_period");
|
||
expect(meaning.asked_domain_family).toBe("vat");
|
||
expect(meaning.asked_action_family).toBe("confirmed_tax_period");
|
||
expect(meaning.stale_replay_forbidden).toBe(false);
|
||
});
|
||
|
||
it("marks broad business evaluation as unsupported-but-understood instead of stale lifecycle replay", () => {
|
||
const policy = buildPolicy({
|
||
resolveAddressIntent: () => ({ intent: "counterparty_activity_lifecycle", confidence: "high" })
|
||
});
|
||
|
||
const meaning = policy.resolveAssistantTurnMeaning({
|
||
rawUserMessage: "Как ты оценишь деятельность компании?"
|
||
});
|
||
|
||
expect(meaning.explicit_intent_candidate).toBeNull();
|
||
expect(meaning.asked_domain_family).toBe("business_summary");
|
||
expect(meaning.asked_action_family).toBe("broad_evaluation");
|
||
expect(meaning.unsupported_but_understood_family).toBe("broad_business_evaluation");
|
||
expect(meaning.stale_replay_forbidden).toBe(true);
|
||
expect(meaning.reason_codes).toContain("broad_business_evaluation_current_turn_signal");
|
||
});
|
||
|
||
it("recognizes broad company analysis wording as the same bounded business overview ask", () => {
|
||
const policy = buildPolicy({
|
||
resolveAddressIntent: () => ({ intent: "unknown", confidence: "low" })
|
||
});
|
||
|
||
const meaning = policy.resolveAssistantTurnMeaning({
|
||
rawUserMessage: "Дай полный анализ компании и LLM-аудит бизнеса"
|
||
});
|
||
|
||
expect(meaning.explicit_intent_candidate).toBeNull();
|
||
expect(meaning.asked_domain_family).toBe("business_summary");
|
||
expect(meaning.asked_action_family).toBe("broad_evaluation");
|
||
expect(meaning.unsupported_but_understood_family).toBe("broad_business_evaluation");
|
||
expect(meaning.reason_codes).toContain("broad_business_evaluation_current_turn_signal");
|
||
});
|
||
|
||
it("lets explicit business overview wording beat turnover and net-flow cues", () => {
|
||
const policy = buildPolicy({
|
||
resolveAddressIntent: () => ({ intent: "customer_revenue_and_payments", confidence: "high" })
|
||
});
|
||
|
||
const meaning = policy.resolveAssistantTurnMeaning({
|
||
rawUserMessage:
|
||
"\u0414\u0430\u0439 \u0431\u0438\u0437\u043d\u0435\u0441-\u043e\u0431\u0437\u043e\u0440 \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441: \u043e\u0431\u043e\u0440\u043e\u0442\u044b, \u043d\u0435\u0442\u0442\u043e, \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u044c."
|
||
});
|
||
|
||
expect(meaning.explicit_intent_candidate).toBeNull();
|
||
expect(meaning.asked_domain_family).toBe("business_summary");
|
||
expect(meaning.asked_action_family).toBe("broad_evaluation");
|
||
expect(meaning.explicit_entity_candidates).toEqual([]);
|
||
expect(meaning.unsupported_but_understood_family).toBe("broad_business_evaluation");
|
||
expect(meaning.stale_replay_forbidden).toBe(true);
|
||
expect(meaning.reason_codes).toContain("broad_business_evaluation_current_turn_signal");
|
||
});
|
||
|
||
it("treats compound money breakdown as business overview instead of narrow customer revenue", () => {
|
||
const policy = buildPolicy({
|
||
resolveAddressIntent: () => ({ intent: "customer_revenue_and_payments", confidence: "high" })
|
||
});
|
||
|
||
const meaning = policy.resolveAssistantTurnMeaning({
|
||
rawUserMessage:
|
||
"\u0420\u0430\u0441\u043a\u0440\u043e\u0439 \u0434\u0435\u043d\u044c\u0433\u0438 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435: \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0432\u0441\u0435\u0433\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438, \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438, \u043a\u0430\u043a\u043e\u0439 \u0447\u0438\u0441\u0442\u044b\u0439 \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u043f\u043e\u0442\u043e\u043a, \u043a\u0442\u043e \u0433\u043b\u0430\u0432\u043d\u044b\u0439 \u043a\u043b\u0438\u0435\u043d\u0442 \u0438 \u043a\u0442\u043e \u0433\u043b\u0430\u0432\u043d\u044b\u0439 \u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a \u0432 2020."
|
||
});
|
||
|
||
expect(meaning.explicit_intent_candidate).toBeNull();
|
||
expect(meaning.asked_domain_family).toBe("business_summary");
|
||
expect(meaning.asked_action_family).toBe("broad_evaluation");
|
||
expect(meaning.explicit_entity_candidates).toEqual([]);
|
||
expect(meaning.unsupported_but_understood_family).toBe("broad_business_evaluation");
|
||
expect(meaning.stale_replay_forbidden).toBe(true);
|
||
expect(meaning.reason_codes).toContain("broad_business_evaluation_current_turn_signal");
|
||
});
|
||
|
||
it("treats organization-level earnings and best-year wording as business overview", () => {
|
||
const policy = buildPolicy({
|
||
resolveAddressIntent: () => ({ intent: "customer_revenue_and_payments", confidence: "high" })
|
||
});
|
||
|
||
const earnings = policy.resolveAssistantTurnMeaning({
|
||
rawUserMessage: "сколько вообще денег мы заработали за все время?"
|
||
});
|
||
expect(earnings.explicit_intent_candidate).toBeNull();
|
||
expect(earnings.asked_domain_family).toBe("business_summary");
|
||
expect(earnings.asked_action_family).toBe("broad_evaluation");
|
||
expect(earnings.unsupported_but_understood_family).toBe("broad_business_evaluation");
|
||
expect(earnings.stale_replay_forbidden).toBe(true);
|
||
|
||
const bestYear = policy.resolveAssistantTurnMeaning({
|
||
rawUserMessage: "какой у нас самый доходный год"
|
||
});
|
||
expect(bestYear.explicit_intent_candidate).toBeNull();
|
||
expect(bestYear.asked_domain_family).toBe("business_summary");
|
||
expect(bestYear.asked_action_family).toBe("broad_evaluation");
|
||
expect(bestYear.reason_codes).toContain("broad_business_evaluation_current_turn_signal");
|
||
|
||
const profitMargin = policy.resolveAssistantTurnMeaning({
|
||
rawUserMessage:
|
||
"\u043a\u0430\u043a\u0430\u044f \u0443 \u043d\u0430\u0441 \u0447\u0438\u0441\u0442\u0430\u044f \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0438 \u043c\u0430\u0440\u0436\u0430 \u0437\u0430 2020?"
|
||
});
|
||
expect(profitMargin.explicit_intent_candidate).toBeNull();
|
||
expect(profitMargin.asked_domain_family).toBe("business_summary");
|
||
expect(profitMargin.asked_action_family).toBe("broad_evaluation");
|
||
expect(profitMargin.unsupported_but_understood_family).toBe("broad_business_evaluation");
|
||
expect(profitMargin.reason_codes).toContain("broad_business_evaluation_current_turn_signal");
|
||
|
||
const overdueDebt = policy.resolveAssistantTurnMeaning({
|
||
rawUserMessage:
|
||
"\u043a\u0430\u043a\u0430\u044f \u0443 \u043d\u0430\u0441 \u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a\u0430 \u043f\u043e \u0434\u043e\u043b\u0433\u0430\u043c \u0437\u0430 2020?"
|
||
});
|
||
expect(overdueDebt.explicit_intent_candidate).toBeNull();
|
||
expect(overdueDebt.asked_domain_family).toBe("business_summary");
|
||
expect(overdueDebt.asked_action_family).toBe("broad_evaluation");
|
||
expect(overdueDebt.unsupported_but_understood_family).toBe("broad_business_evaluation");
|
||
expect(overdueDebt.reason_codes).toContain("broad_business_evaluation_current_turn_signal");
|
||
|
||
const inventoryReserve = policy.resolveAssistantTurnMeaning({
|
||
rawUserMessage:
|
||
"\u043a\u0430\u043a\u0438\u0435 \u0443 \u043d\u0430\u0441 \u0440\u0438\u0441\u043a\u0438 \u043d\u0435\u043b\u0438\u043a\u0432\u0438\u0434\u0430 \u0438 \u0440\u0435\u0437\u0435\u0440\u0432\u044b \u043f\u043e \u0441\u043a\u043b\u0430\u0434\u0443 \u0437\u0430 2020?"
|
||
});
|
||
expect(inventoryReserve.explicit_intent_candidate).toBeNull();
|
||
expect(inventoryReserve.asked_domain_family).toBe("business_summary");
|
||
expect(inventoryReserve.asked_action_family).toBe("broad_evaluation");
|
||
expect(inventoryReserve.unsupported_but_understood_family).toBe("broad_business_evaluation");
|
||
expect(inventoryReserve.reason_codes).toContain("broad_business_evaluation_current_turn_signal");
|
||
|
||
const supplierQuality = policy.resolveAssistantTurnMeaning({
|
||
rawUserMessage:
|
||
"\u043a\u0430\u043a\u0438\u0435 \u0443 \u043d\u0430\u0441 \u0440\u0438\u0441\u043a\u0438 \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u043e\u0432 \u0437\u0430 2020?"
|
||
});
|
||
expect(supplierQuality.explicit_intent_candidate).toBeNull();
|
||
expect(supplierQuality.asked_domain_family).toBe("business_summary");
|
||
expect(supplierQuality.asked_action_family).toBe("broad_evaluation");
|
||
expect(supplierQuality.unsupported_but_understood_family).toBe("broad_business_evaluation");
|
||
expect(supplierQuality.reason_codes).toContain("broad_business_evaluation_current_turn_signal");
|
||
});
|
||
|
||
it("keeps selected-object profitability as an inventory exact intent instead of company overview", () => {
|
||
const policy = buildPolicy();
|
||
|
||
const meaning = policy.resolveAssistantTurnMeaning({
|
||
rawUserMessage:
|
||
'\u041f\u043e \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u043c\u0443 \u043e\u0431\u044a\u0435\u043a\u0442\u0443 "\u0427\u0435\u0442\u043a\u0438 \u041f\u043e\u0441\u0442 (84*117)": \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438 \u043d\u0430 \u043f\u0440\u043e\u0434\u0430\u0436\u0435, \u043a\u0430\u043a\u0438\u0435 \u0437\u0430\u043a\u0443\u043f\u043e\u0447\u043d\u044b\u0435 \u0438 \u043f\u0440\u043e\u0434\u0430\u0436\u043d\u044b\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b \u044d\u0442\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u044e\u0442?'
|
||
});
|
||
|
||
expect(meaning.explicit_intent_candidate).toBe("inventory_profitability_for_item");
|
||
expect(meaning.asked_domain_family).toBe("inventory");
|
||
expect(meaning.unsupported_but_understood_family).toBeNull();
|
||
expect(meaning.stale_replay_forbidden).toBe(false);
|
||
expect(meaning.reason_codes).not.toContain("broad_business_evaluation_current_turn_signal");
|
||
});
|
||
|
||
it("treats paired receivables/payables position as business overview instead of one debt side", () => {
|
||
const policy = buildPolicy();
|
||
|
||
const meaning = policy.resolveAssistantTurnMeaning({
|
||
rawUserMessage:
|
||
"\u041a\u0430\u043a\u0430\u044f \u0434\u0435\u0431\u0438\u0442\u043e\u0440\u043a\u0430 \u0438 \u043a\u0440\u0435\u0434\u0438\u0442\u043e\u0440\u043a\u0430 \u043d\u0430 30.09.2020 \u043f\u043e \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438?"
|
||
});
|
||
|
||
expect(meaning.explicit_intent_candidate).toBeNull();
|
||
expect(meaning.asked_domain_family).toBe("business_summary");
|
||
expect(meaning.asked_action_family).toBe("broad_evaluation");
|
||
expect(meaning.unsupported_but_understood_family).toBe("broad_business_evaluation");
|
||
expect(meaning.reason_codes).toContain("broad_business_evaluation_current_turn_signal");
|
||
});
|
||
|
||
it("treats debt overdue classification follow-up as business interpretation, not capability help", () => {
|
||
const policy = buildPolicy({
|
||
resolveAddressIntent: () => ({ intent: "unknown", confidence: "low" })
|
||
});
|
||
|
||
const meaning = policy.resolveAssistantTurnMeaning({
|
||
rawUserMessage:
|
||
"\u042d\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u0441\u0447\u0438\u0442\u0430\u0442\u044c \u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a\u043e\u0439 \u0438\u043b\u0438 \u043f\u043e\u043a\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u0439 \u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u044c\u044e?"
|
||
});
|
||
|
||
expect(meaning.explicit_intent_candidate).toBeNull();
|
||
expect(meaning.asked_domain_family).toBe("business_summary");
|
||
expect(meaning.asked_action_family).toBe("broad_evaluation");
|
||
expect(meaning.unsupported_but_understood_family).toBe("broad_business_evaluation");
|
||
});
|
||
});
|