254 lines
8.4 KiB
TypeScript
254 lines
8.4 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { buildAssistantDeepTurnNormalizationRuntime } from "../src/services/assistantDeepTurnNormalizationRuntimeAdapter";
|
|
|
|
describe("assistant deep turn normalization runtime adapter", () => {
|
|
it("uses followup state binding when feature flags are enabled and state exists", async () => {
|
|
const followupBinding = {
|
|
normalizedQuestion: "normalized question",
|
|
mergedContext: {
|
|
period_hint: "2020-07",
|
|
business_context: "ctx-from-followup"
|
|
},
|
|
usage: {
|
|
applied: true
|
|
}
|
|
};
|
|
const buildFollowupStateBinding = vi.fn(() => followupBinding);
|
|
const normalize = vi.fn(async (request) => ({
|
|
trace_id: "trace-1",
|
|
ok: true,
|
|
normalized: { schema_version: "normalized_query_v2_0_2" } as any,
|
|
route_hint_summary: null,
|
|
raw_model_output: {},
|
|
validation: { passed: true, errors: [] },
|
|
usage: { input_tokens: 10, output_tokens: 20, total_tokens: 30 },
|
|
latency_ms: 7,
|
|
prompt_version: String(request.promptVersion ?? ""),
|
|
schema_version: "normalized_query_v2_0_2",
|
|
request_count_for_case: 1
|
|
}));
|
|
|
|
const runtime = await buildAssistantDeepTurnNormalizationRuntime({
|
|
userMessage: "raw question",
|
|
payload: {
|
|
llmProvider: "openai",
|
|
apiKey: "k",
|
|
model: "m",
|
|
baseUrl: "https://api.example.com",
|
|
temperature: 0.2,
|
|
maxOutputTokens: 333,
|
|
promptVersion: "normalizer_v2_0_2",
|
|
systemPrompt: "sys",
|
|
developerPrompt: "dev",
|
|
domainPrompt: "dom",
|
|
fewShotExamples: "few",
|
|
context: {
|
|
period_hint: "2020-06"
|
|
},
|
|
useMock: true
|
|
},
|
|
featureInvestigationStateV1: true,
|
|
featureStateFollowupBindingV1: true,
|
|
sessionInvestigationState: {
|
|
active_domain: "settlements_60_62"
|
|
},
|
|
buildFollowupStateBinding,
|
|
normalize
|
|
});
|
|
|
|
expect(buildFollowupStateBinding).toHaveBeenCalledTimes(1);
|
|
expect(normalize).toHaveBeenCalledTimes(1);
|
|
expect(normalize).toHaveBeenCalledWith({
|
|
llmProvider: "openai",
|
|
apiKey: "k",
|
|
model: "m",
|
|
baseUrl: "https://api.example.com",
|
|
temperature: 0.2,
|
|
maxOutputTokens: 333,
|
|
promptVersion: "normalizer_v2_0_2",
|
|
schemaVersion: "v2_0_2",
|
|
systemPrompt: "sys",
|
|
developerPrompt: "dev",
|
|
domainPrompt: "dom",
|
|
fewShotExamples: "few",
|
|
userQuestion: "normalized question",
|
|
context: {
|
|
period_hint: "2020-07",
|
|
business_context: "ctx-from-followup"
|
|
},
|
|
useMock: true
|
|
});
|
|
expect(runtime.followupBinding).toBe(followupBinding);
|
|
expect(runtime.normalizePayload.userQuestion).toBe("normalized question");
|
|
});
|
|
|
|
it("falls back to raw user message when followup binding is disabled", async () => {
|
|
const buildFollowupStateBinding = vi.fn();
|
|
const normalize = vi.fn(async () => ({
|
|
trace_id: "trace-2",
|
|
ok: true,
|
|
normalized: { schema_version: "normalized_query_v2_0_2" } as any,
|
|
route_hint_summary: null,
|
|
raw_model_output: {},
|
|
validation: { passed: true, errors: [] },
|
|
usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 },
|
|
latency_ms: 1,
|
|
prompt_version: "address_query_runtime_v1",
|
|
schema_version: "normalized_query_v2_0_2",
|
|
request_count_for_case: 1
|
|
}));
|
|
|
|
const runtime = await buildAssistantDeepTurnNormalizationRuntime({
|
|
userMessage: "raw fallback",
|
|
payload: {
|
|
llmProvider: "openai",
|
|
context: {
|
|
business_context: "payload-context"
|
|
},
|
|
useMock: undefined
|
|
},
|
|
featureInvestigationStateV1: false,
|
|
featureStateFollowupBindingV1: true,
|
|
sessionInvestigationState: {
|
|
some: "state"
|
|
},
|
|
buildFollowupStateBinding,
|
|
normalize
|
|
});
|
|
|
|
expect(buildFollowupStateBinding).not.toHaveBeenCalled();
|
|
expect(normalize).toHaveBeenCalledWith({
|
|
llmProvider: "openai",
|
|
apiKey: undefined,
|
|
model: undefined,
|
|
baseUrl: undefined,
|
|
temperature: undefined,
|
|
maxOutputTokens: undefined,
|
|
promptVersion: "normalizer_v2_0_2",
|
|
schemaVersion: "v2_0_2",
|
|
systemPrompt: undefined,
|
|
developerPrompt: undefined,
|
|
domainPrompt: undefined,
|
|
fewShotExamples: undefined,
|
|
userQuestion: "raw fallback",
|
|
context: {
|
|
business_context: "payload-context"
|
|
},
|
|
useMock: false
|
|
});
|
|
expect(runtime.followupBinding).toEqual({
|
|
normalizedQuestion: "raw fallback",
|
|
mergedContext: {
|
|
business_context: "payload-context"
|
|
},
|
|
usage: null
|
|
});
|
|
});
|
|
|
|
it("synthesizes route summary from normalized payload when normalize omits it", async () => {
|
|
const normalize = vi.fn(async () => ({
|
|
trace_id: "trace-3",
|
|
ok: true,
|
|
normalized: {
|
|
schema_version: "normalized_query_v2_0_2",
|
|
user_message_raw: "Разложи хвосты по поставщикам",
|
|
message_in_scope: true,
|
|
scope_confidence: "high",
|
|
contains_multiple_tasks: false,
|
|
discarded_fragments: [],
|
|
global_notes: {
|
|
needs_clarification: false,
|
|
clarification_reason: null
|
|
},
|
|
fragments: [
|
|
{
|
|
fragment_id: "F1",
|
|
raw_fragment_text: "Разложи хвосты по поставщикам: где разрыв между оплатой и документами выглядит системным.",
|
|
normalized_fragment_text:
|
|
"Разложи хвосты по поставщикам: где разрыв между оплатой и документами выглядит системным.",
|
|
domain_relevance: "in_scope",
|
|
business_scope: "company_specific_accounting",
|
|
entity_hints: ["контрагент", "документ"],
|
|
account_hints: ["60"],
|
|
document_hints: ["документ"],
|
|
register_hints: [],
|
|
time_scope: {
|
|
type: "missing",
|
|
value: null,
|
|
confidence: "low"
|
|
},
|
|
flags: {
|
|
has_multi_entity_scope: true,
|
|
asks_for_chain_explanation: true,
|
|
asks_for_ranking_or_top: false,
|
|
asks_for_period_summary: false,
|
|
asks_for_rule_check: false,
|
|
asks_for_anomaly_scan: true,
|
|
asks_for_exact_object_trace: false,
|
|
asks_for_evidence: true,
|
|
mentions_period_close_context: false
|
|
},
|
|
semantic_hints: {
|
|
scope_target_kind: "none",
|
|
scope_target_text: null,
|
|
date_scope_kind: "missing",
|
|
self_scope_detected: false,
|
|
selected_object_scope_detected: false
|
|
},
|
|
candidate_labels: ["cross_entity", "anomaly_probe"],
|
|
confidence: "medium",
|
|
execution_readiness: "executable_with_soft_assumptions",
|
|
clarification_reason: null,
|
|
soft_assumption_used: ["problem_scan_mode_enabled"],
|
|
route_status: "routed",
|
|
no_route_reason: null
|
|
}
|
|
]
|
|
},
|
|
route_hint_summary: null,
|
|
raw_model_output: {},
|
|
validation: { passed: true, errors: [] },
|
|
usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 },
|
|
latency_ms: 1,
|
|
prompt_version: "normalizer_v2_0_2",
|
|
schema_version: "normalized_query_v2_0_2",
|
|
request_count_for_case: 1
|
|
}));
|
|
|
|
const runtime = await buildAssistantDeepTurnNormalizationRuntime({
|
|
userMessage: "Разложи хвосты по поставщикам",
|
|
payload: {
|
|
llmProvider: "openai",
|
|
promptVersion: "address_query_runtime_v1",
|
|
useMock: true
|
|
},
|
|
featureInvestigationStateV1: false,
|
|
featureStateFollowupBindingV1: false,
|
|
sessionInvestigationState: null,
|
|
buildFollowupStateBinding: vi.fn(),
|
|
normalize
|
|
});
|
|
|
|
expect(runtime.normalized.route_hint_summary).toEqual(
|
|
expect.objectContaining({
|
|
mode: "deterministic_v2",
|
|
fallback: expect.objectContaining({
|
|
type: "none"
|
|
}),
|
|
decisions: [
|
|
expect.objectContaining({
|
|
fragment_id: "F1",
|
|
route: "hybrid_store_plus_live"
|
|
})
|
|
]
|
|
})
|
|
);
|
|
expect(normalize).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
promptVersion: "normalizer_v2_0_2",
|
|
schemaVersion: "v2_0_2"
|
|
})
|
|
);
|
|
});
|
|
});
|