АДРЕСНЫЙ РЕЖИМ -ADDRESS:Шаг 1 - ЛЛМ ФЕРСТ + feat(address): стабилизация wave1 dynamic resolver контрагентов, follow-up carryover и актуализация docs/tests

This commit is contained in:
2026-04-02 23:11:49 +03:00
parent 53716e548e
commit 88094c09f8
243 changed files with 94296 additions and 181 deletions
@@ -15,6 +15,7 @@ import * as assistantClaimBoundEvidence_1 from "./assistantClaimBoundEvidence";
import * as addressQueryService_1 from "./addressQueryService";
import * as addressQueryClassifier_1 from "./addressQueryClassifier";
import * as addressIntentResolver_1 from "./addressIntentResolver";
import * as predecomposeContract_1 from "./address_runtime/predecomposeContract";
import iconv from "iconv-lite";
function retrievalSummaryForRoute(route) {
if (route === "store_canonical")
@@ -1763,6 +1764,8 @@ function buildAddressDebugPayload(addressDebug, llmPreDecomposeMeta = null) {
llm_decomposition_trace_id: llmMeta?.traceId ?? null,
llm_decomposition_effective_message: llmMeta?.effectiveMessage ?? null,
llm_decomposition_reason: llmMeta?.reason ?? null,
llm_canonical_candidate_detected: Boolean(llmMeta?.llmCanonicalCandidateDetected),
llm_predecompose_contract: llmMeta?.predecomposeContract ?? null,
fallback_rule_hit: llmMeta?.fallbackRuleHit ?? null,
sanitized_user_message: llmMeta?.sanitizedUserMessage ?? null,
tool_gate_decision: llmMeta?.toolGateDecision ?? null,
@@ -1835,6 +1838,16 @@ const ADDRESS_PREDECOMPOSE_NOISE_TOKENS = new Set([
"которые",
"какие",
"какой",
"активный",
"активная",
"активное",
"активности",
"месяц",
"месяца",
"месяцев",
"количество",
"количеству",
"количества",
"были",
"был",
"была",
@@ -1931,6 +1944,8 @@ const ADDRESS_BANK_SIGNAL_PATTERN = /(?:bank|банк|банков|выписк|
const ADDRESS_CONTRACT_SIGNAL_PATTERN = /(?:договор(?:а|у|ом|е)?|(?:^|[^\p{L}\p{N}_])(?:дог\.?|[dд][oо][gг]\.?|dog\.?)(?=$|[^\p{L}\p{N}_])|contract|dogovor)/iu;
const ADDRESS_BALANCE_SIGNAL_PATTERN = /(?:остат|сальдо|баланс|взаиморасч|долг|saldo|balance)/i;
const ADDRESS_ALL_TIME_PATTERN = /(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+всю\s+истори(?:ю|и)|for\s+all\s+time|all\s+time|entire\s+period|full\s+history)/iu;
const ADDRESS_MANAGEMENT_PROFILE_PATTERN =
/(?:за\s+какие\s+год[а-яё]*|сам(?:ый|ая|ое)\s+(?:актив|пассив)|наименее\s+актив|минимальн|покрыт(?:ие|ия)\s+период|диапазон\s+лет|тип[аы]\s+док(?:умент|ов|и)?|раздел[ыа]\s+уч[её]та|по\s+количеств[аоуе]|редк|реже|(?:сколько|скока|скок)\s+(?:всего\s+)?(?:уникальн(?:ых|ые|ого)?\s+)?контрагент(?:ов|а)?|(?:сколько|скока|скок)\s+(?:у\s+нас\s+)?(?:заказчик(?:ов|а)?|поставщик(?:ов|а)?|клиент(?:ов|а)?|покупател(?:ей|я)|смешан(?:ных|ые)\s+контрагент(?:ов|а)?)|(?:покажи|выведи|список|какие|кто).*(?:заказчик(?:ов|а|и)?|клиент(?:ов|а|ы)?|покупател(?:ей|я|и)?).*(?:за\s+вс[её]\s+время|all\s+time|(?:^|[^\d])(19|20)\d{2}(?:[^\d]|$)|(?:^|[^\d])\d{2}\s*(?:г(?:од|ода)?|г)(?:[^\p{L}\p{N}]|$)|за\s+год|в\s+году)|(?:какие|кто|покажи|выведи|список).*(?:заказчик(?:ов|а|и)?|клиент(?:ов|а|ы)?|покупател(?:ей|я|и)?).*(?:работал(?:и)?|активн(?:ые|ых|а|о)?).*(?:за\s+вс[её]\s+время|(?:19|20)\d{2}|за\s+год|в\s+году)|договорн(?:ая|ой)\s+баз[аы]|total\s+vs\s+used)/iu;
function normalizeAddressMonthAliasToken(token) {
const source = String(token ?? "").trim().toLowerCase();
if (!source) {
@@ -2136,6 +2151,9 @@ function resolveAddressDeterministicFallback(userMessage, sanitizedUserMessage)
if (!source) {
return null;
}
if (ADDRESS_MANAGEMENT_PROFILE_PATTERN.test(source)) {
return null;
}
const monthYear = extractAddressFallbackMonthYear(source);
const year = extractAddressFallbackYear(source);
const allTime = ADDRESS_ALL_TIME_PATTERN.test(source);
@@ -2383,6 +2401,14 @@ function hasAddressFollowupContextSignal(userMessage) {
if (shortFollowup && hasFollowupMarker(text)) {
return true;
}
if (shortFollowup && /(?:^|\s)(?:также|тоже|also|same|again|ещ[её]|теперь|then|now)(?=$|[\s,.;:!?])/iu.test(text)) {
return true;
}
if (shortFollowup &&
/(?:^|\s)по\s+[a-zа-яё][a-zа-яё0-9._-]{1,}(?=$|[\s,.;:!?])/iu.test(text) &&
!/(?:по\s+этому|по\s+тому|по\s+нему|по\s+ней|по\s+ним)/iu.test(text)) {
return true;
}
if (shortFollowup && hasPeriodLiteral(text)) {
return true;
}
@@ -2448,7 +2474,6 @@ function extractAddressQuestionFromNormalized(normalized) {
if (domainRelevance === "out_of_scope") {
continue;
}
const readiness = String(fragment.execution_readiness ?? "").trim().toLowerCase();
const normalizedText = toNonEmptyString(fragment.normalized_fragment_text);
const rawText = toNonEmptyString(fragment.raw_fragment_text);
const candidate = selectPreferredAddressFragmentCandidate(rawText ?? "", normalizedText ?? "");
@@ -2456,9 +2481,6 @@ function extractAddressQuestionFromNormalized(normalized) {
continue;
}
if (candidate.length >= 3 && candidate.length <= 500) {
if (readiness === "no_route" && !isAddressLlmPreDecomposeCandidate(candidate)) {
continue;
}
return candidate;
}
}
@@ -2564,7 +2586,6 @@ function extractAddressQuestionFromRawNormalizerOutput(rawModelOutput) {
if (domainRelevance === false) {
continue;
}
const readiness = String(fragment.execution_readiness ?? "").trim().toLowerCase();
const normalizedText = toNonEmptyString(fragment.normalized_fragment_text);
const rawText = toNonEmptyString(fragment.raw_fragment_text);
const candidate = selectPreferredAddressFragmentCandidate(rawText ?? "", normalizedText ?? "");
@@ -2572,19 +2593,25 @@ function extractAddressQuestionFromRawNormalizerOutput(rawModelOutput) {
continue;
}
if (candidate.length >= 3 && candidate.length <= 500) {
if (readiness === "no_route" && !isAddressLlmPreDecomposeCandidate(candidate)) {
continue;
}
return candidate;
}
}
return null;
}
function attachAddressPredecomposeContract(meta, sourceMessage) {
const canonicalMessage = toNonEmptyString(meta?.effectiveMessage) ?? String(sourceMessage ?? "");
return {
...meta,
predecomposeContract: (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({
sourceMessage: String(sourceMessage ?? ""),
canonicalMessage
})
};
}
async function runAddressLlmPreDecompose(normalizerService, payload, userMessage) {
const provider = payload?.llmProvider === "local" ? "local" : payload?.llmProvider === "openai" ? "openai" : null;
const sanitizedUserMessage = sanitizeAddressMessageForFallback(userMessage);
const fallbackCandidate = resolveAddressDeterministicFallback(userMessage, sanitizedUserMessage);
const hasAddressSignal = isAddressLlmPreDecomposeCandidate(userMessage) || isAddressLlmPreDecomposeCandidate(sanitizedUserMessage);
const baseMeta = {
attempted: false,
applied: false,
@@ -2592,6 +2619,7 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
traceId: null,
effectiveMessage: userMessage,
reason: "not_attempted",
llmCanonicalCandidateDetected: false,
fallbackRuleHit: null,
sanitizedUserMessage,
toolGateDecision: null,
@@ -2603,39 +2631,19 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase());
const fallbackApplied = fallbackCompact.length > 0 && fallbackCompact !== sourceCompact;
if (fallbackApplied) {
return {
return attachAddressPredecomposeContract({
...baseMeta,
applied: true,
effectiveMessage: fallbackCandidate.candidate,
reason: "fallback_rule_applied_without_llm",
fallbackRuleHit: fallbackCandidate.rule
};
}, userMessage);
}
}
return {
return attachAddressPredecomposeContract({
...baseMeta,
reason: "skipped_in_mock"
};
}
if (!hasAddressSignal) {
if (fallbackCandidate) {
const fallbackCompact = compactWhitespace(String(fallbackCandidate.candidate ?? "").toLowerCase());
const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase());
const fallbackApplied = fallbackCompact.length > 0 && fallbackCompact !== sourceCompact;
if (fallbackApplied) {
return {
...baseMeta,
applied: true,
effectiveMessage: fallbackCandidate.candidate,
reason: "fallback_rule_applied_without_llm",
fallbackRuleHit: fallbackCandidate.rule
};
}
}
return {
...baseMeta,
reason: "not_address_like"
};
}, userMessage);
}
const normalizePayload = {
llmProvider: payload?.llmProvider,
@@ -2661,7 +2669,7 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase());
const fallbackApplied = fallbackCompact.length > 0 && fallbackCompact !== sourceCompact;
if (fallbackApplied) {
return {
return attachAddressPredecomposeContract({
...baseMeta,
attempted: true,
applied: true,
@@ -2669,15 +2677,15 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
effectiveMessage: fallbackCandidate.candidate,
reason: "fallback_rule_applied_after_llm",
fallbackRuleHit: fallbackCandidate.rule
};
}, userMessage);
}
}
return {
return attachAddressPredecomposeContract({
...baseMeta,
attempted: true,
traceId: normalized?.trace_id ?? null,
reason: normalized?.ok ? "no_usable_fragment" : "normalize_failed"
};
}, userMessage);
}
const repairedSourceMessage = repairAddressMojibake(userMessage);
const sourceIntentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(repairedSourceMessage || userMessage);
@@ -2692,18 +2700,19 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
(intentConflict &&
(sourceIntentResolution.confidence === "high" || candidateIntentResolution.confidence !== "high"));
if (rejectCandidateForIntentSafety) {
return {
return attachAddressPredecomposeContract({
...baseMeta,
attempted: true,
applied: false,
traceId: normalized?.trace_id ?? null,
llmCanonicalCandidateDetected: true,
effectiveMessage: userMessage,
reason: intentDroppedByCandidate
? "normalized_fragment_rejected_intent_drop"
: "normalized_fragment_rejected_intent_conflict",
fallbackRuleHit: null,
sanitizedUserMessage
};
}, userMessage);
}
const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase());
const candidateCompact = compactWhitespace(candidate.toLowerCase());
@@ -2720,16 +2729,17 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
: applied
? "raw_fragment_applied_after_normalize_failed"
: "raw_fragment_same_after_normalize_failed";
return {
return attachAddressPredecomposeContract({
attempted: true,
applied,
provider,
traceId: normalized?.trace_id ?? null,
effectiveMessage: applied ? candidate : userMessage,
reason,
llmCanonicalCandidateDetected: true,
fallbackRuleHit: null,
sanitizedUserMessage
};
}, userMessage);
}
catch (error) {
if (fallbackCandidate) {
@@ -2737,28 +2747,30 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase());
const fallbackApplied = fallbackCompact.length > 0 && fallbackCompact !== sourceCompact;
if (fallbackApplied) {
return {
return attachAddressPredecomposeContract({
...baseMeta,
attempted: true,
applied: true,
effectiveMessage: fallbackCandidate.candidate,
reason: "fallback_rule_applied_after_llm_error",
fallbackRuleHit: fallbackCandidate.rule
};
}, userMessage);
}
}
return {
return attachAddressPredecomposeContract({
...baseMeta,
attempted: true,
reason: `error:${error instanceof Error ? error.message : String(error)}`
};
}, userMessage);
}
}
function resolveAddressToolGateDecision(addressInputMessage, followupContext) {
function resolveAddressToolGateDecision(addressInputMessage, followupContext, llmPreDecomposeMeta = null) {
const repairedInputMessage = repairAddressMojibake(String(addressInputMessage ?? ""));
const modeDetection = (0, addressQueryClassifier_1.detectAddressQuestionMode)(repairedInputMessage || addressInputMessage);
const hasClassifierSignal = modeDetection.mode === "address_query";
const hasLlmCanonicalSignal = Boolean(llmPreDecomposeMeta?.llmCanonicalCandidateDetected);
const hasMessageSignal = hasClassifierSignal ||
hasLlmCanonicalSignal ||
isAddressLlmPreDecomposeCandidate(addressInputMessage) ||
isAddressLlmPreDecomposeCandidate(repairedInputMessage) ||
hasAccountingSignal(addressInputMessage) ||
@@ -2767,7 +2779,11 @@ function resolveAddressToolGateDecision(addressInputMessage, followupContext) {
return {
runAddressLane: true,
decision: "run_address_lane",
reason: hasClassifierSignal ? "address_mode_classifier_detected" : "address_signal_detected"
reason: hasClassifierSignal
? "address_mode_classifier_detected"
: hasLlmCanonicalSignal
? "llm_canonical_candidate_detected"
: "address_signal_detected"
};
}
if (followupContext) {
@@ -2857,6 +2873,9 @@ export class AssistantService {
address_sanitized_user_message: llmPreDecomposeMeta?.sanitizedUserMessage ?? null,
address_tool_gate_decision: llmPreDecomposeMeta?.toolGateDecision ?? null,
address_tool_gate_reason: llmPreDecomposeMeta?.toolGateReason ?? null,
address_llm_predecompose_contract_intent: llmPreDecomposeMeta?.predecomposeContract?.intent ?? null,
address_llm_predecompose_contract_aggregation_profile: llmPreDecomposeMeta?.predecomposeContract?.aggregation_profile ?? null,
address_llm_predecompose_contract_period_scope: llmPreDecomposeMeta?.predecomposeContract?.period?.scope ?? null,
detected_mode: addressLane.debug.detected_mode,
query_shape: addressLane.debug.query_shape,
detected_intent: addressLane.debug.detected_intent,
@@ -2901,6 +2920,7 @@ export class AssistantService {
conversation
};
};
let addressRuntimeMetaForDeep = null;
if (config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1) {
const addressPreDecompose = config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1
? await runAddressLlmPreDecompose(this.normalizerService, payload, userMessage)
@@ -2911,6 +2931,11 @@ export class AssistantService {
traceId: null,
effectiveMessage: userMessage,
reason: "disabled_by_feature_flag",
llmCanonicalCandidateDetected: false,
predecomposeContract: (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({
sourceMessage: userMessage,
canonicalMessage: userMessage
}),
fallbackRuleHit: null,
sanitizedUserMessage: sanitizeAddressMessageForFallback(userMessage),
toolGateDecision: null,
@@ -2918,12 +2943,13 @@ export class AssistantService {
};
const addressInputMessage = toNonEmptyString(addressPreDecompose?.effectiveMessage) ?? userMessage;
const carryover = resolveAddressFollowupCarryoverContext(userMessage, session.items, addressInputMessage);
const toolGate = resolveAddressToolGateDecision(addressInputMessage, carryover?.followupContext ?? null);
const toolGate = resolveAddressToolGateDecision(addressInputMessage, carryover?.followupContext ?? null, addressPreDecompose);
const addressRuntimeMeta = {
...addressPreDecompose,
toolGateDecision: toolGate.decision,
toolGateReason: toolGate.reason
};
addressRuntimeMetaForDeep = addressRuntimeMeta;
if (!toolGate.runAddressLane) {
(0, log_1.logJson)({
timestamp: new Date().toISOString(),
@@ -2941,7 +2967,10 @@ export class AssistantService {
address_fallback_rule_hit: addressRuntimeMeta?.fallbackRuleHit ?? null,
address_sanitized_user_message: addressRuntimeMeta?.sanitizedUserMessage ?? null,
address_tool_gate_decision: addressRuntimeMeta?.toolGateDecision ?? null,
address_tool_gate_reason: addressRuntimeMeta?.toolGateReason ?? null
address_tool_gate_reason: addressRuntimeMeta?.toolGateReason ?? null,
address_llm_predecompose_contract_intent: addressRuntimeMeta?.predecomposeContract?.intent ?? null,
address_llm_predecompose_contract_aggregation_profile: addressRuntimeMeta?.predecomposeContract?.aggregation_profile ?? null,
address_llm_predecompose_contract_period_scope: addressRuntimeMeta?.predecomposeContract?.period?.scope ?? null
}
});
}
@@ -3273,6 +3302,14 @@ export class AssistantService {
problem_unit_ids_used: composition.problem_unit_ids_used
}
: {}),
address_llm_predecompose_attempted: Boolean(addressRuntimeMetaForDeep?.attempted),
address_llm_predecompose_applied: Boolean(addressRuntimeMetaForDeep?.applied),
address_llm_predecompose_reason: addressRuntimeMetaForDeep?.reason ?? null,
address_llm_predecompose_provider: addressRuntimeMetaForDeep?.provider ?? null,
address_fallback_rule_hit: addressRuntimeMetaForDeep?.fallbackRuleHit ?? null,
address_tool_gate_decision: addressRuntimeMetaForDeep?.toolGateDecision ?? null,
address_tool_gate_reason: addressRuntimeMetaForDeep?.toolGateReason ?? null,
address_llm_predecompose_contract: addressRuntimeMetaForDeep?.predecomposeContract ?? null,
answer_structure_v11: answerStructureV11,
investigation_state_snapshot: investigationStateSnapshot,
normalized: normalized.normalized