Исправить компактные follow-up ответы по денежному итогу 1С

This commit is contained in:
2026-05-23 17:30:01 +03:00
parent 91a9dfa3a7
commit f7314b0212
18 changed files with 876 additions and 16 deletions
@@ -138,6 +138,122 @@ function mergeOrganizationIntoDiscoveryFollowupContext(
return base;
}
function compactLower(value: unknown): string {
return String(value ?? "")
.toLowerCase()
.replace(/\s+/g, " ")
.trim();
}
function hasCompactCashflowFollowupSignal(text: string | null): boolean {
const value = compactLower(text);
if (!value) {
return false;
}
return /(?:\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0440\u0435\u0437\p{L}*|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433|\u043f\u0440\u0438\u0448\p{L}*[\s\S]{0,80}\u0443\u0448\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u043f\u043e\u043b\u0443\u0447\p{L}*[\s\S]{0,80}\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*[\s\S]{0,80}\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b)/iu.test(value);
}
function dateScopeToFilterWindow(dateScope: string | null): Record<string, string> | null {
if (!dateScope) {
return null;
}
if (/^(?:19|20)\d{2}$/.test(dateScope)) {
return {
period_from: `${dateScope}-01-01`,
period_to: `${dateScope}-12-31`
};
}
if (/^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(dateScope)) {
return { as_of_date: dateScope };
}
return null;
}
function looksLikeReliableOrganizationScope(value: string | null): boolean {
const text = compactLower(value);
if (!text) {
return false;
}
if (/(?:\u0440\u0430\u0437\u0431\u0438\u0432|\u0440\u0430\u0437\u0440\u0435\u0437|\u0432\u0445\u043e\u0434\u044f\u0449|\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u043d\u0435\u0442\u0442\u043e|\u0442\u043e\u043f)/iu.test(text)) {
return false;
}
return /(?:^|[\s"'\u00ab])(?:\u043e\u043e\u043e|\u0438\u043f|\u043f\u0430\u043e|\u0430\u043e|\u0437\u0430\u043e)(?:[\s"'\u00bb]|$)|\u043e\u0431\u0449\u0435\u0441\u0442\u0432\p{L}*\s+\u0441\s+\u043e\u0433\u0440\u0430\u043d\u0438\u0447\p{L}*\s+\u043e\u0442\u0432\p{L}*|\b(?:llc|inc|corp)\b/iu.test(text);
}
function inferBusinessOverviewDiscoveryContextFromSessionItems(
sessionItems: unknown[],
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]
): Record<string, unknown> | null {
for (let index = sessionItems.length - 1; index >= 0; index -= 1) {
const item = toRecordObject(sessionItems[index]);
if (toNonEmptyString(item?.role) !== "assistant") {
continue;
}
const debug = toRecordObject(item?.debug);
const entryPoint = toRecordObject(debug?.assistant_mcp_discovery_entry_point_v1);
const turnInput = toRecordObject(entryPoint?.turn_input);
const dataNeedGraph = toRecordObject(turnInput?.data_need_graph);
if (toNonEmptyString(dataNeedGraph?.business_fact_family) !== "business_overview") {
continue;
}
const turnMeaningRef = toRecordObject(turnInput?.turn_meaning_ref);
const filterWindow = dateScopeToFilterWindow(toNonEmptyString(turnMeaningRef?.explicit_date_scope));
if (!filterWindow) {
continue;
}
const previousFilters: Record<string, unknown> = { ...filterWindow };
const organization = toNonEmptyString(turnMeaningRef?.explicit_organization_scope);
if (looksLikeReliableOrganizationScope(organization)) {
previousFilters.organization = organization;
}
const rankingNeed = toNonEmptyString(dataNeedGraph?.ranking_need);
return {
previous_discovery_pilot_scope: "business_overview_route_template_v1",
previous_filters: previousFilters,
root_filters: { ...previousFilters },
previous_seeded_ranking_need: rankingNeed,
previous_intent: "business_overview",
root_intent: "business_overview"
};
}
return null;
}
function mergeBusinessOverviewDateContextForCompactCashflow(input: {
userMessage: string;
followupContext: Record<string, unknown> | null;
sessionItems: unknown[];
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"];
}): Record<string, unknown> | null {
if (!hasCompactCashflowFollowupSignal(input.userMessage)) {
return input.followupContext;
}
const existingFilters = toRecordObject(input.followupContext?.previous_filters);
if (
input.toNonEmptyString(existingFilters?.period_from) ||
input.toNonEmptyString(existingFilters?.period_to) ||
input.toNonEmptyString(existingFilters?.as_of_date)
) {
return input.followupContext;
}
const inferred = inferBusinessOverviewDiscoveryContextFromSessionItems(input.sessionItems, input.toNonEmptyString);
if (!inferred) {
return input.followupContext;
}
return {
...inferred,
...(input.followupContext ?? {}),
previous_filters: {
...(toRecordObject(inferred.previous_filters) ?? {}),
...(toRecordObject(input.followupContext?.previous_filters) ?? {})
},
root_filters: {
...(toRecordObject(inferred.root_filters) ?? {}),
...(toRecordObject(input.followupContext?.root_filters) ?? {})
}
};
}
function hasSelectedObjectInventorySignal(text: string | null): boolean {
return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ним|selected\s+object)/iu.test(
String(text ?? "")
@@ -397,12 +513,18 @@ export async function buildAssistantAddressOrchestrationRuntime(
const orchestrationContract = toRecordObject(orchestrationDecision.orchestrationContract);
const predecomposeContract = toRecordObject(addressPreDecompose.predecomposeContract);
const explicitPredecomposeOrganization = predecomposeOrganizationName(predecomposeContract, input.toNonEmptyString);
const discoveryFollowupContext = mergeOrganizationIntoDiscoveryFollowupContext(
const discoveryFollowupContextWithOrganization = mergeOrganizationIntoDiscoveryFollowupContext(
followupContext,
explicitPredecomposeOrganization
? null
: sessionOrganizationName(input.sessionOrganizationScope ?? null, input.toNonEmptyString)
);
const discoveryFollowupContext = mergeBusinessOverviewDateContextForCompactCashflow({
userMessage: input.userMessage,
followupContext: discoveryFollowupContextWithOrganization,
sessionItems: input.sessionItems,
toNonEmptyString: input.toNonEmptyString
});
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(
input.userMessage,
addressInputMessage,
@@ -184,7 +184,7 @@ function hasBusinessOverviewDirectMoneyAnswerHint(input: {
return true;
}
if (
/(?:\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u0434\u0435\u043d\p{L}{0,20}\s+\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\u043b\p{L}*[\s\S]{0,80}\u0443\u0448\u043b\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u0432\u0445\u043e\u0434\u044f\u0449\p{L}*[\s\S]{0,80}\u0438\u0441\u0445\u043e\u0434\u044f\u0449\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e)/iu.test(text)
/(?:\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433|\u0434\u0435\u043d\p{L}{0,20}\s+\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\u043b\p{L}*[\s\S]{0,80}\u0443\u0448\u043b\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u0432\u0445\u043e\u0434\u044f\u0449\p{L}*[\s\S]{0,80}\u0438\u0441\u0445\u043e\u0434\u044f\u0449\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e)/iu.test(text)
) {
return true;
}
@@ -80,7 +80,7 @@ function requestsCompactCashflowAnswer(
) {
return true;
}
return /(?:\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u0434\u0435\u043d\p{L}{0,20}\s+\u043d\u0435\u0442\u0442\u043e|\u043f\u043e\u043b\u0443\u0447\p{L}*[\s\S]{0,80}\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\u043b\p{L}*[\s\S]{0,80}\u0443\u0448\u043b\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*)[\s\S]{0,120}(?:\u0434\u0435\u043d\p{L}*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u043f\u043e\u043b\u0443\u0447|\u043f\u0440\u0438\u0448\u043b))/iu.test(
return /(?:\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}+|\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0440\u0435\u0437\p{L}*|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433|\u0434\u0435\u043d\p{L}{0,20}\s+\u043d\u0435\u0442\u0442\u043e|\u043f\u043e\u043b\u0443\u0447\p{L}*[\s\S]{0,80}\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u043f\u0440\u0438\u0448\u043b\p{L}*[\s\S]{0,80}\u0443\u0448\u043b\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*)[\s\S]{0,120}(?:\u0434\u0435\u043d\p{L}*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u043f\u043e\u043b\u0443\u0447|\u043f\u0440\u0438\u0448\u043b))/iu.test(
text
);
}
@@ -353,6 +353,18 @@ function collectEntityCandidates(value: unknown): string[] {
return result;
}
function collectReasonCodes(value: unknown): string[] {
const result: string[] = [];
if (!Array.isArray(value)) {
const reason = toNonEmptyString(value);
return reason ? [reason] : result;
}
for (const item of value) {
pushUnique(result, item);
}
return result;
}
function collectPredecomposeEntities(predecompose: Record<string, unknown> | null): {
counterparty: string | null;
organization: string | null;
@@ -1782,6 +1794,10 @@ export function buildAssistantMcpDiscoveryTurnInput(
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis);
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
const assistantTurnMeaningReasonCodes = collectReasonCodes(assistantTurnMeaning?.reason_codes);
const compactCashflowDisplayFollowupSignal = assistantTurnMeaningReasonCodes.includes(
"compact_cashflow_display_current_turn_signal"
);
const broadBusinessEvaluationUnsupported = unsupported === "broad_business_evaluation";
const seededBusinessOverviewSignal =
broadBusinessEvaluationUnsupported ||
@@ -2553,6 +2569,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
const suppressNegatedTaxOnlyDateScope = Boolean(businessOverviewSignal && negatedTaxDateScopeOnlySignal);
const topicSwitchSuppressesFollowupScope = Boolean(
rawTopicSwitchSignal &&
!compactCashflowDisplayFollowupSignal &&
(rawMetadataSignal ||
businessOverviewSignal ||
rawEntitySearchOverridesStaleScope ||
@@ -145,7 +145,7 @@ function hasCompactOrganizationCashflowDisplaySignal(text) {
return false;
}
const hasCompactCue =
/(?:\u043a\u043e\u0440\u043e\u0442\u043a\w*|\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}*|\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*)/iu.test(
/(?:\u043a\u043e\u0440\u043e\u0442\u043a\w*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433\p{L}*|\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}*|\u0431\u0435\u0437\s+\u0442\u043e\u043f(?:\u043e\u0432|\u0430)?\b|\u0431\u0435\u0437\s+\u0441\u043f\u0438\u0441(?:\u043a\p{L}*)?|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0440\u0435\u0437\p{L}*|\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\p{L}*)/iu.test(
normalized
);
if (!hasCompactCue) {
@@ -156,14 +156,15 @@ function hasCompactOrganizationCashflowDisplaySignal(text) {
normalized
);
const hasOrganizationEarningsCue =
/(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442\p{L}*|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*|\u0434\u0435\u043d\u0435\u0436\p{L}*\s+\u043d\u0435\u0442\u0442\u043e|how\s+much|earned|received|paid)/iu.test(
/(?:\u0434\u0430\u0439|\u043f\u043e\u043a\u0430\u0436\p{L}*|\u0438\u0442\u043e\u0433\p{L}*|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|\u0437\u0430\u0440\u0430\u0431\u043e\u0442\p{L}*|\u043f\u0440\u0438\u0448\p{L}*|\u0443\u0448\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*|\u0434\u0435\u043d\u0435\u0436\p{L}*\s+\u043d\u0435\u0442\u0442\u043e|how\s+much|show|give|earned|received|paid)/iu.test(
normalized
);
const hasExplicitExclusionCue =
/(?:\u0438\u0441\u043a\u043b\u044e\u0447\p{L}*|\u0443\u0431\u0435\u0440\p{L}*|\u043a\u0440\u043e\u043c\u0435|exclude|excluding|without)[\s\S]{0,80}(?:\u043a\u0440\u0443\u043f\u043d\p{L}*|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\p{L}*|\u043a\u043b\u0438\u0435\u043d\u0442\p{L}*|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\p{L}*|\u043e\u043f\u0435\u0440\u0430\u0446\p{L}*|counterpart|customer|supplier|operation)/iu.test(
normalized
);
return hasMoneyCue && hasOrganizationEarningsCue && !hasExplicitExclusionCue;
const hasSpecificCounterpartyScope = detectScopedCounterpartyEntity(normalized) !== null;
return hasMoneyCue && hasOrganizationEarningsCue && !hasExplicitExclusionCue && !hasSpecificCounterpartyScope;
}
function detectScopedCounterpartyEntity(text) {
@@ -485,8 +486,8 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
const joinedText = fallbackCompactWhitespace(`${rawText} ${effectiveText}`);
const compactOrganizationCashflowDisplay = hasCompactOrganizationCashflowDisplaySignal(rawText);
const supportedIntent = compactOrganizationCashflowDisplay ? null : detectSupportedIntent(joinedText, deps);
const counterpartyBidirectionalValueFlow = detectCounterpartyBidirectionalValueFlowFamily(joinedText);
const counterpartyTurnover = detectCounterpartyTurnoverFamily(joinedText);
const counterpartyBidirectionalValueFlow = compactOrganizationCashflowDisplay ? null : detectCounterpartyBidirectionalValueFlowFamily(joinedText);
const counterpartyTurnover = compactOrganizationCashflowDisplay ? null : detectCounterpartyTurnoverFamily(joinedText);
const selectedObjectInventoryExact = hasSelectedObjectInventoryExactSignal(joinedText);
const broadBusinessEvaluation =
compactOrganizationCashflowDisplay