diff --git a/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md b/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md index 716a01a..88e5a0d 100644 --- a/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md +++ b/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md @@ -245,6 +245,12 @@ Latest continuity-authority convergence evidence after the current route pass: - deterministic organization-fact boundary replies can now trigger from grounded continuity even when `sessionScope.selectedOrganization` and `sessionScope.activeOrganization` are both empty at runtime entry; - the chat layer now records whether it entered with grounded continuity and which organization came from that continuity snapshot, making future saved-session review less blind; - proactive organization offer logic is now explicitly blocked when grounded address continuity already exists, so the chat layer does not re-offer company selection on top of an already grounded business session; +- the next continuity-authority pass now centralizes `organization authority` in the shared continuity policy instead of letting route, living-chat, and data-scope reconstruct it independently: + - a single resolver now merges grounded address continuity, assistant-side living-chat scope memory, session scope, and clarification candidates into one authority object; + - route arbitration now consumes that shared authority for organization clarification continuation instead of rebuilding candidates and fallback selection locally; + - living-chat runtime now boots with the same authority object, so `knownOrganizations`, `selectedOrganization`, and `activeOrganization` no longer start from a different merge order than the route layer; + - data-scope active-organization recovery now also reads the same authority object, reducing one more history-scan seam that used to drift from route and chat; + - targeted tests now protect this precedence layer directly, including the case where assistant-side living-chat selection must outrank older grounded continuity without losing the grounded organization in the merged known set; - the first human-answer-shaping cleanup pass is now applied to heavy profile/aggregate exact answers: - `period_coverage_profile` and `document_type_and_account_section_profile` now start with a direct business-first lead (`Коротко: ...`) instead of service-flavored intros like `профиль собран` / `строк агрегата`; - the top block now states the business conclusion first and leaves ranked detail blocks below, which reduces the catalog-like feel without hiding the actual data; diff --git a/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js b/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js index 365e332..9316e63 100644 --- a/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js +++ b/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js @@ -8,6 +8,8 @@ exports.readAddressDebugScopedDate = readAddressDebugScopedDate; exports.buildInventoryRootFrameFromAddressDebug = buildInventoryRootFrameFromAddressDebug; exports.isGroundedAddressDebug = isGroundedAddressDebug; exports.resolveAssistantContinuitySnapshot = resolveAssistantContinuitySnapshot; +exports.resolveAssistantOrganizationAuthority = resolveAssistantOrganizationAuthority; +const assistantOrganizationMatcher_1 = require("./assistantOrganizationMatcher"); function fallbackToNonEmptyString(value) { if (value === null || value === undefined) { return null; @@ -21,6 +23,12 @@ function toRecordObject(value) { } return value; } +function mergeKnownOrganizationsDefault(values) { + return (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)(values); +} +function normalizeOrganizationScopeDefault(value) { + return (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(value); +} function formatIsoDateForReply(value) { const source = String(value ?? "").trim(); const match = source.match(/^(\d{4})-(\d{2})-(\d{2})$/); @@ -166,3 +174,87 @@ function resolveAssistantContinuitySnapshot(input) { hasGroundedInventoryContext: Boolean(lastGroundedInventoryAddressDebug) }; } +function collectAssistantOrganizationSignals(sessionItems, normalizeOrganizationScopeValue) { + let lastAssistantSelectedOrganization = null; + let lastAssistantActiveOrganization = null; + const knownOrganizationsFromAssistantHistory = []; + for (let index = sessionItems.length - 1; index >= 0; index -= 1) { + const item = sessionItems[index]; + if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") { + continue; + } + const debug = item.debug; + if (!lastAssistantActiveOrganization) { + lastAssistantActiveOrganization = normalizeOrganizationScopeValue(debug.assistant_active_organization); + } + if (!lastAssistantSelectedOrganization) { + lastAssistantSelectedOrganization = normalizeOrganizationScopeValue(debug.living_chat_selected_organization); + } + const directKnownOrganizations = Array.isArray(debug.assistant_known_organizations) + ? debug.assistant_known_organizations + : []; + const probeOrganizations = Array.isArray(debug.living_chat_data_scope_probe_organizations) + ? debug.living_chat_data_scope_probe_organizations + : []; + const organizationCandidates = Array.isArray(debug.organization_candidates) ? debug.organization_candidates : []; + const directResolvedOrganizations = [ + debug.assistant_active_organization, + debug.living_chat_selected_organization, + readAddressDebugOrganization(debug, normalizeOrganizationScopeValue) + ]; + knownOrganizationsFromAssistantHistory.push(...directKnownOrganizations, ...probeOrganizations, ...organizationCandidates, ...directResolvedOrganizations); + } + return { + lastAssistantSelectedOrganization, + lastAssistantActiveOrganization, + knownOrganizationsFromAssistantHistory: mergeKnownOrganizationsDefault(knownOrganizationsFromAssistantHistory) + }; +} +function resolveAssistantOrganizationAuthority(input) { + const normalizeOrganizationScopeValue = input.normalizeOrganizationScopeValue ?? normalizeOrganizationScopeDefault; + const mergeKnownOrganizations = input.mergeKnownOrganizations ?? mergeKnownOrganizationsDefault; + const sessionItems = Array.isArray(input.sessionItems) ? input.sessionItems : []; + const continuitySnapshot = resolveAssistantContinuitySnapshot({ + sessionItems, + toNonEmptyString: input.toNonEmptyString ?? normalizeOrganizationScopeValue + }); + const assistantSignals = collectAssistantOrganizationSignals(sessionItems, normalizeOrganizationScopeValue); + const continuityActiveOrganization = normalizeOrganizationScopeValue(input.sessionActiveOrganization) ?? + assistantSignals.lastAssistantActiveOrganization ?? + normalizeOrganizationScopeValue(continuitySnapshot.activeOrganization); + const knownOrganizations = mergeKnownOrganizations([ + ...(Array.isArray(input.sessionKnownOrganizations) ? input.sessionKnownOrganizations : []), + ...assistantSignals.knownOrganizationsFromAssistantHistory, + input.sessionSelectedOrganization, + input.sessionActiveOrganization, + continuityActiveOrganization + ]); + const selectedOrganization = normalizeOrganizationScopeValue(input.sessionSelectedOrganization) ?? + assistantSignals.lastAssistantSelectedOrganization; + const activeOrganization = normalizeOrganizationScopeValue(input.sessionActiveOrganization) ?? + selectedOrganization ?? + assistantSignals.lastAssistantActiveOrganization ?? + continuityActiveOrganization ?? + (knownOrganizations.length === 1 ? knownOrganizations[0] : null); + const organizationClarificationCandidates = Array.isArray(input.lastOrganizationClarificationDebug?.organization_candidates) + ? mergeKnownOrganizations([ + ...input.lastOrganizationClarificationDebug.organization_candidates, + ...knownOrganizations, + selectedOrganization, + activeOrganization, + continuityActiveOrganization + ]) + : []; + const organizationClarificationSelectionFromScope = selectedOrganization ?? activeOrganization; + return { + continuitySnapshot, + continuityActiveOrganization, + lastAssistantSelectedOrganization: assistantSignals.lastAssistantSelectedOrganization, + lastAssistantActiveOrganization: assistantSignals.lastAssistantActiveOrganization, + knownOrganizations, + selectedOrganization, + activeOrganization, + organizationClarificationCandidates, + organizationClarificationSelectionFromScope + }; +} diff --git a/llm_normalizer/backend/dist/services/assistantDataScopePolicy.js b/llm_normalizer/backend/dist/services/assistantDataScopePolicy.js index 1820c81..d08e4e7 100644 --- a/llm_normalizer/backend/dist/services/assistantDataScopePolicy.js +++ b/llm_normalizer/backend/dist/services/assistantDataScopePolicy.js @@ -219,36 +219,13 @@ function createAssistantDataScopePolicy(deps) { return (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)(collected, 20); } function findLastAssistantActiveOrganization(items) { - const continuitySnapshot = (0, assistantContinuityPolicy_1.resolveAssistantContinuitySnapshot)({ + const authority = (0, assistantContinuityPolicy_1.resolveAssistantOrganizationAuthority)({ sessionItems: items, - toNonEmptyString: assistantOrganizationMatcher_1.normalizeOrganizationScopeValue + toNonEmptyString: assistantOrganizationMatcher_1.normalizeOrganizationScopeValue, + normalizeOrganizationScopeValue: assistantOrganizationMatcher_1.normalizeOrganizationScopeValue, + mergeKnownOrganizations: assistantOrganizationMatcher_1.mergeKnownOrganizations }); - const continuityOrganization = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(continuitySnapshot.activeOrganization); - for (let index = (Array.isArray(items) ? items.length : 0) - 1; index >= 0; index -= 1) { - const item = Array.isArray(items) ? items[index] : null; - if (!item || typeof item !== "object" || item.role !== "assistant") { - continue; - } - const debug = item.debug; - if (!debug || typeof debug !== "object") { - continue; - } - const direct = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(debug.assistant_active_organization); - if (direct) { - return direct; - } - const selected = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(debug.living_chat_selected_organization); - if (selected) { - return selected; - } - if ((0, assistantContinuityPolicy_1.isGroundedAddressDebug)(debug)) { - const groundedOrganization = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)((0, assistantContinuityPolicy_1.readAddressDebugOrganization)(debug)); - if (groundedOrganization) { - return groundedOrganization; - } - } - } - return continuityOrganization; + return (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(authority.activeOrganization); } function extractOrganizationFactsFromRows(rows) { const names = []; diff --git a/llm_normalizer/backend/dist/services/assistantLivingChatRuntimeAdapter.js b/llm_normalizer/backend/dist/services/assistantLivingChatRuntimeAdapter.js index 0324270..781d45d 100644 --- a/llm_normalizer/backend/dist/services/assistantLivingChatRuntimeAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantLivingChatRuntimeAdapter.js @@ -152,10 +152,18 @@ function buildAddressMemoryRecapReply(input) { } async function runAssistantLivingChatRuntime(input) { const userMessage = String(input.userMessage ?? ""); - const continuitySnapshot = (0, assistantContinuityPolicy_1.resolveAssistantContinuitySnapshot)({ + const organizationAuthority = (0, assistantContinuityPolicy_1.resolveAssistantOrganizationAuthority)({ sessionItems: input.sessionItems, - toNonEmptyString: input.toNonEmptyString + sessionKnownOrganizations: Array.isArray(input.sessionScope.knownOrganizations) + ? input.sessionScope.knownOrganizations + : [], + sessionSelectedOrganization: input.sessionScope.selectedOrganization, + sessionActiveOrganization: input.sessionScope.activeOrganization, + toNonEmptyString: input.toNonEmptyString, + normalizeOrganizationScopeValue: input.toNonEmptyString, + mergeKnownOrganizations: input.mergeKnownOrganizations }); + const continuitySnapshot = organizationAuthority.continuitySnapshot; const dataScopeMetaQuery = input.hasAssistantDataScopeMetaQuestionSignal(userMessage); const capabilityMetaQuery = input.shouldHandleAsAssistantCapabilityMetaQuery(userMessage); const destructiveSignal = input.hasDestructiveDataActionSignal(userMessage); @@ -169,13 +177,10 @@ async function runAssistantLivingChatRuntime(input) { let livingChatGroundingGuardApplied = false; let livingChatGroundingGuardReason = null; let livingChatProactiveScopeOfferApplied = false; - const continuityActiveOrganization = input.toNonEmptyString(continuitySnapshot.activeOrganization); - let knownOrganizations = input.mergeKnownOrganizations([ - ...(Array.isArray(input.sessionScope.knownOrganizations) ? input.sessionScope.knownOrganizations : []), - continuityActiveOrganization - ]); - let selectedOrganization = input.toNonEmptyString(input.sessionScope.selectedOrganization); - let activeOrganization = input.toNonEmptyString(input.sessionScope.activeOrganization) ?? continuityActiveOrganization; + const continuityActiveOrganization = organizationAuthority.continuityActiveOrganization; + let knownOrganizations = [...organizationAuthority.knownOrganizations]; + let selectedOrganization = organizationAuthority.selectedOrganization; + let activeOrganization = organizationAuthority.activeOrganization; const memoryRecapContext = (0, assistantMemoryRecapPolicy_1.resolveAssistantLivingChatMemoryContext)({ modeDecisionReason: input.modeDecision?.reason ?? null, sessionItems: input.sessionItems diff --git a/llm_normalizer/backend/dist/services/assistantRoutePolicy.js b/llm_normalizer/backend/dist/services/assistantRoutePolicy.js index 0afc1a3..1712c8d 100644 --- a/llm_normalizer/backend/dist/services/assistantRoutePolicy.js +++ b/llm_normalizer/backend/dist/services/assistantRoutePolicy.js @@ -72,26 +72,25 @@ function createAssistantRoutePolicy(deps) { const sessionOrganizationScope = input?.sessionOrganizationScope && typeof input.sessionOrganizationScope === "object" ? input.sessionOrganizationScope : null; - const continuitySnapshot = (0, assistantContinuityPolicy_1.resolveAssistantContinuitySnapshot)({ + const organizationAuthority = (0, assistantContinuityPolicy_1.resolveAssistantOrganizationAuthority)({ sessionItems, - toNonEmptyString + sessionKnownOrganizations: Array.isArray(sessionOrganizationScope?.knownOrganizations) + ? sessionOrganizationScope.knownOrganizations + : [], + sessionSelectedOrganization: sessionOrganizationScope?.selectedOrganization, + sessionActiveOrganization: sessionOrganizationScope?.activeOrganization, + lastOrganizationClarificationDebug: findLastOrganizationClarificationAddressDebug(sessionItems), + toNonEmptyString, + normalizeOrganizationScopeValue, + mergeKnownOrganizations }); - const continuityActiveOrganization = normalizeOrganizationScopeValue(sessionOrganizationScope?.activeOrganization) ?? - normalizeOrganizationScopeValue(continuitySnapshot.activeOrganization); + const continuitySnapshot = organizationAuthority.continuitySnapshot; + const continuityActiveOrganization = organizationAuthority.continuityActiveOrganization; const lastGroundedAddressDebug = findLastGroundedAddressAnswerDebug(sessionItems) ?? continuitySnapshot.lastGroundedAddressDebug; const lastOrganizationClarificationDebug = findLastOrganizationClarificationAddressDebug(sessionItems); - const organizationClarificationCandidates = Array.isArray(lastOrganizationClarificationDebug?.organization_candidates) - ? mergeKnownOrganizations([ - ...lastOrganizationClarificationDebug.organization_candidates, - ...((Array.isArray(sessionOrganizationScope?.knownOrganizations) - ? sessionOrganizationScope.knownOrganizations - : [])), - continuityActiveOrganization - ]) - : []; - const organizationClarificationSelectionFromScope = normalizeOrganizationScopeValue(sessionOrganizationScope?.selectedOrganization) ?? - continuityActiveOrganization; + const organizationClarificationCandidates = organizationAuthority.organizationClarificationCandidates; + const organizationClarificationSelectionFromScope = organizationAuthority.organizationClarificationSelectionFromScope; const organizationClarificationSelection = resolveOrganizationSelectionFromMessage(rawUserMessage, organizationClarificationCandidates) ?? resolveOrganizationSelectionFromMessage(repairedRawUserMessage, organizationClarificationCandidates) ?? resolveOrganizationSelectionFromMessage(effectiveAddressUserMessage, organizationClarificationCandidates) ?? diff --git a/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts b/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts index f99474f..b5189a5 100644 --- a/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts +++ b/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts @@ -1,3 +1,8 @@ +import { + mergeKnownOrganizations as mergeKnownOrganizationsFromMatcher, + normalizeOrganizationScopeValue as normalizeOrganizationScopeValueFromMatcher +} from "./assistantOrganizationMatcher"; + export interface AssistantContinuitySnapshotInput { sessionItems?: unknown[]; toNonEmptyString?: (value: unknown) => string | null; @@ -22,6 +27,29 @@ export interface AssistantContinuitySnapshot { hasGroundedInventoryContext: boolean; } +export interface AssistantOrganizationAuthorityInput { + sessionItems?: unknown[]; + sessionKnownOrganizations?: unknown[]; + sessionSelectedOrganization?: unknown; + sessionActiveOrganization?: unknown; + lastOrganizationClarificationDebug?: Record | null; + toNonEmptyString?: (value: unknown) => string | null; + normalizeOrganizationScopeValue?: (value: unknown) => string | null; + mergeKnownOrganizations?: (values: unknown[]) => string[]; +} + +export interface AssistantOrganizationAuthority { + continuitySnapshot: AssistantContinuitySnapshot; + continuityActiveOrganization: string | null; + lastAssistantSelectedOrganization: string | null; + lastAssistantActiveOrganization: string | null; + knownOrganizations: string[]; + selectedOrganization: string | null; + activeOrganization: string | null; + organizationClarificationCandidates: string[]; + organizationClarificationSelectionFromScope: string | null; +} + function fallbackToNonEmptyString(value: unknown): string | null { if (value === null || value === undefined) { return null; @@ -37,6 +65,14 @@ function toRecordObject(value: unknown): Record | null { return value as Record; } +function mergeKnownOrganizationsDefault(values: unknown[]): string[] { + return mergeKnownOrganizationsFromMatcher(values); +} + +function normalizeOrganizationScopeDefault(value: unknown): string | null { + return normalizeOrganizationScopeValueFromMatcher(value); +} + export function formatIsoDateForReply(value: unknown): string | null { const source = String(value ?? "").trim(); const match = source.match(/^(\d{4})-(\d{2})-(\d{2})$/); @@ -228,3 +264,113 @@ export function resolveAssistantContinuitySnapshot( hasGroundedInventoryContext: Boolean(lastGroundedInventoryAddressDebug) }; } + +function collectAssistantOrganizationSignals( + sessionItems: unknown[], + normalizeOrganizationScopeValue: (value: unknown) => string | null +): { + lastAssistantSelectedOrganization: string | null; + lastAssistantActiveOrganization: string | null; + knownOrganizationsFromAssistantHistory: string[]; +} { + let lastAssistantSelectedOrganization: string | null = null; + let lastAssistantActiveOrganization: string | null = null; + const knownOrganizationsFromAssistantHistory: unknown[] = []; + + for (let index = sessionItems.length - 1; index >= 0; index -= 1) { + const item = sessionItems[index] as { role?: string; debug?: Record } | null; + if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") { + continue; + } + + const debug = item.debug; + if (!lastAssistantActiveOrganization) { + lastAssistantActiveOrganization = normalizeOrganizationScopeValue(debug.assistant_active_organization); + } + if (!lastAssistantSelectedOrganization) { + lastAssistantSelectedOrganization = normalizeOrganizationScopeValue(debug.living_chat_selected_organization); + } + + const directKnownOrganizations = Array.isArray(debug.assistant_known_organizations) + ? debug.assistant_known_organizations + : []; + const probeOrganizations = Array.isArray(debug.living_chat_data_scope_probe_organizations) + ? debug.living_chat_data_scope_probe_organizations + : []; + const organizationCandidates = Array.isArray(debug.organization_candidates) ? debug.organization_candidates : []; + const directResolvedOrganizations = [ + debug.assistant_active_organization, + debug.living_chat_selected_organization, + readAddressDebugOrganization(debug, normalizeOrganizationScopeValue) + ]; + + knownOrganizationsFromAssistantHistory.push( + ...directKnownOrganizations, + ...probeOrganizations, + ...organizationCandidates, + ...directResolvedOrganizations + ); + } + + return { + lastAssistantSelectedOrganization, + lastAssistantActiveOrganization, + knownOrganizationsFromAssistantHistory: mergeKnownOrganizationsDefault(knownOrganizationsFromAssistantHistory) + }; +} + +export function resolveAssistantOrganizationAuthority( + input: AssistantOrganizationAuthorityInput +): AssistantOrganizationAuthority { + const normalizeOrganizationScopeValue = + input.normalizeOrganizationScopeValue ?? normalizeOrganizationScopeDefault; + const mergeKnownOrganizations = input.mergeKnownOrganizations ?? mergeKnownOrganizationsDefault; + const sessionItems = Array.isArray(input.sessionItems) ? input.sessionItems : []; + const continuitySnapshot = resolveAssistantContinuitySnapshot({ + sessionItems, + toNonEmptyString: input.toNonEmptyString ?? normalizeOrganizationScopeValue + }); + const assistantSignals = collectAssistantOrganizationSignals(sessionItems, normalizeOrganizationScopeValue); + const continuityActiveOrganization = + normalizeOrganizationScopeValue(input.sessionActiveOrganization) ?? + assistantSignals.lastAssistantActiveOrganization ?? + normalizeOrganizationScopeValue(continuitySnapshot.activeOrganization); + const knownOrganizations = mergeKnownOrganizations([ + ...(Array.isArray(input.sessionKnownOrganizations) ? input.sessionKnownOrganizations : []), + ...assistantSignals.knownOrganizationsFromAssistantHistory, + input.sessionSelectedOrganization, + input.sessionActiveOrganization, + continuityActiveOrganization + ]); + const selectedOrganization = + normalizeOrganizationScopeValue(input.sessionSelectedOrganization) ?? + assistantSignals.lastAssistantSelectedOrganization; + const activeOrganization = + normalizeOrganizationScopeValue(input.sessionActiveOrganization) ?? + selectedOrganization ?? + assistantSignals.lastAssistantActiveOrganization ?? + continuityActiveOrganization ?? + (knownOrganizations.length === 1 ? knownOrganizations[0] : null); + const organizationClarificationCandidates = Array.isArray(input.lastOrganizationClarificationDebug?.organization_candidates) + ? mergeKnownOrganizations([ + ...input.lastOrganizationClarificationDebug.organization_candidates, + ...knownOrganizations, + selectedOrganization, + activeOrganization, + continuityActiveOrganization + ]) + : []; + const organizationClarificationSelectionFromScope = selectedOrganization ?? activeOrganization; + + return { + continuitySnapshot, + continuityActiveOrganization, + lastAssistantSelectedOrganization: assistantSignals.lastAssistantSelectedOrganization, + lastAssistantActiveOrganization: assistantSignals.lastAssistantActiveOrganization, + knownOrganizations, + selectedOrganization, + activeOrganization, + organizationClarificationCandidates, + organizationClarificationSelectionFromScope + }; +} diff --git a/llm_normalizer/backend/src/services/assistantDataScopePolicy.ts b/llm_normalizer/backend/src/services/assistantDataScopePolicy.ts index 37970bd..ac1bd39 100644 --- a/llm_normalizer/backend/src/services/assistantDataScopePolicy.ts +++ b/llm_normalizer/backend/src/services/assistantDataScopePolicy.ts @@ -4,9 +4,7 @@ import { normalizeOrganizationScopeValue } from "./assistantOrganizationMatcher"; import { - resolveAssistantContinuitySnapshot, - isGroundedAddressDebug, - readAddressDebugOrganization + resolveAssistantOrganizationAuthority } from "./assistantContinuityPolicy"; const DATA_SCOPE_CACHE_TTL_MS = 60_000; @@ -297,40 +295,13 @@ export function createAssistantDataScopePolicy(deps: AssistantDataScopePolicyDep } function findLastAssistantActiveOrganization(items: unknown[]): string | null { - const continuitySnapshot = resolveAssistantContinuitySnapshot({ + const authority = resolveAssistantOrganizationAuthority({ sessionItems: items, - toNonEmptyString: normalizeOrganizationScopeValue + toNonEmptyString: normalizeOrganizationScopeValue, + normalizeOrganizationScopeValue, + mergeKnownOrganizations }); - const continuityOrganization = normalizeOrganizationScopeValue(continuitySnapshot.activeOrganization); - - for (let index = (Array.isArray(items) ? items.length : 0) - 1; index >= 0; index -= 1) { - const item = Array.isArray(items) ? items[index] : null; - if (!item || typeof item !== "object" || (item as { role?: string }).role !== "assistant") { - continue; - } - - const debug = (item as { debug?: Record }).debug; - if (!debug || typeof debug !== "object") { - continue; - } - - const direct = normalizeOrganizationScopeValue(debug.assistant_active_organization); - if (direct) { - return direct; - } - const selected = normalizeOrganizationScopeValue(debug.living_chat_selected_organization); - if (selected) { - return selected; - } - if (isGroundedAddressDebug(debug)) { - const groundedOrganization = normalizeOrganizationScopeValue(readAddressDebugOrganization(debug)); - if (groundedOrganization) { - return groundedOrganization; - } - } - } - - return continuityOrganization; + return normalizeOrganizationScopeValue(authority.activeOrganization); } function extractOrganizationFactsFromRows(rows: unknown[]): AssistantOrganizationFacts { diff --git a/llm_normalizer/backend/src/services/assistantLivingChatRuntimeAdapter.ts b/llm_normalizer/backend/src/services/assistantLivingChatRuntimeAdapter.ts index 808d4de..e857fcc 100644 --- a/llm_normalizer/backend/src/services/assistantLivingChatRuntimeAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantLivingChatRuntimeAdapter.ts @@ -3,7 +3,7 @@ import { buildInventoryHistoryCapabilityFollowupReply as buildInventoryHistoryCapabilityFollowupReplyFromPolicy, resolveAssistantLivingChatMemoryContext } from "./assistantMemoryRecapPolicy"; -import { resolveAssistantContinuitySnapshot } from "./assistantContinuityPolicy"; +import { resolveAssistantOrganizationAuthority } from "./assistantContinuityPolicy"; export interface AssistantLivingChatSessionScopeInput { knownOrganizations?: unknown[]; @@ -249,10 +249,18 @@ export async function runAssistantLivingChatRuntime( input: AssistantLivingChatRuntimeInput ): Promise { const userMessage = String(input.userMessage ?? ""); - const continuitySnapshot = resolveAssistantContinuitySnapshot({ + const organizationAuthority = resolveAssistantOrganizationAuthority({ sessionItems: input.sessionItems, - toNonEmptyString: input.toNonEmptyString + sessionKnownOrganizations: Array.isArray(input.sessionScope.knownOrganizations) + ? input.sessionScope.knownOrganizations + : [], + sessionSelectedOrganization: input.sessionScope.selectedOrganization, + sessionActiveOrganization: input.sessionScope.activeOrganization, + toNonEmptyString: input.toNonEmptyString, + normalizeOrganizationScopeValue: input.toNonEmptyString, + mergeKnownOrganizations: input.mergeKnownOrganizations }); + const continuitySnapshot = organizationAuthority.continuitySnapshot; const dataScopeMetaQuery = input.hasAssistantDataScopeMetaQuestionSignal(userMessage); const capabilityMetaQuery = input.shouldHandleAsAssistantCapabilityMetaQuery(userMessage); const destructiveSignal = input.hasDestructiveDataActionSignal(userMessage); @@ -267,13 +275,10 @@ export async function runAssistantLivingChatRuntime( let livingChatGroundingGuardApplied = false; let livingChatGroundingGuardReason: string | null = null; let livingChatProactiveScopeOfferApplied = false; - const continuityActiveOrganization = input.toNonEmptyString(continuitySnapshot.activeOrganization); - let knownOrganizations = input.mergeKnownOrganizations([ - ...(Array.isArray(input.sessionScope.knownOrganizations) ? input.sessionScope.knownOrganizations : []), - continuityActiveOrganization - ]); - let selectedOrganization = input.toNonEmptyString(input.sessionScope.selectedOrganization); - let activeOrganization = input.toNonEmptyString(input.sessionScope.activeOrganization) ?? continuityActiveOrganization; + const continuityActiveOrganization = organizationAuthority.continuityActiveOrganization; + let knownOrganizations = [...organizationAuthority.knownOrganizations]; + let selectedOrganization = organizationAuthority.selectedOrganization; + let activeOrganization = organizationAuthority.activeOrganization; const memoryRecapContext = resolveAssistantLivingChatMemoryContext({ modeDecisionReason: input.modeDecision?.reason ?? null, sessionItems: input.sessionItems diff --git a/llm_normalizer/backend/src/services/assistantRoutePolicy.ts b/llm_normalizer/backend/src/services/assistantRoutePolicy.ts index ee08cd6..6154db3 100644 --- a/llm_normalizer/backend/src/services/assistantRoutePolicy.ts +++ b/llm_normalizer/backend/src/services/assistantRoutePolicy.ts @@ -1,5 +1,7 @@ // @ts-nocheck -import { resolveAssistantContinuitySnapshot } from "./assistantContinuityPolicy"; +import { + resolveAssistantOrganizationAuthority +} from "./assistantContinuityPolicy"; const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([ "period_coverage_profile", @@ -109,26 +111,25 @@ export function createAssistantRoutePolicy(deps) { const sessionOrganizationScope = input?.sessionOrganizationScope && typeof input.sessionOrganizationScope === "object" ? input.sessionOrganizationScope : null; - const continuitySnapshot = resolveAssistantContinuitySnapshot({ + const organizationAuthority = resolveAssistantOrganizationAuthority({ sessionItems, - toNonEmptyString + sessionKnownOrganizations: Array.isArray(sessionOrganizationScope?.knownOrganizations) + ? sessionOrganizationScope.knownOrganizations + : [], + sessionSelectedOrganization: sessionOrganizationScope?.selectedOrganization, + sessionActiveOrganization: sessionOrganizationScope?.activeOrganization, + lastOrganizationClarificationDebug: findLastOrganizationClarificationAddressDebug(sessionItems), + toNonEmptyString, + normalizeOrganizationScopeValue, + mergeKnownOrganizations }); - const continuityActiveOrganization = normalizeOrganizationScopeValue(sessionOrganizationScope?.activeOrganization) ?? - normalizeOrganizationScopeValue(continuitySnapshot.activeOrganization); + const continuitySnapshot = organizationAuthority.continuitySnapshot; + const continuityActiveOrganization = organizationAuthority.continuityActiveOrganization; const lastGroundedAddressDebug = findLastGroundedAddressAnswerDebug(sessionItems) ?? continuitySnapshot.lastGroundedAddressDebug; const lastOrganizationClarificationDebug = findLastOrganizationClarificationAddressDebug(sessionItems); - const organizationClarificationCandidates = Array.isArray(lastOrganizationClarificationDebug?.organization_candidates) - ? mergeKnownOrganizations([ - ...lastOrganizationClarificationDebug.organization_candidates, - ...((Array.isArray(sessionOrganizationScope?.knownOrganizations) - ? sessionOrganizationScope.knownOrganizations - : [])), - continuityActiveOrganization - ]) - : []; - const organizationClarificationSelectionFromScope = normalizeOrganizationScopeValue(sessionOrganizationScope?.selectedOrganization) ?? - continuityActiveOrganization; + const organizationClarificationCandidates = organizationAuthority.organizationClarificationCandidates; + const organizationClarificationSelectionFromScope = organizationAuthority.organizationClarificationSelectionFromScope; const organizationClarificationSelection = resolveOrganizationSelectionFromMessage(rawUserMessage, organizationClarificationCandidates) ?? resolveOrganizationSelectionFromMessage(repairedRawUserMessage, organizationClarificationCandidates) ?? resolveOrganizationSelectionFromMessage(effectiveAddressUserMessage, organizationClarificationCandidates) ?? diff --git a/llm_normalizer/backend/tests/assistantContinuityPolicy.test.ts b/llm_normalizer/backend/tests/assistantContinuityPolicy.test.ts new file mode 100644 index 0000000..58f41c6 --- /dev/null +++ b/llm_normalizer/backend/tests/assistantContinuityPolicy.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "vitest"; +import { resolveAssistantOrganizationAuthority } from "../src/services/assistantContinuityPolicy"; + +describe("assistantContinuityPolicy organization authority", () => { + it("prefers explicit assistant organization authority over older grounded continuity and merges known organizations once", () => { + const authority = resolveAssistantOrganizationAuthority({ + sessionItems: [ + { + role: "assistant", + debug: { + execution_lane: "address_query", + answer_grounding_check: { status: "grounded" }, + extracted_filters: { + organization: "Org Grounded", + as_of_date: "2020-03-31" + }, + detected_intent: "receivables_confirmed_as_of_date" + } + }, + { + role: "assistant", + debug: { + execution_lane: "living_chat", + living_chat_selected_organization: "Org Selected", + assistant_active_organization: "Org Selected", + assistant_known_organizations: ["Org Selected", "Org Known"], + living_chat_data_scope_probe_organizations: ["Org Probe"] + } + } + ], + sessionKnownOrganizations: ["Org Session"], + lastOrganizationClarificationDebug: { + organization_candidates: ["Org Candidate", "Org Selected"] + } + }); + + expect(authority.continuitySnapshot.activeOrganization).toBe("Org Grounded"); + expect(authority.continuityActiveOrganization).toBe("Org Selected"); + expect(authority.selectedOrganization).toBe("Org Selected"); + expect(authority.activeOrganization).toBe("Org Selected"); + expect(authority.knownOrganizations).toEqual([ + "Org Session", + "Org Selected", + "Org Known", + "Org Probe", + "Org Grounded" + ]); + expect(authority.organizationClarificationCandidates).toEqual([ + "Org Candidate", + "Org Selected", + "Org Session", + "Org Known", + "Org Probe", + "Org Grounded" + ]); + expect(authority.organizationClarificationSelectionFromScope).toBe("Org Selected"); + }); +}); diff --git a/llm_normalizer/backend/tests/assistantDataScopePolicy.test.ts b/llm_normalizer/backend/tests/assistantDataScopePolicy.test.ts new file mode 100644 index 0000000..e6fcd03 --- /dev/null +++ b/llm_normalizer/backend/tests/assistantDataScopePolicy.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it, vi } from "vitest"; +import { createAssistantDataScopePolicy } from "../src/services/assistantDataScopePolicy"; + +function createPolicy() { + return createAssistantDataScopePolicy({ + activeMcpChannel: "default", + mcpProxyUrl: "http://localhost", + executeAddressMcpQuery: vi.fn(async () => ({ rows: [] })), + repairAddressMojibake: (value: unknown) => String(value ?? "") + }); +} + +describe("assistantDataScopePolicy", () => { + it("recovers active organization from assistant-side living chat authority when grounded answer is absent", () => { + const policy = createPolicy(); + + const activeOrganization = policy.findLastAssistantActiveOrganization([ + { + role: "assistant", + debug: { + execution_lane: "living_chat", + living_chat_selected_organization: "Org Selected", + assistant_active_organization: "Org Selected", + assistant_known_organizations: ["Org Selected", "Org Backup"] + } + } + ]); + + expect(activeOrganization).toBe("Org Selected"); + }); +}); diff --git a/llm_normalizer/data/autorun_annotations/annotations.json b/llm_normalizer/data/autorun_annotations/annotations.json index 986cfaa..c9f56d3 100644 --- a/llm_normalizer/data/autorun_annotations/annotations.json +++ b/llm_normalizer/data/autorun_annotations/annotations.json @@ -916,5 +916,59 @@ "question_text": "а по Альтернативе Плюс сколько лет активности в базе 1С?", "answer_text": "На данный момент не удается точно определить, сколько лет Альтернатива Плюс активна в базе 1С — данные по истории деятельности контрагента в текущем сценарии недоступны.\n\nПроверено: документы, платежи и договоры по ООО «Альтернатива Плюс» за последние периоды. Результатов, подтверждающих начало активности, не обнаружено.\n\nМогу уточнить остаток по счёту или проверить наличие договоров/платежей за конкретные годы — сообщите, какой период вас интересует." } + }, + { + "annotation_id": "ann-mo4e8t0p-3n95ceh", + "run_id": "assistant-stage1-9VDVeE165e", + "case_id": "SAVED-001", + "session_id": "assistant-stage1-9VDVeE165e-SAVED-001", + "message_index": 35, + "rating": 1, + "comment": "написано что самый дохожный год 2020 8 мультов а слоджом в сл сообщении за 17 год заработали 12 смультов - хуйцня", + "manual_case_decision": "needs_dialog_policy_fix", + "annotation_author": "manual_reviewer", + "resolved": true, + "resolved_at": "2026-04-18T13:52:00.664Z", + "resolved_by": "manual_reviewer", + "created_at": "2026-04-18T13:49:36.360Z", + "updated_at": "2026-04-18T13:52:00.664Z", + "context": { + "message_id": "msg-dVgccEymIx", + "trace_id": "address-7XZM6cSlG2", + "reply_type": "factual", + "eval_target": "assistant_stage1", + "prompt_version": "address_query_runtime_v1", + "domain": null, + "query_class": null, + "question_text": "какой у нас самый доходный год", + "answer_text": "Самый доходный год по подтвержденным поступлениям: 2021 (8.560.025,00 ₽ по 13 операциям). Это денежный поток, а не чистая прибыль.\nТоп-3 лет по сумме поступлений:\nКоротко: в выборке 5 заказчиков по 20 подтвержденным платежным строкам.\nПодтвержденный денежный поток в выборке: 18.269.640,00 ₽.\n1. 2021 | сумма: 8.560.025,00 ₽ | операций: 13 | контрагентов: 1 | максимальная разовая сумма: 1.882.400,00 ₽\n2. 2022 | сумма: 6.044.715,00 ₽ | операций: 4 | контрагентов: 4 | максимальная разовая сумма: 2.254.115,00 ₽\n3. 2020 | сумма: 3.664.900,00 ₽ | операций: 3 | контрагентов: 1 | максимальная разовая сумма: 3.248.300,00 ₽" + } + }, + { + "annotation_id": "ann-mo4ebs7r-4qjefdf", + "run_id": "assistant-stage1-9VDVeE165e", + "case_id": "SAVED-001", + "session_id": "assistant-stage1-9VDVeE165e-SAVED-001", + "message_index": 39, + "rating": 1, + "comment": "не сходиться - там вы райноне 30 млн за все время - надо проверить маршрут", + "manual_case_decision": "needs_routing_extension", + "annotation_author": "manual_reviewer", + "resolved": false, + "resolved_at": null, + "resolved_by": null, + "created_at": "2026-04-18T13:51:55.286Z", + "updated_at": "2026-04-18T13:51:55.286Z", + "context": { + "message_id": "msg-SAw2wG77rB", + "trace_id": "address-QszgwNAdwL", + "reply_type": "factual", + "eval_target": "assistant_stage1", + "prompt_version": "address_query_runtime_v1", + "domain": null, + "query_class": null, + "question_text": "сколько вообще денег мы заработали за все время?", + "answer_text": "За все доступное время подтверждено 18.269.640,00 ₽ входящих поступлений. Это денежный поток от клиентов, а не чистая прибыль.\n\nПодтверждение:\n- Операций в выборке: 20.\n- Контрагентов в выборке: 5.\n- Самый сильный год по поступлениям: 2021 (8.560.025,00 ₽).\n- Крупнейший контрагент по потоку: Группа СВК (12.224.925,00 ₽)." + } } ] \ No newline at end of file diff --git a/llm_normalizer/data/autorun_generators/history.json b/llm_normalizer/data/autorun_generators/history.json index c71c931..51f9d05 100644 --- a/llm_normalizer/data/autorun_generators/history.json +++ b/llm_normalizer/data/autorun_generators/history.json @@ -446,38 +446,6 @@ "latest_acceptance": null } }, - { - "generation_id": "gen-mo1s0m9z-ndf56a3", - "created_at": "2026-04-16T17:51:50.476Z", - "mode": "saved_user_sessions", - "title": "Ручная сессия 16.04.2026, 20:51:30", - "count": 1, - "domain": null, - "questions": [ - "привет как дела" - ], - "generated_by": "manual_reviewer", - "saved_case_set_file": "assistant_autogen_saved_user_sessions_20260416175150_gen-mo1s0m9z-ndf56a3.json", - "context": { - "llm_provider": "local", - "model": "unsloth/qwen3-30b-a3b-instruct-2507", - "assistant_prompt_version": "address_query_runtime_v1", - "decomposition_prompt_version": "normalizer_v2_0_2", - "prompt_fingerprint": "Ты semantic-normalizer для бухгалтерского ассистента NDC.\nТвоя роль: только нормализация запроса пользователя в строгий JSON-контракт.\n\nЖесткие правила:\n1) Не давай бухгалтерский ответ по сути вопроса.\n2) Возвращай только JSON без markdown и пояснений.\n3) JSON обязан соответствовать переданной schema normalized_query_v1.\n4) Если период не указан, не выдумывай его; отмечай ambiguity.\n5) Для цепочек документов/проводок/оплат поднимай causal и cross-entity признаки.\n6) Для точечного object trace (номер/строка/ref) поднимай needs_exact_object_trace=true.\n7) Используй терминологию NDC.||You are semantic-normalizer for accounting assistant NDC.\nReturn strict JSON only, no markdown, no comments.\n\nTarget schema: normalized_query_v2_0_2.\n\nCore behavior (v2.0.2):\n1. Decompose message into semantic fragments.\n2. Classify fragment domain relevance and business scope.\n3. Fill route-critical flags and candidate labels.\n4. For each fragment set execution state fields:\n - execution_readiness\n - clarification_reason\n - soft_assumption_used\n - route_status\n - no_route_reason\n5. Clarification must be rare and justified.\n\nExecution-state policy:\n- Every in-scope fragment must produce a consistent execution state.\n- If a fragment is routable, mark it as executable or executable_with_soft_assumptions.\n- Do not leave routable fragments in unresolved state.\n- If a fragment cannot be routed, set route_status=no_route and provide explicit no_route_reason.\n\nReadiness values:\n- executable\n- executable_with_soft_assumptions\n- needs_clarification\n- no_route\n\nRoute status values:\n- routed\n- no_route\n\nNo-route reason values:\n- out_of_scope\n- insufficient_specificity\n- missing_mapping\n- unsupported_fragment_type\n\nDo not over-require formality:\n- Do not require document IDs, exact periods, or exact object references for scan/review/anomaly/rule-check requests.\n- Colloquial accounting phrases like \"что висит\", \"что подозрительно\", \"что не сходится\", \"что криво\", \"что аукнется\" are executable if accounting area is understandable.\n\nFragment required fields:\n- fragment_id\n- raw_fragment_text\n- normalized_fragment_text\n- domain_relevance\n- business_scope\n- entity_hints\n- account_hints\n- document_hints\n- register_hints\n- time_scope\n- flags\n- candidate_labels\n- confidence\n- execution_readiness\n- clarification_reason\n- soft_assumption_used\n- route_status\n- no_route_reason\n\nSoft assumptions (`soft_assumption_used`) allowed values:\n- period_from_session_context\n- company_scope_defaulted\n- problem_scan_mode_enabled\n\nGlobal notes:\n- global_notes.needs_clarification should be true only when execution is truly blocked for all in-scope fragments.\n- global_notes.clarification_reason must explain the blocker.\n\nSchema version must be:\n- \"schema_version\": \"normalized_query_v2_0_2\"||Контекст домена: бухгалтерия 1С/NDC.\n\nКлючевые счета:\n- 01, 02, 10, 41, 51, 60, 62, 68.02, 90, 97.\n\nТиповые сущности:\n- контрагент, договор, документ реализации, документ поступления, оплата, проводка, регистр, закрывающий документ.\n\nЛексика causal и сверки (сильные сигналы для cross_entity):\n- \"не бьется\", \"не сходится\", \"не видно\", \"не собралось\", \"повисло\", \"хвост\";\n- \"разложи по документам/оплатам/закрывающим\";\n- \"чем подтверждается\", \"где ошибка в цепочке\", \"что пошло криво\".\n\nЛексика точечного drilldown:\n- \"документ №...\", \"ref\", \"строка проводки\", \"покажи конкретную операцию\", \"точный source-of-record\".\n\nЛексика rule-based контроля:\n- \"проверь настройки\", \"ошибка срока/даты\", \"контроль 97/10/ОС\", \"нарушение правила учета\".\n\nЛексика обзорной аналитики:\n- \"рейтинг\", \"топ рисков\", \"в целом по компании\", \"перед закрытием периода\", \"приоритизация проверок\".\n\nВажное правило:\nЕсли в одном вопросе есть и риск-лексика, и цепочка document/payment/posting, не понижать задачу до чистого `store_feature_risk`.\nПриоритет у causal cross-entity семантики.||v2.0.2: execution-state hardening + explicit route_status/no_route_reason. Схема normalized_query_v2_0_2.||Q: По каким поставщикам висят хвосты по 60, что подозрительно по цепочке оплат?\nA: in_scope fragment, execution_readiness=executable_with_soft_assumptions, route_status=routed, no_route_reason=null.\n\nQ: Покажи записи по 97, которые повисли и могут аукнуться.\nA: in_scope fragment, execution_readiness=executable_with_soft_assumptions, route_status=routed, no_route_reason=null.\n\nQ: Чекни, что у нас не так.\nA: unclear/in_scope fragment, execution_readiness=needs_clarification, route_status=no_route, no_route_reason=insufficient_specificity.\n\nQ: Как вообще по ФСБУ правильно?\nA: out_of_scope fragment, execution_readiness=no_route, route_status=no_route, no_route_reason=out_of_scope.", - "autogen_personality_id": null, - "autogen_personality_prompt": null, - "source_session_id": "asst-iIpNheKZpP", - "saved_session_file": "assistant_saved_session_20260416175150_gen-mo1s0m9z-ndf56a3.json", - "saved_case_set_kind": "assistant_session_scenario", - "agent_run": null, - "agent_focus": null, - "architecture_phase": null, - "source_spec_file": null, - "scenario_id": null, - "semantic_tags": null, - "latest_acceptance": null - } - }, { "generation_id": "gen-mnubheq4-7h5v00u", "created_at": "2026-04-11T12:34:37.133Z", diff --git a/llm_normalizer/data/autorun_generators/saved_sessions/assistant_saved_session_20260416175150_gen-mo1s0m9z-ndf56a3.json b/llm_normalizer/data/autorun_generators/saved_sessions/assistant_saved_session_20260416175150_gen-mo1s0m9z-ndf56a3.json deleted file mode 100644 index da73427..0000000 --- a/llm_normalizer/data/autorun_generators/saved_sessions/assistant_saved_session_20260416175150_gen-mo1s0m9z-ndf56a3.json +++ /dev/null @@ -1,215 +0,0 @@ -{ - "saved_at": "2026-04-16T17:51:50.474Z", - "generation_id": "gen-mo1s0m9z-ndf56a3", - "mode": "saved_user_sessions", - "title": "Ручная сессия 16.04.2026, 20:51:30", - "source_session_id": "asst-iIpNheKZpP", - "questions": [ - "привет как дела" - ], - "session": { - "session_id": "asst-iIpNheKZpP", - "updated_at": "2026-04-16T17:51:30.711Z", - "items": [ - { - "message_id": "msg-R2if3yso9x", - "session_id": "asst-iIpNheKZpP", - "role": "user", - "text": "привет как дела", - "reply_type": null, - "created_at": "2026-04-16T17:51:19.402Z", - "trace_id": null, - "debug": null - }, - { - "message_id": "msg-tRiU0RadHy", - "session_id": "asst-iIpNheKZpP", - "role": "assistant", - "text": "Привет! У меня всё хорошо, спасибо за вопрос. А у тебя как дела? 😊", - "reply_type": "factual_with_explanation", - "created_at": "2026-04-16T17:51:30.711Z", - "trace_id": "chat-2RCiQ9Whq9", - "debug": { - "trace_id": "chat-2RCiQ9Whq9", - "prompt_version": "living_chat_router_v1", - "schema_version": "living_chat_router_v1", - "fallback_type": "none", - "detected_mode": "chat", - "detected_mode_confidence": "high", - "execution_lane": "living_chat", - "living_router_mode": "chat", - "living_router_reason": "non_domain_query_indexed", - "living_chat_response_source": "llm_chat", - "living_chat_script_guard_applied": false, - "living_chat_script_guard_reason": null, - "living_chat_grounding_guard_applied": false, - "living_chat_grounding_guard_reason": null, - "living_chat_data_scope_probe_status": null, - "living_chat_data_scope_probe_channel": null, - "living_chat_data_scope_probe_org_count": 0, - "living_chat_data_scope_probe_organizations": [], - "living_chat_data_scope_probe_error": null, - "living_chat_selected_organization": null, - "assistant_known_organizations": [], - "assistant_active_organization": null, - "address_llm_predecompose_attempted": true, - "address_llm_predecompose_applied": false, - "address_llm_predecompose_reason": "no_usable_fragment", - "address_llm_predecompose_contract": { - "schema_version": "address_llm_predecompose_contract_v1", - "source_message": "привет как дела", - "canonical_message": "привет как дела", - "mode": "unsupported", - "mode_confidence": "low", - "query_shape": "UNKNOWN", - "query_shape_confidence": "low", - "intent": "unknown", - "intent_confidence": "low", - "entities": { - "account": null, - "counterparty": null, - "contract": null, - "document_type": null, - "document_ref": null, - "organization": null - }, - "period": { - "scope": "unspecified", - "period_from": null, - "period_to": null, - "as_of_date": null, - "has_explicit_period": false - }, - "semantics": { - "scope_kind": "none", - "anchor_kind": "none", - "anchor_value": null, - "date_scope_kind": "none", - "date_basis_hint": null, - "self_scope_detected": false, - "selected_object_scope_detected": false - }, - "aggregation_profile": "unknown" - }, - "address_semantic_extraction_contract": { - "schema_version": "address_semantic_extraction_contract_v1", - "source_message": "привет как дела", - "canonical_message": "привет как дела", - "canonical_rewrite_applied": false, - "extraction": { - "mode": "unsupported", - "mode_confidence": "low", - "query_shape": "UNKNOWN", - "query_shape_confidence": "low", - "intent": "unknown", - "intent_confidence": "low", - "aggregation_profile": "unknown" - }, - "entities": { - "account": null, - "counterparty": null, - "contract": null, - "document_type": null, - "document_ref": null, - "organization": null - }, - "period": { - "scope": "unspecified", - "period_from": null, - "period_to": null, - "as_of_date": null, - "has_explicit_period": false - }, - "semantics": { - "scope_kind": "none", - "anchor_kind": "none", - "anchor_value": null, - "date_scope_kind": "none", - "date_basis_hint": null, - "self_scope_detected": false, - "selected_object_scope_detected": false - }, - "guard_hints": { - "source_data_signal_detected": false, - "canonical_data_signal_detected": false, - "data_scope_meta_query_detected": false, - "deep_investigation_signal_detected": false, - "required_anchor_missing": false, - "unsupported_low_confidence": true, - "semantic_drift_suspected": false - }, - "quality": "low", - "valid": false, - "apply_canonical_recommended": false, - "reason_codes": [ - "unsupported_low_confidence_contract" - ] - }, - "orchestration_contract_v1": { - "schema_version": "assistant_orchestration_contract_v1", - "hard_meta_mode": "non_domain", - "address_mode": "unsupported", - "address_mode_confidence": "low", - "address_intent": "unknown", - "address_intent_confidence": "low", - "strong_data_signal_detected": false, - "data_retrieval_signal_detected": false, - "followup_context_detected": false, - "unsupported_address_intent_fallback_to_deep": false, - "final_decision": { - "run_address_lane": false, - "tool_gate_decision": "skip_address_lane", - "tool_gate_reason": "non_domain_query_indexed", - "living_mode": "chat", - "living_reason": "non_domain_query_indexed" - } - }, - "tool_gate_decision": "skip_address_lane", - "tool_gate_reason": "non_domain_query_indexed", - "normalized": null, - "normalizer_output": null - } - } - ], - "investigation_state": { - "schema_version": "investigation_state_v1", - "session_id": "asst-iIpNheKZpP", - "status": "idle", - "turn_index": 0, - "updated_at": "2026-04-16T17:51:19.401Z", - "question_id": null, - "question_scope_id": null, - "scope_origin": null, - "focus": { - "domain": null, - "period": null, - "primary_accounts": [], - "active_query_subject": null - }, - "narrowing_status": "unknown", - "evidence_refs": [], - "open_uncertainties": [], - "last_answer_mode": null, - "followup_context": null, - "query_mode_hint": "direct_answer" - }, - "address_navigation_state": { - "schema_version": "address_navigation_state_v1", - "session_id": "asst-iIpNheKZpP", - "updated_at": "2026-04-16T17:51:19.401Z", - "session_context": { - "active_result_set_id": null, - "active_focus_object": null, - "last_confirmed_route": null, - "date_scope": { - "as_of_date": null, - "period_from": null, - "period_to": null - }, - "organization_scope": null - }, - "result_sets": [], - "navigation_history": [] - } - } -} \ No newline at end of file diff --git a/llm_normalizer/data/eval_cases/assistant_saved_session_runtime_job-DpGiicqgIN.json b/llm_normalizer/data/eval_cases/assistant_saved_session_runtime_job-DpGiicqgIN.json new file mode 100644 index 0000000..602b6c1 --- /dev/null +++ b/llm_normalizer/data/eval_cases/assistant_saved_session_runtime_job-DpGiicqgIN.json @@ -0,0 +1,114 @@ +{ + "suite_id": "assistant_saved_session_runtime_job-DpGiicqgIN", + "suite_version": "0.1.0", + "schema_version": "assistant_saved_session_runtime_v0_1", + "title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06", + "scenario_count": 1, + "case_ids": [ + "SAVED-001" + ], + "cases": [ + { + "case_id": "SAVED-001", + "scenario_tag": "saved_user_sessions_runtime", + "title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06", + "question_type": "followup", + "broadness_level": "medium", + "turns": [ + { + "user_message": "приветик - че как там дела" + }, + { + "user_message": "расскажи что можешь интересного" + }, + { + "user_message": "кайф - что там на складе по остаткам?" + }, + { + "user_message": "а исторические остатки на другие даты умеешь?" + }, + { + "user_message": "давай на июль 2017" + }, + { + "user_message": "март 2016" + }, + { + "user_message": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?" + }, + { + "user_message": "а кому продали?" + }, + { + "user_message": "у тебя написано кто контрагент: рабочая станция - это ошибка?" + }, + { + "user_message": "ндс можешь прикинуть на дату покупки рабочей станции?" + }, + { + "user_message": "а какой ндс мы должны сгрузить на март 2020?" + }, + { + "user_message": "прикинь какой ндс нам надо заплатить на февраль 2017" + }, + { + "user_message": "кто у нас самый доходный клиент за все время" + }, + { + "user_message": "кто нам должен денег на май 2017" + }, + { + "user_message": "а какой ндс мы должны примерно заплатить за этот период?" + }, + { + "user_message": "мы должны комуто денег на сегодня?" + }, + { + "user_message": "а нам?" + }, + { + "user_message": "какой у нас самый доходный год" + }, + { + "user_message": "а за 2017 мы скок заработали?" + }, + { + "user_message": "сколько вообще денег мы заработали за все время?" + }, + { + "user_message": "ты умеешь считать дельту по договорам?" + }, + { + "user_message": "по чепурнову покажи все доки" + }, + { + "user_message": "а по свк" + }, + { + "user_message": "а сейчас у нас есть что на складе?" + }, + { + "user_message": "что нам отгружал чепурнов? какой товар или услугу?" + }, + { + "user_message": "какие остатки на складе на сегодня" + }, + { + "user_message": "остатки на март 2016" + }, + { + "user_message": "хвосты покажи по счету 60 на август 2022" + }, + { + "user_message": "Есть ли остатки товара, которые закупались очень давно" + }, + { + "user_message": "Какие конкретно номенклатуры формируют остаток по складу на май 2020" + }, + { + "user_message": "а по Альтернативе Плюс сколько лет активности в базе 1С?" + } + ] + } + ] +} \ No newline at end of file diff --git a/llm_normalizer/data/eval_cases/assistant_autogen_saved_user_sessions_20260416175150_gen-mo1s0m9z-ndf56a3.json b/llm_normalizer/data/eval_cases/assistant_saved_session_runtime_job-L2a8rqCJma.json similarity index 60% rename from llm_normalizer/data/eval_cases/assistant_autogen_saved_user_sessions_20260416175150_gen-mo1s0m9z-ndf56a3.json rename to llm_normalizer/data/eval_cases/assistant_saved_session_runtime_job-L2a8rqCJma.json index 8ae58a0..9e4c12d 100644 --- a/llm_normalizer/data/eval_cases/assistant_autogen_saved_user_sessions_20260416175150_gen-mo1s0m9z-ndf56a3.json +++ b/llm_normalizer/data/eval_cases/assistant_saved_session_runtime_job-L2a8rqCJma.json @@ -1,10 +1,7 @@ { - "suite_id": "assistant_saved_session_gen-mo1s0m9z-ndf56a3", + "suite_id": "assistant_saved_session_runtime_job-L2a8rqCJma", "suite_version": "0.1.0", - "schema_version": "assistant_saved_session_suite_v0_1", - "generated_at": "2026-04-16T17:51:50.471Z", - "generation_id": "gen-mo1s0m9z-ndf56a3", - "mode": "saved_user_sessions", + "schema_version": "assistant_saved_session_runtime_v0_1", "title": "Ручная сессия 16.04.2026, 20:51:30", "scenario_count": 1, "case_ids": [ @@ -13,7 +10,7 @@ "cases": [ { "case_id": "SAVED-001", - "scenario_tag": "saved_user_sessions", + "scenario_tag": "saved_user_sessions_runtime", "title": "Ручная сессия 16.04.2026, 20:51:30", "question_type": "direct", "broadness_level": "medium",