АДРЕСНЫЙ РЕЖИМ - локальная подель на декомпозе
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user