ARCH: усилить semantic integrity и добавить human AGENT сценарии

This commit is contained in:
2026-04-23 19:10:28 +03:00
parent 62e2b3323f
commit 65d15c156b
29 changed files with 1966 additions and 93 deletions
@@ -5,6 +5,7 @@ exports.cloneAddressNavigationState = cloneAddressNavigationState;
exports.normalizeAddressNavigationState = normalizeAddressNavigationState;
exports.evolveAddressNavigationStateWithAssistantItem = evolveAddressNavigationStateWithAssistantItem;
const nanoid_1 = require("nanoid");
const assistantContinuityPolicy_1 = require("./assistantContinuityPolicy");
const addressNavigation_1 = require("../types/addressNavigation");
const MAX_RESULT_SETS = 40;
const MAX_NAVIGATION_EVENTS = 120;
@@ -242,24 +243,32 @@ function resolveNavigationAction(debug, hasFocusObject) {
}
return hasFocusObject ? "drilldown" : "open";
}
function buildFocusObjectFromDebug(debug, resultSetId, createdAt) {
const extractedFilters = toObject(debug.extracted_filters) ?? {};
const rawValue = toNonEmptyString(debug.anchor_value_resolved) ??
toNonEmptyString(debug.anchor_value_raw) ??
toNonEmptyString(extractedFilters.item);
if (!rawValue) {
return null;
}
const objectType = toAddressFocusObjectType(debug.anchor_type);
const canonicalType = objectType === "unknown" ? inferDisplayEntityType(toAddressIntent(debug.detected_intent)) : objectType;
function buildFocusObject(objectType, label, resultSetId, createdAt) {
return {
object_type: canonicalType,
object_id: `${canonicalType}:${rawValue}`.toLowerCase(),
label: rawValue,
object_type: objectType,
object_id: `${objectType}:${label}`.toLowerCase(),
label,
provenance_result_set_id: resultSetId,
selected_at: createdAt
};
}
function buildFocusObjectFromDebug(debug, resultSetId, createdAt) {
const extractedFilters = toObject(debug.extracted_filters) ?? {};
const objectType = toAddressFocusObjectType(debug.anchor_type);
const canonicalType = objectType === "unknown" ? inferDisplayEntityType(toAddressIntent(debug.detected_intent)) : objectType;
if (canonicalType === "item") {
const item = (0, assistantContinuityPolicy_1.readAddressDebugItem)(debug, toNonEmptyString);
return item ? buildFocusObject(canonicalType, item, resultSetId, createdAt) : null;
}
if (canonicalType === "counterparty" && debug.mcp_discovery_response_applied === true) {
const counterparty = (0, assistantContinuityPolicy_1.readAddressDebugCounterparty)(debug, toNonEmptyString);
return counterparty ? buildFocusObject(canonicalType, counterparty, resultSetId, createdAt) : null;
}
const rawValue = toNonEmptyString(debug.anchor_value_resolved) ??
toNonEmptyString(debug.anchor_value_raw) ??
toNonEmptyString(extractedFilters.item);
return rawValue ? buildFocusObject(canonicalType, rawValue, resultSetId, createdAt) : null;
}
function capResultSets(resultSets) {
if (resultSets.length <= MAX_RESULT_SETS) {
return resultSets;
@@ -0,0 +1,73 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.repairAddressMojibakeText = repairAddressMojibakeText;
exports.normalizeRussianComparableText = normalizeRussianComparableText;
const iconv_lite_1 = __importDefault(require("iconv-lite"));
function compactWhitespace(value) {
return value.replace(/\s+/g, " ").trim();
}
function textMojibakeScore(value) {
const source = String(value ?? "");
const cyrillic = (source.match(/[\u0400-\u04ff]/g) ?? []).length;
const latin = (source.match(/[A-Za-z]/g) ?? []).length;
const replacement = (source.match(/[�]/g) ?? []).length;
const pairMarkers = (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length;
const doubleEncodedMarkers = (source.match(/(?:Р“[Р-џ]|Р’[Р-џ]|Ã.|Â.)/gu) ?? []).length;
return cyrillic + latin - replacement * 3 - pairMarkers * 2 - doubleEncodedMarkers * 2;
}
function looksLikeAddressMojibake(value) {
const source = String(value ?? "");
if (!source.trim()) {
return false;
}
if (/[�]/.test(source)) {
return true;
}
if ((source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length >= 2) {
return true;
}
if ((source.match(/(?:Р“[Р-џ]|Р’[Р-џ]|Ã.|Â.)/gu) ?? []).length >= 2) {
return true;
}
return false;
}
function repairAddressMojibakeText(value) {
const source = String(value ?? "");
if (!looksLikeAddressMojibake(source)) {
return source;
}
let candidate = source;
for (let pass = 0; pass < 3; pass += 1) {
let improved = false;
try {
const fromWin1251 = iconv_lite_1.default.encode(candidate, "win1251").toString("utf8");
if (textMojibakeScore(fromWin1251) > textMojibakeScore(candidate)) {
candidate = fromWin1251;
improved = true;
}
}
catch {
// Ignore decode failures and keep the current candidate.
}
try {
const fromLatin1 = Buffer.from(candidate, "latin1").toString("utf8");
if (textMojibakeScore(fromLatin1) > textMojibakeScore(candidate)) {
candidate = fromLatin1;
improved = true;
}
}
catch {
// Ignore decode failures and keep the current candidate.
}
if (!improved) {
break;
}
}
return candidate;
}
function normalizeRussianComparableText(value) {
return compactWhitespace(repairAddressMojibakeText(String(value ?? "")).toLowerCase()).replace(/ё/g, "е");
}
@@ -77,12 +77,17 @@ function shouldPreferRawFollowupMessage(userMessage, addressInputMessage, carryo
const previousIntent = toNonEmptyString(followupContext?.previous_intent);
const rootIntent = toNonEmptyString(followupContext?.root_intent);
const previousAnchorType = toNonEmptyString(followupContext?.previous_anchor_type);
const hasReferentialDocumentExclusionFollowupCue = /(?:\u043a\u0440\u043e\u043c\u0435|\u043f\u043e\u043c\u0438\u043c\u043e)\s+(?:\u044d\u0442\u043e\u0433\u043e|\u044d\u0442\u043e\u0439|\u044d\u0442\u043e\u0442|\u044d\u0442\u0443|\u044d\u0442\u0438\u0445)(?:\s+(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430|\u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0430|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430))?/iu.test(rawMessage);
const hasInventoryItemCarryover = previousAnchorType === "item" && isInventorySelectedObjectOrRootIntent(previousIntent);
const hasInventoryFrameCarryover = isInventorySelectedObjectOrRootIntent(previousIntent) ||
isInventorySelectedObjectOrRootIntent(rootIntent);
const hasDocumentCarryover = previousIntent === "list_documents_by_counterparty" || previousIntent === "list_documents_by_contract";
if (mode === "unsupported" && intent === "unknown") {
return true;
}
if (hasDocumentCarryover && hasReferentialDocumentExclusionFollowupCue) {
return true;
}
if (hasSameDateFollowupSignal(rawMessage) && hasExplicitCurrentDateSignal(canonicalMessage)) {
return true;
}
@@ -51,6 +51,7 @@ exports.resolveAssistantContinuitySnapshot = resolveAssistantContinuitySnapshot;
exports.resolveAssistantOrganizationAuthority = resolveAssistantOrganizationAuthority;
exports.resolveOrganizationClarificationContinuation = resolveOrganizationClarificationContinuation;
const assistantOrganizationMatcher_1 = require("./assistantOrganizationMatcher");
const addressTextRepair_1 = require("./addressTextRepair");
function fallbackToNonEmptyString(value) {
if (value === null || value === undefined) {
return null;
@@ -365,14 +366,45 @@ function readAddressDebugItem(debug, toNonEmptyString = fallbackToNonEmptyString
? toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw)
: null));
}
function readAddressDebugCounterparty(debug, toNonEmptyString = fallbackToNonEmptyString) {
const extractedFilters = readAddressDebugFilters(debug);
if (toNonEmptyString(extractedFilters?.counterparty)) {
return toNonEmptyString(extractedFilters?.counterparty);
function isReferentialCounterpartyPlaceholder(value) {
if (!value) {
return false;
}
if (String(debug?.anchor_type ?? "") === "counterparty") {
return toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw);
return new Set([
"он",
"она",
"оно",
"они",
"ему",
"ней",
"нему",
"ним",
"ними",
"его",
"ее",
"их",
"этому",
"этой",
"этом",
"этим",
"эта",
"этот",
"эти"
]).has((0, addressTextRepair_1.normalizeRussianComparableText)(value));
}
function normalizeCounterpartyCandidate(value, toNonEmptyString) {
const text = toNonEmptyString(value);
if (!text || isReferentialCounterpartyPlaceholder(text)) {
return null;
}
return text;
}
function sameCounterpartyCandidate(left, right) {
return Boolean(left &&
right &&
(0, addressTextRepair_1.normalizeRussianComparableText)(left) === (0, addressTextRepair_1.normalizeRussianComparableText)(right));
}
function readGroundedDiscoveryCounterparty(debug, toNonEmptyString = fallbackToNonEmptyString) {
const discoveryPilotScope = readAssistantMcpDiscoveryPilotScope(debug, toNonEmptyString);
const suppressDiscoveryEntityCarryover = discoveryPilotScope === "metadata_inspection_v1" ||
readAssistantMcpDiscoveryLoopSubjectResolutionOptional(debug);
@@ -381,12 +413,27 @@ function readAddressDebugCounterparty(debug, toNonEmptyString = fallbackToNonEmp
}
const discoveryEntities = collectAssistantMcpDiscoveryEntityCandidates(debug, toNonEmptyString);
for (const entity of discoveryEntities) {
const text = toNonEmptyString(entity);
if (text) {
return text;
const normalized = normalizeCounterpartyCandidate(entity, toNonEmptyString);
if (normalized) {
return normalized;
}
}
return null;
return normalizeCounterpartyCandidate(readAssistantMcpDiscoveryLoopMetadataScopeHint(debug, toNonEmptyString), toNonEmptyString);
}
function readAddressDebugCounterparty(debug, toNonEmptyString = fallbackToNonEmptyString) {
const extractedFilters = readAddressDebugFilters(debug);
const extractedCounterparty = normalizeCounterpartyCandidate(extractedFilters?.counterparty, toNonEmptyString);
const anchorCounterparty = String(debug?.anchor_type ?? "") === "counterparty"
? normalizeCounterpartyCandidate(toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw), toNonEmptyString)
: null;
const groundedDiscoveryCounterparty = readGroundedDiscoveryCounterparty(debug, toNonEmptyString);
if (hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) && groundedDiscoveryCounterparty) {
if (!extractedCounterparty || !sameCounterpartyCandidate(extractedCounterparty, groundedDiscoveryCounterparty)) {
return groundedDiscoveryCounterparty;
}
return extractedCounterparty;
}
return extractedCounterparty ?? anchorCounterparty ?? groundedDiscoveryCounterparty;
}
function readAddressDebugIntent(debug, toNonEmptyString = fallbackToNonEmptyString) {
const detectedIntent = toNonEmptyString(debug?.detected_intent);
@@ -431,8 +478,16 @@ function readAddressDebugTemporalScope(debug, toNonEmptyString = fallbackToNonEm
}
function resolveAddressDebugAnchorContext(debug, toNonEmptyString = fallbackToNonEmptyString) {
const explicitAnchorType = toNonEmptyString(debug?.anchor_type);
const explicitAnchorValue = toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw);
if (explicitAnchorType || explicitAnchorValue) {
const explicitAnchorValueRaw = toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw);
const explicitAnchorValue = explicitAnchorType === "counterparty"
? normalizeCounterpartyCandidate(explicitAnchorValueRaw, toNonEmptyString)
: explicitAnchorValueRaw;
const groundedDiscoveryCounterparty = readGroundedDiscoveryCounterparty(debug, toNonEmptyString);
const shouldPreferDiscoveryCounterparty = explicitAnchorType === "counterparty" &&
Boolean(groundedDiscoveryCounterparty &&
hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) &&
(!explicitAnchorValue || !sameCounterpartyCandidate(explicitAnchorValue, groundedDiscoveryCounterparty)));
if ((explicitAnchorType || explicitAnchorValue) && !shouldPreferDiscoveryCounterparty) {
return {
anchorType: explicitAnchorType,
anchorValue: explicitAnchorValue
@@ -446,8 +501,11 @@ function resolveAddressDebugAnchorContext(debug, toNonEmptyString = fallbackToNo
anchorValue: item
};
}
const counterparty = toNonEmptyString(extractedFilters?.counterparty);
if (counterparty) {
const counterparty = normalizeCounterpartyCandidate(extractedFilters?.counterparty, toNonEmptyString);
if (counterparty &&
!(groundedDiscoveryCounterparty &&
hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) &&
!sameCounterpartyCandidate(counterparty, groundedDiscoveryCounterparty))) {
return {
anchorType: "counterparty",
anchorValue: counterparty
@@ -512,7 +570,9 @@ function resolveAddressDebugCarryoverFilters(debug, toNonEmptyString = fallbackT
Boolean(discoveryDateScope.asOfDate || discoveryDateScope.periodFrom || discoveryDateScope.periodTo);
const counterparty = readAddressDebugCounterparty(debug, toNonEmptyString);
const organization = readAddressDebugOrganization(debug, toNonEmptyString);
if (counterparty && !toNonEmptyString(nextFilters.counterparty)) {
const preferGroundedDiscoveryCounterparty = hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) && Boolean(counterparty);
const existingCounterparty = normalizeCounterpartyCandidate(nextFilters.counterparty, toNonEmptyString);
if (counterparty && (preferGroundedDiscoveryCounterparty || !existingCounterparty)) {
nextFilters.counterparty = counterparty;
}
if (organization && !toNonEmptyString(nextFilters.organization)) {
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = void 0;
exports.buildAssistantMcpDiscoveryTurnInput = buildAssistantMcpDiscoveryTurnInput;
const assistantMcpDiscoveryDataNeedGraph_1 = require("./assistantMcpDiscoveryDataNeedGraph");
const addressTextRepair_1 = require("./addressTextRepair");
exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = "assistant_mcp_discovery_turn_input_v1";
function toRecordObject(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -38,7 +39,32 @@ function pushUnique(target, value) {
}
}
function isReferentialEntityPlaceholder(value) {
return /^(?:\u043d\u0435\u043c\u0443|\u043d\u0435\u0439|\u043d\u0438\u043c|\u043d\u0438\u043c\u0438|\u0435\u0433\u043e|\u0435\u0435|\u0435\u0451|\u0438\u0445|\u044d\u0442\u043e\u043c\u0443|\u044d\u0442\u043e\u0439|\u044d\u0442\u0438\u043c|\u044d\u0442\u0438\u043c\u0438|\u044d\u0442\u043e\u043c)$/iu.test(value.trim());
return new Set([
"он",
"она",
"оно",
"они",
"ему",
"ней",
"нему",
"ним",
"ними",
"его",
"ее",
"их",
"этому",
"этой",
"этим",
"этими",
"этом"
]).has((0, addressTextRepair_1.normalizeRussianComparableText)(value));
}
function normalizeFollowupCounterpartyCandidate(value) {
const text = candidateValue(value);
if (!text || isReferentialEntityPlaceholder(text)) {
return null;
}
return text;
}
function pushScopedEntityCandidate(target, value, groundedFollowupEntity) {
const text = candidateValue(value);
@@ -347,14 +373,22 @@ function collectFollowupDiscoverySeed(followupContext) {
const entityResolutionAmbiguityCandidates = collectEntityCandidates(followupContext?.previous_discovery_entity_ambiguity_candidates);
const ambiguityBlocksImplicitGrounding = effectivePilotScope === "entity_resolution_search_v1" && entityResolutionStatus === "ambiguous";
const metadataPilotCarriesScopeOnly = effectivePilotScope === "metadata_inspection_v1" || loopSubjectResolutionOptional;
const normalizedDiscoveryEntities = discoveryEntities
.map((entity) => normalizeFollowupCounterpartyCandidate(entity))
.filter((entity) => Boolean(entity));
const groundedDiscoveryCounterparty = ambiguityBlocksImplicitGrounding || metadataPilotCarriesScopeOnly
? null
: normalizedDiscoveryEntities[0] ?? normalizeFollowupCounterpartyCandidate(loopMetadataScopeHint);
const metadataScopeHint = loopMetadataScopeHint ??
(loopSubjectResolutionOptional ? discoveryEntities[0] ?? null : null);
const counterparty = toNonEmptyString(previousFilters?.counterparty) ??
toNonEmptyString(rootFilters?.counterparty) ??
(toNonEmptyString(followupContext?.previous_anchor_type) === "counterparty"
? toNonEmptyString(followupContext?.previous_anchor_value)
: null) ??
(ambiguityBlocksImplicitGrounding || metadataPilotCarriesScopeOnly ? null : discoveryEntities[0] ?? null);
(loopSubjectResolutionOptional ? normalizedDiscoveryEntities[0] ?? null : null);
const previousFiltersCounterparty = normalizeFollowupCounterpartyCandidate(previousFilters?.counterparty);
const rootFiltersCounterparty = normalizeFollowupCounterpartyCandidate(rootFilters?.counterparty);
const previousAnchorCounterparty = toNonEmptyString(followupContext?.previous_anchor_type) === "counterparty"
? normalizeFollowupCounterpartyCandidate(followupContext?.previous_anchor_value)
: null;
const counterparty = groundedDiscoveryCounterparty
? groundedDiscoveryCounterparty
: previousFiltersCounterparty ?? rootFiltersCounterparty ?? previousAnchorCounterparty;
const organization = toNonEmptyString(previousFilters?.organization) ??
toNonEmptyString(rootFilters?.organization) ??
(toNonEmptyString(followupContext?.previous_anchor_type) === "organization"
@@ -372,7 +406,7 @@ function collectFollowupDiscoverySeed(followupContext) {
loopPendingAxes,
loopProvidedAxes,
counterparty,
discoveryEntity: ambiguityBlocksImplicitGrounding || loopSubjectResolutionOptional ? null : discoveryEntities[0] ?? null,
discoveryEntity: ambiguityBlocksImplicitGrounding || loopSubjectResolutionOptional ? null : normalizedDiscoveryEntities[0] ?? null,
entityResolutionStatus,
entityResolutionAmbiguityCandidates,
rankingNeed: toNonEmptyString(followupContext?.previous_discovery_ranking_need),
@@ -472,6 +506,9 @@ function hasMetadataSignal(text) {
return (/(?:\u043e\u0431\u044a\u0435\u043a\u0442(?:\u044b|\u0430|\u043e\u0432)?|\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u044b|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b|\u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a\u0438|\u043f\u043e\u043b(?:\u0435|\u044f)|objects?|registers?|documents?|catalogs?|fields?)/iu.test(text) &&
/(?:\u0435\u0441\u0442\u044c|\u043a\u0430\u043a\u0438\u0435|\u0434\u043e\u0441\u0442\u0443\u043f\u043d|\u0432\s+1\u0441|1\u0441|available|exist|which)/iu.test(text));
}
function hasReferentialDocumentExclusionFollowupSignal(text) {
return /(?:\u043a\u0440\u043e\u043c\u0435|\u043f\u043e\u043c\u0438\u043c\u043e)\s+(?:\u044d\u0442\u043e\u0433\u043e|\u044d\u0442\u043e\u0439|\u044d\u0442\u043e\u0442|\u044d\u0442\u0443|\u044d\u0442\u0438\u0445)(?:\s+(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430|\u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0430|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430))?/iu.test(text);
}
function hasMetadataObjectHint(text) {
return /(?:\u043e\u0431\u044a\u0435\u043a\u0442(?:\u044b|\u0430|\u043e\u0432)?|\u0440\u0435\u0433\u0438\u0441\u0442\u0440(?:\u044b)?|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|\u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a(?:\u0438)?|\u043f\u043e\u043b(?:\u0435|\u044f)|objects?|registers?|documents?|catalogs?|fields?)/iu.test(text);
}
@@ -732,14 +769,20 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const reasonCodes = [];
const rawUserText = toNonEmptyString(input.userMessage);
const rawEffectiveText = toNonEmptyString(input.effectiveMessage);
const rawSignalSourceText = `${rawUserText ?? ""} ${rawEffectiveText ?? ""}`.trim();
const rawEntitySourceText = rawUserText ?? rawEffectiveText ?? rawSignalSourceText;
const repairedUserText = rawUserText ? (0, addressTextRepair_1.repairAddressMojibakeText)(rawUserText) : null;
const repairedEffectiveText = rawEffectiveText ? (0, addressTextRepair_1.repairAddressMojibakeText)(rawEffectiveText) : null;
const rawSignalSourceText = `${repairedUserText ?? rawUserText ?? ""} ${repairedEffectiveText ?? rawEffectiveText ?? ""}`.trim();
const rawEntitySourceText = repairedUserText ?? rawUserText ?? repairedEffectiveText ?? rawEffectiveText ?? rawSignalSourceText;
const rawText = compactLower(rawSignalSourceText);
const rawReferentialDocumentExclusionSignal = hasReferentialDocumentExclusionFollowupSignal(repairedUserText ?? rawUserText ?? "");
const rawLifecycleSignal = hasLifecycleSignal(rawText);
const rawBidirectionalValueFlowSignal = !rawLifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
const rawValueFlowSignal = !rawLifecycleSignal &&
(hasValueFlowSignal(rawText) || hasValueRankingSignal(rawText) || rawBidirectionalValueFlowSignal);
const rawMetadataSignal = !rawLifecycleSignal && !rawValueFlowSignal && hasMetadataSignal(rawText);
const rawMetadataSignal = !rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawReferentialDocumentExclusionSignal &&
hasMetadataSignal(rawText);
const rawEntityResolutionSignal = !rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
const rawPayoutSignal = rawValueFlowSignal && !rawBidirectionalValueFlowSignal && hasPayoutSignal(rawText);
const monthlyAggregationSignal = hasMonthlyAggregationSignal(rawText);
@@ -495,12 +495,35 @@ function createAssistantRoutePolicy(deps) {
!effectiveAddressFollowupSignal &&
resolvedModeDetection.mode === "unsupported" &&
resolvedIntentResolution.intent === "unknown");
const groundedValueFlowFollowupContextDetected = Boolean(followupContext &&
[
"counterparty_value_flow_query_movements_v1",
"counterparty_supplier_payout_query_movements_v1",
"counterparty_bidirectional_value_flow_query_movements_v1"
].includes(String(toNonEmptyString(followupContext?.previous_discovery_pilot_scope) ?? "")) &&
!dangerOrCoercionSignal &&
(toNonEmptyString(assistantTurnMeaning?.asked_domain_family) === "counterparty_value" ||
[
"turnover",
"payout",
"net_value_flow"
].includes(String(toNonEmptyString(assistantTurnMeaning?.asked_action_family) ?? "")) ||
/(?:нетто|сальдо|сколько\s+мы\s+(?:получили|заплатили)|incoming|outgoing)/iu.test(analyticsSample)));
const baseToolGatePreservesAddressLane = Boolean(baseToolGate?.runAddressLane &&
["address_intent_resolver_detected", "address_mode_classifier_detected", "address_signal_detected", "llm_canonical_data_signal_detected"].includes(String(baseToolGate?.reason ?? "")));
[
"address_intent_resolver_detected",
"address_mode_classifier_detected",
"address_signal_detected",
"llm_canonical_data_signal_detected"
].includes(String(baseToolGate?.reason ?? ""))) ||
Boolean(baseToolGate?.runAddressLane &&
String(baseToolGate?.reason ?? "") === "followup_context_detected" &&
groundedValueFlowFollowupContextDetected);
const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate &&
deterministicNonDomainGuard &&
(llmFirstUnsupportedCandidate || llmContractMode === null) &&
!baseToolGatePreservesAddressLane &&
!groundedValueFlowFollowupContextDetected &&
!protectedInventoryShortFollowup &&
!organizationClarificationContinuationDetected);
const lastAddressAssistantDebug = sessionItems
@@ -664,9 +687,11 @@ function createAssistantRoutePolicy(deps) {
const unsupportedCurrentTurnMeaningBoundary = Boolean(assistantTurnMeaning?.unsupported_but_understood_family &&
assistantTurnMeaning?.stale_replay_forbidden === true &&
!turnMeaningIntentCandidate &&
!aggregateBusinessAnalyticsSignal &&
!dataScopeMetaQuery &&
!capabilityMetaQuery &&
!dangerOrCoercionSignal &&
!groundedValueFlowFollowupContextDetected &&
!organizationClarificationContinuationDetected);
if (unsupportedCurrentTurnMeaningBoundary) {
return {
@@ -1,6 +1,10 @@
import { nanoid } from "nanoid";
import type { AssistantConversationItem } from "../types/assistant";
import type { AddressIntent } from "../types/addressQuery";
import {
readAddressDebugCounterparty,
readAddressDebugItem
} from "./assistantContinuityPolicy";
import {
ADDRESS_NAVIGATION_STATE_SCHEMA_VERSION,
type AddressFocusObject,
@@ -277,24 +281,38 @@ function resolveNavigationAction(debug: Record<string, unknown>, hasFocusObject:
return hasFocusObject ? "drilldown" : "open";
}
function buildFocusObject(
objectType: AddressFocusObjectType,
label: string,
resultSetId: string,
createdAt: string
): AddressFocusObject {
return {
object_type: objectType,
object_id: `${objectType}:${label}`.toLowerCase(),
label,
provenance_result_set_id: resultSetId,
selected_at: createdAt
};
}
function buildFocusObjectFromDebug(debug: Record<string, unknown>, resultSetId: string, createdAt: string): AddressFocusObject | null {
const extractedFilters = toObject(debug.extracted_filters) ?? {};
const objectType = toAddressFocusObjectType(debug.anchor_type);
const canonicalType = objectType === "unknown" ? inferDisplayEntityType(toAddressIntent(debug.detected_intent)) : objectType;
if (canonicalType === "item") {
const item = readAddressDebugItem(debug, toNonEmptyString);
return item ? buildFocusObject(canonicalType, item, resultSetId, createdAt) : null;
}
if (canonicalType === "counterparty" && debug.mcp_discovery_response_applied === true) {
const counterparty = readAddressDebugCounterparty(debug, toNonEmptyString);
return counterparty ? buildFocusObject(canonicalType, counterparty, resultSetId, createdAt) : null;
}
const rawValue =
toNonEmptyString(debug.anchor_value_resolved) ??
toNonEmptyString(debug.anchor_value_raw) ??
toNonEmptyString(extractedFilters.item);
if (!rawValue) {
return null;
}
const objectType = toAddressFocusObjectType(debug.anchor_type);
const canonicalType = objectType === "unknown" ? inferDisplayEntityType(toAddressIntent(debug.detected_intent)) : objectType;
return {
object_type: canonicalType,
object_id: `${canonicalType}:${rawValue}`.toLowerCase(),
label: rawValue,
provenance_result_set_id: resultSetId,
selected_at: createdAt
};
return rawValue ? buildFocusObject(canonicalType, rawValue, resultSetId, createdAt) : null;
}
function capResultSets(resultSets: AddressResultSet[]): AddressResultSet[] {
@@ -0,0 +1,74 @@
import iconv from "iconv-lite";
function compactWhitespace(value: string): string {
return value.replace(/\s+/g, " ").trim();
}
function textMojibakeScore(value: string): number {
const source = String(value ?? "");
const cyrillic = (source.match(/[\u0400-\u04ff]/g) ?? []).length;
const latin = (source.match(/[A-Za-z]/g) ?? []).length;
const replacement = (source.match(/[�]/g) ?? []).length;
const pairMarkers = (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length;
const doubleEncodedMarkers = (source.match(/(?:Р“[Р-џ]|Р’[Р-џ]|Ã.|Â.)/gu) ?? []).length;
return cyrillic + latin - replacement * 3 - pairMarkers * 2 - doubleEncodedMarkers * 2;
}
function looksLikeAddressMojibake(value: string): boolean {
const source = String(value ?? "");
if (!source.trim()) {
return false;
}
if (/[�]/.test(source)) {
return true;
}
if ((source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length >= 2) {
return true;
}
if ((source.match(/(?:Р“[Р-џ]|Р’[Р-џ]|Ã.|Â.)/gu) ?? []).length >= 2) {
return true;
}
return false;
}
export function repairAddressMojibakeText(value: string): string {
const source = String(value ?? "");
if (!looksLikeAddressMojibake(source)) {
return source;
}
let candidate = source;
for (let pass = 0; pass < 3; pass += 1) {
let improved = false;
try {
const fromWin1251 = iconv.encode(candidate, "win1251").toString("utf8");
if (textMojibakeScore(fromWin1251) > textMojibakeScore(candidate)) {
candidate = fromWin1251;
improved = true;
}
} catch {
// Ignore decode failures and keep the current candidate.
}
try {
const fromLatin1 = Buffer.from(candidate, "latin1").toString("utf8");
if (textMojibakeScore(fromLatin1) > textMojibakeScore(candidate)) {
candidate = fromLatin1;
improved = true;
}
} catch {
// Ignore decode failures and keep the current candidate.
}
if (!improved) {
break;
}
}
return candidate;
}
export function normalizeRussianComparableText(value: unknown): string {
return compactWhitespace(repairAddressMojibakeText(String(value ?? "")).toLowerCase()).replace(/ё/g, "е");
}
@@ -178,16 +178,25 @@ function shouldPreferRawFollowupMessage(
const previousIntent = toNonEmptyString(followupContext?.previous_intent);
const rootIntent = toNonEmptyString(followupContext?.root_intent);
const previousAnchorType = toNonEmptyString(followupContext?.previous_anchor_type);
const hasReferentialDocumentExclusionFollowupCue = /(?:\u043a\u0440\u043e\u043c\u0435|\u043f\u043e\u043c\u0438\u043c\u043e)\s+(?:\u044d\u0442\u043e\u0433\u043e|\u044d\u0442\u043e\u0439|\u044d\u0442\u043e\u0442|\u044d\u0442\u0443|\u044d\u0442\u0438\u0445)(?:\s+(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430|\u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0430|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430))?/iu.test(
rawMessage
);
const hasInventoryItemCarryover =
previousAnchorType === "item" && isInventorySelectedObjectOrRootIntent(previousIntent);
const hasInventoryFrameCarryover =
isInventorySelectedObjectOrRootIntent(previousIntent) ||
isInventorySelectedObjectOrRootIntent(rootIntent);
const hasDocumentCarryover =
previousIntent === "list_documents_by_counterparty" || previousIntent === "list_documents_by_contract";
if (mode === "unsupported" && intent === "unknown") {
return true;
}
if (hasDocumentCarryover && hasReferentialDocumentExclusionFollowupCue) {
return true;
}
if (hasSameDateFollowupSignal(rawMessage) && hasExplicitCurrentDateSignal(canonicalMessage)) {
return true;
}
@@ -2,6 +2,7 @@ import {
mergeKnownOrganizations as mergeKnownOrganizationsFromMatcher,
normalizeOrganizationScopeValue as normalizeOrganizationScopeValueFromMatcher
} from "./assistantOrganizationMatcher";
import { normalizeRussianComparableText } from "./addressTextRepair";
export interface AssistantContinuitySnapshotInput {
sessionItems?: unknown[];
@@ -567,17 +568,61 @@ export function readAddressDebugItem(
);
}
export function readAddressDebugCounterparty(
function isReferentialCounterpartyPlaceholder(
value: string | null
): boolean {
if (!value) {
return false;
}
return new Set([
"он",
"она",
"оно",
"они",
"ему",
"ней",
"нему",
"ним",
"ними",
"его",
"ее",
"их",
"этому",
"этой",
"этом",
"этим",
"эта",
"этот",
"эти"
]).has(normalizeRussianComparableText(value));
}
function normalizeCounterpartyCandidate(
value: unknown,
toNonEmptyString: (value: unknown) => string | null
): string | null {
const text = toNonEmptyString(value);
if (!text || isReferentialCounterpartyPlaceholder(text)) {
return null;
}
return text;
}
function sameCounterpartyCandidate(
left: string | null,
right: string | null
): boolean {
return Boolean(
left &&
right &&
normalizeRussianComparableText(left) === normalizeRussianComparableText(right)
);
}
function readGroundedDiscoveryCounterparty(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string | null {
const extractedFilters = readAddressDebugFilters(debug);
if (toNonEmptyString(extractedFilters?.counterparty)) {
return toNonEmptyString(extractedFilters?.counterparty);
}
if (String(debug?.anchor_type ?? "") === "counterparty") {
return toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw);
}
const discoveryPilotScope = readAssistantMcpDiscoveryPilotScope(debug, toNonEmptyString);
const suppressDiscoveryEntityCarryover =
discoveryPilotScope === "metadata_inspection_v1" ||
@@ -587,12 +632,38 @@ export function readAddressDebugCounterparty(
}
const discoveryEntities = collectAssistantMcpDiscoveryEntityCandidates(debug, toNonEmptyString);
for (const entity of discoveryEntities) {
const text = toNonEmptyString(entity);
if (text) {
return text;
const normalized = normalizeCounterpartyCandidate(entity, toNonEmptyString);
if (normalized) {
return normalized;
}
}
return null;
return normalizeCounterpartyCandidate(
readAssistantMcpDiscoveryLoopMetadataScopeHint(debug, toNonEmptyString),
toNonEmptyString
);
}
export function readAddressDebugCounterparty(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string | null {
const extractedFilters = readAddressDebugFilters(debug);
const extractedCounterparty = normalizeCounterpartyCandidate(extractedFilters?.counterparty, toNonEmptyString);
const anchorCounterparty =
String(debug?.anchor_type ?? "") === "counterparty"
? normalizeCounterpartyCandidate(
toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw),
toNonEmptyString
)
: null;
const groundedDiscoveryCounterparty = readGroundedDiscoveryCounterparty(debug, toNonEmptyString);
if (hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) && groundedDiscoveryCounterparty) {
if (!extractedCounterparty || !sameCounterpartyCandidate(extractedCounterparty, groundedDiscoveryCounterparty)) {
return groundedDiscoveryCounterparty;
}
return extractedCounterparty;
}
return extractedCounterparty ?? anchorCounterparty ?? groundedDiscoveryCounterparty;
}
export function readAddressDebugIntent(
@@ -664,9 +735,21 @@ export function resolveAddressDebugAnchorContext(
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): AssistantAddressDebugAnchorContext {
const explicitAnchorType = toNonEmptyString(debug?.anchor_type);
const explicitAnchorValue =
const explicitAnchorValueRaw =
toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw);
if (explicitAnchorType || explicitAnchorValue) {
const explicitAnchorValue =
explicitAnchorType === "counterparty"
? normalizeCounterpartyCandidate(explicitAnchorValueRaw, toNonEmptyString)
: explicitAnchorValueRaw;
const groundedDiscoveryCounterparty = readGroundedDiscoveryCounterparty(debug, toNonEmptyString);
const shouldPreferDiscoveryCounterparty =
explicitAnchorType === "counterparty" &&
Boolean(
groundedDiscoveryCounterparty &&
hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) &&
(!explicitAnchorValue || !sameCounterpartyCandidate(explicitAnchorValue, groundedDiscoveryCounterparty))
);
if ((explicitAnchorType || explicitAnchorValue) && !shouldPreferDiscoveryCounterparty) {
return {
anchorType: explicitAnchorType,
anchorValue: explicitAnchorValue
@@ -681,8 +764,15 @@ export function resolveAddressDebugAnchorContext(
anchorValue: item
};
}
const counterparty = toNonEmptyString(extractedFilters?.counterparty);
if (counterparty) {
const counterparty = normalizeCounterpartyCandidate(extractedFilters?.counterparty, toNonEmptyString);
if (
counterparty &&
!(
groundedDiscoveryCounterparty &&
hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) &&
!sameCounterpartyCandidate(counterparty, groundedDiscoveryCounterparty)
)
) {
return {
anchorType: "counterparty",
anchorValue: counterparty
@@ -761,7 +851,10 @@ export function resolveAddressDebugCarryoverFilters(
Boolean(discoveryDateScope.asOfDate || discoveryDateScope.periodFrom || discoveryDateScope.periodTo);
const counterparty = readAddressDebugCounterparty(debug, toNonEmptyString);
const organization = readAddressDebugOrganization(debug, toNonEmptyString);
if (counterparty && !toNonEmptyString(nextFilters.counterparty)) {
const preferGroundedDiscoveryCounterparty =
hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) && Boolean(counterparty);
const existingCounterparty = normalizeCounterpartyCandidate(nextFilters.counterparty, toNonEmptyString);
if (counterparty && (preferGroundedDiscoveryCounterparty || !existingCounterparty)) {
nextFilters.counterparty = counterparty;
}
if (organization && !toNonEmptyString(nextFilters.organization)) {
@@ -3,6 +3,7 @@ import {
buildAssistantMcpDiscoveryDataNeedGraph,
type AssistantMcpDiscoveryDataNeedGraphContract
} from "./assistantMcpDiscoveryDataNeedGraph";
import { normalizeRussianComparableText, repairAddressMojibakeText } from "./addressTextRepair";
import type {
AssistantMcpDiscoveryMetadataRecommendedPrimitive,
AssistantMcpDiscoveryMetadataRouteFamily,
@@ -80,9 +81,33 @@ function pushUnique(target: string[], value: unknown): void {
}
function isReferentialEntityPlaceholder(value: string): boolean {
return /^(?:\u043d\u0435\u043c\u0443|\u043d\u0435\u0439|\u043d\u0438\u043c|\u043d\u0438\u043c\u0438|\u0435\u0433\u043e|\u0435\u0435|\u0435\u0451|\u0438\u0445|\u044d\u0442\u043e\u043c\u0443|\u044d\u0442\u043e\u0439|\u044d\u0442\u0438\u043c|\u044d\u0442\u0438\u043c\u0438|\u044d\u0442\u043e\u043c)$/iu.test(
value.trim()
);
return new Set([
"он",
"она",
"оно",
"они",
"ему",
"ней",
"нему",
"ним",
"ними",
"его",
"ее",
"их",
"этому",
"этой",
"этим",
"этими",
"этом"
]).has(normalizeRussianComparableText(value));
}
function normalizeFollowupCounterpartyCandidate(value: unknown): string | null {
const text = candidateValue(value);
if (!text || isReferentialEntityPlaceholder(text)) {
return null;
}
return text;
}
function pushScopedEntityCandidate(
@@ -482,16 +507,25 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
effectivePilotScope === "entity_resolution_search_v1" && entityResolutionStatus === "ambiguous";
const metadataPilotCarriesScopeOnly =
effectivePilotScope === "metadata_inspection_v1" || loopSubjectResolutionOptional;
const normalizedDiscoveryEntities = discoveryEntities
.map((entity) => normalizeFollowupCounterpartyCandidate(entity))
.filter((entity): entity is string => Boolean(entity));
const groundedDiscoveryCounterparty =
ambiguityBlocksImplicitGrounding || metadataPilotCarriesScopeOnly
? null
: normalizedDiscoveryEntities[0] ?? normalizeFollowupCounterpartyCandidate(loopMetadataScopeHint);
const metadataScopeHint =
loopMetadataScopeHint ??
(loopSubjectResolutionOptional ? discoveryEntities[0] ?? null : null);
const counterparty =
toNonEmptyString(previousFilters?.counterparty) ??
toNonEmptyString(rootFilters?.counterparty) ??
(toNonEmptyString(followupContext?.previous_anchor_type) === "counterparty"
? toNonEmptyString(followupContext?.previous_anchor_value)
: null) ??
(ambiguityBlocksImplicitGrounding || metadataPilotCarriesScopeOnly ? null : discoveryEntities[0] ?? null);
(loopSubjectResolutionOptional ? normalizedDiscoveryEntities[0] ?? null : null);
const previousFiltersCounterparty = normalizeFollowupCounterpartyCandidate(previousFilters?.counterparty);
const rootFiltersCounterparty = normalizeFollowupCounterpartyCandidate(rootFilters?.counterparty);
const previousAnchorCounterparty =
toNonEmptyString(followupContext?.previous_anchor_type) === "counterparty"
? normalizeFollowupCounterpartyCandidate(followupContext?.previous_anchor_value)
: null;
const counterparty = groundedDiscoveryCounterparty
? groundedDiscoveryCounterparty
: previousFiltersCounterparty ?? rootFiltersCounterparty ?? previousAnchorCounterparty;
const organization =
toNonEmptyString(previousFilters?.organization) ??
toNonEmptyString(rootFilters?.organization) ??
@@ -512,7 +546,7 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
loopProvidedAxes,
counterparty,
discoveryEntity:
ambiguityBlocksImplicitGrounding || loopSubjectResolutionOptional ? null : discoveryEntities[0] ?? null,
ambiguityBlocksImplicitGrounding || loopSubjectResolutionOptional ? null : normalizedDiscoveryEntities[0] ?? null,
entityResolutionStatus,
entityResolutionAmbiguityCandidates,
rankingNeed: toNonEmptyString(followupContext?.previous_discovery_ranking_need),
@@ -672,6 +706,12 @@ function hasMetadataSignal(text: string): boolean {
);
}
function hasReferentialDocumentExclusionFollowupSignal(text: string): boolean {
return /(?:\u043a\u0440\u043e\u043c\u0435|\u043f\u043e\u043c\u0438\u043c\u043e)\s+(?:\u044d\u0442\u043e\u0433\u043e|\u044d\u0442\u043e\u0439|\u044d\u0442\u043e\u0442|\u044d\u0442\u0443|\u044d\u0442\u0438\u0445)(?:\s+(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430|\u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0430|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430))?/iu.test(
text
);
}
function hasMetadataObjectHint(text: string): boolean {
return /(?:\u043e\u0431\u044a\u0435\u043a\u0442(?:\u044b|\u0430|\u043e\u0432)?|\u0440\u0435\u0433\u0438\u0441\u0442\u0440(?:\u044b)?|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|\u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a(?:\u0438)?|\u043f\u043e\u043b(?:\u0435|\u044f)|objects?|registers?|documents?|catalogs?|fields?)/iu.test(
text
@@ -1007,15 +1047,24 @@ export function buildAssistantMcpDiscoveryTurnInput(
const reasonCodes: string[] = [];
const rawUserText = toNonEmptyString(input.userMessage);
const rawEffectiveText = toNonEmptyString(input.effectiveMessage);
const rawSignalSourceText = `${rawUserText ?? ""} ${rawEffectiveText ?? ""}`.trim();
const rawEntitySourceText = rawUserText ?? rawEffectiveText ?? rawSignalSourceText;
const repairedUserText = rawUserText ? repairAddressMojibakeText(rawUserText) : null;
const repairedEffectiveText = rawEffectiveText ? repairAddressMojibakeText(rawEffectiveText) : null;
const rawSignalSourceText = `${repairedUserText ?? rawUserText ?? ""} ${repairedEffectiveText ?? rawEffectiveText ?? ""}`.trim();
const rawEntitySourceText = repairedUserText ?? rawUserText ?? repairedEffectiveText ?? rawEffectiveText ?? rawSignalSourceText;
const rawText = compactLower(rawSignalSourceText);
const rawReferentialDocumentExclusionSignal = hasReferentialDocumentExclusionFollowupSignal(
repairedUserText ?? rawUserText ?? ""
);
const rawLifecycleSignal = hasLifecycleSignal(rawText);
const rawBidirectionalValueFlowSignal = !rawLifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
const rawValueFlowSignal =
!rawLifecycleSignal &&
(hasValueFlowSignal(rawText) || hasValueRankingSignal(rawText) || rawBidirectionalValueFlowSignal);
const rawMetadataSignal = !rawLifecycleSignal && !rawValueFlowSignal && hasMetadataSignal(rawText);
const rawMetadataSignal =
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawReferentialDocumentExclusionSignal &&
hasMetadataSignal(rawText);
const rawEntityResolutionSignal =
!rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
const rawPayoutSignal = rawValueFlowSignal && !rawBidirectionalValueFlowSignal && hasPayoutSignal(rawText);
@@ -579,12 +579,35 @@ export function createAssistantRoutePolicy(deps) {
!effectiveAddressFollowupSignal &&
resolvedModeDetection.mode === "unsupported" &&
resolvedIntentResolution.intent === "unknown");
const groundedValueFlowFollowupContextDetected = Boolean(followupContext &&
[
"counterparty_value_flow_query_movements_v1",
"counterparty_supplier_payout_query_movements_v1",
"counterparty_bidirectional_value_flow_query_movements_v1"
].includes(String(toNonEmptyString(followupContext?.previous_discovery_pilot_scope) ?? "")) &&
!dangerOrCoercionSignal &&
(toNonEmptyString(assistantTurnMeaning?.asked_domain_family) === "counterparty_value" ||
[
"turnover",
"payout",
"net_value_flow"
].includes(String(toNonEmptyString(assistantTurnMeaning?.asked_action_family) ?? "")) ||
/(?:нетто|сальдо|сколько\s+мы\s+(?:получили|заплатили)|incoming|outgoing)/iu.test(analyticsSample)));
const baseToolGatePreservesAddressLane = Boolean(baseToolGate?.runAddressLane &&
["address_intent_resolver_detected", "address_mode_classifier_detected", "address_signal_detected", "llm_canonical_data_signal_detected"].includes(String(baseToolGate?.reason ?? "")));
[
"address_intent_resolver_detected",
"address_mode_classifier_detected",
"address_signal_detected",
"llm_canonical_data_signal_detected"
].includes(String(baseToolGate?.reason ?? ""))) ||
Boolean(baseToolGate?.runAddressLane &&
String(baseToolGate?.reason ?? "") === "followup_context_detected" &&
groundedValueFlowFollowupContextDetected);
const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate &&
deterministicNonDomainGuard &&
(llmFirstUnsupportedCandidate || llmContractMode === null) &&
!baseToolGatePreservesAddressLane &&
!groundedValueFlowFollowupContextDetected &&
!protectedInventoryShortFollowup &&
!organizationClarificationContinuationDetected);
const lastAddressAssistantDebug = sessionItems
@@ -749,9 +772,11 @@ export function createAssistantRoutePolicy(deps) {
assistantTurnMeaning?.unsupported_but_understood_family &&
assistantTurnMeaning?.stale_replay_forbidden === true &&
!turnMeaningIntentCandidate &&
!aggregateBusinessAnalyticsSignal &&
!dataScopeMetaQuery &&
!capabilityMetaQuery &&
!dangerOrCoercionSignal &&
!groundedValueFlowFollowupContextDetected &&
!organizationClarificationContinuationDetected);
if (unsupportedCurrentTurnMeaningBoundary) {
return {
@@ -112,6 +112,89 @@ describe("address navigation state", () => {
expect(evolved.session_context.date_scope.period_to).toBe("2020-12-31");
});
it("prefers grounded discovery counterparty over stale referential anchor when updating focus", () => {
const initial = normalizeAddressNavigationState(
{
schema_version: "address_navigation_state_v1",
session_id: "asst-3b",
updated_at: "2026-04-12T10:00:00.000Z",
session_context: {
active_result_set_id: "rs-prev",
active_focus_object: {
object_type: "counterparty",
object_id: "counterparty:нортон",
label: "НОРТОН",
provenance_result_set_id: "rs-prev",
selected_at: "2026-04-12T09:59:00.000Z"
},
last_confirmed_route: "address_customer_revenue_and_payments_v1",
date_scope: {
as_of_date: null,
period_from: "2020-01-01",
period_to: "2020-12-31"
},
organization_scope: "ООО Альтернатива Плюс"
},
result_sets: [],
navigation_history: []
} as any,
"asst-3b"
);
const assistantItem = {
message_id: "msg-a2b",
session_id: "asst-3b",
role: "assistant",
text: "По Группа СВК подтверждены исходящие платежи за 2020 год.",
reply_type: "factual",
created_at: "2026-04-12T10:02:30.000Z",
trace_id: "address-456b",
debug: {
detected_mode: "address_query",
detected_intent: "supplier_payouts_profile",
selected_recipe: "address_supplier_payouts_profile_v1",
extracted_filters: {
counterparty: "НОРТОН",
period_from: "2020-01-01",
period_to: "2020-12-31"
},
anchor_type: "counterparty",
anchor_value_raw: "он",
anchor_value_resolved: "он",
mcp_discovery_response_applied: true,
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "payout",
explicit_entity_candidates: ["Группа СВК"]
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
pilot: {
pilot_scope: "counterparty_supplier_payout_query_movements_v1"
},
answer_draft: {
answer_mode: "confirmed_factual"
}
}
},
dialog_continuation_contract_v2: {
decision: "continue_previous"
}
}
} as any;
const evolved = evolveAddressNavigationStateWithAssistantItem(initial, assistantItem, 4);
expect(evolved.session_context.active_focus_object?.object_type).toBe("counterparty");
expect(evolved.session_context.active_focus_object?.label).toBe("Группа СВК");
expect(evolved.session_context.active_focus_object?.object_id).toBe("counterparty:группа свк");
expect(evolved.navigation_history[0]?.target_object_id).toBe("counterparty:группа свк");
});
it("captures item focus from inventory answers when no anchor is materialized", () => {
const base = createEmptyAddressNavigationState("asst-4", "2026-04-12T10:00:00.000Z");
const assistantItem = {
@@ -731,13 +731,14 @@ describe("assistant address follow-up carryover", () => {
expect(second.reply_type).toBe("factual");
expect(calls).toHaveLength(2);
expect(calls[1].message).toBe(followupMessage);
expect(calls[1].options?.followupContext?.previous_anchor_type).toBe("counterparty");
expect(String(calls[1].options?.followupContext?.previous_anchor_value ?? "")).toContain("Жуковка 51");
expect(String(calls[1].options?.followupContext?.previous_filters?.counterparty ?? "")).toContain("жуковке 51");
expect(normalizerService.normalize).not.toHaveBeenCalled();
});
it("retries with raw user message after rewrite degraded anchor and returns factual follow-up result", async () => {
it("prefers the raw referential document follow-up over a degraded rewrite and returns factual follow-up result", async () => {
const calls: Array<{ message: string; options?: any }> = [];
const firstMessage = "покажи документы по жуковке 51";
const followupMessage = "кроме этого документа есть еще чтото?";
@@ -826,12 +827,10 @@ describe("assistant address follow-up carryover", () => {
} as any);
expect(second.ok).toBe(true);
expect(second.reply_type).toBe("factual");
expect(second.debug?.address_retry_audit?.attempted).toBe(true);
expect(second.debug?.address_retry_audit?.initial_limited_category).toBe("missing_anchor");
expect(second.debug?.address_retry_audit?.retry_message).toBe(followupMessage);
expect(second.debug?.address_retry_audit?.attempted).toBe(false);
expect(calls.some((entry) => String(entry.message).toLowerCase().startsWith("документы по контрагенту"))).toBe(true);
expect(calls.some((entry) => String(entry.message).toLowerCase() === followupMessage)).toBe(true);
expect(calls).toHaveLength(2);
expect(calls[1].message).toBe(followupMessage);
expect(normalizerService.normalize).not.toHaveBeenCalled();
});
@@ -230,6 +230,102 @@ describe("assistantContinuityPolicy organization authority", () => {
});
});
it("replaces referential counterparty placeholders with grounded discovery entity during carryover", () => {
const debug = {
execution_lane: "address_query",
extracted_filters: {
counterparty: "РѕРЅ",
period_from: "2020-01-01",
period_to: "2020-12-31"
},
anchor_type: "counterparty",
anchor_value_raw: "РѕРЅ",
anchor_value_resolved: "РѕРЅ",
mcp_discovery_response_applied: true,
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
pilot: {
pilot_scope: "counterparty_value_flow_query_movements_v1"
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
}
}
}
};
expect(readAddressDebugCounterparty(debug)).toBe("Группа СВК");
expect(resolveAddressDebugCarryoverFilters(debug)).toEqual({
counterparty: "Группа СВК",
period_from: "2020-01-01",
period_to: "2020-12-31"
});
expect(resolveAddressDebugAnchorContext(debug)).toEqual({
anchorType: "counterparty",
anchorValue: "Группа СВК"
});
});
it("prefers grounded discovery metadata scope over conflicting stale exact-route counterparty", () => {
const debug = {
execution_lane: "address_query",
extracted_filters: {
counterparty: "РќРћР РўРћРќ",
period_from: "2020-01-01",
period_to: "2020-12-31"
},
anchor_type: "counterparty",
anchor_value_resolved: "СервисКонсалт, ООО",
mcp_discovery_response_applied: true,
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "payout",
explicit_entity_candidates: ["РѕРЅ"],
metadata_scope_hint: "Группа СВК",
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
pilot: {
pilot_scope: "counterparty_supplier_payout_query_movements_v1"
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
}
}
}
};
expect(readAddressDebugCounterparty(debug)).toBe("Группа СВК");
expect(resolveAddressDebugCarryoverFilters(debug)).toEqual({
counterparty: "Группа СВК",
period_from: "2020-01-01",
period_to: "2020-12-31"
});
expect(resolveAddressDebugAnchorContext(debug)).toEqual({
anchorType: "counterparty",
anchorValue: "Группа СВК"
});
});
it("prefers the resolved entity from grounded entity-resolution discovery for counterparty carryover", () => {
const debug = {
execution_lane: "living_chat",
@@ -235,6 +235,40 @@ describe("assistant orchestration contract", () => {
expect(decision.orchestrationContract?.hard_meta_mode).toBe("non_domain");
});
it("keeps address lane for a short net follow-up over grounded value-flow context", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "А какое получилось нетто?",
effectiveAddressUserMessage: "А какое получилось нетто?",
followupContext: {
previous_intent: "customer_revenue_and_payments",
previous_filters: {
counterparty: "Группа СВК",
period_from: "2020-01-01",
period_to: "2020-12-31"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "Группа СВК",
previous_discovery_pilot_scope: "counterparty_supplier_payout_query_movements_v1"
},
llmPreDecomposeMeta: {
applied: false,
reason: "no_usable_fragment",
predecomposeContract: {
mode: "unsupported",
mode_confidence: "low",
intent: "unknown",
intent_confidence: "low"
}
} as any,
useMock: false
});
expect(decision.runAddressLane).toBe(true);
expect(decision.toolGateDecision).toBe("run_address_lane");
expect(decision.toolGateReason).not.toBe("non_domain_query_indexed");
expect(decision.livingMode).toBe("address_data");
});
it("routes historical capability follow-up over grounded inventory answer to contextual chat", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "а исторические данные ты можешь же показать?",
@@ -585,6 +585,41 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
});
it("prefers grounded discovery metadata scope over stale conflicting counterparty in a short net follow-up", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "а какое нетто?",
assistantTurnMeaning: {
asked_domain_family: "counterparty",
asked_action_family: "turnover",
explicit_intent_candidate: "customer_revenue_and_payments"
},
followupContext: {
previous_discovery_pilot_scope: "counterparty_supplier_payout_query_movements_v1",
previous_filters: {
counterparty: "РќРћР РўРћРќ",
period_from: "2020-01-01",
period_to: "2020-12-31"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "СервисКонсалт, ООО",
previous_discovery_loop_metadata_scope_hint: "Группа СВК"
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).not.toContain("РќРћР РўРћРќ");
expect(result.reason_codes).toContain("mcp_discovery_grounded_value_flow_followup");
});
it.skip("switches from a grounded exact value-flow answer into document evidence without restating the counterparty", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "а по документам?",
@@ -1268,6 +1303,35 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
});
it("does not bootstrap metadata discovery from a referential document exclusion follow-up over exact document context", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "кроме этого документа есть еще что-то?",
effectiveMessage: "документы по контрагенту документа",
assistantTurnMeaning: {
asked_domain_family: "counterparty",
asked_action_family: "list_documents",
explicit_intent_candidate: "list_documents_by_counterparty",
explicit_entity_candidates: [{ value: "ТСЖ \"Жуковка 51\"" }]
},
followupContext: {
previous_intent: "list_documents_by_counterparty",
target_intent: "list_documents_by_counterparty",
previous_anchor_type: "counterparty",
previous_anchor_value: "ТСЖ \"Жуковка 51\"",
previous_filters: {
counterparty: "жуковке 51"
}
}
});
expect(result.adapter_status).toBe("not_applicable");
expect(result.should_run_discovery).toBe(false);
expect(result.turn_meaning_ref).toBeNull();
expect(result.reason_codes).toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
expect(result.reason_codes).not.toContain("mcp_discovery_metadata_signal_detected");
expect(result.reason_codes).not.toContain("mcp_discovery_metadata_scope_hint_from_raw_text");
});
it("never serializes object candidates as [object Object]", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
assistantTurnMeaning: {
@@ -1,4 +1,147 @@
[
{
"generation_id": "gen-ag04231336-3d4cc9",
"created_at": "2026-04-23T13:36:22+00:00",
"mode": "saved_user_sessions",
"title": "AGENT | Живой диалог по организации: денежный срез, сравнение и рейтинг",
"count": 7,
"domain": "address_phase66_human_org_open_scope_dialog",
"questions": [
"Хочу быстрый денежный срез по одной организации без привязки к контрагенту. Сколько вообще входящих денег было за 2020 год?",
"По ООО Альтернатива Плюс.",
"Понял, тогда за все время.",
"Хорошо. А что по ООО Альтернатива Плюс больше в 2020 году: входящие или исходящие деньги?",
"А что по ООО Альтернатива Плюс больше уже за 2021 год: входящие или исходящие деньги?",
"И кто больше всего принес денег этой организации в 2020 году?",
"А в 2021 году?"
],
"generated_by": "codex_agent",
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260423133622_gen-ag04231336-3d4cc9.json",
"context": {
"llm_provider": null,
"model": null,
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"autogen_personality_id": null,
"autogen_personality_prompt": null,
"source_session_id": null,
"saved_session_file": "assistant_saved_session_20260423133622_gen-ag04231336-3d4cc9.json",
"saved_case_set_kind": "agent_semantic_scenario",
"agent_run": true,
"agent_focus": "Human organization open-scope dialog: org clarification, all-time incoming total, comparison, ranking",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase66_human_org_open_scope_dialog.json",
"scenario_id": "address_truth_harness_phase66_human_org_open_scope_dialog",
"semantic_tags": [
"all_time_followup",
"human_dialog",
"open_scope_total",
"organization_clarification",
"organization_scope",
"value_flow_comparison",
"value_flow_ranking",
"year_switch"
]
}
},
{
"generation_id": "gen-ag04231336-db78b3",
"created_at": "2026-04-23T13:36:22+00:00",
"mode": "saved_user_sessions",
"title": "AGENT | Живой диалог по СВК: деньги, нетто, документы и движения",
"count": 7,
"domain": "address_phase65_human_svk_money_dialog",
"questions": [
"Хочу проверить одного контрагента. Найди в 1С Группу СВК.",
"Посмотри, сколько денег мы получили от него за 2020 год.",
"А теперь сколько мы ему заплатили?",
"А какое получилось нетто?",
"А по документам?",
"А по движениям?",
"А теперь за 2021 год?"
],
"generated_by": "codex_agent",
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260423133622_gen-ag04231336-db78b3.json",
"context": {
"llm_provider": null,
"model": null,
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"autogen_personality_id": null,
"autogen_personality_prompt": null,
"source_session_id": null,
"saved_session_file": "assistant_saved_session_20260423133622_gen-ag04231336-db78b3.json",
"saved_case_set_kind": "agent_semantic_scenario",
"agent_run": true,
"agent_focus": "Human SVK counterparty dialog: grounding, incoming, outgoing, net, documents, movements",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase65_human_svk_money_dialog.json",
"scenario_id": "address_truth_harness_phase65_human_svk_money_dialog",
"semantic_tags": [
"counterparty_resolution",
"document_pivot_after_value_flow",
"entity_grounding",
"grounded_counterparty_followup",
"human_dialog",
"incoming_value_flow",
"movement_pivot_after_value_flow",
"net_value_flow",
"outgoing_value_flow",
"year_switch"
]
}
},
{
"generation_id": "gen-ag04231336-4fa660",
"created_at": "2026-04-23T13:36:22+00:00",
"mode": "saved_user_sessions",
"title": "AGENT | Живой диалог по НДС: от ориентации до документов",
"count": 6,
"domain": "address_phase64_human_vat_investigation_dialog",
"questions": [
"Мне нужно понять, где в 1С по НДС вообще лежат данные. Какие объекты стоит смотреть по НДС?",
"Хорошо, тогда покажи движения по ООО Альтернатива Плюс.",
"За 2020 год.",
"А теперь по документам?",
"А теперь за 2021 год?",
"А теперь за все время?"
],
"generated_by": "codex_agent",
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260423133622_gen-ag04231336-4fa660.json",
"context": {
"llm_provider": null,
"model": null,
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"autogen_personality_id": null,
"autogen_personality_prompt": null,
"source_session_id": null,
"saved_session_file": "assistant_saved_session_20260423133622_gen-ag04231336-4fa660.json",
"saved_case_set_kind": "agent_semantic_scenario",
"agent_run": true,
"agent_focus": "Human VAT dialog: metadata orientation, movement lane, document pivot, year switch, all-time continuity",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase64_human_vat_investigation_dialog.json",
"scenario_id": "address_truth_harness_phase64_human_vat_investigation_dialog",
"semantic_tags": [
"all_time_followup",
"bounded_retrieval",
"document_lane_continuity",
"document_pivot_after_movement_retrieval",
"human_dialog",
"inline_organization_clarification",
"metadata_surface",
"movement_lane_after_metadata",
"movement_lane_execution",
"scope_reuse",
"vat_orientation",
"year_switch_after_pivot"
]
}
},
{
"generation_id": "gen-moa1y0lw-m30gdsz",
"created_at": "2026-04-22T12:51:54.657Z",
@@ -1080,4 +1223,4 @@
"latest_acceptance": null
}
}
]
]
@@ -0,0 +1,125 @@
{
"saved_at": "2026-04-23T13:36:22+00:00",
"generation_id": "gen-ag04231336-3d4cc9",
"mode": "saved_user_sessions",
"title": "AGENT | Живой диалог по организации: денежный срез, сравнение и рейтинг",
"agent_run": true,
"questions": [
"Хочу быстрый денежный срез по одной организации без привязки к контрагенту. Сколько вообще входящих денег было за 2020 год?",
"По ООО Альтернатива Плюс.",
"Понял, тогда за все время.",
"Хорошо. А что по ООО Альтернатива Плюс больше в 2020 году: входящие или исходящие деньги?",
"А что по ООО Альтернатива Плюс больше уже за 2021 год: входящие или исходящие деньги?",
"И кто больше всего принес денег этой организации в 2020 году?",
"А в 2021 году?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Human organization open-scope dialog: org clarification, all-time incoming total, comparison, ranking",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase66_human_org_open_scope_dialog.json",
"scenario_id": "address_truth_harness_phase66_human_org_open_scope_dialog",
"semantic_tags": [
"all_time_followup",
"human_dialog",
"open_scope_total",
"organization_clarification",
"organization_scope",
"value_flow_comparison",
"value_flow_ranking",
"year_switch"
]
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "Хочу быстрый денежный срез по одной организации без привязки к контрагенту. Сколько вообще входящих денег было за 2020 год?",
"created_at": "2026-04-23T13:36:22+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "По ООО Альтернатива Плюс.",
"created_at": "2026-04-23T13:36:22+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "Понял, тогда за все время.",
"created_at": "2026-04-23T13:36:22+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "Хорошо. А что по ООО Альтернатива Плюс больше в 2020 году: входящие или исходящие деньги?",
"created_at": "2026-04-23T13:36:22+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "А что по ООО Альтернатива Плюс больше уже за 2021 год: входящие или исходящие деньги?",
"created_at": "2026-04-23T13:36:22+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "И кто больше всего принес денег этой организации в 2020 году?",
"created_at": "2026-04-23T13:36:22+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-007",
"role": "user",
"text": "А в 2021 году?",
"created_at": "2026-04-23T13:36:22+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
}
],
"agent_run": true,
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Human organization open-scope dialog: org clarification, all-time incoming total, comparison, ranking",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase66_human_org_open_scope_dialog.json",
"scenario_id": "address_truth_harness_phase66_human_org_open_scope_dialog",
"semantic_tags": [
"all_time_followup",
"human_dialog",
"open_scope_total",
"organization_clarification",
"organization_scope",
"value_flow_comparison",
"value_flow_ranking",
"year_switch"
]
}
}
}
@@ -0,0 +1,123 @@
{
"saved_at": "2026-04-23T13:36:22+00:00",
"generation_id": "gen-ag04231336-4fa660",
"mode": "saved_user_sessions",
"title": "AGENT | Живой диалог по НДС: от ориентации до документов",
"agent_run": true,
"questions": [
"Мне нужно понять, где в 1С по НДС вообще лежат данные. Какие объекты стоит смотреть по НДС?",
"Хорошо, тогда покажи движения по ООО Альтернатива Плюс.",
"За 2020 год.",
"А теперь по документам?",
"А теперь за 2021 год?",
"А теперь за все время?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Human VAT dialog: metadata orientation, movement lane, document pivot, year switch, all-time continuity",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase64_human_vat_investigation_dialog.json",
"scenario_id": "address_truth_harness_phase64_human_vat_investigation_dialog",
"semantic_tags": [
"all_time_followup",
"bounded_retrieval",
"document_lane_continuity",
"document_pivot_after_movement_retrieval",
"human_dialog",
"inline_organization_clarification",
"metadata_surface",
"movement_lane_after_metadata",
"movement_lane_execution",
"scope_reuse",
"vat_orientation",
"year_switch_after_pivot"
]
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "Мне нужно понять, где в 1С по НДС вообще лежат данные. Какие объекты стоит смотреть по НДС?",
"created_at": "2026-04-23T13:36:22+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "Хорошо, тогда покажи движения по ООО Альтернатива Плюс.",
"created_at": "2026-04-23T13:36:22+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "За 2020 год.",
"created_at": "2026-04-23T13:36:22+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "А теперь по документам?",
"created_at": "2026-04-23T13:36:22+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "А теперь за 2021 год?",
"created_at": "2026-04-23T13:36:22+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "А теперь за все время?",
"created_at": "2026-04-23T13:36:22+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
}
],
"agent_run": true,
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Human VAT dialog: metadata orientation, movement lane, document pivot, year switch, all-time continuity",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase64_human_vat_investigation_dialog.json",
"scenario_id": "address_truth_harness_phase64_human_vat_investigation_dialog",
"semantic_tags": [
"all_time_followup",
"bounded_retrieval",
"document_lane_continuity",
"document_pivot_after_movement_retrieval",
"human_dialog",
"inline_organization_clarification",
"metadata_surface",
"movement_lane_after_metadata",
"movement_lane_execution",
"scope_reuse",
"vat_orientation",
"year_switch_after_pivot"
]
}
}
}
@@ -0,0 +1,129 @@
{
"saved_at": "2026-04-23T13:36:22+00:00",
"generation_id": "gen-ag04231336-db78b3",
"mode": "saved_user_sessions",
"title": "AGENT | Живой диалог по СВК: деньги, нетто, документы и движения",
"agent_run": true,
"questions": [
"Хочу проверить одного контрагента. Найди в 1С Группу СВК.",
"Посмотри, сколько денег мы получили от него за 2020 год.",
"А теперь сколько мы ему заплатили?",
"А какое получилось нетто?",
"А по документам?",
"А по движениям?",
"А теперь за 2021 год?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Human SVK counterparty dialog: grounding, incoming, outgoing, net, documents, movements",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase65_human_svk_money_dialog.json",
"scenario_id": "address_truth_harness_phase65_human_svk_money_dialog",
"semantic_tags": [
"counterparty_resolution",
"document_pivot_after_value_flow",
"entity_grounding",
"grounded_counterparty_followup",
"human_dialog",
"incoming_value_flow",
"movement_pivot_after_value_flow",
"net_value_flow",
"outgoing_value_flow",
"year_switch"
]
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "Хочу проверить одного контрагента. Найди в 1С Группу СВК.",
"created_at": "2026-04-23T13:36:22+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "Посмотри, сколько денег мы получили от него за 2020 год.",
"created_at": "2026-04-23T13:36:22+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "А теперь сколько мы ему заплатили?",
"created_at": "2026-04-23T13:36:22+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "А какое получилось нетто?",
"created_at": "2026-04-23T13:36:22+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "А по документам?",
"created_at": "2026-04-23T13:36:22+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "А по движениям?",
"created_at": "2026-04-23T13:36:22+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-007",
"role": "user",
"text": "А теперь за 2021 год?",
"created_at": "2026-04-23T13:36:22+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
}
],
"agent_run": true,
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Human SVK counterparty dialog: grounding, incoming, outgoing, net, documents, movements",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase65_human_svk_money_dialog.json",
"scenario_id": "address_truth_harness_phase65_human_svk_money_dialog",
"semantic_tags": [
"counterparty_resolution",
"document_pivot_after_value_flow",
"entity_grounding",
"grounded_counterparty_followup",
"human_dialog",
"incoming_value_flow",
"movement_pivot_after_value_flow",
"net_value_flow",
"outgoing_value_flow",
"year_switch"
]
}
}
}
@@ -0,0 +1,46 @@
{
"suite_id": "assistant_saved_session_gen-ag04231336-3d4cc9",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-04-23T13:36:22+00:00",
"generation_id": "gen-ag04231336-3d4cc9",
"mode": "saved_user_sessions",
"title": "AGENT | Живой диалог по организации: денежный срез, сравнение и рейтинг",
"domain": "address_phase66_human_org_open_scope_dialog",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Живой диалог по организации: денежный срез, сравнение и рейтинг",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "Хочу быстрый денежный срез по одной организации без привязки к контрагенту. Сколько вообще входящих денег было за 2020 год?"
},
{
"user_message": "По ООО Альтернатива Плюс."
},
{
"user_message": "Понял, тогда за все время."
},
{
"user_message": "Хорошо. А что по ООО Альтернатива Плюс больше в 2020 году: входящие или исходящие деньги?"
},
{
"user_message": "А что по ООО Альтернатива Плюс больше уже за 2021 год: входящие или исходящие деньги?"
},
{
"user_message": "И кто больше всего принес денег этой организации в 2020 году?"
},
{
"user_message": "А в 2021 году?"
}
]
}
]
}
@@ -0,0 +1,43 @@
{
"suite_id": "assistant_saved_session_gen-ag04231336-4fa660",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-04-23T13:36:22+00:00",
"generation_id": "gen-ag04231336-4fa660",
"mode": "saved_user_sessions",
"title": "AGENT | Живой диалог по НДС: от ориентации до документов",
"domain": "address_phase64_human_vat_investigation_dialog",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Живой диалог по НДС: от ориентации до документов",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "Мне нужно понять, где в 1С по НДС вообще лежат данные. Какие объекты стоит смотреть по НДС?"
},
{
"user_message": "Хорошо, тогда покажи движения по ООО Альтернатива Плюс."
},
{
"user_message": "За 2020 год."
},
{
"user_message": "А теперь по документам?"
},
{
"user_message": "А теперь за 2021 год?"
},
{
"user_message": "А теперь за все время?"
}
]
}
]
}
@@ -0,0 +1,46 @@
{
"suite_id": "assistant_saved_session_gen-ag04231336-db78b3",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-04-23T13:36:22+00:00",
"generation_id": "gen-ag04231336-db78b3",
"mode": "saved_user_sessions",
"title": "AGENT | Живой диалог по СВК: деньги, нетто, документы и движения",
"domain": "address_phase65_human_svk_money_dialog",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Живой диалог по СВК: деньги, нетто, документы и движения",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "Хочу проверить одного контрагента. Найди в 1С Группу СВК."
},
{
"user_message": "Посмотри, сколько денег мы получили от него за 2020 год."
},
{
"user_message": "А теперь сколько мы ему заплатили?"
},
{
"user_message": "А какое получилось нетто?"
},
{
"user_message": "А по документам?"
},
{
"user_message": "А по движениям?"
},
{
"user_message": "А теперь за 2021 год?"
}
]
}
]
}