АДРЕСНЫЙ РЕЖИМ - M2.3e: стабилизация address runtime парсинг периодов, LLM pre-decompose fallback и приоритет intent для банковских операций

This commit is contained in:
2026-04-01 19:35:25 +03:00
parent 4d59672576
commit 18e0f1364d
16 changed files with 1467 additions and 562 deletions
@@ -1885,6 +1885,119 @@ function extractAddressQuestionFromNormalized(normalized) {
}
return null;
}
function stripMarkdownJsonFence(text) {
return String(text ?? "")
.trim()
.replace(/^```json\s*/i, "")
.replace(/^```\s*/i, "")
.replace(/```$/i, "")
.trim();
}
function safeParseLooseJson(text) {
const fenced = stripMarkdownJsonFence(text);
if (!fenced) {
return null;
}
try {
return JSON.parse(fenced);
}
catch (_error) {
// Local OpenAI-compatible models often wrap JSON with extra text.
// Try extracting the first top-level JSON object defensively.
const start = fenced.indexOf("{");
const end = fenced.lastIndexOf("}");
if (start < 0 || end < 0 || end <= start) {
return null;
}
const candidate = fenced.slice(start, end + 1).trim();
try {
return JSON.parse(candidate);
}
catch (_nestedError) {
return null;
}
}
}
function extractOutputTextFromRawNormalizerOutput(raw) {
if (!raw || typeof raw !== "object") {
return null;
}
const source = raw;
if (typeof source.output_text === "string" && source.output_text.trim().length > 0) {
return source.output_text;
}
if (Array.isArray(source.output)) {
for (const item of source.output) {
if (!item || typeof item !== "object") {
continue;
}
const content = item.content;
if (!Array.isArray(content)) {
continue;
}
for (const block of content) {
if (!block || typeof block !== "object") {
continue;
}
if (typeof block.text === "string" && block.text.trim().length > 0) {
return block.text;
}
}
}
}
if (source.response && typeof source.response === "object") {
const nested = source.response;
if (typeof nested.output_text === "string" && nested.output_text.trim().length > 0) {
return nested.output_text;
}
}
if (Array.isArray(source.choices) && source.choices.length > 0) {
const first = source.choices[0];
if (first && typeof first === "object" && first.message && typeof first.message === "object") {
const message = first.message;
if (typeof message.content === "string" && message.content.trim().length > 0) {
return message.content;
}
}
}
return null;
}
function extractAddressQuestionFromRawNormalizerOutput(rawModelOutput) {
const outputText = extractOutputTextFromRawNormalizerOutput(rawModelOutput);
if (!outputText) {
return null;
}
const parsed = safeParseLooseJson(outputText);
if (!parsed || typeof parsed !== "object") {
return null;
}
const source = parsed;
const fragments = Array.isArray(source.fragments) ? source.fragments : [];
for (const item of fragments) {
if (!item || typeof item !== "object") {
continue;
}
const fragment = item;
const domainRelevance = fragment.domain_relevance;
if (typeof domainRelevance === "string" && domainRelevance.trim().toLowerCase() === "out_of_scope") {
continue;
}
if (domainRelevance === false) {
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 = {
@@ -1922,8 +2035,10 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
};
try {
const normalized = await normalizerService.normalize(normalizePayload);
const candidate = extractAddressQuestionFromNormalized(normalized?.normalized);
if (!normalized?.ok || !candidate) {
const candidateFromNormalized = extractAddressQuestionFromNormalized(normalized?.normalized);
const candidateFromRaw = candidateFromNormalized ? null : extractAddressQuestionFromRawNormalizerOutput(normalized?.raw_model_output);
const candidate = candidateFromNormalized ?? candidateFromRaw;
if (!candidate) {
return {
...baseMeta,
attempted: true,
@@ -1934,13 +2049,25 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase());
const candidateCompact = compactWhitespace(candidate.toLowerCase());
const applied = sourceCompact !== candidateCompact;
const candidateSource = candidateFromNormalized ? "normalized" : "raw";
const reason = candidateSource === "normalized"
? applied
? "normalized_fragment_applied"
: "normalized_fragment_same"
: normalized?.ok
? applied
? "raw_fragment_applied"
: "raw_fragment_same"
: applied
? "raw_fragment_applied_after_normalize_failed"
: "raw_fragment_same_after_normalize_failed";
return {
attempted: true,
applied,
provider,
traceId: normalized?.trace_id ?? null,
effectiveMessage: applied ? candidate : userMessage,
reason: applied ? "normalized_fragment_applied" : "normalized_fragment_same"
reason
};
}
catch (error) {