Post-F: закрепить семантическую целостность MCP-цепочек
This commit is contained in:
+18
-8
@@ -52,6 +52,14 @@ function isInternalMechanicsLine(value) {
|
||||
function userFacingUnknowns(values) {
|
||||
return uniqueStrings(values).filter((value) => !isInternalMechanicsLine(value));
|
||||
}
|
||||
function rankedValueFlowUnknownLines(pilot) {
|
||||
if (!pilot.derived_ranked_value_flow) {
|
||||
return userFacingUnknowns(pilot.evidence.unknown_facts);
|
||||
}
|
||||
const ranking = pilot.derived_ranked_value_flow;
|
||||
const period = ranking.period_scope ? `периода ${ranking.period_scope}` : "проверенного окна";
|
||||
return [`Полный рейтинг контрагентов вне ${period} этим поиском не подтвержден.`];
|
||||
}
|
||||
function userFacingLimitations(values) {
|
||||
return uniqueStrings(values).filter((value) => !isInternalMechanicsLine(value));
|
||||
}
|
||||
@@ -718,13 +726,15 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
||||
if (monthlyConfirmedLines.length > 0) {
|
||||
pushReason(reasonCodes, "answer_contains_monthly_breakdown");
|
||||
}
|
||||
const confirmedLines = derivedValueLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedValueLine, ...monthlyConfirmedLines]
|
||||
: derivedEntityResolutionLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
|
||||
: derivedMetadataLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedMetadataLine]
|
||||
: pilot.evidence.confirmed_facts;
|
||||
const confirmedLines = pilot.derived_ranked_value_flow && derivedValueLine
|
||||
? [derivedValueLine]
|
||||
: derivedValueLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedValueLine, ...monthlyConfirmedLines]
|
||||
: derivedEntityResolutionLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
|
||||
: derivedMetadataLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedMetadataLine]
|
||||
: pilot.evidence.confirmed_facts;
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryAnswerAdapter",
|
||||
@@ -732,7 +742,7 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
||||
headline: headlineFor(mode, pilot),
|
||||
confirmed_lines: uniqueStrings(confirmedLines),
|
||||
inference_lines: uniqueStrings(inferenceLines),
|
||||
unknown_lines: userFacingUnknowns(pilot.evidence.unknown_facts),
|
||||
unknown_lines: rankedValueFlowUnknownLines(pilot),
|
||||
limitation_lines: userFacingLimitations([...pilot.query_limitations, ...pilot.evidence.query_limitations]),
|
||||
next_step_line: nextStepFor(mode, pilot),
|
||||
internal_mechanics_allowed: false,
|
||||
|
||||
+117
-48
@@ -59,9 +59,42 @@ function isReferentialEntityPlaceholder(value) {
|
||||
"этом"
|
||||
]).has((0, addressTextRepair_1.normalizeRussianComparableText)(value));
|
||||
}
|
||||
function isReferentialOrganizationPlaceholder(value) {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
return new Set([
|
||||
"эта организация",
|
||||
"этой организации",
|
||||
"этой организацией",
|
||||
"эту организацию",
|
||||
"эта компания",
|
||||
"этой компании",
|
||||
"этой компанией",
|
||||
"эту компанию",
|
||||
"наша организация",
|
||||
"нашей организации",
|
||||
"нашей компанией"
|
||||
]).has((0, addressTextRepair_1.normalizeRussianComparableText)(value));
|
||||
}
|
||||
function isValueFlowPredicateEntityCandidate(value) {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
const text = compactLower(value);
|
||||
const looksLikeRankingPredicate = /(?:прин[её]с|принес|выручк|доходн|больше\s+всего|наибольш)/iu.test(text) &&
|
||||
/(?:организац|компан|ден[её]г|выручк|поступлен|плат[её]ж)/iu.test(text);
|
||||
if (!looksLikeRankingPredicate) {
|
||||
return false;
|
||||
}
|
||||
return !/(?<!\p{L})(?:ооо|ип|ао|пао|зао|llc|inc|corp)(?!\p{L})/iu.test(text);
|
||||
}
|
||||
function isInvalidEntityCandidate(value) {
|
||||
return Boolean(value && (isReferentialEntityPlaceholder(value) || isValueFlowPredicateEntityCandidate(value)));
|
||||
}
|
||||
function normalizeFollowupCounterpartyCandidate(value) {
|
||||
const text = candidateValue(value);
|
||||
if (!text || isReferentialEntityPlaceholder(text)) {
|
||||
if (!text || isInvalidEntityCandidate(text)) {
|
||||
return null;
|
||||
}
|
||||
return text;
|
||||
@@ -71,7 +104,7 @@ function pushScopedEntityCandidate(target, value, groundedFollowupEntity) {
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
if (groundedFollowupEntity && isReferentialEntityPlaceholder(text)) {
|
||||
if ((groundedFollowupEntity && isReferentialEntityPlaceholder(text)) || isValueFlowPredicateEntityCandidate(text)) {
|
||||
return;
|
||||
}
|
||||
pushUnique(target, text);
|
||||
@@ -88,7 +121,7 @@ function pushNormalizedEntityResolutionCandidate(target, value) {
|
||||
return;
|
||||
}
|
||||
const normalized = canonicalizeEntityResolutionCandidate(text);
|
||||
if (normalized && !target.includes(normalized)) {
|
||||
if (normalized && !isInvalidEntityCandidate(normalized) && !target.includes(normalized)) {
|
||||
target.push(normalized);
|
||||
}
|
||||
}
|
||||
@@ -119,18 +152,25 @@ function collectEntityCandidates(value) {
|
||||
const result = [];
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
pushUnique(result, candidateValue(item));
|
||||
const candidate = candidateValue(item);
|
||||
if (!isInvalidEntityCandidate(candidate)) {
|
||||
pushUnique(result, candidate);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
pushUnique(result, candidateValue(value));
|
||||
const candidate = candidateValue(value);
|
||||
if (!isInvalidEntityCandidate(candidate)) {
|
||||
pushUnique(result, candidate);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function collectPredecomposeEntities(predecompose) {
|
||||
const entities = toRecordObject(predecompose?.entities);
|
||||
const organization = toNonEmptyString(entities?.organization);
|
||||
return {
|
||||
counterparty: toNonEmptyString(entities?.counterparty),
|
||||
organization: toNonEmptyString(entities?.organization)
|
||||
organization: isReferentialOrganizationPlaceholder(organization) ? null : organization
|
||||
};
|
||||
}
|
||||
function collectDateScope(predecompose) {
|
||||
@@ -541,23 +581,25 @@ function hasMetadataDownstreamContinuationSignal(text) {
|
||||
}
|
||||
function hasEntityResolutionSignal(text) {
|
||||
const hasSearchVerb = /(?:найд(?:и|ите|ем|у)|поищ(?:и|ите|ем)|найти|поиск|search|find|look\s*up)/iu.test(text);
|
||||
const hasEntityNoun = /(?:контрагент(?:а|ов)?|поставщик(?:а|ов)?|клиент(?:а|ов)?|counterpart(?:y|ies)|supplier(?:s)?|customer(?:s)?)/iu.test(text);
|
||||
const hasEntityNoun = /(?:контрагент(?:а|ов|у|ом|е)?|поставщик(?:а|ов|у|ом|е)?|клиент(?:а|ов|у|ом|е)?|counterpart(?:y|ies)|supplier(?:s)?|customer(?:s)?)/iu.test(text);
|
||||
return hasSearchVerb && hasEntityNoun;
|
||||
}
|
||||
function normalizeEntityResolutionCandidate(value) {
|
||||
return value
|
||||
.replace(/^(?:в\s*1с\s+|в\s+1c\s+|по\s+имени\s+)/iu, "")
|
||||
.replace(/[?!.]+$/gu, "")
|
||||
.replace(/^(?:контрагент(?:а|ов)?|поставщик(?:а|ов)?|клиент(?:а|ов)?)\s+/iu, "")
|
||||
.replace(/^(?:контрагент(?:а|ов|у|ом|е)?|поставщик(?:а|ов|у|ом|е)?|клиент(?:а|ов|у|ом|е)?)\s+/iu, "")
|
||||
.replace(/^(?:counterpart(?:y|ies)|supplier(?:s)?|customer(?:s)?)\s+/iu, "")
|
||||
.replace(/^группу\s+/iu, "Группа ")
|
||||
.replace(/^[«"'\s]+|[»"'\s]+$/gu, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
function rawEntityResolutionCandidate(text) {
|
||||
const patterns = [
|
||||
/(?:найд(?:и|ите|ем|у)|поищ(?:и|ите|ем)|найти|search|find|look\s*up)\s+(?:в\s*1с\s+|в\s+1c\s+)?(?:контрагент(?:а|ов)?|поставщик(?:а|ов)?|клиент(?:а|ов)?|counterpart(?:y|ies)|supplier(?:s)?|customer(?:s)?)\s+(.+)$/iu,
|
||||
/(?:контрагент(?:а|ов)?|поставщик(?:а|ов)?|клиент(?:а|ов)?|counterpart(?:y|ies)|supplier(?:s)?|customer(?:s)?)\s+(.+?)\s+(?:найд(?:и|ите|ем|у)|поищ(?:и|ите|ем)|найти|search|find|look\s*up)\b/iu
|
||||
/(?:найд(?:и|ите|ем|у)|поищ(?:и|ите|ем)|найти|search|find|look\s*up)\s+(?:в\s*1с\s+|в\s+1c\s+)?(?:контрагент(?:а|ов|у|ом|е)?|поставщик(?:а|ов|у|ом|е)?|клиент(?:а|ов|у|ом|е)?|counterpart(?:y|ies)|supplier(?:s)?|customer(?:s)?)\s+(.+)$/iu,
|
||||
/(?:найд(?:и|ите|ем|у)|поищ(?:и|ите|ем)|найти|search|find|look\s*up)\s+(?:в\s*1с\s+|в\s+1c\s+)(.+)$/iu,
|
||||
/(?:контрагент(?:а|ов|у|ом|е)?|поставщик(?:а|ов|у|ом|е)?|клиент(?:а|ов|у|ом|е)?|counterpart(?:y|ies)|supplier(?:s)?|customer(?:s)?)\s+(.+?)\s+(?:найд(?:и|ите|ем|у)|поищ(?:и|ите|ем)|найти|search|find|look\s*up)\b/iu
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
const match = text.match(pattern);
|
||||
@@ -797,6 +839,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||
? resolveEntityResolutionAmbiguityChoice(rawEntitySourceText, followupSeed.entityResolutionAmbiguityCandidates)
|
||||
: null;
|
||||
const entityResolutionSignal = rawEntityResolutionSignal || Boolean(rawEntityCandidate) || Boolean(entityResolutionClarificationCandidate);
|
||||
const rawEntitySearchOverridesStaleScope = Boolean(rawEntityCandidate && entityResolutionSignal);
|
||||
const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family);
|
||||
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
|
||||
const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis);
|
||||
@@ -815,10 +858,14 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||
hasMovementEvidenceFollowupSignal(rawText) ||
|
||||
hasPronounMovementEvidenceFollowupSignal(rawText);
|
||||
const assistantTurnMeaningDateScope = toNonEmptyString(assistantTurnMeaning?.explicit_date_scope);
|
||||
const assistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
|
||||
const rawAssistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
|
||||
const assistantTurnMeaningOrganizationScope = isReferentialOrganizationPlaceholder(rawAssistantTurnMeaningOrganizationScope)
|
||||
? null
|
||||
: rawAssistantTurnMeaningOrganizationScope;
|
||||
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
|
||||
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
|
||||
const currentTurnOrganizationScope = rawOrganizationScope ?? predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope;
|
||||
const currentTurnFreshOrganizationScope = rawOrganizationScope ?? predecomposeEntities.organization;
|
||||
const currentTurnOrganizationScope = currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope;
|
||||
const explicitOrganizationScopeSignal = Boolean(rawOrganizationMentionSignal && currentTurnOrganizationScope);
|
||||
const organizationClarificationFollowupApplicable = Boolean(followupSeed.domain === "counterparty_value" &&
|
||||
!followupSeed.counterparty &&
|
||||
@@ -1130,30 +1177,40 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||
lifecycleSignal ||
|
||||
metadataGroundedDocumentLaneApplicable ||
|
||||
metadataGroundedMovementLaneApplicable));
|
||||
const metadataLaneScopeHint = explicitCurrentCounterpartyOverridesFollowupEntity
|
||||
const metadataLaneScopeHint = rawEntitySearchOverridesStaleScope
|
||||
? null
|
||||
: rawMetadataScopeHint ??
|
||||
followupSeed.metadataScopeHint ??
|
||||
followupSeed.discoveryEntity ??
|
||||
followupSeed.metadataSelectedEntitySet ??
|
||||
null;
|
||||
: explicitCurrentCounterpartyOverridesFollowupEntity
|
||||
? null
|
||||
: rawMetadataScopeHint ??
|
||||
followupSeed.metadataScopeHint ??
|
||||
followupSeed.discoveryEntity ??
|
||||
followupSeed.metadataSelectedEntitySet ??
|
||||
null;
|
||||
const metadataScopedLaneWithoutSubject = Boolean((metadataGroundedMovementLaneApplicable || metadataGroundedDocumentLaneApplicable) &&
|
||||
!followupSeed.counterparty &&
|
||||
metadataLaneCarryoverAvailable);
|
||||
const groundedFollowupEntity = metadataScopedLaneWithoutSubject
|
||||
? null
|
||||
: explicitCurrentCounterpartyOverridesFollowupEntity
|
||||
: rawEntitySearchOverridesStaleScope
|
||||
? null
|
||||
: followupSeed.counterparty ?? followupSeed.discoveryEntity;
|
||||
: explicitCurrentCounterpartyOverridesFollowupEntity
|
||||
? null
|
||||
: followupSeed.counterparty ?? followupSeed.discoveryEntity;
|
||||
const entityCandidates = entityResolutionSignal ? [] : [];
|
||||
if (entityResolutionSignal) {
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, entityResolutionClarificationCandidate);
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, rawEntityCandidate);
|
||||
for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) {
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, candidate);
|
||||
if (!rawEntitySearchOverridesStaleScope || sameScopedName(candidate, rawEntityCandidate)) {
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, candidate);
|
||||
}
|
||||
}
|
||||
if (!rawEntitySearchOverridesStaleScope || sameScopedName(normalizedPredecomposeCounterparty, rawEntityCandidate)) {
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, normalizedPredecomposeCounterparty);
|
||||
}
|
||||
if (!rawEntitySearchOverridesStaleScope) {
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, followupSeed.counterparty);
|
||||
}
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, normalizedPredecomposeCounterparty);
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, followupSeed.counterparty);
|
||||
}
|
||||
else {
|
||||
if (groundedFollowupEntity) {
|
||||
@@ -1190,9 +1247,12 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||
pushUnique(entityCandidates, predecomposeEntities.organization);
|
||||
pushUnique(entityCandidates, followupSeed.organization);
|
||||
}
|
||||
const explicitOrganizationScope = valueFlowOrganizationStaysScope || !openScopeValueFlowWithoutCounterparty
|
||||
? currentTurnOrganizationScope ?? followupSeed.organization
|
||||
: null;
|
||||
const explicitOrganizationScope = rawEntitySearchOverridesStaleScope && !currentTurnFreshOrganizationScope
|
||||
? null
|
||||
: valueFlowOrganizationStaysScope || !openScopeValueFlowWithoutCounterparty
|
||||
? (rawEntitySearchOverridesStaleScope ? currentTurnFreshOrganizationScope : currentTurnOrganizationScope) ??
|
||||
followupSeed.organization
|
||||
: null;
|
||||
if (explicitCurrentCounterpartyCandidate &&
|
||||
(valueFlowSignal || lifecycleSignal || metadataGroundedDocumentLaneApplicable || metadataGroundedMovementLaneApplicable)) {
|
||||
for (let index = entityCandidates.length - 1; index >= 0; index -= 1) {
|
||||
@@ -1226,11 +1286,16 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||
(clarificationLoopStillNeedsPeriod ||
|
||||
openScopeValueFlowWithoutResolvedCounterparty ||
|
||||
(valueFlowOrganizationStaysScope && (Boolean(followupSeed.rankingNeed) || bidirectionalValueFlowSignal))));
|
||||
const normalizedPredecomposeDateScope = suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(predecomposeDateScope) ? null : predecomposeDateScope;
|
||||
const normalizedAssistantTurnMeaningDateScope = suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(assistantTurnMeaningDateScope)
|
||||
const normalizedPredecomposeDateScope = (rawEntitySearchOverridesStaleScope && !currentTurnCarriesExplicitPeriod) ||
|
||||
(suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(predecomposeDateScope))
|
||||
? null
|
||||
: predecomposeDateScope;
|
||||
const normalizedAssistantTurnMeaningDateScope = rawEntitySearchOverridesStaleScope ||
|
||||
(suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(assistantTurnMeaningDateScope))
|
||||
? null
|
||||
: assistantTurnMeaningDateScope;
|
||||
const normalizedFollowupDateScope = suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(followupSeed.dateScope)
|
||||
const normalizedFollowupDateScope = rawEntitySearchOverridesStaleScope ||
|
||||
(suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(followupSeed.dateScope))
|
||||
? null
|
||||
: followupSeed.dateScope;
|
||||
const explicitDateScope = rawAllTimeScopeSignal
|
||||
@@ -1277,7 +1342,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||
? metadataActionFromRawText(rawText) ?? seededAction
|
||||
: rawAction ?? seededAction,
|
||||
asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis,
|
||||
seeded_ranking_need: valueFlowSignal && followupSeed.rankingNeed ? followupSeed.rankingNeed : undefined,
|
||||
seeded_ranking_need: valueFlowSignal && followupSeed.rankingNeed && !rawEntitySearchOverridesStaleScope
|
||||
? followupSeed.rankingNeed
|
||||
: undefined,
|
||||
explicit_entity_candidates: entityCandidates,
|
||||
metadata_ambiguity_entity_sets: metadataAmbiguityLaneClarificationApplicable && followupSeed.metadataAmbiguityEntitySets.length > 0
|
||||
? followupSeed.metadataAmbiguityEntitySets
|
||||
@@ -1381,28 +1448,30 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||
groundedValueFlowFollowupApplicable
|
||||
});
|
||||
const hasTurnMeaning = Object.keys(cleanTurnMeaning).length > 0;
|
||||
const sourceSignal = assistantTurnMeaning
|
||||
? "assistant_turn_meaning"
|
||||
: followupDiscoverySeedApplicable ||
|
||||
Boolean(entityResolutionClarificationCandidate) ||
|
||||
effectiveMetadataFollowupSeedApplicable ||
|
||||
metadataAmbiguityLaneClarificationApplicable
|
||||
? "followup_context"
|
||||
: metadataGroundedMovementLaneApplicable
|
||||
const sourceSignal = rawEntitySearchOverridesStaleScope
|
||||
? "raw_text"
|
||||
: assistantTurnMeaning
|
||||
? "assistant_turn_meaning"
|
||||
: followupDiscoverySeedApplicable ||
|
||||
Boolean(entityResolutionClarificationCandidate) ||
|
||||
effectiveMetadataFollowupSeedApplicable ||
|
||||
metadataAmbiguityLaneClarificationApplicable
|
||||
? "followup_context"
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
: metadataGroundedMovementLaneApplicable
|
||||
? "followup_context"
|
||||
: predecomposeContract
|
||||
? "predecompose_contract"
|
||||
: lifecycleSignal
|
||||
? "raw_text"
|
||||
: valueFlowSignal
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
? "followup_context"
|
||||
: predecomposeContract
|
||||
? "predecompose_contract"
|
||||
: lifecycleSignal
|
||||
? "raw_text"
|
||||
: entityResolutionSignal
|
||||
: valueFlowSignal
|
||||
? "raw_text"
|
||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||
: entityResolutionSignal
|
||||
? "raw_text"
|
||||
: "none";
|
||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||
? "raw_text"
|
||||
: "none";
|
||||
if (lifecycleSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_lifecycle_signal_detected");
|
||||
}
|
||||
@@ -1521,7 +1590,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||
normalizedPredecomposeCounterparty) {
|
||||
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
|
||||
}
|
||||
if (followupSeed.counterparty) {
|
||||
if (followupSeed.counterparty && !rawEntitySearchOverridesStaleScope) {
|
||||
pushReason(reasonCodes, "mcp_discovery_counterparty_from_followup_context");
|
||||
}
|
||||
if (followupDateScopeApplied) {
|
||||
|
||||
+20
-8
@@ -1932,7 +1932,7 @@ function textMojibakeScoreForAddress(value) {
|
||||
const source = String(value ?? "");
|
||||
const cyrillic = (source.match(/[А-Яа-яЁё]/g) ?? []).length;
|
||||
const latin = (source.match(/[A-Za-z]/g) ?? []).length;
|
||||
const hardMarkers = (source.match(/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ�?’“”•–—™љ›њќћџ]/g) ?? []).length;
|
||||
const hardMarkers = (source.match(/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ\uFFFD?’“”•–—™љ›њќћџ]/g) ?? []).length;
|
||||
const pairMarkers = (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length;
|
||||
const doubleEncodedMarkers = (source.match(/(?:Г[Ђ-џ]|В[Ђ-џ]|Ã.|Â.)/gu) ?? []).length;
|
||||
return cyrillic + latin - hardMarkers * 3 - pairMarkers * 2 - doubleEncodedMarkers * 2;
|
||||
@@ -1942,7 +1942,7 @@ function looksLikeMojibakeForAddress(value) {
|
||||
if (!source.trim()) {
|
||||
return false;
|
||||
}
|
||||
if (/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ�?’“”•–—™љ›њќћџ]/.test(source)) {
|
||||
if (/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ\uFFFD?’“”•–—™љ›њќћџ]/.test(source)) {
|
||||
return true;
|
||||
}
|
||||
if ((source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length >= 2) {
|
||||
@@ -2173,7 +2173,7 @@ function normalizeCounterpartyForFollowupMatch(value) {
|
||||
return compactWhitespace(repairAddressMojibake(String(value ?? ""))
|
||||
.toLowerCase()
|
||||
.replace(/ё/g, "е")
|
||||
.replace(/[«»"'`“”„’�?]/g, " ")
|
||||
.replace(/[«»"'`“”„’\uFFFD?]/g, " ")
|
||||
.replace(/[^a-zа-я0-9\s._-]+/giu, " "));
|
||||
}
|
||||
function normalizeCounterpartyTokenForFollowupMatch(value) {
|
||||
@@ -2219,7 +2219,7 @@ function extractDisplayedAddressEntityCandidates(replyText, entityType = "unknow
|
||||
if (parts.length >= 2 && /^\d{4}-\d{2}-\d{2}/.test(parts[0] ?? "")) {
|
||||
counterpartyCandidate = parts[1] ?? counterpartyCandidate;
|
||||
}
|
||||
const cleanedCandidate = compactWhitespace(counterpartyCandidate.replace(/^["'«»“”„`’�?]+|["'«»“”„`’�?]+$/gu, ""));
|
||||
const cleanedCandidate = compactWhitespace(counterpartyCandidate.replace(/^["'«»“”„`’\uFFFD?]+|["'«»“”„`’\uFFFD?]+$/gu, ""));
|
||||
if (!cleanedCandidate || cleanedCandidate.length < 2) {
|
||||
continue;
|
||||
}
|
||||
@@ -3268,6 +3268,11 @@ function hasSameDateAccountFollowupSignalForPredecompose(text) {
|
||||
/(?:^|\s)по\s+\d{2}(?:[.,]\d{1,2})?(?=$|[\s,.;:!?])/iu.test(source) ||
|
||||
/\b\d{2}(?:[.,]\d{1,2})\b/u.test(source));
|
||||
}
|
||||
function isCounterpartyDrilldownIntentForPredecompose(intent) {
|
||||
return intent === "list_documents_by_counterparty" ||
|
||||
intent === "bank_operations_by_counterparty" ||
|
||||
intent === "list_contracts_by_counterparty";
|
||||
}
|
||||
function hasPredecomposeDiagnosticUncertaintyLead(text) {
|
||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
||||
if (!normalized) {
|
||||
@@ -3486,8 +3491,16 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
||||
const sourceHasExplicitAccountAnchor = (0, addressIntentResolver_1.hasAccountNumberAnchor)(repairedSourceMessage || userMessage) ||
|
||||
(0, addressIntentResolver_1.hasCompactAccountCodeToken)(repairedSourceMessage || userMessage);
|
||||
const candidateInjectsAccountAnchor = Boolean(toNonEmptyString(candidatePredecomposeContract?.entities?.account));
|
||||
if (sourceIntentResolution.intent === "inventory_on_hand_as_of_date" &&
|
||||
candidateIntentResolution.intent === "inventory_on_hand_as_of_date" &&
|
||||
const sourceAnchorQuality = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage);
|
||||
const candidateAccountInjectedIntoCounterpartyAnchor = isCounterpartyDrilldownIntentForPredecompose(sourceIntentResolution.intent) &&
|
||||
sourceIntentResolution.intent === candidateIntentResolution.intent &&
|
||||
sourceAnchorQuality.anchorType === "counterparty" &&
|
||||
sourceAnchorQuality.quality >= 2 &&
|
||||
!sourceHasExplicitAccountAnchor &&
|
||||
candidateInjectsAccountAnchor;
|
||||
if (((sourceIntentResolution.intent === "inventory_on_hand_as_of_date" &&
|
||||
candidateIntentResolution.intent === "inventory_on_hand_as_of_date") ||
|
||||
candidateAccountInjectedIntoCounterpartyAnchor) &&
|
||||
!sourceHasExplicitAccountAnchor &&
|
||||
candidateInjectsAccountAnchor) {
|
||||
return attachAddressPredecomposeContract({
|
||||
@@ -3500,10 +3513,9 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
||||
reason: "normalized_fragment_rejected_anchor_injection",
|
||||
fallbackRuleHit: null,
|
||||
sanitizedUserMessage,
|
||||
semanticHints: candidateMeta?.semanticHints ?? null
|
||||
semanticHints: null
|
||||
}, userMessage);
|
||||
}
|
||||
const sourceAnchorQuality = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage);
|
||||
const candidateAnchorQuality = evaluateAddressAnchorQuality(candidate);
|
||||
const sameIntentForAnchorSafety = sourceAnchorQuality.intent !== "unknown" && sourceAnchorQuality.intent === candidateAnchorQuality.intent;
|
||||
const sourceSelectedObjectItemAnchorValue = toNonEmptyString((0, addressFilterExtractor_1.extractSelectedObjectQuotedValue)(userMessage)) ??
|
||||
|
||||
@@ -82,6 +82,15 @@ function userFacingUnknowns(values: string[]): string[] {
|
||||
return uniqueStrings(values).filter((value) => !isInternalMechanicsLine(value));
|
||||
}
|
||||
|
||||
function rankedValueFlowUnknownLines(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
|
||||
if (!pilot.derived_ranked_value_flow) {
|
||||
return userFacingUnknowns(pilot.evidence.unknown_facts);
|
||||
}
|
||||
const ranking = pilot.derived_ranked_value_flow;
|
||||
const period = ranking.period_scope ? `периода ${ranking.period_scope}` : "проверенного окна";
|
||||
return [`Полный рейтинг контрагентов вне ${period} этим поиском не подтвержден.`];
|
||||
}
|
||||
|
||||
function userFacingLimitations(values: string[]): string[] {
|
||||
return uniqueStrings(values).filter((value) => !isInternalMechanicsLine(value));
|
||||
}
|
||||
@@ -856,7 +865,9 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
|
||||
if (monthlyConfirmedLines.length > 0) {
|
||||
pushReason(reasonCodes, "answer_contains_monthly_breakdown");
|
||||
}
|
||||
const confirmedLines = derivedValueLine
|
||||
const confirmedLines = pilot.derived_ranked_value_flow && derivedValueLine
|
||||
? [derivedValueLine]
|
||||
: derivedValueLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedValueLine, ...monthlyConfirmedLines]
|
||||
: derivedEntityResolutionLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
|
||||
@@ -871,7 +882,7 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
|
||||
headline: headlineFor(mode, pilot),
|
||||
confirmed_lines: uniqueStrings(confirmedLines),
|
||||
inference_lines: uniqueStrings(inferenceLines),
|
||||
unknown_lines: userFacingUnknowns(pilot.evidence.unknown_facts),
|
||||
unknown_lines: rankedValueFlowUnknownLines(pilot),
|
||||
limitation_lines: userFacingLimitations([...pilot.query_limitations, ...pilot.evidence.query_limitations]),
|
||||
next_step_line: nextStepFor(mode, pilot),
|
||||
internal_mechanics_allowed: false,
|
||||
|
||||
@@ -102,9 +102,46 @@ function isReferentialEntityPlaceholder(value: string): boolean {
|
||||
]).has(normalizeRussianComparableText(value));
|
||||
}
|
||||
|
||||
function isReferentialOrganizationPlaceholder(value: string | null): boolean {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
return new Set([
|
||||
"эта организация",
|
||||
"этой организации",
|
||||
"этой организацией",
|
||||
"эту организацию",
|
||||
"эта компания",
|
||||
"этой компании",
|
||||
"этой компанией",
|
||||
"эту компанию",
|
||||
"наша организация",
|
||||
"нашей организации",
|
||||
"нашей компанией"
|
||||
]).has(normalizeRussianComparableText(value));
|
||||
}
|
||||
|
||||
function isValueFlowPredicateEntityCandidate(value: string | null): boolean {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
const text = compactLower(value);
|
||||
const looksLikeRankingPredicate =
|
||||
/(?:прин[её]с|принес|выручк|доходн|больше\s+всего|наибольш)/iu.test(text) &&
|
||||
/(?:организац|компан|ден[её]г|выручк|поступлен|плат[её]ж)/iu.test(text);
|
||||
if (!looksLikeRankingPredicate) {
|
||||
return false;
|
||||
}
|
||||
return !/(?<!\p{L})(?:ооо|ип|ао|пао|зао|llc|inc|corp)(?!\p{L})/iu.test(text);
|
||||
}
|
||||
|
||||
function isInvalidEntityCandidate(value: string | null): boolean {
|
||||
return Boolean(value && (isReferentialEntityPlaceholder(value) || isValueFlowPredicateEntityCandidate(value)));
|
||||
}
|
||||
|
||||
function normalizeFollowupCounterpartyCandidate(value: unknown): string | null {
|
||||
const text = candidateValue(value);
|
||||
if (!text || isReferentialEntityPlaceholder(text)) {
|
||||
if (!text || isInvalidEntityCandidate(text)) {
|
||||
return null;
|
||||
}
|
||||
return text;
|
||||
@@ -119,7 +156,7 @@ function pushScopedEntityCandidate(
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
if (groundedFollowupEntity && isReferentialEntityPlaceholder(text)) {
|
||||
if ((groundedFollowupEntity && isReferentialEntityPlaceholder(text)) || isValueFlowPredicateEntityCandidate(text)) {
|
||||
return;
|
||||
}
|
||||
pushUnique(target, text);
|
||||
@@ -138,7 +175,7 @@ function pushNormalizedEntityResolutionCandidate(target: string[], value: unknow
|
||||
return;
|
||||
}
|
||||
const normalized = canonicalizeEntityResolutionCandidate(text);
|
||||
if (normalized && !target.includes(normalized)) {
|
||||
if (normalized && !isInvalidEntityCandidate(normalized) && !target.includes(normalized)) {
|
||||
target.push(normalized);
|
||||
}
|
||||
}
|
||||
@@ -175,11 +212,17 @@ function collectEntityCandidates(value: unknown): string[] {
|
||||
const result: string[] = [];
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
pushUnique(result, candidateValue(item));
|
||||
const candidate = candidateValue(item);
|
||||
if (!isInvalidEntityCandidate(candidate)) {
|
||||
pushUnique(result, candidate);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
pushUnique(result, candidateValue(value));
|
||||
const candidate = candidateValue(value);
|
||||
if (!isInvalidEntityCandidate(candidate)) {
|
||||
pushUnique(result, candidate);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -188,9 +231,10 @@ function collectPredecomposeEntities(predecompose: Record<string, unknown> | nul
|
||||
organization: string | null;
|
||||
} {
|
||||
const entities = toRecordObject(predecompose?.entities);
|
||||
const organization = toNonEmptyString(entities?.organization);
|
||||
return {
|
||||
counterparty: toNonEmptyString(entities?.counterparty),
|
||||
organization: toNonEmptyString(entities?.organization)
|
||||
organization: isReferentialOrganizationPlaceholder(organization) ? null : organization
|
||||
};
|
||||
}
|
||||
|
||||
@@ -767,7 +811,7 @@ function hasMetadataDownstreamContinuationSignal(text: string): boolean {
|
||||
function hasEntityResolutionSignal(text: string): boolean {
|
||||
const hasSearchVerb = /(?:найд(?:и|ите|ем|у)|поищ(?:и|ите|ем)|найти|поиск|search|find|look\s*up)/iu.test(text);
|
||||
const hasEntityNoun =
|
||||
/(?:контрагент(?:а|ов)?|поставщик(?:а|ов)?|клиент(?:а|ов)?|counterpart(?:y|ies)|supplier(?:s)?|customer(?:s)?)/iu.test(
|
||||
/(?:контрагент(?:а|ов|у|ом|е)?|поставщик(?:а|ов|у|ом|е)?|клиент(?:а|ов|у|ом|е)?|counterpart(?:y|ies)|supplier(?:s)?|customer(?:s)?)/iu.test(
|
||||
text
|
||||
);
|
||||
return hasSearchVerb && hasEntityNoun;
|
||||
@@ -777,8 +821,9 @@ function normalizeEntityResolutionCandidate(value: string): string {
|
||||
return value
|
||||
.replace(/^(?:в\s*1с\s+|в\s+1c\s+|по\s+имени\s+)/iu, "")
|
||||
.replace(/[?!.]+$/gu, "")
|
||||
.replace(/^(?:контрагент(?:а|ов)?|поставщик(?:а|ов)?|клиент(?:а|ов)?)\s+/iu, "")
|
||||
.replace(/^(?:контрагент(?:а|ов|у|ом|е)?|поставщик(?:а|ов|у|ом|е)?|клиент(?:а|ов|у|ом|е)?)\s+/iu, "")
|
||||
.replace(/^(?:counterpart(?:y|ies)|supplier(?:s)?|customer(?:s)?)\s+/iu, "")
|
||||
.replace(/^группу\s+/iu, "Группа ")
|
||||
.replace(/^[«"'\s]+|[»"'\s]+$/gu, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
@@ -786,8 +831,9 @@ function normalizeEntityResolutionCandidate(value: string): string {
|
||||
|
||||
function rawEntityResolutionCandidate(text: string): string | null {
|
||||
const patterns = [
|
||||
/(?:найд(?:и|ите|ем|у)|поищ(?:и|ите|ем)|найти|search|find|look\s*up)\s+(?:в\s*1с\s+|в\s+1c\s+)?(?:контрагент(?:а|ов)?|поставщик(?:а|ов)?|клиент(?:а|ов)?|counterpart(?:y|ies)|supplier(?:s)?|customer(?:s)?)\s+(.+)$/iu,
|
||||
/(?:контрагент(?:а|ов)?|поставщик(?:а|ов)?|клиент(?:а|ов)?|counterpart(?:y|ies)|supplier(?:s)?|customer(?:s)?)\s+(.+?)\s+(?:найд(?:и|ите|ем|у)|поищ(?:и|ите|ем)|найти|search|find|look\s*up)\b/iu
|
||||
/(?:найд(?:и|ите|ем|у)|поищ(?:и|ите|ем)|найти|search|find|look\s*up)\s+(?:в\s*1с\s+|в\s+1c\s+)?(?:контрагент(?:а|ов|у|ом|е)?|поставщик(?:а|ов|у|ом|е)?|клиент(?:а|ов|у|ом|е)?|counterpart(?:y|ies)|supplier(?:s)?|customer(?:s)?)\s+(.+)$/iu,
|
||||
/(?:найд(?:и|ите|ем|у)|поищ(?:и|ите|ем)|найти|search|find|look\s*up)\s+(?:в\s*1с\s+|в\s+1c\s+)(.+)$/iu,
|
||||
/(?:контрагент(?:а|ов|у|ом|е)?|поставщик(?:а|ов|у|ом|е)?|клиент(?:а|ов|у|ом|е)?|counterpart(?:y|ies)|supplier(?:s)?|customer(?:s)?)\s+(.+?)\s+(?:найд(?:и|ите|ем|у)|поищ(?:и|ите|ем)|найти|search|find|look\s*up)\b/iu
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
const match = text.match(pattern);
|
||||
@@ -1085,6 +1131,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
: null;
|
||||
const entityResolutionSignal =
|
||||
rawEntityResolutionSignal || Boolean(rawEntityCandidate) || Boolean(entityResolutionClarificationCandidate);
|
||||
const rawEntitySearchOverridesStaleScope = Boolean(rawEntityCandidate && entityResolutionSignal);
|
||||
const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family);
|
||||
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
|
||||
const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis);
|
||||
@@ -1105,11 +1152,17 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
hasMovementEvidenceFollowupSignal(rawText) ||
|
||||
hasPronounMovementEvidenceFollowupSignal(rawText);
|
||||
const assistantTurnMeaningDateScope = toNonEmptyString(assistantTurnMeaning?.explicit_date_scope);
|
||||
const assistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
|
||||
const rawAssistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
|
||||
const assistantTurnMeaningOrganizationScope = isReferentialOrganizationPlaceholder(
|
||||
rawAssistantTurnMeaningOrganizationScope
|
||||
)
|
||||
? null
|
||||
: rawAssistantTurnMeaningOrganizationScope;
|
||||
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
|
||||
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
|
||||
const currentTurnFreshOrganizationScope = rawOrganizationScope ?? predecomposeEntities.organization;
|
||||
const currentTurnOrganizationScope =
|
||||
rawOrganizationScope ?? predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope;
|
||||
currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope;
|
||||
const explicitOrganizationScopeSignal = Boolean(rawOrganizationMentionSignal && currentTurnOrganizationScope);
|
||||
const organizationClarificationFollowupApplicable = Boolean(
|
||||
followupSeed.domain === "counterparty_value" &&
|
||||
@@ -1492,7 +1545,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
metadataGroundedMovementLaneApplicable)
|
||||
);
|
||||
const metadataLaneScopeHint =
|
||||
explicitCurrentCounterpartyOverridesFollowupEntity
|
||||
rawEntitySearchOverridesStaleScope
|
||||
? null
|
||||
: explicitCurrentCounterpartyOverridesFollowupEntity
|
||||
? null
|
||||
: rawMetadataScopeHint ??
|
||||
followupSeed.metadataScopeHint ??
|
||||
@@ -1506,6 +1561,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
);
|
||||
const groundedFollowupEntity = metadataScopedLaneWithoutSubject
|
||||
? null
|
||||
: rawEntitySearchOverridesStaleScope
|
||||
? null
|
||||
: explicitCurrentCounterpartyOverridesFollowupEntity
|
||||
? null
|
||||
: followupSeed.counterparty ?? followupSeed.discoveryEntity;
|
||||
@@ -1514,10 +1571,16 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, entityResolutionClarificationCandidate);
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, rawEntityCandidate);
|
||||
for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) {
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, candidate);
|
||||
if (!rawEntitySearchOverridesStaleScope || sameScopedName(candidate, rawEntityCandidate)) {
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, candidate);
|
||||
}
|
||||
}
|
||||
if (!rawEntitySearchOverridesStaleScope || sameScopedName(normalizedPredecomposeCounterparty, rawEntityCandidate)) {
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, normalizedPredecomposeCounterparty);
|
||||
}
|
||||
if (!rawEntitySearchOverridesStaleScope) {
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, followupSeed.counterparty);
|
||||
}
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, normalizedPredecomposeCounterparty);
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, followupSeed.counterparty);
|
||||
} else {
|
||||
if (groundedFollowupEntity) {
|
||||
pushUnique(entityCandidates, groundedFollowupEntity);
|
||||
@@ -1562,8 +1625,11 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
pushUnique(entityCandidates, followupSeed.organization);
|
||||
}
|
||||
const explicitOrganizationScope =
|
||||
valueFlowOrganizationStaysScope || !openScopeValueFlowWithoutCounterparty
|
||||
? currentTurnOrganizationScope ?? followupSeed.organization
|
||||
rawEntitySearchOverridesStaleScope && !currentTurnFreshOrganizationScope
|
||||
? null
|
||||
: valueFlowOrganizationStaysScope || !openScopeValueFlowWithoutCounterparty
|
||||
? (rawEntitySearchOverridesStaleScope ? currentTurnFreshOrganizationScope : currentTurnOrganizationScope) ??
|
||||
followupSeed.organization
|
||||
: null;
|
||||
if (
|
||||
explicitCurrentCounterpartyCandidate &&
|
||||
@@ -1609,13 +1675,18 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
(valueFlowOrganizationStaysScope && (Boolean(followupSeed.rankingNeed) || bidirectionalValueFlowSignal)))
|
||||
);
|
||||
const normalizedPredecomposeDateScope =
|
||||
suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(predecomposeDateScope) ? null : predecomposeDateScope;
|
||||
(rawEntitySearchOverridesStaleScope && !currentTurnCarriesExplicitPeriod) ||
|
||||
(suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(predecomposeDateScope))
|
||||
? null
|
||||
: predecomposeDateScope;
|
||||
const normalizedAssistantTurnMeaningDateScope =
|
||||
suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(assistantTurnMeaningDateScope)
|
||||
rawEntitySearchOverridesStaleScope ||
|
||||
(suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(assistantTurnMeaningDateScope))
|
||||
? null
|
||||
: assistantTurnMeaningDateScope;
|
||||
const normalizedFollowupDateScope =
|
||||
suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(followupSeed.dateScope)
|
||||
rawEntitySearchOverridesStaleScope ||
|
||||
(suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(followupSeed.dateScope))
|
||||
? null
|
||||
: followupSeed.dateScope;
|
||||
const explicitDateScope =
|
||||
@@ -1670,7 +1741,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
: rawAction ?? seededAction,
|
||||
asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis,
|
||||
seeded_ranking_need:
|
||||
valueFlowSignal && followupSeed.rankingNeed ? followupSeed.rankingNeed : undefined,
|
||||
valueFlowSignal && followupSeed.rankingNeed && !rawEntitySearchOverridesStaleScope
|
||||
? followupSeed.rankingNeed
|
||||
: undefined,
|
||||
explicit_entity_candidates: entityCandidates,
|
||||
metadata_ambiguity_entity_sets:
|
||||
metadataAmbiguityLaneClarificationApplicable && followupSeed.metadataAmbiguityEntitySets.length > 0
|
||||
@@ -1782,9 +1855,11 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
groundedValueFlowFollowupApplicable
|
||||
});
|
||||
const hasTurnMeaning = Object.keys(cleanTurnMeaning).length > 0;
|
||||
const sourceSignal: AssistantMcpDiscoveryTurnInputSource = assistantTurnMeaning
|
||||
? "assistant_turn_meaning"
|
||||
: followupDiscoverySeedApplicable ||
|
||||
const sourceSignal: AssistantMcpDiscoveryTurnInputSource = rawEntitySearchOverridesStaleScope
|
||||
? "raw_text"
|
||||
: assistantTurnMeaning
|
||||
? "assistant_turn_meaning"
|
||||
: followupDiscoverySeedApplicable ||
|
||||
Boolean(entityResolutionClarificationCandidate) ||
|
||||
effectiveMetadataFollowupSeedApplicable ||
|
||||
metadataAmbiguityLaneClarificationApplicable
|
||||
@@ -1925,7 +2000,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
) {
|
||||
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
|
||||
}
|
||||
if (followupSeed.counterparty) {
|
||||
if (followupSeed.counterparty && !rawEntitySearchOverridesStaleScope) {
|
||||
pushReason(reasonCodes, "mcp_discovery_counterparty_from_followup_context");
|
||||
}
|
||||
if (followupDateScopeApplied) {
|
||||
|
||||
@@ -1888,7 +1888,7 @@ function textMojibakeScoreForAddress(value) {
|
||||
const source = String(value ?? "");
|
||||
const cyrillic = (source.match(/[А-Яа-яЁё]/g) ?? []).length;
|
||||
const latin = (source.match(/[A-Za-z]/g) ?? []).length;
|
||||
const hardMarkers = (source.match(/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ�?’“”•–—™љ›њќћџ]/g) ?? []).length;
|
||||
const hardMarkers = (source.match(/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ\uFFFD?’“”•–—™љ›њќћџ]/g) ?? []).length;
|
||||
const pairMarkers = (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length;
|
||||
const doubleEncodedMarkers = (source.match(/(?:Г[Ђ-џ]|В[Ђ-џ]|Ã.|Â.)/gu) ?? []).length;
|
||||
return cyrillic + latin - hardMarkers * 3 - pairMarkers * 2 - doubleEncodedMarkers * 2;
|
||||
@@ -1898,7 +1898,7 @@ function looksLikeMojibakeForAddress(value) {
|
||||
if (!source.trim()) {
|
||||
return false;
|
||||
}
|
||||
if (/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ�?’“”•–—™љ›њќћџ]/.test(source)) {
|
||||
if (/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ\uFFFD?’“”•–—™љ›њќћџ]/.test(source)) {
|
||||
return true;
|
||||
}
|
||||
if ((source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length >= 2) {
|
||||
@@ -2129,7 +2129,7 @@ function normalizeCounterpartyForFollowupMatch(value) {
|
||||
return compactWhitespace(repairAddressMojibake(String(value ?? ""))
|
||||
.toLowerCase()
|
||||
.replace(/ё/g, "е")
|
||||
.replace(/[«»"'`“”„’�?]/g, " ")
|
||||
.replace(/[«»"'`“”„’\uFFFD?]/g, " ")
|
||||
.replace(/[^a-zа-я0-9\s._-]+/giu, " "));
|
||||
}
|
||||
function normalizeCounterpartyTokenForFollowupMatch(value) {
|
||||
@@ -2175,7 +2175,7 @@ function extractDisplayedAddressEntityCandidates(replyText, entityType = "unknow
|
||||
if (parts.length >= 2 && /^\d{4}-\d{2}-\d{2}/.test(parts[0] ?? "")) {
|
||||
counterpartyCandidate = parts[1] ?? counterpartyCandidate;
|
||||
}
|
||||
const cleanedCandidate = compactWhitespace(counterpartyCandidate.replace(/^["'«»“”„`’�?]+|["'«»“”„`’�?]+$/gu, ""));
|
||||
const cleanedCandidate = compactWhitespace(counterpartyCandidate.replace(/^["'«»“”„`’\uFFFD?]+|["'«»“”„`’\uFFFD?]+$/gu, ""));
|
||||
if (!cleanedCandidate || cleanedCandidate.length < 2) {
|
||||
continue;
|
||||
}
|
||||
@@ -3224,6 +3224,11 @@ function hasSameDateAccountFollowupSignalForPredecompose(text) {
|
||||
/(?:^|\s)по\s+\d{2}(?:[.,]\d{1,2})?(?=$|[\s,.;:!?])/iu.test(source) ||
|
||||
/\b\d{2}(?:[.,]\d{1,2})\b/u.test(source));
|
||||
}
|
||||
function isCounterpartyDrilldownIntentForPredecompose(intent) {
|
||||
return intent === "list_documents_by_counterparty" ||
|
||||
intent === "bank_operations_by_counterparty" ||
|
||||
intent === "list_contracts_by_counterparty";
|
||||
}
|
||||
function hasPredecomposeDiagnosticUncertaintyLead(text) {
|
||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
||||
if (!normalized) {
|
||||
@@ -3442,8 +3447,16 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
||||
const sourceHasExplicitAccountAnchor = (0, addressIntentResolver_1.hasAccountNumberAnchor)(repairedSourceMessage || userMessage) ||
|
||||
(0, addressIntentResolver_1.hasCompactAccountCodeToken)(repairedSourceMessage || userMessage);
|
||||
const candidateInjectsAccountAnchor = Boolean(toNonEmptyString(candidatePredecomposeContract?.entities?.account));
|
||||
if (sourceIntentResolution.intent === "inventory_on_hand_as_of_date" &&
|
||||
candidateIntentResolution.intent === "inventory_on_hand_as_of_date" &&
|
||||
const sourceAnchorQuality = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage);
|
||||
const candidateAccountInjectedIntoCounterpartyAnchor = isCounterpartyDrilldownIntentForPredecompose(sourceIntentResolution.intent) &&
|
||||
sourceIntentResolution.intent === candidateIntentResolution.intent &&
|
||||
sourceAnchorQuality.anchorType === "counterparty" &&
|
||||
sourceAnchorQuality.quality >= 2 &&
|
||||
!sourceHasExplicitAccountAnchor &&
|
||||
candidateInjectsAccountAnchor;
|
||||
if (((sourceIntentResolution.intent === "inventory_on_hand_as_of_date" &&
|
||||
candidateIntentResolution.intent === "inventory_on_hand_as_of_date") ||
|
||||
candidateAccountInjectedIntoCounterpartyAnchor) &&
|
||||
!sourceHasExplicitAccountAnchor &&
|
||||
candidateInjectsAccountAnchor) {
|
||||
return attachAddressPredecomposeContract({
|
||||
@@ -3456,10 +3469,9 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
||||
reason: "normalized_fragment_rejected_anchor_injection",
|
||||
fallbackRuleHit: null,
|
||||
sanitizedUserMessage,
|
||||
semanticHints: candidateMeta?.semanticHints ?? null
|
||||
semanticHints: null
|
||||
}, userMessage);
|
||||
}
|
||||
const sourceAnchorQuality = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage);
|
||||
const candidateAnchorQuality = evaluateAddressAnchorQuality(candidate);
|
||||
const sameIntentForAnchorSafety = sourceAnchorQuality.intent !== "unknown" && sourceAnchorQuality.intent === candidateAnchorQuality.intent;
|
||||
const sourceSelectedObjectItemAnchorValue = toNonEmptyString((0, addressFilterExtractor_1.extractSelectedObjectQuotedValue)(userMessage)) ??
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { AssistantService } from "../src/services/assistantService";
|
||||
import { AssistantSessionStore } from "../src/services/assistantSessionStore";
|
||||
|
||||
@@ -1444,6 +1444,102 @@ describe("assistant address llm pre-decompose candidate preference", () => {
|
||||
expect(response.debug?.fallback_rule_hit).toBe("documents_counterparty_year_rewrite");
|
||||
});
|
||||
|
||||
it("rejects account injection when LLM truncates a numeric counterparty suffix", async () => {
|
||||
const calls: Array<{ message: string }> = [];
|
||||
const addressQueryService = {
|
||||
tryHandle: vi.fn(async (message: string) => {
|
||||
calls.push({ message });
|
||||
return buildAddressLaneResult(message);
|
||||
})
|
||||
} as any;
|
||||
|
||||
const normalizerService = {
|
||||
normalize: vi.fn(async () => ({
|
||||
trace_id: "norm-predecompose-counterparty-suffix-account-injection",
|
||||
ok: true,
|
||||
normalized: {
|
||||
schema_version: "normalized_query_v2_0_2",
|
||||
user_message_raw: "Покажи документы по Жуковке 51.",
|
||||
message_in_scope: true,
|
||||
scope_confidence: "high",
|
||||
contains_multiple_tasks: false,
|
||||
fragments: [
|
||||
{
|
||||
fragment_id: "F1",
|
||||
raw_fragment_text: "Покажи документы по Жуковке 51.",
|
||||
normalized_fragment_text: "Показать документы, связанные с контрагентом Жуковка по счету 51",
|
||||
domain_relevance: "in_scope",
|
||||
business_scope: "company_specific_accounting",
|
||||
entity_hints: ["Жуковка"],
|
||||
account_hints: ["51"],
|
||||
document_hints: ["документы"],
|
||||
register_hints: [],
|
||||
semantic_hints: {
|
||||
scope_target_kind: "counterparty",
|
||||
scope_target_text: "Жуковка",
|
||||
date_scope_kind: "implicit_current",
|
||||
self_scope_detected: false,
|
||||
selected_object_scope_detected: false
|
||||
},
|
||||
time_scope: { type: "unspecified", value: null, confidence: "low" },
|
||||
flags: {
|
||||
has_multi_entity_scope: false,
|
||||
asks_for_chain_explanation: false,
|
||||
asks_for_ranking_or_top: false,
|
||||
asks_for_period_summary: false,
|
||||
asks_for_rule_check: false,
|
||||
asks_for_anomaly_scan: false,
|
||||
asks_for_exact_object_trace: false,
|
||||
asks_for_evidence: false,
|
||||
mentions_period_close_context: false
|
||||
},
|
||||
candidate_labels: ["simple_factual"],
|
||||
confidence: "high",
|
||||
execution_readiness: "executable",
|
||||
clarification_reason: null,
|
||||
soft_assumption_used: [],
|
||||
route_status: "routed",
|
||||
no_route_reason: null
|
||||
}
|
||||
],
|
||||
discarded_fragments: [],
|
||||
global_notes: { needs_clarification: false, clarification_reason: null }
|
||||
},
|
||||
raw_model_output: null,
|
||||
validation: { passed: true, errors: [] },
|
||||
usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 },
|
||||
latency_ms: 10,
|
||||
prompt_version: "normalizer_v2_0_2",
|
||||
schema_version: "v2_0_2",
|
||||
request_count_for_case: 1
|
||||
}))
|
||||
} as any;
|
||||
|
||||
const sessions = new AssistantSessionStore();
|
||||
const service = new AssistantService(
|
||||
normalizerService,
|
||||
sessions as any,
|
||||
{} as any,
|
||||
{ persistSession: vi.fn() } as any,
|
||||
addressQueryService
|
||||
);
|
||||
|
||||
const response = await service.handleMessage({
|
||||
session_id: `asst-predecompose-counterparty-suffix-${Date.now()}`,
|
||||
user_message: "Покажи документы по Жуковке 51.",
|
||||
llmProvider: "local",
|
||||
useMock: false
|
||||
} as any);
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0].message).toBe("Покажи документы по Жуковке 51.");
|
||||
expect(response.debug?.llm_decomposition_applied).toBe(false);
|
||||
expect(response.debug?.llm_decomposition_reason).toBe("normalized_fragment_rejected_anchor_injection");
|
||||
expect(response.debug?.llm_predecompose_contract?.entities?.account).toBeNull();
|
||||
expect(response.debug?.llm_predecompose_contract?.entities?.counterparty).toBe("Жуковке 51");
|
||||
});
|
||||
|
||||
it("rewrites payment-style counterparty phrasing to bank operations", async () => {
|
||||
const calls: Array<{ message: string }> = [];
|
||||
const addressQueryService = {
|
||||
|
||||
@@ -341,6 +341,62 @@ describe("assistant MCP discovery answer adapter", () => {
|
||||
expect(draft.next_step_line).toContain("организац");
|
||||
});
|
||||
|
||||
it("renders confirmed ranked value-flow without raw technical evidence lines", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
dataNeedGraph: {
|
||||
schema_version: "assistant_data_need_graph_v1",
|
||||
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||
subject_candidates: [],
|
||||
business_fact_family: "value_flow",
|
||||
action_family: "turnover",
|
||||
aggregation_need: null,
|
||||
time_scope_need: "explicit_period",
|
||||
comparison_need: null,
|
||||
ranking_need: "top_desc",
|
||||
proof_expectation: "coverage_checked_fact",
|
||||
clarification_gaps: [],
|
||||
decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"],
|
||||
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
|
||||
reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"]
|
||||
},
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
explicit_organization_scope: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "counterparty_value_or_turnover"
|
||||
}
|
||||
});
|
||||
const pilot = await executeAssistantMcpDiscoveryPilot(
|
||||
planner,
|
||||
buildDeps([
|
||||
{
|
||||
Period: "2020-01-15T00:00:00",
|
||||
Amount: 12000,
|
||||
Counterparty: "\u0421\u0411\u0415\u0420\u0411\u0410\u041d\u041a, \u041f\u0410\u041e",
|
||||
Organization: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
|
||||
},
|
||||
{
|
||||
Period: "2020-02-20T00:00:00",
|
||||
Amount: 5000,
|
||||
Counterparty: "\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a",
|
||||
Organization: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
const userText = [...draft.confirmed_lines, ...draft.inference_lines, ...draft.unknown_lines].join("\n");
|
||||
|
||||
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
|
||||
expect(draft.confirmed_lines).toHaveLength(1);
|
||||
expect(userText).toContain("\u0411\u043e\u043b\u044c\u0448\u0435 \u0432\u0441\u0435\u0433\u043e \u0434\u0435\u043d\u0435\u0433 \u043f\u0440\u0438\u043d\u0451\u0441 \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442");
|
||||
expect(userText).toContain("\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441");
|
||||
expect(userText).not.toContain("1C incoming value-flow");
|
||||
expect(userText).not.toContain("Full ranking outside");
|
||||
expect(draft.unknown_lines[0]).toContain("\u041f\u043e\u043b\u043d\u044b\u0439 \u0440\u0435\u0439\u0442\u0438\u043d\u0433");
|
||||
});
|
||||
|
||||
it("asks for both organization and period when an open total still misses both axes", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
dataNeedGraph: {
|
||||
|
||||
@@ -1426,6 +1426,45 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("lets an explicit current entity search override stale ranking follow-up scope", () => {
|
||||
const staleEntity = "\u043f\u0440\u0438\u043d\u0451\u0441 \u043d\u0430\u0438\u0431\u043e\u043b\u044c\u0448\u0443\u044e \u0432\u044b\u0440\u0443\u0447\u043a\u0443 \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u0438 \u0432";
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage:
|
||||
"\u0422\u0435\u043f\u0435\u0440\u044c \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u0430\u044f \u0442\u0435\u043c\u0430 \u043f\u043e \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u043c\u0443 \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0443. \u041d\u0430\u0439\u0434\u0438 \u0432 1\u0421 \u0413\u0440\u0443\u043f\u043f\u0443 \u0421\u0412\u041a.",
|
||||
assistantTurnMeaning: {
|
||||
asked_domain_family: "entity_resolution",
|
||||
asked_action_family: "search_business_entity",
|
||||
explicit_entity_candidates: [staleEntity],
|
||||
explicit_organization_scope: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
|
||||
explicit_date_scope: "2021",
|
||||
unsupported_but_understood_family: "entity_resolution",
|
||||
stale_replay_forbidden: true
|
||||
},
|
||||
followupContext: {
|
||||
previous_discovery_pilot_scope: "counterparty_value_flow_query_movements_v1",
|
||||
previous_discovery_ranking_need: "top_desc",
|
||||
previous_filters: {
|
||||
organization: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
|
||||
counterparty: staleEntity,
|
||||
period_from: "2021-01-01",
|
||||
period_to: "2021-12-31"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.adapter_status).toBe("ready");
|
||||
expect(result.should_run_discovery).toBe(true);
|
||||
expect(result.source_signal).toBe("raw_text");
|
||||
expect(result.turn_meaning_ref?.explicit_entity_candidates).toEqual(["\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a"]);
|
||||
expect(result.turn_meaning_ref?.metadata_scope_hint).toBeUndefined();
|
||||
expect(result.turn_meaning_ref?.explicit_organization_scope).toBeUndefined();
|
||||
expect(result.turn_meaning_ref?.explicit_date_scope).toBeUndefined();
|
||||
expect(result.turn_meaning_ref?.seeded_ranking_need).toBeUndefined();
|
||||
expect(result.data_need_graph?.subject_candidates).toEqual(["\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a"]);
|
||||
expect(result.reason_codes).toContain("mcp_discovery_entity_scope_from_raw_entity_search");
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_followup_context");
|
||||
});
|
||||
|
||||
it("marks top-value wording as a ranking data need without inventing a missing subject gap", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage: "кто больше всего принес денег в 2020"
|
||||
@@ -1444,6 +1483,46 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps open organization ranking subjectless when assistant meaning invents a predicate-shaped entity", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage:
|
||||
"\u0418 \u043a\u0442\u043e \u0431\u043e\u043b\u044c\u0448\u0435 \u0432\u0441\u0435\u0433\u043e \u043f\u0440\u0438\u043d\u0435\u0441 \u0434\u0435\u043d\u0435\u0433 \u044d\u0442\u043e\u0439 \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u0438 \u0432 2020 \u0433\u043e\u0434\u0443?",
|
||||
assistantTurnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
explicit_entity_candidates: [
|
||||
"\u043f\u0440\u0438\u043d\u0451\u0441 \u043d\u0430\u0438\u0431\u043e\u043b\u044c\u0448\u0443\u044e \u0432\u044b\u0440\u0443\u0447\u043a\u0443 \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u0438 \u0432"
|
||||
],
|
||||
explicit_organization_scope: "\u044d\u0442\u0430 \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044f",
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "counterparty_value_or_turnover",
|
||||
stale_replay_forbidden: true
|
||||
},
|
||||
predecomposeContract: {
|
||||
entities: {
|
||||
organization: "\u044d\u0442\u0430 \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044f"
|
||||
}
|
||||
},
|
||||
followupContext: {
|
||||
previous_discovery_pilot_scope: "counterparty_value_flow_query_movements_v1",
|
||||
previous_filters: {
|
||||
organization: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.adapter_status).toBe("ready");
|
||||
expect(result.should_run_discovery).toBe(true);
|
||||
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||
expect(result.turn_meaning_ref?.explicit_organization_scope).toBe(
|
||||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
|
||||
);
|
||||
expect(result.turn_meaning_ref?.explicit_date_scope).toBe("2020");
|
||||
expect(result.data_need_graph?.subject_candidates).toEqual([]);
|
||||
expect(result.data_need_graph?.ranking_need).toBe("top_desc");
|
||||
expect(result.data_need_graph?.decomposition_candidates).toContain("aggregate_ranked_axis_values");
|
||||
});
|
||||
|
||||
it("keeps organization as scope for open bidirectional comparison wording instead of inventing a subject candidate", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage: "что больше: входящие или исходящие деньги за 2020 год по ООО Альтернатива Плюс?",
|
||||
|
||||
Reference in New Issue
Block a user