NODEDC_1C/llm_normalizer/backend/tests/assistantRoutePolicy.test.ts

148 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, expect, it } from "vitest";
import { createAssistantRoutePolicy } from "../src/services/assistantRoutePolicy";
function toNonEmptyString(value: unknown): string | null {
if (value === null || value === undefined) {
return null;
}
const text = String(value).trim();
return text.length > 0 ? text : null;
}
function normalizeOrganizationScopeValue(value: unknown): string | null {
const text = toNonEmptyString(value);
if (!text) {
return null;
}
return text.replace(/^"+|"+$/g, "").replace(/^'+|'+$/g, "");
}
function buildPolicy(overrides: Record<string, unknown> = {}) {
return createAssistantRoutePolicy({
repairAddressMojibake: (text: string) => text,
findLastGroundedAddressAnswerDebug: () => null,
findLastOrganizationClarificationAddressDebug: () => null,
mergeKnownOrganizations: (values: unknown[]) =>
Array.from(
new Set(
(Array.isArray(values) ? values : [])
.map((item) => normalizeOrganizationScopeValue(item))
.filter((item): item is string => Boolean(item))
)
),
normalizeOrganizationScopeValue,
resolveOrganizationSelectionFromMessage: () => null,
hasAssistantDataScopeMetaQuestionSignal: (text: string) => /по какой компании|какая база|по каким конторам/i.test(text),
shouldHandleAsAssistantCapabilityMetaQuery: (text: string) => /что ты можешь|что ты умеешь/i.test(text),
hasDataRetrievalRequestSignal: () => false,
hasAggregateBusinessAnalyticsSignal: () => false,
hasStandaloneAddressTopicSignal: () => false,
hasOpenContractsAddressSignal: () => false,
detectAddressQuestionMode: () => ({ mode: "unsupported", confidence: "low" }),
resolveAddressIntent: () => ({ intent: "unknown", confidence: "low" }),
toNonEmptyString,
hasStrictDeepInvestigationCue: () => false,
hasStrongDataIntentSignal: () => false,
hasAccountingSignal: () => false,
hasDangerOrCoercionSignal: () => false,
hasAddressFollowupContextSignal: () => false,
hasShortDebtMirrorFollowupSignal: () => false,
isInventorySelectedObjectIntent: (intent: unknown) => /inventory/i.test(String(intent ?? "")),
hasShortInventoryObjectFollowupSignal: () => false,
hasHistoricalCapabilityFollowupSignal: () => false,
isGroundedInventoryContextDebug: (debug: unknown) => Boolean(debug),
hasConversationMemoryRecallFollowupSignal: () => false,
findLastAddressAssistantItem: () => null,
hasMetaAnswerFollowupSignal: () => false,
resolveAddressToolGateDecision: () => ({
runAddressLane: false,
decision: "skip_address_lane",
reason: "no_address_signal_after_l0"
}),
hasSameDateAccountFollowupSignalForPredecompose: () => false,
hasLooseAllTimeAddressLookupSignal: () => false,
hasDeepAnalysisPreferenceSignal: () => false,
hasDirectDeepAnalysisSignal: () => false,
compactWhitespace: (text: string) => String(text ?? "").replace(/\s+/g, " ").trim(),
hasDeepSessionContinuationSignal: () => false,
resolveLivingAssistantModeDecision: (input: { addressLaneTriggered?: boolean }) =>
input.addressLaneTriggered
? { mode: "address_data", reason: "address_lane_triggered" }
: { mode: "chat", reason: "living_chat_signal_detected" },
...overrides
});
}
describe("assistantRoutePolicy", () => {
it("routes data-scope meta question to chat contract", () => {
const policy = buildPolicy();
const decision = policy.resolveAssistantOrchestrationDecision({
rawUserMessage: "по какой компании мы можем работать?",
effectiveAddressUserMessage: "по какой компании мы можем работать?",
followupContext: null,
llmPreDecomposeMeta: null,
useMock: false
});
expect(decision.runAddressLane).toBe(false);
expect(decision.toolGateReason).toBe("assistant_data_scope_query_detected");
expect(decision.livingMode).toBe("chat");
expect(decision.orchestrationContract?.hard_meta_mode).toBe("data_scope");
});
it("keeps supported address intent in address lane", () => {
const policy = buildPolicy({
detectAddressQuestionMode: () => ({ mode: "address_query", confidence: "high" }),
resolveAddressIntent: () => ({ intent: "inventory_on_hand_as_of_date", confidence: "high" }),
resolveAddressToolGateDecision: () => ({
runAddressLane: true,
decision: "run_address_lane",
reason: "address_mode_classifier_detected"
})
});
const decision = policy.resolveAssistantOrchestrationDecision({
rawUserMessage: "какие товары сейчас лежат на складе",
effectiveAddressUserMessage: "какие товары сейчас лежат на складе",
followupContext: null,
llmPreDecomposeMeta: null,
useMock: false
});
expect(decision.runAddressLane).toBe(true);
expect(decision.toolGateReason).toBe("address_mode_classifier_detected");
expect(decision.livingMode).toBe("address_data");
expect(decision.orchestrationContract?.semantic_route_arbitration?.supported_address_intent_detected).toBe(true);
});
it("routes memory recap follow-up over grounded answer to chat", () => {
const policy = buildPolicy({
hasConversationMemoryRecallFollowupSignal: () => true,
findLastGroundedAddressAnswerDebug: () => ({ execution_lane: "address_query" })
});
const decision = policy.resolveAssistantOrchestrationDecision({
rawUserMessage: "а ты помнишь что мы обсуждали?",
effectiveAddressUserMessage: "а ты помнишь что мы обсуждали?",
followupContext: null,
llmPreDecomposeMeta: {
applied: false,
reason: "normalized_fragment_rejected_semantic_guard",
predecomposeContract: {
mode: "unsupported",
mode_confidence: "low",
intent: "unknown",
intent_confidence: "low"
}
},
useMock: false
});
expect(decision.runAddressLane).toBe(false);
expect(decision.toolGateReason).toBe("memory_recap_followup_detected");
expect(decision.livingMode).toBe("chat");
expect(decision.livingReason).toBe("memory_recap_followup_detected");
});
});