АРЧ АП11 - Вынести политику оркестрационной маршрутизации из assistantService в отдельный модуль

This commit is contained in:
2026-04-16 09:47:26 +03:00
parent 606654641b
commit f1333c457e
8 changed files with 2283 additions and 1688 deletions
@@ -0,0 +1,81 @@
import { describe, expect, it } from "vitest";
import { createAssistantLivingModePolicy } from "../src/services/assistantLivingModePolicy";
function buildPolicy() {
return createAssistantLivingModePolicy({
featureAssistantLivingChatRouterV1: true,
compactWhitespace: (text: string) => text.replace(/\s+/g, " ").trim(),
repairAddressMojibake: (text: string) => text,
toNonEmptyString: (value: unknown) => {
if (value === null || value === undefined) {
return null;
}
const text = String(value).trim();
return text.length > 0 ? text : null;
},
normalizeOrganizationScopeValue: (value: unknown) => {
if (value === null || value === undefined) {
return null;
}
const text = String(value).trim().replace(/^"+|"+$/g, "").replace(/^'+|'+$/g, "");
return text.length > 0 ? text : null;
},
hasReferentialPointer: (text: string) =>
/(по этому|по тому|это же|этой|этим|этому|этого|этот|эту|этом|это|эти|этих|из этого|из них|из этих|из тех|в этом|тот же|same thing|that one|po etomu|po tomu)/i.test(
text.toLowerCase()
),
hasSmallTalkSignal: (text: string) => /(привет|как дела|спасибо|благодарю|thanks|thank you|hello|hi)\b/i.test(text.toLowerCase()),
hasAssistantCapabilityQuestionSignal: (text: string) =>
/(?:кто ты|что ты можешь|какие фичи|полный список возможностей|чем ты можешь помочь|что ты умеешь)/i.test(text),
hasOperationalAdminActionRequestSignal: (text: string) =>
/(?:настро|установ|подключ|обнов|почин|исправ|удал|снеси|delete\s+database|drop\s+database)/i.test(text)
});
}
describe("assistantLivingModePolicy", () => {
it("routes data-scope question to chat mode", () => {
const policy = buildPolicy();
const decision = policy.resolveLivingAssistantModeDecision({
userMessage: "по какой компании мы можем работать?",
addressLaneTriggered: false,
useMock: false,
predecomposeMode: "unsupported",
predecomposeModeConfidence: "low"
});
expect(decision.mode).toBe("chat");
expect(decision.reason).toBe("assistant_data_scope_query_detected");
});
it("keeps explicit accounting question in deep mode", () => {
const policy = buildPolicy();
const decision = policy.resolveLivingAssistantModeDecision({
userMessage: "покажи документы по сверке за 2020",
addressLaneTriggered: false,
useMock: false,
predecomposeMode: "unsupported",
predecomposeModeConfidence: "low"
});
expect(decision.mode).toBe("deep_analysis");
expect(decision.reason).toBe("strong_data_signal_detected");
});
it("detects organization fact follow-up after prior boundary reply", () => {
const policy = buildPolicy();
const detected = policy.hasOrganizationFactFollowupSignal("давай", [
{
role: "assistant",
debug: {
living_chat_response_source: "deterministic_organization_fact_boundary",
living_chat_grounding_guard_reason: null
}
}
]);
expect(detected).toBe(true);
});
});
@@ -0,0 +1,147 @@
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");
});
});