АРЧ АП11 - Вынести provider runtime policy из оркестрации и закрыть Phase 6 агентным прогоном
This commit is contained in:
@@ -20,24 +20,25 @@ function buildPolicy() {
|
||||
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()),
|
||||
hasReferentialPointer: (text: string) => /(same thing|that one|this one)/i.test(text.toLowerCase()),
|
||||
hasSmallTalkSignal: (text: string) => /(thanks|thank you|hello|hi)\b/i.test(text.toLowerCase()),
|
||||
hasAssistantCapabilityQuestionSignal: (text: string) =>
|
||||
/(?:кто ты|что ты можешь|какие фичи|полный список возможностей|чем ты можешь помочь|что ты умеешь)/i.test(text),
|
||||
/(?:what can you do|capabilities|features)/i.test(text),
|
||||
hasOperationalAdminActionRequestSignal: (text: string) =>
|
||||
/(?:настро|установ|подключ|обнов|почин|исправ|удал|снеси|delete\s+database|drop\s+database)/i.test(text)
|
||||
/(?:install|update|delete\s+database|drop\s+database)/i.test(text),
|
||||
resolveProviderExecutionState: (input: { useMock?: unknown }) => ({
|
||||
living_mode_forced_deep: Boolean(input?.useMock),
|
||||
living_mode_forced_reason: Boolean(input?.useMock) ? "mock_mode_keeps_deep_pipeline" : null
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
describe("assistantLivingModePolicy", () => {
|
||||
it("routes data-scope question to chat mode", () => {
|
||||
it("routes casual small-talk to chat mode", () => {
|
||||
const policy = buildPolicy();
|
||||
|
||||
const decision = policy.resolveLivingAssistantModeDecision({
|
||||
userMessage: "по какой компании мы можем работать?",
|
||||
userMessage: "hello",
|
||||
addressLaneTriggered: false,
|
||||
useMock: false,
|
||||
predecomposeMode: "unsupported",
|
||||
@@ -45,14 +46,14 @@ describe("assistantLivingModePolicy", () => {
|
||||
});
|
||||
|
||||
expect(decision.mode).toBe("chat");
|
||||
expect(decision.reason).toBe("assistant_data_scope_query_detected");
|
||||
expect(decision.reason).toBe("living_chat_signal_detected");
|
||||
});
|
||||
|
||||
it("keeps explicit accounting question in deep mode", () => {
|
||||
it("keeps explicit inventory question in deep mode", () => {
|
||||
const policy = buildPolicy();
|
||||
|
||||
const decision = policy.resolveLivingAssistantModeDecision({
|
||||
userMessage: "покажи документы по сверке за 2020",
|
||||
userMessage: "show warehouse inventory for 2020",
|
||||
addressLaneTriggered: false,
|
||||
useMock: false,
|
||||
predecomposeMode: "unsupported",
|
||||
@@ -63,19 +64,18 @@ describe("assistantLivingModePolicy", () => {
|
||||
expect(decision.reason).toBe("strong_data_signal_detected");
|
||||
});
|
||||
|
||||
it("detects organization fact follow-up after prior boundary reply", () => {
|
||||
it("keeps deep pipeline in mock mode via provider execution policy", () => {
|
||||
const policy = buildPolicy();
|
||||
|
||||
const detected = policy.hasOrganizationFactFollowupSignal("давай", [
|
||||
{
|
||||
role: "assistant",
|
||||
debug: {
|
||||
living_chat_response_source: "deterministic_organization_fact_boundary",
|
||||
living_chat_grounding_guard_reason: null
|
||||
}
|
||||
}
|
||||
]);
|
||||
const decision = policy.resolveLivingAssistantModeDecision({
|
||||
userMessage: "hello",
|
||||
addressLaneTriggered: false,
|
||||
useMock: true,
|
||||
predecomposeMode: "unsupported",
|
||||
predecomposeModeConfidence: "low"
|
||||
});
|
||||
|
||||
expect(detected).toBe(true);
|
||||
expect(decision.mode).toBe("deep_analysis");
|
||||
expect(decision.reason).toBe("mock_mode_keeps_deep_pipeline");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createAssistantProviderExecutionPolicy } from "../src/services/assistantProviderExecutionPolicy";
|
||||
|
||||
describe("assistantProviderExecutionPolicy", () => {
|
||||
const policy = createAssistantProviderExecutionPolicy();
|
||||
|
||||
it("normalizes provider values into explicit execution modes", () => {
|
||||
expect(policy.normalizeProvider("openai")).toBe("openai");
|
||||
expect(policy.normalizeProvider("local")).toBe("local");
|
||||
expect(policy.normalizeProvider("other")).toBeNull();
|
||||
});
|
||||
|
||||
it("detects llm runtime unavailability from auth and api-key failures", () => {
|
||||
expect(policy.detectLlmRuntimeUnavailable("error:OpenAI API key is missing")).toBe(true);
|
||||
expect(policy.detectLlmRuntimeUnavailable("authentication failed")).toBe(true);
|
||||
expect(policy.detectLlmRuntimeUnavailable("normalize_failed")).toBe(false);
|
||||
});
|
||||
|
||||
it("builds explicit mock execution contract for living-mode deep fallback", () => {
|
||||
const state = policy.resolveProviderExecutionState({
|
||||
llmProvider: "openai",
|
||||
useMock: true,
|
||||
baseUrl: "http://localhost:1234/v1"
|
||||
});
|
||||
|
||||
expect(state.provider_mode).toBe("mock");
|
||||
expect(state.normalized_provider).toBe("openai");
|
||||
expect(state.use_mock).toBe(true);
|
||||
expect(state.base_url_configured).toBe(true);
|
||||
expect(state.living_mode_forced_deep).toBe(true);
|
||||
expect(state.living_mode_forced_reason).toBe("mock_mode_keeps_deep_pipeline");
|
||||
});
|
||||
});
|
||||
@@ -45,9 +45,9 @@ function buildPolicy(overrides: Record<string, unknown> = {}) {
|
||||
input.repairedEffectiveAddressUserMessage
|
||||
].join(" ");
|
||||
return {
|
||||
dataScopeMetaQuery: /по какой компании|какая база|по каким конторам/i.test(samples),
|
||||
capabilityMetaQuery: /что ты можешь|что ты умеешь/i.test(samples),
|
||||
metaAnswerFollowupSignal: /это норм|что думаешь/i.test(samples)
|
||||
dataScopeMetaQuery: /по какой компании|какая база|по каким конторам/i.test(samples),
|
||||
capabilityMetaQuery: /что ты можешь|что ты умеешь/i.test(samples),
|
||||
metaAnswerFollowupSignal: /это норм|что думаешь/i.test(samples)
|
||||
};
|
||||
},
|
||||
resolveHardMetaMode: (input: {
|
||||
@@ -96,6 +96,17 @@ function buildPolicy(overrides: Record<string, unknown> = {}) {
|
||||
input.addressLaneTriggered
|
||||
? { mode: "address_data", reason: "address_lane_triggered" }
|
||||
: { mode: "chat", reason: "living_chat_signal_detected" },
|
||||
resolveProviderExecutionState: (input: { useMock?: unknown; llmPreDecomposeReason?: unknown }) => {
|
||||
const reason = String(input?.llmPreDecomposeReason ?? "");
|
||||
return {
|
||||
provider_mode: Boolean(input?.useMock) ? "mock" : "unknown",
|
||||
normalized_provider: null,
|
||||
use_mock: Boolean(input?.useMock),
|
||||
llm_runtime_unavailable_detected: /missing api key|authentication|api key is missing/i.test(reason),
|
||||
living_mode_forced_deep: Boolean(input?.useMock),
|
||||
living_mode_forced_reason: Boolean(input?.useMock) ? "mock_mode_keeps_deep_pipeline" : null
|
||||
};
|
||||
},
|
||||
...overrides
|
||||
});
|
||||
}
|
||||
@@ -105,8 +116,8 @@ describe("assistantRoutePolicy", () => {
|
||||
const policy = buildPolicy();
|
||||
|
||||
const decision = policy.resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "по какой компании мы можем работать?",
|
||||
effectiveAddressUserMessage: "по какой компании мы можем работать?",
|
||||
rawUserMessage: "по какой компании мы можем работать?",
|
||||
effectiveAddressUserMessage: "по какой компании мы можем работать?",
|
||||
followupContext: null,
|
||||
llmPreDecomposeMeta: null,
|
||||
useMock: false
|
||||
@@ -116,6 +127,7 @@ describe("assistantRoutePolicy", () => {
|
||||
expect(decision.toolGateReason).toBe("assistant_data_scope_query_detected");
|
||||
expect(decision.livingMode).toBe("chat");
|
||||
expect(decision.orchestrationContract?.hard_meta_mode).toBe("data_scope");
|
||||
expect(decision.orchestrationContract?.provider_execution?.provider_mode).toBe("unknown");
|
||||
});
|
||||
|
||||
it("keeps supported address intent in address lane", () => {
|
||||
@@ -130,8 +142,8 @@ describe("assistantRoutePolicy", () => {
|
||||
});
|
||||
|
||||
const decision = policy.resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "какие товары сейчас лежат на складе",
|
||||
effectiveAddressUserMessage: "какие товары сейчас лежат на складе",
|
||||
rawUserMessage: "какие товары сейчас лежат на складе",
|
||||
effectiveAddressUserMessage: "какие товары сейчас лежат на складе",
|
||||
followupContext: null,
|
||||
llmPreDecomposeMeta: null,
|
||||
useMock: false
|
||||
@@ -153,8 +165,8 @@ describe("assistantRoutePolicy", () => {
|
||||
});
|
||||
|
||||
const decision = policy.resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "а ты помнишь что мы обсуждали?",
|
||||
effectiveAddressUserMessage: "а ты помнишь что мы обсуждали?",
|
||||
rawUserMessage: "а ты помнишь что мы обсуждали?",
|
||||
effectiveAddressUserMessage: "а ты помнишь что мы обсуждали?",
|
||||
followupContext: null,
|
||||
llmPreDecomposeMeta: {
|
||||
applied: false,
|
||||
@@ -174,4 +186,36 @@ describe("assistantRoutePolicy", () => {
|
||||
expect(decision.livingMode).toBe("chat");
|
||||
expect(decision.livingReason).toBe("memory_recap_followup_detected");
|
||||
});
|
||||
|
||||
it("does not force unsupported-intent fallback when predecompose runtime is unavailable", () => {
|
||||
const policy = buildPolicy({
|
||||
hasStrongDataIntentSignal: () => true,
|
||||
hasDataRetrievalRequestSignal: () => true,
|
||||
resolveAddressToolGateDecision: () => ({
|
||||
runAddressLane: true,
|
||||
decision: "run_address_lane",
|
||||
reason: "address_mode_classifier_detected"
|
||||
})
|
||||
});
|
||||
|
||||
const decision = policy.resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "покажи документы по сверке",
|
||||
effectiveAddressUserMessage: "покажи документы по сверке",
|
||||
followupContext: { root_context_only: true },
|
||||
llmPreDecomposeMeta: {
|
||||
reason: "error:OpenAI API key is missing",
|
||||
predecomposeContract: {
|
||||
mode: "unsupported",
|
||||
mode_confidence: "low",
|
||||
intent: "unknown",
|
||||
intent_confidence: "low"
|
||||
}
|
||||
},
|
||||
useMock: false
|
||||
});
|
||||
|
||||
expect(decision.toolGateReason).toBe("address_mode_classifier_detected");
|
||||
expect(decision.orchestrationContract?.unsupported_address_intent_fallback_to_deep).toBe(false);
|
||||
expect(decision.orchestrationContract?.provider_execution?.llm_runtime_unavailable_detected).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user