АДРЕСНЫЙ РЕЖИМ - локальная подель на декомпозе

This commit is contained in:
2026-04-01 17:55:02 +03:00
parent 4060a5e575
commit 4d59672576
90 changed files with 19595 additions and 785 deletions
@@ -1693,8 +1693,9 @@ function buildAddressCoverageReport() {
out_of_scope_requirements: []
};
}
function buildAddressDebugPayload(addressDebug) {
function buildAddressDebugPayload(addressDebug, llmPreDecomposeMeta = null) {
const grounded = addressDebug.response_type === "LIMITED_WITH_REASON" ? "partial" : "grounded";
const llmMeta = llmPreDecomposeMeta && typeof llmPreDecomposeMeta === "object" ? llmPreDecomposeMeta : null;
return {
trace_id: `address-${(0, nanoid_1.nanoid)(10)}`,
prompt_version: "address_query_runtime_v1",
@@ -1752,12 +1753,204 @@ function buildAddressDebugPayload(addressDebug) {
runtime_readiness: addressDebug.runtime_readiness,
limited_reason_category: addressDebug.limited_reason_category,
response_type: addressDebug.response_type,
execution_lane: "address_query",
llm_decomposition_applied: Boolean(llmMeta?.applied),
llm_decomposition_attempted: Boolean(llmMeta?.attempted),
llm_provider_used: llmMeta?.provider ?? null,
llm_decomposition_trace_id: llmMeta?.traceId ?? null,
llm_decomposition_effective_message: llmMeta?.effectiveMessage ?? null,
llm_decomposition_reason: llmMeta?.reason ?? null,
answer_structure_v11: null,
investigation_state_snapshot: null,
normalized: null,
normalizer_output: null
normalizer_output: llmMeta?.traceId
? {
trace_id: llmMeta.traceId,
prompt_version: "normalizer_v2_0_2",
applied: Boolean(llmMeta?.applied),
effective_message: llmMeta?.effectiveMessage ?? null
}
: null
};
}
function toNonEmptyString(value) {
if (value === null || value === undefined) {
return null;
}
const text = String(value).trim();
return text.length > 0 ? text : null;
}
function readAddressFilterString(addressDebug, key) {
const filters = addressDebug?.extracted_filters;
if (!filters || typeof filters !== "object") {
return null;
}
return toNonEmptyString(filters[key]);
}
function findLastAddressAssistantDebug(items) {
for (let index = items.length - 1; index >= 0; index -= 1) {
const item = items[index];
if (!item || item.role !== "assistant" || !item.debug) {
continue;
}
const debug = item.debug;
if (debug.detected_mode === "address_query" || debug.prompt_version === "address_query_runtime_v1") {
return debug;
}
}
return null;
}
function hasAddressFollowupContextSignal(userMessage) {
const text = compactWhitespace(String(userMessage ?? "").toLowerCase());
if (!text) {
return false;
}
if (/(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+всю\s+истори(?:ю|и)|за\s+любой\s+период|for\s+all\s+time|all\s+time|for\s+entire\s+period|entire\s+period|for\s+any\s+period|any\s+period)/iu.test(text)) {
return true;
}
if (hasReferentialPointer(text)) {
return true;
}
const shortFollowup = countTokens(text) <= 8;
if (shortFollowup && hasFollowupMarker(text)) {
return true;
}
return false;
}
function resolveAddressFollowupCarryoverContext(userMessage, items) {
if (!hasAddressFollowupContextSignal(userMessage)) {
return null;
}
const previousAddressDebug = findLastAddressAssistantDebug(items);
if (!previousAddressDebug) {
return null;
}
const previousIntent = toNonEmptyString(previousAddressDebug.detected_intent);
const previousAnchorType = toNonEmptyString(previousAddressDebug.anchor_type);
const previousAnchor = toNonEmptyString(previousAddressDebug.anchor_value_resolved) ??
toNonEmptyString(previousAddressDebug.anchor_value_raw) ??
readAddressFilterString(previousAddressDebug, "counterparty") ??
readAddressFilterString(previousAddressDebug, "account") ??
readAddressFilterString(previousAddressDebug, "contract");
const previousFiltersRaw = previousAddressDebug.extracted_filters;
const previousFilters = previousFiltersRaw && typeof previousFiltersRaw === "object"
? { ...previousFiltersRaw }
: {};
if (!previousIntent && !previousAnchor && Object.keys(previousFilters).length === 0) {
return null;
}
return {
followupContext: {
previous_intent: previousIntent ?? undefined,
previous_filters: previousFilters,
previous_anchor_type: previousAnchorType ?? undefined,
previous_anchor_value: previousAnchor
},
previousAddressIntent: previousIntent,
previousAddressAnchor: previousAnchor
};
}
function isAddressLlmPreDecomposeCandidate(userMessage) {
const text = compactWhitespace(String(userMessage ?? "").toLowerCase());
if (!text) {
return false;
}
return /(?:\bдок\b|доки|документ|контрагент|договор|остаток|сч(?:е|ё)т|банк|выписк|платеж|оплат|поступлен|реализац|сверк|взаиморасч|кто\s+должен|show|list|documents?|counterparty|contract|account|balance|bank\s+operations?)/i.test(text);
}
function extractAddressQuestionFromNormalized(normalized) {
if (!normalized || typeof normalized !== "object") {
return null;
}
const source = normalized;
const fragments = Array.isArray(source.fragments) ? source.fragments : [];
for (const item of fragments) {
if (!item || typeof item !== "object") {
continue;
}
const fragment = item;
const domainRelevance = String(fragment.domain_relevance ?? "").trim().toLowerCase();
if (domainRelevance === "out_of_scope") {
continue;
}
const readiness = String(fragment.execution_readiness ?? "").trim().toLowerCase();
if (readiness === "no_route") {
continue;
}
const normalizedText = toNonEmptyString(fragment.normalized_fragment_text);
const rawText = toNonEmptyString(fragment.raw_fragment_text);
const candidate = compactWhitespace(normalizedText ?? rawText ?? "");
if (candidate.length >= 3 && candidate.length <= 500) {
return candidate;
}
}
return null;
}
async function runAddressLlmPreDecompose(normalizerService, payload, userMessage) {
const provider = payload?.llmProvider === "local" ? "local" : payload?.llmProvider === "openai" ? "openai" : null;
const baseMeta = {
attempted: false,
applied: false,
provider,
traceId: null,
effectiveMessage: userMessage,
reason: "not_attempted"
};
if (Boolean(payload?.useMock)) {
return {
...baseMeta,
reason: "skipped_in_mock"
};
}
if (!isAddressLlmPreDecomposeCandidate(userMessage)) {
return {
...baseMeta,
reason: "not_address_like"
};
}
const normalizePayload = {
llmProvider: payload?.llmProvider,
apiKey: payload?.apiKey,
model: payload?.model,
baseUrl: payload?.baseUrl,
temperature: 0,
maxOutputTokens: payload?.maxOutputTokens,
promptVersion: "normalizer_v2_0_2",
userQuestion: userMessage,
context: payload?.context,
useMock: Boolean(payload?.useMock),
retryPolicy: "single-pass-strict"
};
try {
const normalized = await normalizerService.normalize(normalizePayload);
const candidate = extractAddressQuestionFromNormalized(normalized?.normalized);
if (!normalized?.ok || !candidate) {
return {
...baseMeta,
attempted: true,
traceId: normalized?.trace_id ?? null,
reason: normalized?.ok ? "no_usable_fragment" : "normalize_failed"
};
}
const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase());
const candidateCompact = compactWhitespace(candidate.toLowerCase());
const applied = sourceCompact !== candidateCompact;
return {
attempted: true,
applied,
provider,
traceId: normalized?.trace_id ?? null,
effectiveMessage: applied ? candidate : userMessage,
reason: applied ? "normalized_fragment_applied" : "normalized_fragment_same"
};
}
catch (error) {
return {
...baseMeta,
attempted: true,
reason: `error:${error instanceof Error ? error.message : String(error)}`
};
}
}
export class AssistantService {
normalizerService;
sessions;
@@ -1789,80 +1982,112 @@ export class AssistantService {
debug: null
};
this.sessions.appendItem(sessionId, userItem);
if (config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1) {
const addressLane = await this.addressQueryService.tryHandle(userMessage);
if (addressLane?.handled) {
const debug = buildAddressDebugPayload(addressLane.debug);
const assistantItem = {
message_id: `msg-${(0, nanoid_1.nanoid)(10)}`,
session_id: sessionId,
role: "assistant",
text: addressLane.reply_text,
reply_type: addressLane.reply_type,
created_at: new Date().toISOString(),
trace_id: debug.trace_id,
debug
};
this.sessions.appendItem(sessionId, assistantItem);
const current = this.sessions.getSession(sessionId);
if (current) {
this.sessionLogger.persistSession(current);
}
const conversation = cloneItems(current?.items ?? []);
(0, log_1.logJson)({
timestamp: new Date().toISOString(),
level: "info",
service: "assistant_loop",
message: "assistant_message_processed",
sessionId,
eventType: "assistant_message_address",
details: {
session_id: sessionId,
message_id: assistantItem.message_id,
user_message: userMessage,
detected_mode: addressLane.debug.detected_mode,
query_shape: addressLane.debug.query_shape,
detected_intent: addressLane.debug.detected_intent,
extracted_filters: addressLane.debug.extracted_filters,
selected_recipe: addressLane.debug.selected_recipe,
mcp_call_status_legacy: addressLane.debug.mcp_call_status_legacy,
account_scope_mode: addressLane.debug.account_scope_mode,
account_scope_fallback_applied: addressLane.debug.account_scope_fallback_applied,
anchor_type: addressLane.debug.anchor_type,
resolver_confidence: addressLane.debug.resolver_confidence,
match_failure_stage: addressLane.debug.match_failure_stage,
match_failure_reason: addressLane.debug.match_failure_reason,
mcp_call_status: addressLane.debug.mcp_call_status,
rows_fetched: addressLane.debug.rows_fetched,
raw_rows_received: addressLane.debug.raw_rows_received,
rows_after_account_scope: addressLane.debug.rows_after_account_scope,
rows_after_recipe_filter: addressLane.debug.rows_after_recipe_filter,
rows_materialized: addressLane.debug.rows_materialized,
rows_matched: addressLane.debug.rows_matched,
materialization_drop_reason: addressLane.debug.materialization_drop_reason,
account_token_raw: addressLane.debug.account_token_raw,
account_token_normalized: addressLane.debug.account_token_normalized,
account_scope_fields_checked: addressLane.debug.account_scope_fields_checked,
account_scope_match_strategy: addressLane.debug.account_scope_match_strategy,
account_scope_drop_reason: addressLane.debug.account_scope_drop_reason,
runtime_readiness: addressLane.debug.runtime_readiness,
limited_reason_category: addressLane.debug.limited_reason_category,
response_type: addressLane.debug.response_type,
limitations: addressLane.debug.limitations,
assistant_reply: assistantItem.text,
reply_type: assistantItem.reply_type,
trace_id: assistantItem.trace_id
}
});
return {
ok: true,
const finalizeAddressLaneResponse = (addressLane, effectiveAddressUserMessage, carryoverMeta = null, llmPreDecomposeMeta = null) => {
const debug = buildAddressDebugPayload(addressLane.debug, llmPreDecomposeMeta);
const assistantItem = {
message_id: `msg-${(0, nanoid_1.nanoid)(10)}`,
session_id: sessionId,
role: "assistant",
text: addressLane.reply_text,
reply_type: addressLane.reply_type,
created_at: new Date().toISOString(),
trace_id: debug.trace_id,
debug
};
this.sessions.appendItem(sessionId, assistantItem);
const current = this.sessions.getSession(sessionId);
if (current) {
this.sessionLogger.persistSession(current);
}
const conversation = cloneItems(current?.items ?? []);
(0, log_1.logJson)({
timestamp: new Date().toISOString(),
level: "info",
service: "assistant_loop",
message: "assistant_message_processed",
sessionId,
eventType: "assistant_message_address",
details: {
session_id: sessionId,
message_id: assistantItem.message_id,
user_message: userMessage,
effective_address_user_message: effectiveAddressUserMessage,
address_followup_context_applied: Boolean(carryoverMeta),
address_followup_context_previous_intent: carryoverMeta?.previousAddressIntent ?? null,
address_followup_context_previous_anchor: carryoverMeta?.previousAddressAnchor ?? null,
address_llm_predecompose_attempted: Boolean(llmPreDecomposeMeta?.attempted),
address_llm_predecompose_applied: Boolean(llmPreDecomposeMeta?.applied),
address_llm_predecompose_provider: llmPreDecomposeMeta?.provider ?? null,
address_llm_predecompose_trace_id: llmPreDecomposeMeta?.traceId ?? null,
address_llm_predecompose_reason: llmPreDecomposeMeta?.reason ?? null,
detected_mode: addressLane.debug.detected_mode,
query_shape: addressLane.debug.query_shape,
detected_intent: addressLane.debug.detected_intent,
extracted_filters: addressLane.debug.extracted_filters,
selected_recipe: addressLane.debug.selected_recipe,
mcp_call_status_legacy: addressLane.debug.mcp_call_status_legacy,
account_scope_mode: addressLane.debug.account_scope_mode,
account_scope_fallback_applied: addressLane.debug.account_scope_fallback_applied,
anchor_type: addressLane.debug.anchor_type,
resolver_confidence: addressLane.debug.resolver_confidence,
match_failure_stage: addressLane.debug.match_failure_stage,
match_failure_reason: addressLane.debug.match_failure_reason,
mcp_call_status: addressLane.debug.mcp_call_status,
rows_fetched: addressLane.debug.rows_fetched,
raw_rows_received: addressLane.debug.raw_rows_received,
rows_after_account_scope: addressLane.debug.rows_after_account_scope,
rows_after_recipe_filter: addressLane.debug.rows_after_recipe_filter,
rows_materialized: addressLane.debug.rows_materialized,
rows_matched: addressLane.debug.rows_matched,
materialization_drop_reason: addressLane.debug.materialization_drop_reason,
account_token_raw: addressLane.debug.account_token_raw,
account_token_normalized: addressLane.debug.account_token_normalized,
account_scope_fields_checked: addressLane.debug.account_scope_fields_checked,
account_scope_match_strategy: addressLane.debug.account_scope_match_strategy,
account_scope_drop_reason: addressLane.debug.account_scope_drop_reason,
runtime_readiness: addressLane.debug.runtime_readiness,
limited_reason_category: addressLane.debug.limited_reason_category,
response_type: addressLane.debug.response_type,
limitations: addressLane.debug.limitations,
assistant_reply: assistantItem.text,
reply_type: assistantItem.reply_type,
conversation_item: assistantItem,
debug,
conversation
trace_id: assistantItem.trace_id
}
});
return {
ok: true,
session_id: sessionId,
assistant_reply: assistantItem.text,
reply_type: assistantItem.reply_type,
conversation_item: assistantItem,
debug,
conversation
};
};
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)
: {
attempted: false,
applied: false,
provider: payload?.llmProvider === "local" ? "local" : payload?.llmProvider === "openai" ? "openai" : null,
traceId: null,
effectiveMessage: userMessage,
reason: "disabled_by_feature_flag"
};
const addressInputMessage = toNonEmptyString(addressPreDecompose?.effectiveMessage) ?? userMessage;
const primaryAddressLane = await this.addressQueryService.tryHandle(addressInputMessage);
if (primaryAddressLane?.handled) {
return finalizeAddressLaneResponse(primaryAddressLane, addressInputMessage, null, addressPreDecompose);
}
const carryover = resolveAddressFollowupCarryoverContext(userMessage, session.items);
if (carryover?.followupContext) {
const contextualAddressLane = await this.addressQueryService.tryHandle(addressInputMessage, {
followupContext: carryover.followupContext
});
if (contextualAddressLane?.handled) {
return finalizeAddressLaneResponse(contextualAddressLane, addressInputMessage, carryover, addressPreDecompose);
}
}
}
const followupBinding = config_1.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 &&
@@ -1879,12 +2104,13 @@ export class AssistantService {
usage: null
};
const normalizePayload = {
llmProvider: payload.llmProvider,
apiKey: payload.apiKey,
model: payload.model,
baseUrl: payload.baseUrl,
temperature: payload.temperature,
maxOutputTokens: payload.maxOutputTokens,
promptVersion: payload.promptVersion ?? "normalizer_v2_0_2",
promptVersion: payload.promptVersion ?? "address_query_runtime_v1",
systemPrompt: payload.systemPrompt,
developerPrompt: payload.developerPrompt,
domainPrompt: payload.domainPrompt,