АДРЕСНЫЙ РЕЖИМ -ADDRESS:Шаг 1 - ЛЛМ ФЕРСТ + feat(address): стабилизация wave1 dynamic resolver контрагентов, follow-up carryover и актуализация docs/tests
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user