178 lines
7.1 KiB
TypeScript
178 lines
7.1 KiB
TypeScript
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,
|
||
resolveMetaSignalSet: (input: {
|
||
rawUserMessage?: string;
|
||
repairedRawUserMessage?: string;
|
||
effectiveAddressUserMessage?: string;
|
||
repairedEffectiveAddressUserMessage?: string;
|
||
}) => {
|
||
const samples = [
|
||
input.rawUserMessage,
|
||
input.repairedRawUserMessage,
|
||
input.effectiveAddressUserMessage,
|
||
input.repairedEffectiveAddressUserMessage
|
||
].join(" ");
|
||
return {
|
||
dataScopeMetaQuery: /по какой компании|какая база|по каким конторам/i.test(samples),
|
||
capabilityMetaQuery: /что ты можешь|что ты умеешь/i.test(samples),
|
||
metaAnswerFollowupSignal: /это норм|что думаешь/i.test(samples)
|
||
};
|
||
},
|
||
resolveHardMetaMode: (input: {
|
||
dataScopeMetaQuery?: boolean;
|
||
capabilityMetaQuery?: boolean;
|
||
dataRetrievalSignal?: boolean;
|
||
}) =>
|
||
input.dataScopeMetaQuery
|
||
? "data_scope"
|
||
: input.capabilityMetaQuery && !input.dataRetrievalSignal
|
||
? "capability"
|
||
: null,
|
||
isMetaFollowupOverGroundedAnswer: () => false,
|
||
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,
|
||
resolveRouteMemorySignals: () => ({
|
||
contextualHistoricalCapabilityFollowupDetected: false,
|
||
contextualMemoryRecapFollowupDetected: false
|
||
}),
|
||
findLastAddressAssistantItem: () => null,
|
||
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({
|
||
resolveRouteMemorySignals: () => ({
|
||
contextualHistoricalCapabilityFollowupDetected: false,
|
||
contextualMemoryRecapFollowupDetected: 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");
|
||
});
|
||
});
|