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

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
@@ -3,12 +3,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.extractAddressFilters = extractAddressFilters;
const ACCOUNT_PATTERN = /(?:сч[её]т|счет|account)[^0-9]{0,12}(\d{2}(?:[.,]\d{1,2})?)/i;
const LIMIT_PATTERN = /(?:\btop\b|\blimit\b|\bпервые\b|\bтоп\b)\s*(\d{1,3})/i;
const COUNTERPARTY_PATTERN = /(?:по\s+контрагенту|контрагент(?:у|а)?|by\s+counterparty|counterparty)\s+([^\r\n,.;:]+)/i;
const COUNTERPARTY_PATTERN = /(?:по\s+контрагенту|контрагент(?:у|а)?|по\s+контре|контра|по\s+компан(?:ии|ию|ия)|компан(?:ия|ии|ию)|по\s+организац(?:ии|ию|ия)|организац(?:ия|ии|ию)|по\s+поставщик(?:у|а)?|поставщик(?:у|а)?|по\s+клиент(?:у|а)?|клиент(?:у|а)?|по\s+покупател(?:ю|я)|покупател(?:ю|я)|по\s+партнер(?:у|а)?|партнер(?:у|а)?|by\s+counterparty|counterparty|by\s+company|company|by\s+supplier|supplier|by\s+vendor|vendor|by\s+customer|customer|by\s+client|client|by\s+partner|partner)\s+([^\r\n,.;:]+)/iu;
const CONTRACT_PATTERN = /(?:по\s+договору|договор(?:у|а)?\s*(?:№|#|n)?|by\s+contract|contract(?:\s*(?:no|number|#|n))?)\s+([^\r\n,.;:]+)/i;
const DATE_DMY_PATTERN = /\b(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{2,4})\b/;
const DATE_YMD_PATTERN = /\b(20\d{2})[.\/-](\d{1,2})[.\/-](\d{1,2})\b/;
const PERIOD_RANGE_PATTERN_1 = /(?:from|с)\s+(\d{1,4}[.\/-]\d{1,2}[.\/-]\d{1,4})\s+(?:to|по)\s+(\d{1,4}[.\/-]\d{1,2}[.\/-]\d{1,4})/i;
const PERIOD_RANGE_PATTERN_2 = /(?:between|за\s+период\s+с)\s+(\d{1,4}[.\/-]\d{1,2}[.\/-]\d{1,4})\s+(?:and|по)\s+(\d{1,4}[.\/-]\d{1,2}[.\/-]\d{1,4})/i;
const YEAR_RANGE_PATTERN = /(?:за|for|с|from)?\s*(20\d{2})\s*(?:[-‐‑‒–—―−]|до|to|по)\s*(20\d{2})(?:\s*(?:г(?:од|ода)?\.?|year))?(?=[^\d]|$)/iu;
const YEAR_RANGE_LOOSE_PATTERN = /\b(20\d{2})\b\s*(?:[-‐‑‒–—―−]|до|to|по)\s*\b(20\d{2})\b/iu;
const YEAR_PERIOD_PATTERN = /(?:за|for)\s*(20\d{2})(?!\s*(?:[-‐‑‒–—―−]|до|to|по)\s*20\d{2})\s*(?:г(?:од|ода)?\.?|year)?/iu;
const YEAR_PERIOD_SHORT_PATTERN = /(?:^|[\s,.;:!?()\-])(\d{2})\s*(?:г(?:од|ода)?\.?|year)(?=$|[\s,.;:!?()\-])/iu;
const YEAR_PERIOD_ANY_PATTERN = /(?:^|[\s,.;:!?()\-])((?:19|20)\d{2})(?!\s*(?:[-‐‑‒–—―−]|до|to|по)\s*(?:19|20)\d{2})(?![.\/-]\d)(?:\s*(?:г(?:од|ода)?\.?|year))?(?=$|[\s,.;:!?()\-])/iu;
const MONTH_PERIOD_NUMERIC_PATTERN = /(?:за|for)\s*(0?[1-9]|1[0-2])[.\/-](20\d{2})/i;
const MONTH_PERIOD_NAME_PATTERN = /(?:за|for)\s+([a-zа-яё]+)\s+(20\d{2})(?:\s*г(?:од|ода|\\.)?)?/iu;
function toIsoDate(year, month, day) {
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) {
return null;
@@ -61,6 +68,64 @@ function parseDateToken(token) {
}
return undefined;
}
function resolveMonthByName(rawMonthName) {
const token = String(rawMonthName ?? "").trim().toLowerCase();
if (!token) {
return undefined;
}
if (/^янв|^january|^jan/.test(token))
return 1;
if (/^фев|^february|^feb/.test(token))
return 2;
if (/^мар|^march|^mar/.test(token))
return 3;
if (/^апр|^april|^apr/.test(token))
return 4;
if (/^ма[йя]|^may/.test(token))
return 5;
if (/^июн|^june|^jun/.test(token))
return 6;
if (/^июл|^july|^jul/.test(token))
return 7;
if (/^авг|^august|^aug/.test(token))
return 8;
if (/^сен|^сент|^september|^sep/.test(token))
return 9;
if (/^окт|^october|^oct/.test(token))
return 10;
if (/^ноя|^november|^nov/.test(token))
return 11;
if (/^дек|^december|^dec/.test(token))
return 12;
return undefined;
}
function extractMonthPeriod(text) {
const numericMatch = text.match(MONTH_PERIOD_NUMERIC_PATTERN);
if (numericMatch) {
const month = Number(numericMatch[1]);
const year = Number(numericMatch[2]);
if (month >= 1 && month <= 12 && year >= 2000 && year <= 2099) {
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
return {
period_from: `${year}-${String(month).padStart(2, "0")}-01`,
period_to: `${year}-${String(month).padStart(2, "0")}-${String(lastDay).padStart(2, "0")}`
};
}
}
const byNameMatch = text.match(MONTH_PERIOD_NAME_PATTERN);
if (byNameMatch) {
const month = resolveMonthByName(String(byNameMatch[1]));
const year = Number(byNameMatch[2]);
if (month && year >= 2000 && year <= 2099) {
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
return {
period_from: `${year}-${String(month).padStart(2, "0")}-01`,
period_to: `${year}-${String(month).padStart(2, "0")}-${String(lastDay).padStart(2, "0")}`
};
}
}
return {};
}
function extractPeriodRange(text) {
const directMatch = text.match(PERIOD_RANGE_PATTERN_1) ?? text.match(PERIOD_RANGE_PATTERN_2);
if (!directMatch) {
@@ -73,6 +138,64 @@ function extractPeriodRange(text) {
...(periodTo ? { period_to: periodTo } : {})
};
}
function extractYearPeriod(text) {
const match = text.match(YEAR_PERIOD_PATTERN);
if (match) {
const year = Number(match[1]);
if (!Number.isFinite(year) || year < 2000 || year > 2099) {
return {};
}
return {
period_from: `${year}-01-01`,
period_to: `${year}-12-31`
};
}
const relaxedYearMatch = text.match(YEAR_PERIOD_ANY_PATTERN);
if (relaxedYearMatch) {
const year = Number(relaxedYearMatch[1]);
if (Number.isFinite(year) && year >= 2000 && year <= 2099) {
return {
period_from: `${year}-01-01`,
period_to: `${year}-12-31`
};
}
}
const shortYearMatch = text.match(YEAR_PERIOD_SHORT_PATTERN);
if (!shortYearMatch) {
return {};
}
const shortYear = Number(shortYearMatch[1]);
if (!Number.isFinite(shortYear) || shortYear < 0 || shortYear > 99) {
return {};
}
const year = 2000 + shortYear;
return {
period_from: `${year}-01-01`,
period_to: `${year}-12-31`
};
}
function extractYearRangePeriod(text) {
const match = text.match(YEAR_RANGE_PATTERN) ?? text.match(YEAR_RANGE_LOOSE_PATTERN);
if (!match) {
return {};
}
const leftYear = Number(match[1]);
const rightYear = Number(match[2]);
if (!Number.isFinite(leftYear) ||
!Number.isFinite(rightYear) ||
leftYear < 2000 ||
leftYear > 2099 ||
rightYear < 2000 ||
rightYear > 2099) {
return {};
}
const fromYear = Math.min(leftYear, rightYear);
const toYear = Math.max(leftYear, rightYear);
return {
period_from: `${fromYear}-01-01`,
period_to: `${toYear}-12-31`
};
}
function cleanupAnchorValue(value) {
const normalized = String(value ?? "").trim();
if (!normalized) {
@@ -84,11 +207,11 @@ function cleanupAnchorValue(value) {
if (periodTailPattern.test(normalized)) {
return normalized.replace(periodTailPattern, "").trim();
}
const allTimeTailPattern = /\s+за\s+вс[её]\s+время(?:\s+|$)[\s\S]*$/iu;
const allTimeTailPattern = /\s+за\s+(?:вс[её]\s+время|весь\s+период|весь\s+срок|всю\s+истори(?:ю|и)|любой\s+период|любой\s+срок)(?:\s+|$)[\s\S]*$/iu;
if (allTimeTailPattern.test(normalized)) {
return normalized.replace(allTimeTailPattern, "").trim();
}
const allTimeTailPatternEn = /\s+(?:for\s+all\s+time|all\s+time)(?:\s+|$)[\s\S]*$/iu;
const allTimeTailPatternEn = /\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|for\s+full\s+history|full\s+history)(?:\s+|$)[\s\S]*$/iu;
if (allTimeTailPatternEn.test(normalized)) {
return normalized.replace(allTimeTailPatternEn, "").trim();
}
@@ -99,7 +222,186 @@ function cleanupAnchorValue(value) {
}
function hasAllTimeHint(text) {
const value = String(text ?? "");
return /(?:за\s+вс[её]\s+время|for\s+all\s+time|all\s+time)/iu.test(value);
return /(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\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|for\s+full\s+history|full\s+history)/iu.test(value);
}
function extractLooseByAnchorValue(text) {
const match = String(text ?? "").match(/(?:^|\s)по\s+([a-zа-яё][a-zа-яё0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu);
if (!match) {
return undefined;
}
const token = String(match[1] ?? "").trim();
if (!token) {
return undefined;
}
const lowered = token.toLowerCase();
const stopWords = new Set([
"контрагенту",
"контрагента",
"контре",
"компании",
"компанию",
"организации",
"организацию",
"поставщику",
"поставщика",
"клиенту",
"клиента",
"покупателю",
"покупателя",
"партнеру",
"партнера",
"договору",
"договора",
"счету",
"счёту",
"дате",
"периоду",
"период",
"документам",
"докам",
"взаиморасчетам",
"взаиморасчётам"
]);
if (stopWords.has(lowered)) {
return undefined;
}
return token;
}
function isLikelyCounterpartyToken(rawToken) {
const token = String(rawToken ?? "").trim();
const lowered = token.toLowerCase();
if (!token || token.length < 2) {
return false;
}
if (/^\d+$/.test(lowered)) {
return false;
}
if (/^(?:19|20)\d{2}$/.test(lowered)) {
return false;
}
const stopWords = new Set([
"за",
"с",
"по",
"на",
"и",
"или",
"док",
"доки",
"документ",
"документы",
"документов",
"банк",
"банковские",
"операции",
"платежи",
"платеж",
"платёж",
"контрагент",
"контрагенту",
"контрагента",
"компания",
"компании",
"организация",
"организации",
"год",
"года",
"г",
"плс",
"pls",
"пж",
"пжлст",
"пожалуйста",
"бля",
"блять",
"епт",
"ёпт",
"епта",
"нах",
"нахуй",
"покеж",
"покажи",
"выведи"
]);
return !stopWords.has(lowered);
}
function hasDocsOrBankSignal(text) {
const lowered = String(text ?? "").toLowerCase();
return /(?:док(?:и|умент|ументы|ументов)|docs?|documents?|банк|выписк|платеж|платёж|оплат|transactions?|bank\s+ops|bank\s+operations?)/iu.test(lowered);
}
function extractCounterpartyFromFreeTextHeuristic(text) {
if (!hasDocsOrBankSignal(text)) {
return undefined;
}
const tokens = String(text ?? "")
.split(/[^a-zа-яё0-9._-]+/iu)
.map((item) => item.trim())
.filter((item) => item.length > 0);
if (tokens.length === 0) {
return undefined;
}
const monthTokens = [
"янв",
"фев",
"мар",
"апр",
"май",
"июн",
"июл",
"авг",
"сен",
"сент",
"окт",
"ноя",
"дек",
"january",
"february",
"march",
"april",
"may",
"june",
"july",
"august",
"september",
"october",
"november",
"december"
];
for (const token of tokens) {
const lowered = token.toLowerCase();
if (!isLikelyCounterpartyToken(lowered)) {
continue;
}
if (/^\d{2}$/.test(lowered) || /^\d{4}$/.test(lowered)) {
continue;
}
if (monthTokens.some((prefix) => lowered.startsWith(prefix))) {
continue;
}
if (/(?:^за$|^for$|^from$|^to$|^по$|^с$|^год$|^года$|^г$|^year$)/iu.test(lowered)) {
continue;
}
return token;
}
return undefined;
}
function extractImplicitCounterpartyValue(text) {
const input = String(text ?? "");
const beforeDocsMatch = input.match(/(?:^|\s)([a-zа-яё][a-zа-яё0-9._-]{1,})\s+(?:док(?:и|ум(?:ент(?:ы|ов|ам|а)?)?)|docs?|documents?)(?=[\s,.;:!?)]|$)/iu);
if (beforeDocsMatch) {
const candidate = String(beforeDocsMatch[1] ?? "").trim();
if (isLikelyCounterpartyToken(candidate)) {
return candidate;
}
}
const afterDocsMatch = input.match(/(?:док(?:и|ум(?:ент(?:ы|ов|ам|а)?)?)|docs?|documents?)\s+(?:по\s+)?([a-zа-яё][a-zа-яё0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu);
if (afterDocsMatch) {
const candidate = String(afterDocsMatch[1] ?? "").trim();
if (isLikelyCounterpartyToken(candidate)) {
return candidate;
}
}
return undefined;
}
function shiftDaysIso(baseIso, deltaDays) {
const date = new Date(`${baseIso}T00:00:00.000Z`);
@@ -137,6 +439,27 @@ function extractAddressFilters(userMessage, intent) {
if (counterpartyMatch) {
filters.counterparty = cleanupAnchorValue(String(counterpartyMatch[1]));
}
if (!filters.counterparty && (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty")) {
const fallbackCounterparty = extractLooseByAnchorValue(text);
if (fallbackCounterparty) {
filters.counterparty = cleanupAnchorValue(fallbackCounterparty);
warnings.push("counterparty_anchor_derived_from_loose_by_phrase");
}
}
if (!filters.counterparty && (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty")) {
const implicitCounterparty = extractImplicitCounterpartyValue(text);
if (implicitCounterparty) {
filters.counterparty = cleanupAnchorValue(implicitCounterparty);
warnings.push("counterparty_anchor_derived_from_implicit_phrase");
}
}
if (!filters.counterparty && (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty")) {
const heuristicCounterparty = extractCounterpartyFromFreeTextHeuristic(text);
if (heuristicCounterparty) {
filters.counterparty = cleanupAnchorValue(heuristicCounterparty);
warnings.push("counterparty_anchor_derived_from_free_text_heuristic");
}
}
const contractMatch = text.match(CONTRACT_PATTERN);
if (contractMatch) {
filters.contract = cleanupAnchorValue(String(contractMatch[1]));
@@ -148,6 +471,30 @@ function extractAddressFilters(userMessage, intent) {
if (periodRange.period_to) {
filters.period_to = periodRange.period_to;
}
if (!filters.period_from && !filters.period_to) {
const monthPeriod = extractMonthPeriod(text);
if (monthPeriod.period_from && monthPeriod.period_to) {
filters.period_from = monthPeriod.period_from;
filters.period_to = monthPeriod.period_to;
warnings.push("period_derived_from_month_phrase");
}
}
if (!filters.period_from && !filters.period_to) {
const yearRangePeriod = extractYearRangePeriod(text);
if (yearRangePeriod.period_from && yearRangePeriod.period_to) {
filters.period_from = yearRangePeriod.period_from;
filters.period_to = yearRangePeriod.period_to;
warnings.push("period_derived_from_year_range_phrase");
}
}
if (!filters.period_from && !filters.period_to) {
const yearPeriod = extractYearPeriod(text);
if (yearPeriod.period_from && yearPeriod.period_to) {
filters.period_from = yearPeriod.period_from;
filters.period_to = yearPeriod.period_to;
warnings.push("period_derived_from_year_phrase");
}
}
// If explicit period window exists, do not infer as_of_date from one of its boundary dates.
if (!filters.period_from && !filters.period_to) {
const asOfDate = extractAsOfDate(text);
+191 -3
View File
@@ -62,23 +62,201 @@ const OPEN_ITEMS_HINTS = [
const DOCUMENTS_BY_COUNTERPARTY_HINTS = [
"documents by counterparty",
"docs by counterparty",
"documents by company",
"documents by supplier",
"documents by customer",
"documents by client",
"documents by partner",
"show documents by counterparty",
"list documents by counterparty",
"документы по",
"доступные документы",
"список документов",
"документ",
"доки",
"доки по",
"док по",
"по контрагент"
];
const BANK_OPERATIONS_BY_COUNTERPARTY_HINTS = [
"bank operations by counterparty",
"bank payments by counterparty",
"payment orders by counterparty",
"bank operations by company",
"bank operations by supplier",
"bank operations by customer",
"show bank operations by counterparty",
"bank ops",
"transactions by counterparty",
"банков",
"выписк",
"платеж"
"платеж",
"платёж",
"оплат",
"списан",
"поступлен",
"движени"
];
function hasAny(text, patterns) {
return patterns.some((item) => text.includes(item));
}
function isLikelyCounterpartyToken(rawToken) {
const token = String(rawToken ?? "").trim().toLowerCase();
if (!token || token.length < 2) {
return false;
}
if (/^\d+$/.test(token)) {
return false;
}
if (/^(?:19|20)\d{2}$/.test(token)) {
return false;
}
const stopWords = new Set([
"за",
"с",
"по",
"на",
"и",
"или",
"док",
"доки",
"доки?",
"документ",
"документы",
"документов",
"банк",
"банковские",
"операции",
"платежи",
"платеж",
"платёж",
"контрагент",
"контрагенту",
"контрагента",
"компания",
"компании",
"организация",
"организации",
"год",
"года",
"г",
"плс",
"pls",
"пж",
"пжлст",
"пожалуйста",
"бля",
"блять",
"епт",
"ёпт",
"епта",
"нах",
"нахуй"
]);
return !stopWords.has(token);
}
function hasPartyAnchorMention(text) {
return (text.includes("контраг") ||
text.includes("контра") ||
text.includes("counterparty") ||
text.includes("компан") ||
text.includes("company") ||
text.includes("организац") ||
text.includes("supplier") ||
text.includes("vendor") ||
text.includes("customer") ||
text.includes("client") ||
text.includes("partner") ||
text.includes("поставщик") ||
text.includes("клиент") ||
text.includes("покупател") ||
text.includes("партнер"));
}
function hasLooseByAnchorMention(text) {
const match = text.match(/(?:^|\s)по\s+([a-zа-яё][a-zа-яё0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu);
if (!match) {
return false;
}
const token = String(match[1] ?? "").toLowerCase();
if (!token) {
return false;
}
const stopWords = new Set([
"контрагенту",
"контрагента",
"контре",
"компании",
"компанию",
"организации",
"организацию",
"поставщику",
"поставщика",
"клиенту",
"клиента",
"покупателю",
"покупателя",
"партнеру",
"партнера",
"договору",
"договора",
"счету",
"счёту",
"дате",
"периоду",
"период",
"документам",
"докам"
]);
return !stopWords.has(token);
}
function hasImplicitCounterpartyAnchorAroundDocs(text) {
const beforeDocsMatch = text.match(/(?:^|\s)([a-zа-яё][a-zа-яё0-9._-]{1,})\s+(?:док(?:и|ум(?:ент(?:ы|ов|ам|а)?)?)|docs?|documents?)(?=[\s,.;:!?)]|$)/iu);
if (beforeDocsMatch && isLikelyCounterpartyToken(String(beforeDocsMatch[1] ?? ""))) {
return true;
}
const afterDocsMatch = text.match(/(?:док(?:и|ум(?:ент(?:ы|ов|ам|а)?)?)|docs?|documents?)\s+(?:по\s+)?([a-zа-яё][a-zа-яё0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu);
if (afterDocsMatch && isLikelyCounterpartyToken(String(afterDocsMatch[1] ?? ""))) {
return true;
}
return false;
}
function hasDocsOrBankSignal(text) {
return /(?:док(?:и|умент|ументы|ументов)|docs?|documents?|банк|выписк|платеж|платёж|оплат|transactions?|bank\s+ops|bank\s+operations?)/iu.test(text);
}
function hasHeuristicCounterpartyAnchor(text) {
if (!hasDocsOrBankSignal(text)) {
return false;
}
const tokens = String(text ?? "")
.split(/[^a-zа-яё0-9._-]+/iu)
.map((item) => item.trim())
.filter((item) => item.length > 0);
for (const token of tokens) {
const lowered = token.toLowerCase();
if (!isLikelyCounterpartyToken(lowered)) {
continue;
}
if (/^\d{2}$/.test(lowered) || /^\d{4}$/.test(lowered)) {
continue;
}
if (/(?:^за$|^for$|^from$|^to$|^по$|^с$|^год$|^года$|^г$|^year$)/iu.test(lowered)) {
continue;
}
return true;
}
return false;
}
function hasGenericAddressLookupSignal(text) {
return (/\bесть\b/iu.test(text) ||
/\bпокажи\b/iu.test(text) ||
/\bвыведи\b/iu.test(text) ||
/\bкакие\b/iu.test(text) ||
/\bчто(?:-|\s)?то\b/iu.test(text) ||
/за\s+любой\s+период/iu.test(text) ||
/за\s+вс[её]\s+время/iu.test(text) ||
/for\s+all\s+time/iu.test(text) ||
/all\s+time/iu.test(text));
}
function hasAccountNumberAnchor(text) {
return /(?:account|сч[её]т|счет)\D{0,12}\d{2}(?:[.,]\d{1,2})?/i.test(text);
}
@@ -113,7 +291,7 @@ function resolveAddressIntent(userMessage) {
};
}
if (hasAny(text, BANK_OPERATIONS_BY_COUNTERPARTY_HINTS) &&
(text.includes("контраг") || text.includes("counterparty"))) {
(hasPartyAnchorMention(text) || hasLooseByAnchorMention(text) || hasHeuristicCounterpartyAnchor(text))) {
return {
intent: "bank_operations_by_counterparty",
confidence: "medium",
@@ -121,13 +299,23 @@ function resolveAddressIntent(userMessage) {
};
}
if (hasAny(text, DOCUMENTS_BY_COUNTERPARTY_HINTS) &&
(text.includes("контраг") || text.includes("counterparty"))) {
(hasPartyAnchorMention(text) ||
hasLooseByAnchorMention(text) ||
hasImplicitCounterpartyAnchorAroundDocs(text) ||
hasHeuristicCounterpartyAnchor(text))) {
return {
intent: "list_documents_by_counterparty",
confidence: "medium",
reasons: ["documents_by_counterparty_signal_detected"]
};
}
if (hasLooseByAnchorMention(text) && hasGenericAddressLookupSignal(text)) {
return {
intent: "list_documents_by_counterparty",
confidence: "low",
reasons: ["generic_lookup_with_loose_anchor_fallback"]
};
}
if (hasAny(text, OPEN_ITEMS_HINTS) && (text.includes("контраг") || text.includes("договор") || text.includes("counterparty") || text.includes("contract"))) {
return {
intent: "open_items_by_counterparty_or_contract",
+78 -6
View File
@@ -1,7 +1,11 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.executeAddressMcpQuery = executeAddressMcpQuery;
const config_1 = require("../config");
const iconv_lite_1 = __importDefault(require("iconv-lite"));
function toStringValue(value) {
if (value === null || value === undefined) {
return "";
@@ -20,8 +24,76 @@ function parseFiniteNumber(value) {
}
return null;
}
function textMojibakeScore(value) {
const source = String(value ?? "");
const cyrillic = (source.match(/[А-Яа-яЁё]/g) ?? []).length;
const latin = (source.match(/[A-Za-z]/g) ?? []).length;
const hardMarkers = (source.match(/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ‘’“”•–—™љ›њќћџ]/g) ?? []).length;
const pairMarkers = (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length;
return cyrillic + latin - hardMarkers * 3 - pairMarkers * 2;
}
function looksLikeMojibake(value) {
const source = String(value ?? "");
if (!source.trim()) {
return false;
}
if (/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ‘’“”•–—™љ›њќћџ]/.test(source)) {
return true;
}
return (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length >= 2;
}
function decodeUtf8FromWin1251Mojibake(value) {
if (!looksLikeMojibake(value)) {
return value;
}
try {
const bytes = iconv_lite_1.default.encode(value, "win1251");
const decoded = bytes.toString("utf8");
return textMojibakeScore(decoded) > textMojibakeScore(value) ? decoded : value;
}
catch {
return value;
}
}
function decodeUtf8FromLatin1Mojibake(value) {
if (!looksLikeMojibake(value)) {
return value;
}
try {
const decoded = Buffer.from(value, "latin1").toString("utf8");
return textMojibakeScore(decoded) > textMojibakeScore(value) ? decoded : value;
}
catch {
return value;
}
}
function normalizeMojibakeString(value) {
const fromWin1251 = decodeUtf8FromWin1251Mojibake(value);
return decodeUtf8FromLatin1Mojibake(fromWin1251);
}
function normalizeMojibakeValue(value) {
if (typeof value === "string") {
return normalizeMojibakeString(value);
}
if (Array.isArray(value)) {
return value.map((item) => normalizeMojibakeValue(item));
}
if (value && typeof value === "object") {
const source = value;
const normalized = {};
for (const [key, raw] of Object.entries(source)) {
const repairedKey = normalizeMojibakeString(key);
normalized[repairedKey] = normalizeMojibakeValue(raw);
}
return normalized;
}
return value;
}
function normalizeMojibakeRows(rows) {
return rows.map((row) => normalizeMojibakeValue(row));
}
function parseRowsFromTextTable(source) {
const normalized = String(source ?? "").replace(/\r/g, "").trim();
const normalized = normalizeMojibakeString(String(source ?? "")).replace(/\r/g, "").trim();
if (!normalized) {
return [];
}
@@ -91,7 +163,7 @@ function parseRowsFromTextTable(source) {
row.Amount = parseFiniteNumber(values[4]) ?? values[4];
rows.push(row);
}
return rows;
return normalizeMojibakeRows(rows);
}
function parseExecutePayload(payload) {
if (!payload || typeof payload !== "object") {
@@ -110,9 +182,9 @@ function parseExecutePayload(payload) {
};
}
if (Array.isArray(source.data)) {
const rows = source.data
const rows = normalizeMojibakeRows(source.data
.map((item) => (item && typeof item === "object" ? item : null))
.filter((item) => item !== null);
.filter((item) => item !== null));
return {
ok: true,
rows,
@@ -127,9 +199,9 @@ function parseExecutePayload(payload) {
};
}
if (source.data && typeof source.data === "object" && Array.isArray(source.data.rows)) {
const rows = (source.data.rows ?? [])
const rows = normalizeMojibakeRows((source.data.rows ?? [])
.map((item) => (item && typeof item === "object" ? item : null))
.filter((item) => item !== null);
.filter((item) => item !== null));
return {
ok: true,
rows,
@@ -27,6 +27,13 @@ const ADDRESS_ACTION_TOKENS = [
const ADDRESS_ENTITY_TOKENS = [
"counterparty",
"counterparties",
"company",
"organization",
"supplier",
"vendor",
"customer",
"client",
"partner",
"contract",
"contracts",
"account",
@@ -42,10 +49,22 @@ const ADDRESS_ENTITY_TOKENS = [
"owes",
"owed",
"контрагент",
"контра",
"компан",
"организац",
"поставщик",
"клиент",
"покупател",
"партнер",
"банк",
"выписк",
"операц",
"договор",
"счет",
"счёт",
"документ",
"доки",
"док",
"остаток",
"дебитор",
"кредитор",
@@ -71,6 +90,54 @@ const DEEP_REASONING_TOKENS = [
"разрыв",
"ошибк"
];
function hasLooseByAnchorMention(text) {
const match = text.match(/(?:^|\s)по\s+([a-zа-яё][a-zа-яё0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu);
if (!match) {
return false;
}
const token = String(match[1] ?? "").toLowerCase();
if (!token) {
return false;
}
const stopWords = new Set([
"контрагенту",
"контрагента",
"контре",
"компании",
"компанию",
"организации",
"организацию",
"поставщику",
"поставщика",
"клиенту",
"клиента",
"покупателю",
"покупателя",
"партнеру",
"партнера",
"договору",
"договора",
"счету",
"счёту",
"дате",
"периоду",
"период",
"документам",
"докам",
"взаиморасчетам",
"взаиморасчётам"
]);
return !stopWords.has(token);
}
function hasAddressFollowupSignal(text) {
if (/(?:за\s+любой\s+период|за\s+вс[её]\s+время|for\s+all\s+time|all\s+time)/iu.test(text)) {
return true;
}
if (/(?:\bесть\s+что(?:-|\s)?то\b|\bесть\s+ли\b|\bчто\s+есть\b)/iu.test(text)) {
return true;
}
return false;
}
function hasAnyToken(text, tokens) {
return tokens.some((token) => text.includes(token));
}
@@ -86,6 +153,8 @@ function detectAddressQuestionMode(userMessage) {
const hasAddressAction = hasAnyToken(text, ADDRESS_ACTION_TOKENS);
const hasAddressEntity = hasAnyToken(text, ADDRESS_ENTITY_TOKENS);
const hasDeepReasoning = hasAnyToken(text, DEEP_REASONING_TOKENS);
const hasLooseByAnchor = hasLooseByAnchorMention(text);
const hasFollowupSignal = hasAddressFollowupSignal(text);
if (hasAddressAction && hasAddressEntity && !hasDeepReasoning) {
return {
mode: "address_query",
@@ -93,6 +162,13 @@ function detectAddressQuestionMode(userMessage) {
reasons: ["address_action_detected", "address_entity_detected"]
};
}
if (hasLooseByAnchor && (hasAddressAction || hasAddressEntity || hasFollowupSignal) && !hasDeepReasoning) {
return {
mode: "address_query",
confidence: "medium",
reasons: ["loose_by_anchor_detected", ...(hasFollowupSignal ? ["address_followup_signal_detected"] : [])]
};
}
if (hasAddressEntity && !hasDeepReasoning) {
return {
mode: "address_query",
+160 -215
View File
@@ -2,12 +2,11 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.AddressQueryService = void 0;
const config_1 = require("../config");
const addressQueryClassifier_1 = require("./addressQueryClassifier");
const addressQueryShapeClassifier_1 = require("./addressQueryShapeClassifier");
const addressIntentResolver_1 = require("./addressIntentResolver");
const addressFilterExtractor_1 = require("./addressFilterExtractor");
const addressRecipeCatalog_1 = require("./addressRecipeCatalog");
const addressMcpClient_1 = require("./addressMcpClient");
const decomposeStage_1 = require("./address_runtime/decomposeStage");
const resolveStage_1 = require("./address_runtime/resolveStage");
const composeStage_1 = require("./address_runtime/composeStage");
const ACCOUNT_SCOPE_FIELDS_CHECKED = ["account_dt", "account_kt", "registrator", "analytics"];
const ACCOUNT_SCOPE_MATCH_STRATEGY = "account_code_regex_plus_alias_map_v1";
const PARTY_ANCHOR_STOPWORDS = new Set([
@@ -323,20 +322,56 @@ function applyIntentSpecificFilter(intent, rows) {
}
return rows;
}
function formatTopRows(rows, limit = 6) {
return rows.slice(0, limit).map((row, index) => {
const period = row.period ?? "дата не указана";
const amount = row.amount !== null ? `${row.amount}` : "сумма не указана";
const accounts = [row.account_dt ?? "-", row.account_kt ?? "-"].join(" / ");
const analytics = row.analytics.length > 0 ? ` | аналитика: ${row.analytics.slice(0, 2).join("; ")}` : "";
return `${index + 1}. ${period} | ${row.registrator} | ${accounts} | ${amount}${analytics}`;
});
function hasExplicitPeriodWindow(filters) {
return ((typeof filters.period_from === "string" && filters.period_from.trim().length > 0) ||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0));
}
function inferReplyType(responseType) {
if (responseType === "FACTUAL_LIST" || responseType === "FACTUAL_SUMMARY") {
return "factual";
function canAutoBroadenPeriodWindow(intent, filters) {
if (!hasExplicitPeriodWindow(filters)) {
return false;
}
return "partial_coverage";
return intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty";
}
function toIsoDatePrefix(value) {
if (!value) {
return null;
}
const normalized = String(value).trim();
if (!normalized) {
return null;
}
const match = normalized.match(/^(\d{4}-\d{2}-\d{2})/);
if (match) {
return match[1];
}
return null;
}
function deriveObservedPeriodWindow(rows) {
const dates = rows
.map((row) => toIsoDatePrefix(row.period))
.filter((item) => Boolean(item))
.sort();
if (dates.length === 0) {
return {
period_from: null,
period_to: null
};
}
return {
period_from: dates[0],
period_to: dates[dates.length - 1]
};
}
function composeAutoBroadenedPeriodPrefix(requested, observed) {
const requestedFrom = typeof requested.period_from === "string" ? requested.period_from : null;
const requestedTo = typeof requested.period_to === "string" ? requested.period_to : null;
if (requestedFrom && requestedTo && observed.period_from && observed.period_to) {
return `По окну ${requestedFrom}..${requestedTo} строк не найдено; показаны ближайшие доступные данные ${observed.period_from}..${observed.period_to}.`;
}
if (requestedFrom && requestedTo) {
return `По окну ${requestedFrom}..${requestedTo} строк не найдено; показаны ближайшие доступные данные по этому якорю.`;
}
return "По заданному периоду строк не найдено; показаны ближайшие доступные данные по этому якорю.";
}
function runtimeReadinessForLimitedCategory(category) {
if (category === "empty_match" || category === "missing_anchor") {
@@ -449,90 +484,6 @@ function toLegacyMcpStatus(status) {
}
return status;
}
function resolvePrimaryAnchor(intent, filters) {
const account = typeof filters.account === "string" ? filters.account.trim() : "";
const counterparty = typeof filters.counterparty === "string" ? filters.counterparty.trim() : "";
const contract = typeof filters.contract === "string" ? filters.contract.trim() : "";
const documentRef = typeof filters.document_ref === "string" ? filters.document_ref.trim() : "";
if (intent === "account_balance_snapshot" || intent === "documents_forming_balance") {
if (account) {
return {
anchor_type: "account",
anchor_value_raw: account,
anchor_value_resolved: account,
resolver_confidence: "high",
ambiguity_count: 0
};
}
}
if (counterparty) {
return {
anchor_type: "counterparty",
anchor_value_raw: counterparty,
anchor_value_resolved: counterparty,
resolver_confidence: "medium",
ambiguity_count: 0
};
}
if (contract) {
return {
anchor_type: "contract",
anchor_value_raw: contract,
anchor_value_resolved: contract,
resolver_confidence: "medium",
ambiguity_count: 0
};
}
if (documentRef) {
return {
anchor_type: "document_ref",
anchor_value_raw: documentRef,
anchor_value_resolved: documentRef,
resolver_confidence: "medium",
ambiguity_count: 0
};
}
return {
anchor_type: "unknown",
anchor_value_raw: null,
anchor_value_resolved: null,
resolver_confidence: "low",
ambiguity_count: 0
};
}
function refineAnchorFromRows(anchor, rows) {
if (rows.length === 0) {
return anchor;
}
if (anchor.anchor_type !== "counterparty" && anchor.anchor_type !== "contract") {
return anchor;
}
const needleRaw = String(anchor.anchor_value_raw ?? "").trim();
if (!needleRaw) {
return anchor;
}
const candidates = uniqueStrings(rows
.flatMap((row) => row.analytics)
.map((value) => value.trim())
.filter((value) => value.length >= 2 && matchesAnchorText(value, needleRaw)));
if (candidates.length === 0) {
return anchor;
}
if (candidates.length === 1) {
return {
...anchor,
anchor_value_resolved: candidates[0],
resolver_confidence: anchor.resolver_confidence === "high" ? "high" : "medium",
ambiguity_count: 0
};
}
return {
...anchor,
anchor_value_resolved: candidates[0],
resolver_confidence: "low",
ambiguity_count: candidates.length - 1
};
}
function composeLimitedReply(category, reason, nextStep) {
const heading = category === "empty_match"
? "В live-данных по текущему фильтру записи не найдены."
@@ -601,124 +552,19 @@ function buildLimitedExecutionResult(input) {
}
};
}
function contractCandidatesFromRows(rows) {
const candidates = [];
for (const row of rows) {
for (const token of [row.registrator, ...row.analytics]) {
const normalized = token.trim();
if (!normalized) {
continue;
}
if (/договор|contract|дог\./i.test(normalized)) {
candidates.push(normalized);
}
}
}
return uniqueStrings(candidates);
}
function composeFactualReply(intent, rows) {
if (intent === "account_balance_snapshot") {
const movementSum = rows.reduce((sum, row) => sum + (row.amount ?? 0), 0);
const lines = [
"Адресный срез по счету собран (по движениям live MCP).",
`Строк отобрано: ${rows.length}.`,
`Сумма по отобранным движениям: ${movementSum}.`,
...formatTopRows(rows, 4)
];
return {
responseType: "FACTUAL_SUMMARY",
text: lines.join("\n")
};
}
if (intent === "documents_forming_balance") {
const movementSum = rows.reduce((sum, row) => sum + (row.amount ?? 0), 0);
const lines = [
"Собран drilldown документов, формирующих остаток по счету на указанную дату.",
`Документных строк отобрано: ${rows.length}.`,
`Сумма по отобранным движениям: ${movementSum}.`,
...formatTopRows(rows, 8),
"Можно уточнить выборку по контрагенту, договору или периоду."
];
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
if (intent === "list_open_contracts") {
const contracts = contractCandidatesFromRows(rows);
const lines = [
"Собраны кандидаты по незакрытым договорным позициям (по live движениям 60/62/76).",
`Строк движения: ${rows.length}.`,
`Договорных кандидатов: ${contracts.length}.`
];
lines.push(...contracts.slice(0, 8).map((item, index) => `${index + 1}. ${item}`));
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
if (intent === "open_items_by_counterparty_or_contract") {
const lines = [
"Собраны открытые позиции по указанному фильтру (контрагент/договор).",
`Строк отобрано: ${rows.length}.`,
...formatTopRows(rows, 6)
];
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
if (intent === "list_documents_by_counterparty") {
const lines = [
"Собран список документов по контрагенту (live address lane).",
`Строк отобрано: ${rows.length}.`,
...formatTopRows(rows, 8)
];
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
if (intent === "bank_operations_by_counterparty") {
const lines = [
"Собран список банковских операций по контрагенту (live address lane).",
`Строк отобрано: ${rows.length}.`,
...formatTopRows(rows, 8)
];
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
const title = intent === "list_payables_counterparties"
? "Срез обязательств (payables) собран по движениям с account scope 60/76."
: intent === "list_receivables_counterparties"
? "Срез требований (receivables) собран по движениям с account scope 62/76."
: "Срез адресного запроса собран.";
const lines = [title, `Строк отобрано: ${rows.length}.`, ...formatTopRows(rows, 6)];
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
class AddressQueryService {
async tryHandle(userMessage) {
async tryHandle(userMessage, options = {}) {
if (!config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1) {
return null;
}
const mode = (0, addressQueryClassifier_1.detectAddressQuestionMode)(userMessage);
if (mode.mode !== "address_query") {
const followupContext = options.followupContext ?? null;
const decompose = (0, decomposeStage_1.runAddressDecomposeStage)(userMessage, followupContext);
if (!decompose) {
return null;
}
const shape = (0, addressQueryShapeClassifier_1.classifyAddressQueryShape)(userMessage);
if (shape.shape === "EXPLAIN_OR_REASON") {
return null;
}
const intent = (0, addressIntentResolver_1.resolveAddressIntent)(userMessage);
const filters = (0, addressFilterExtractor_1.extractAddressFilters)(userMessage, intent.intent);
let anchor = resolvePrimaryAnchor(intent.intent, filters.extracted_filters);
const { mode, shape, intent, filters, baseReasons } = decompose;
let anchor = (0, resolveStage_1.resolvePrimaryAnchor)(intent.intent, filters.extracted_filters);
const recipeSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(intent.intent, filters.extracted_filters);
const baseReasons = [...mode.reasons, ...shape.reasons, ...intent.reasons];
if (intent.intent === "unknown") {
return buildLimitedExecutionResult({
mode,
@@ -862,7 +708,7 @@ class AddressQueryService {
normalizedRawRows.length > 0 &&
scopedRows.length === 0;
const normalizedRows = accountScopeFallbackApplied ? normalizedRawRows : scopedRows;
anchor = refineAnchorFromRows(anchor, normalizedRows);
anchor = (0, resolveStage_1.refineAnchorFromRows)(anchor, normalizedRows);
const filtersForMatching = anchor.anchor_type === "counterparty" && anchor.anchor_value_resolved
? { ...filters.extracted_filters, counterparty: anchor.anchor_value_resolved }
: anchor.anchor_type === "contract" && anchor.anchor_value_resolved
@@ -895,7 +741,7 @@ class AddressQueryService {
: matchFailureStage === "materialized_but_filtered_out_by_recipe"
? "rows_filtered_out_by_intent_recipe_after_anchor_match"
: null;
if (intent.intent === "list_open_contracts" && filteredRows.length > 0 && contractCandidatesFromRows(filteredRows).length === 0) {
if (intent.intent === "list_open_contracts" && filteredRows.length > 0 && (0, composeStage_1.contractCandidatesFromRows)(filteredRows).length === 0) {
return buildLimitedExecutionResult({
mode,
shape,
@@ -925,6 +771,105 @@ class AddressQueryService {
reasons: baseReasons
});
}
if (filteredRows.length === 0 && canAutoBroadenPeriodWindow(intent.intent, filters.extracted_filters)) {
const autoBroadenedFilters = { ...filters.extracted_filters };
delete autoBroadenedFilters.period_from;
delete autoBroadenedFilters.period_to;
const broadenedSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(intent.intent, autoBroadenedFilters);
if (broadenedSelection.selected_recipe && broadenedSelection.missing_required_filters.length === 0) {
const broadenedPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(broadenedSelection.selected_recipe, autoBroadenedFilters);
const broadenedMcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({
query: broadenedPlan.query,
limit: broadenedPlan.limit
});
if (!broadenedMcp.error) {
const broadenedRawRows = toNormalizedRows(broadenedMcp.raw_rows);
const broadenedScopedRows = applyAccountScopeFilter(broadenedRawRows, broadenedPlan.account_scope);
const broadenedAccountScopeFallbackApplied = broadenedPlan.account_scope_mode === "preferred" &&
broadenedPlan.account_scope.length > 0 &&
broadenedRawRows.length > 0 &&
broadenedScopedRows.length === 0;
const broadenedNormalizedRows = broadenedAccountScopeFallbackApplied ? broadenedRawRows : broadenedScopedRows;
let broadenedAnchor = (0, resolveStage_1.resolvePrimaryAnchor)(intent.intent, autoBroadenedFilters);
broadenedAnchor = (0, resolveStage_1.refineAnchorFromRows)(broadenedAnchor, broadenedNormalizedRows);
const broadenedFiltersForMatching = broadenedAnchor.anchor_type === "counterparty" && broadenedAnchor.anchor_value_resolved
? { ...autoBroadenedFilters, counterparty: broadenedAnchor.anchor_value_resolved }
: broadenedAnchor.anchor_type === "contract" && broadenedAnchor.anchor_value_resolved
? { ...autoBroadenedFilters, contract: broadenedAnchor.anchor_value_resolved }
: autoBroadenedFilters;
const broadenedAccountScopeAudit = buildAccountScopeAudit({
intent: intent.intent,
filters: broadenedFiltersForMatching,
accountScope: broadenedPlan.account_scope,
rowsBeforeScope: broadenedRawRows.length,
rowsAfterScope: broadenedNormalizedRows.length
});
const broadenedAnchorFilter = applyAddressFilters(broadenedNormalizedRows, broadenedFiltersForMatching);
const broadenedRowsByAnchor = broadenedAnchorFilter.rows;
const broadenedFilteredRows = applyIntentSpecificFilter(intent.intent, broadenedRowsByAnchor);
if (broadenedFilteredRows.length > 0) {
const broadenedRowDiagnostics = deriveRowStageDiagnostics(broadenedMcp.raw_rows, broadenedNormalizedRows.length, broadenedNormalizedRows.length);
const broadenedStageStatus = deriveMcpStageStatus({
rawRowsReceived: broadenedMcp.raw_rows.length,
rowsMaterialized: broadenedNormalizedRows.length,
rowsAnchorMatched: broadenedRowsByAnchor.length,
rowsMatched: broadenedFilteredRows.length
});
const observedWindow = deriveObservedPeriodWindow(broadenedFilteredRows);
const broadenedPrefix = composeAutoBroadenedPeriodPrefix(filters.extracted_filters, observedWindow);
const broadenedFactual = (0, composeStage_1.composeFactualReply)(intent.intent, broadenedFilteredRows);
const broadenedLimitations = [...filters.warnings, "period_window_auto_broadened_to_available_data"];
const broadenedReasons = [...baseReasons, "period_window_auto_broadened_to_available_data"];
return {
handled: true,
reply_text: `${broadenedPrefix}\n${broadenedFactual.text}`,
reply_type: (0, composeStage_1.inferReplyType)(broadenedFactual.responseType),
response_type: broadenedFactual.responseType,
debug: {
detected_mode: mode.mode,
detected_mode_confidence: mode.confidence,
query_shape: shape.shape,
query_shape_confidence: shape.confidence,
detected_intent: intent.intent,
detected_intent_confidence: intent.confidence,
extracted_filters: filters.extracted_filters,
missing_required_filters: [],
selected_recipe: broadenedSelection.selected_recipe.recipe_id,
mcp_call_status_legacy: toLegacyMcpStatus(broadenedStageStatus),
account_scope_mode: broadenedPlan.account_scope_mode,
account_scope_fallback_applied: broadenedAccountScopeFallbackApplied,
anchor_type: broadenedAnchor.anchor_type,
anchor_value_raw: broadenedAnchor.anchor_value_raw,
anchor_value_resolved: broadenedAnchor.anchor_value_resolved,
resolver_confidence: broadenedAnchor.resolver_confidence,
ambiguity_count: broadenedAnchor.ambiguity_count,
match_failure_stage: "none",
match_failure_reason: null,
mcp_call_status: broadenedStageStatus,
rows_fetched: broadenedMcp.fetched_rows,
raw_rows_received: broadenedMcp.raw_rows.length,
rows_after_account_scope: broadenedNormalizedRows.length,
rows_after_recipe_filter: broadenedRowsByAnchor.length,
rows_materialized: broadenedNormalizedRows.length,
rows_matched: broadenedFilteredRows.length,
raw_row_keys_sample: broadenedRowDiagnostics.rawRowKeysSample,
materialization_drop_reason: broadenedRowDiagnostics.materializationDropReason,
account_token_raw: broadenedAccountScopeAudit.accountTokenRaw,
account_token_normalized: broadenedAccountScopeAudit.accountTokenNormalized,
account_scope_fields_checked: broadenedAccountScopeAudit.accountScopeFieldsChecked,
account_scope_match_strategy: broadenedAccountScopeAudit.accountScopeMatchStrategy,
account_scope_drop_reason: broadenedAccountScopeAudit.accountScopeDropReason,
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
limited_reason_category: null,
response_type: broadenedFactual.responseType,
limitations: broadenedLimitations,
reasons: broadenedReasons
}
};
}
}
}
}
if (filteredRows.length === 0) {
const hadBaseRows = normalizedRows.length > 0 || mcp.fetched_rows > 0;
const hadAnchorMatchedRows = filterByAnchors.length > 0;
@@ -992,11 +937,11 @@ class AddressQueryService {
reasons: baseReasons
});
}
const factual = composeFactualReply(intent.intent, filteredRows);
const factual = (0, composeStage_1.composeFactualReply)(intent.intent, filteredRows);
return {
handled: true,
reply_text: factual.text,
reply_type: inferReplyType(factual.responseType),
reply_type: (0, composeStage_1.inferReplyType)(factual.responseType),
response_type: factual.responseType,
debug: {
detected_mode: mode.mode,
@@ -122,6 +122,8 @@ const BASE_RECIPES = [
account_scope_mode: "strict"
}
];
const ADDRESS_MAX_LIMIT_DEFAULT = 200;
const ADDRESS_MAX_LIMIT_EXTENDED = 1000;
function toDateTimeExpr(isoDate, endOfDay) {
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
@@ -172,6 +174,12 @@ function shouldBoostLimitForAllTimeCounterparty(filters) {
(typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0));
return !hasPeriod;
}
function maxLimitForIntent(intent) {
if (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty") {
return ADDRESS_MAX_LIMIT_EXTENDED;
}
return ADDRESS_MAX_LIMIT_DEFAULT;
}
function selectAddressRecipe(intent, filters) {
const recipe = BASE_RECIPES.find((item) => item.intent === intent) ?? null;
if (!recipe) {
@@ -192,14 +200,19 @@ function selectAddressRecipe(intent, filters) {
};
}
function buildAddressRecipePlan(recipe, filters) {
const maxLimit = maxLimitForIntent(recipe.intent);
const baseLimit = typeof filters.limit === "number" && Number.isFinite(filters.limit)
? Math.max(1, Math.min(200, Math.trunc(filters.limit)))
? Math.max(1, Math.min(maxLimit, Math.trunc(filters.limit)))
: recipe.default_limit;
const boostedLimit = (recipe.intent === "list_documents_by_counterparty" || recipe.intent === "bank_operations_by_counterparty") &&
shouldBoostLimitForAllTimeCounterparty(filters)
? Math.max(baseLimit, 200)
: baseLimit;
const resolvedLimit = Math.max(1, Math.min(200, boostedLimit));
? Math.max(baseLimit, maxLimit)
: (recipe.intent === "account_balance_snapshot" || recipe.intent === "documents_forming_balance") &&
typeof filters.account === "string" &&
filters.account.trim().length > 0
? Math.max(baseLimit, ADDRESS_MAX_LIMIT_DEFAULT)
: baseLimit;
const resolvedLimit = Math.max(1, Math.min(maxLimit, boostedLimit));
const accountScope = (recipe.intent === "account_balance_snapshot" || recipe.intent === "documents_forming_balance") && filters.account
? [String(filters.account)]
: Array.isArray(recipe.account_scope)
@@ -0,0 +1,125 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.contractCandidatesFromRows = contractCandidatesFromRows;
exports.composeFactualReply = composeFactualReply;
exports.inferReplyType = inferReplyType;
function uniqueStrings(values) {
return Array.from(new Set(values
.map((item) => item.trim())
.filter((item) => item.length > 0)));
}
function formatTopRows(rows, limit = 6) {
return rows.slice(0, limit).map((row, index) => {
const period = row.period ?? "дата не указана";
const amount = row.amount !== null ? `${row.amount}` : "сумма не указана";
const accounts = [row.account_dt ?? "-", row.account_kt ?? "-"].join(" / ");
const analytics = row.analytics.length > 0 ? ` | аналитика: ${row.analytics.slice(0, 2).join("; ")}` : "";
return `${index + 1}. ${period} | ${row.registrator} | ${accounts} | ${amount}${analytics}`;
});
}
function contractCandidatesFromRows(rows) {
const candidates = [];
for (const row of rows) {
for (const token of [row.registrator, ...row.analytics]) {
const normalized = token.trim();
if (!normalized) {
continue;
}
if (/договор|contract|дог\./i.test(normalized)) {
candidates.push(normalized);
}
}
}
return uniqueStrings(candidates);
}
function composeFactualReply(intent, rows) {
if (intent === "account_balance_snapshot") {
const movementSum = rows.reduce((sum, row) => sum + (row.amount ?? 0), 0);
const lines = [
"Адресный срез по счету собран (по движениям live MCP).",
`Строк отобрано: ${rows.length}.`,
`Сумма по отобранным движениям: ${movementSum}.`,
...formatTopRows(rows, 4)
];
return {
responseType: "FACTUAL_SUMMARY",
text: lines.join("\n")
};
}
if (intent === "documents_forming_balance") {
const movementSum = rows.reduce((sum, row) => sum + (row.amount ?? 0), 0);
const lines = [
"Собран drilldown документов, формирующих остаток по счету на указанную дату.",
`Документных строк отобрано: ${rows.length}.`,
`Сумма по отобранным движениям: ${movementSum}.`,
...formatTopRows(rows, 8),
"Можно уточнить выборку по контрагенту, договору или периоду."
];
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
if (intent === "list_open_contracts") {
const contracts = contractCandidatesFromRows(rows);
const lines = [
"Собраны кандидаты по незакрытым договорным позициям (по live движениям 60/62/76).",
`Строк движения: ${rows.length}.`,
`Договорных кандидатов: ${contracts.length}.`
];
lines.push(...contracts.slice(0, 8).map((item, index) => `${index + 1}. ${item}`));
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
if (intent === "open_items_by_counterparty_or_contract") {
const lines = [
"Собраны открытые позиции по указанному фильтру (контрагент/договор).",
`Строк отобрано: ${rows.length}.`,
...formatTopRows(rows, 6)
];
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
if (intent === "list_documents_by_counterparty") {
const lines = [
"Собран список документов по контрагенту (live address lane).",
`Строк отобрано: ${rows.length}.`,
...formatTopRows(rows, rows.length)
];
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
if (intent === "bank_operations_by_counterparty") {
const lines = [
"Собран список банковских операций по контрагенту (live address lane).",
`Строк отобрано: ${rows.length}.`,
...formatTopRows(rows, rows.length)
];
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
const title = intent === "list_payables_counterparties"
? "Срез обязательств (payables) собран по движениям с account scope 60/76."
: intent === "list_receivables_counterparties"
? "Срез требований (receivables) собран по движениям с account scope 62/76."
: "Срез адресного запроса собран.";
const lines = [title, `Строк отобрано: ${rows.length}.`, ...formatTopRows(rows, 6)];
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
function inferReplyType(responseType) {
if (responseType === "FACTUAL_LIST" || responseType === "FACTUAL_SUMMARY") {
return "factual";
}
return "partial_coverage";
}
@@ -0,0 +1,181 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.hasAddressFollowupContextSignal = hasAddressFollowupContextSignal;
exports.runAddressDecomposeStage = runAddressDecomposeStage;
const addressQueryClassifier_1 = require("../addressQueryClassifier");
const addressQueryShapeClassifier_1 = require("../addressQueryShapeClassifier");
const addressIntentResolver_1 = require("../addressIntentResolver");
const addressFilterExtractor_1 = require("../addressFilterExtractor");
function hasExplicitPeriodWindow(filters) {
return ((typeof filters.period_from === "string" && filters.period_from.trim().length > 0) ||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0));
}
function toNonEmptyString(value) {
if (value === null || value === undefined) {
return null;
}
const normalized = String(value).trim();
return normalized.length > 0 ? normalized : null;
}
function hasAllTimeHint(text) {
return /(?:за\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|for\s+full\s+history|full\s+history)/iu.test(String(text ?? ""));
}
function hasAddressFollowupContextSignal(text) {
const normalized = String(text ?? "").trim();
if (!normalized) {
return false;
}
if (hasAllTimeHint(normalized)) {
return true;
}
if (/(?:^|\s)(?:и|а\s+еще|а\s+ещё|еще|ещё|также|по\s+этому|по\s+тому|это\s+же|в\s+этом|тот\s+же|also|same|that)/iu.test(normalized)) {
return true;
}
return normalized.split(/\s+/).filter(Boolean).length <= 8;
}
function mergeFollowupFilters(current, intent, userMessage, followupContext) {
const merged = { ...current };
const reasons = [];
if (!followupContext) {
return { filters: merged, reasons };
}
const previous = followupContext.previous_filters ?? {};
const previousAnchorValue = toNonEmptyString(followupContext.previous_anchor_value);
const previousCounterparty = toNonEmptyString(previous.counterparty);
const previousContract = toNonEmptyString(previous.contract);
const previousAccount = toNonEmptyString(previous.account);
const allTimeRequested = hasAllTimeHint(userMessage);
if (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty") {
if (!toNonEmptyString(merged.counterparty)) {
const inheritedCounterparty = previousCounterparty ??
(followupContext.previous_anchor_type === "counterparty" ? previousAnchorValue : null);
if (inheritedCounterparty) {
merged.counterparty = inheritedCounterparty;
reasons.push("counterparty_from_followup_context");
}
}
}
if (intent === "account_balance_snapshot" || intent === "documents_forming_balance") {
if (!toNonEmptyString(merged.account)) {
const inheritedAccount = previousAccount ??
(followupContext.previous_anchor_type === "account" ? previousAnchorValue : null);
if (inheritedAccount) {
merged.account = inheritedAccount;
reasons.push("account_from_followup_context");
}
}
}
if (intent === "open_items_by_counterparty_or_contract" || intent === "list_open_contracts") {
if (!toNonEmptyString(merged.contract)) {
const inheritedContract = previousContract ??
(followupContext.previous_anchor_type === "contract" ? previousAnchorValue : null);
if (inheritedContract) {
merged.contract = inheritedContract;
reasons.push("contract_from_followup_context");
}
}
if (!toNonEmptyString(merged.counterparty)) {
const inheritedCounterparty = previousCounterparty ??
(followupContext.previous_anchor_type === "counterparty" ? previousAnchorValue : null);
if (inheritedCounterparty) {
merged.counterparty = inheritedCounterparty;
reasons.push("counterparty_from_followup_context");
}
}
}
if (allTimeRequested) {
if (toNonEmptyString(merged.period_from) || toNonEmptyString(merged.period_to)) {
delete merged.period_from;
delete merged.period_to;
reasons.push("period_cleared_by_all_time_followup");
}
return { filters: merged, reasons };
}
const currentHasPeriod = hasExplicitPeriodWindow(merged);
const previousHasPeriod = hasExplicitPeriodWindow(previous);
if (!currentHasPeriod && previousHasPeriod && hasAddressFollowupContextSignal(userMessage)) {
if (toNonEmptyString(previous.period_from)) {
merged.period_from = previous.period_from;
}
if (toNonEmptyString(previous.period_to)) {
merged.period_to = previous.period_to;
}
reasons.push("period_from_followup_context");
}
return { filters: merged, reasons };
}
function resolveMissingRequiredFilters(intent, filters) {
const requiredByIntent = {
account_balance_snapshot: ["account", "as_of_date"],
documents_forming_balance: ["account", "as_of_date"],
list_documents_by_counterparty: ["counterparty"],
bank_operations_by_counterparty: ["counterparty"]
};
const required = requiredByIntent[intent] ?? [];
return required.filter((key) => {
const value = filters[key];
return value === undefined || value === null || String(value).trim() === "";
});
}
function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupContext) {
if (!followupContext || !followupContext.previous_intent) {
return detectedIntent;
}
if (detectedIntent.intent !== "unknown") {
return detectedIntent;
}
if (!hasAddressFollowupContextSignal(userMessage)) {
return detectedIntent;
}
return {
intent: followupContext.previous_intent,
confidence: "low",
reasons: [...detectedIntent.reasons, "intent_from_followup_context"]
};
}
function runAddressDecomposeStage(userMessage, followupContext) {
const detectedMode = (0, addressQueryClassifier_1.detectAddressQuestionMode)(userMessage);
const mode = detectedMode.mode === "address_query"
? detectedMode
: followupContext && hasAddressFollowupContextSignal(userMessage)
? {
mode: "address_query",
confidence: "medium",
reasons: [...detectedMode.reasons, "address_mode_from_followup_context"]
}
: detectedMode;
if (mode.mode !== "address_query") {
return null;
}
const shape = (0, addressQueryShapeClassifier_1.classifyAddressQueryShape)(userMessage);
if (shape.shape === "EXPLAIN_OR_REASON") {
return null;
}
const detectedIntent = (0, addressIntentResolver_1.resolveAddressIntent)(userMessage);
const intent = deriveIntentWithFollowupContext(detectedIntent, userMessage, followupContext);
const extractedFilters = (0, addressFilterExtractor_1.extractAddressFilters)(userMessage, intent.intent);
const followupMerged = mergeFollowupFilters(extractedFilters.extracted_filters, intent.intent, userMessage, followupContext);
const filters = {
extracted_filters: followupMerged.filters,
missing_required_filters: resolveMissingRequiredFilters(intent.intent, followupMerged.filters),
warnings: [...new Set([...extractedFilters.warnings, ...followupMerged.reasons])]
};
const followupContextApplied = Boolean(followupContext) &&
(mode.reasons.includes("address_mode_from_followup_context") ||
intent.reasons.includes("intent_from_followup_context") ||
followupMerged.reasons.length > 0);
const baseReasons = [
...mode.reasons,
...shape.reasons,
...intent.reasons,
...followupMerged.reasons,
...(followupContextApplied ? ["address_followup_context_applied"] : [])
];
return {
mode,
shape,
intent,
filters,
baseReasons
};
}
@@ -0,0 +1,179 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolvePrimaryAnchor = resolvePrimaryAnchor;
exports.refineAnchorFromRows = refineAnchorFromRows;
const PARTY_ANCHOR_STOPWORDS = new Set([
"ооо",
"ао",
"зао",
"ип",
"llc",
"ltd",
"company",
"компания",
"контрагент",
"counterparty",
"по",
"by"
]);
function transliterateCyrillicToLatin(value) {
const map = {
а: "a",
б: "b",
в: "v",
г: "g",
д: "d",
е: "e",
ё: "e",
ж: "zh",
з: "z",
и: "i",
й: "y",
к: "k",
л: "l",
м: "m",
н: "n",
о: "o",
п: "p",
р: "r",
с: "s",
т: "t",
у: "u",
ф: "f",
х: "h",
ц: "ts",
ч: "ch",
ш: "sh",
щ: "sch",
ъ: "",
ы: "y",
ь: "",
э: "e",
ю: "yu",
я: "ya"
};
let out = "";
for (const char of String(value ?? "").toLowerCase()) {
out += map[char] ?? char;
}
return out;
}
function normalizeSearchText(value) {
return String(value ?? "")
.toLowerCase()
.replace(/ё/g, "е")
.replace(/[^a-zа-я0-9]+/gi, " ")
.replace(/\s+/g, " ")
.trim();
}
function tokenizeAnchor(value) {
return normalizeSearchText(value)
.split(" ")
.map((token) => token.trim())
.filter((token) => token.length >= 2 && !PARTY_ANCHOR_STOPWORDS.has(token));
}
function matchesAnchorText(searchable, anchor) {
const searchableNormalized = normalizeSearchText(searchable);
const searchableLatin = transliterateCyrillicToLatin(searchableNormalized);
const tokens = tokenizeAnchor(anchor);
if (tokens.length === 0) {
const direct = normalizeSearchText(anchor);
if (!direct) {
return false;
}
return searchableNormalized.includes(direct) || searchableLatin.includes(transliterateCyrillicToLatin(direct));
}
return tokens.every((token) => {
const tokenLatin = transliterateCyrillicToLatin(token);
return searchableNormalized.includes(token) || searchableLatin.includes(tokenLatin);
});
}
function uniqueStrings(values) {
return Array.from(new Set(values
.map((item) => item.trim())
.filter((item) => item.length > 0)));
}
function resolvePrimaryAnchor(intent, filters) {
const account = typeof filters.account === "string" ? filters.account.trim() : "";
const counterparty = typeof filters.counterparty === "string" ? filters.counterparty.trim() : "";
const contract = typeof filters.contract === "string" ? filters.contract.trim() : "";
const documentRef = typeof filters.document_ref === "string" ? filters.document_ref.trim() : "";
if (intent === "account_balance_snapshot" || intent === "documents_forming_balance") {
if (account) {
return {
anchor_type: "account",
anchor_value_raw: account,
anchor_value_resolved: account,
resolver_confidence: "high",
ambiguity_count: 0
};
}
}
if (counterparty) {
return {
anchor_type: "counterparty",
anchor_value_raw: counterparty,
anchor_value_resolved: counterparty,
resolver_confidence: "medium",
ambiguity_count: 0
};
}
if (contract) {
return {
anchor_type: "contract",
anchor_value_raw: contract,
anchor_value_resolved: contract,
resolver_confidence: "medium",
ambiguity_count: 0
};
}
if (documentRef) {
return {
anchor_type: "document_ref",
anchor_value_raw: documentRef,
anchor_value_resolved: documentRef,
resolver_confidence: "medium",
ambiguity_count: 0
};
}
return {
anchor_type: "unknown",
anchor_value_raw: null,
anchor_value_resolved: null,
resolver_confidence: "low",
ambiguity_count: 0
};
}
function refineAnchorFromRows(anchor, rows) {
if (rows.length === 0) {
return anchor;
}
if (anchor.anchor_type !== "counterparty" && anchor.anchor_type !== "contract") {
return anchor;
}
const needleRaw = String(anchor.anchor_value_raw ?? "").trim();
if (!needleRaw) {
return anchor;
}
const candidates = uniqueStrings(rows
.flatMap((row) => row.analytics)
.map((value) => value.trim())
.filter((value) => value.length >= 2 && matchesAnchorText(value, needleRaw)));
if (candidates.length === 0) {
return anchor;
}
if (candidates.length === 1) {
return {
...anchor,
anchor_value_resolved: candidates[0],
resolver_confidence: anchor.resolver_confidence === "high" ? "high" : "medium",
ambiguity_count: 0
};
}
return {
...anchor,
anchor_value_resolved: candidates[0],
resolver_confidence: "low",
ambiguity_count: candidates.length - 1
};
}
+299 -73
View File
@@ -1731,8 +1731,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",
@@ -1790,12 +1791,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)}`
};
}
}
class AssistantService {
normalizerService;
sessions;
@@ -1827,80 +2020,112 @@ 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 &&
@@ -1917,12 +2142,13 @@ 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,
@@ -871,6 +871,7 @@ class NormalizerService {
async normalize(payload) {
const traceId = (0, nanoid_1.nanoid)(14);
const startedAt = Date.now();
const llmProvider = payload.llmProvider === "local" ? "local" : "openai";
const model = payload.model ?? config_1.DEFAULT_MODEL;
const baseUrl = payload.baseUrl ?? config_1.DEFAULT_OPENAI_BASE_URL;
const temperature = payload.temperature ?? config_1.DEFAULT_TEMPERATURE;
@@ -903,6 +904,7 @@ class NormalizerService {
else {
const apiKey = payload.apiKey ?? process.env.OPENAI_API_KEY;
const firstTry = await this.openaiClient.normalize({
llmProvider,
apiKey: String(apiKey ?? ""),
model,
baseUrl,
@@ -946,6 +948,7 @@ class NormalizerService {
if (!payload.useMock && !validation.passed && canRetry) {
const retryMaxOutputTokens = computeRetryMaxOutputTokens(maxOutputTokens, rawModelResponse);
const retry = await this.openaiClient.normalize({
llmProvider,
apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ""),
model,
baseUrl,
+230 -44
View File
@@ -8,6 +8,20 @@ const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const config_1 = require("../config");
const http_1 = require("../utils/http");
function resolveProvider(config) {
return config.llmProvider === "local" ? "local" : "openai";
}
function resolveApiKey(config) {
const candidate = String(config.apiKey ?? "").trim();
if (candidate.length > 0) {
return candidate;
}
if (resolveProvider(config) === "local") {
// Local OpenAI-compatible servers often accept any token.
return "local-dev-token";
}
throw new http_1.ApiError("OPENAI_API_KEY_MISSING", "OpenAI API key is missing.", 400);
}
function extractUsage(raw) {
const usage = (raw.usage ?? {});
const input = Number(usage.input_tokens ?? usage.prompt_tokens ?? 0);
@@ -19,7 +33,7 @@ function extractUsage(raw) {
total_tokens: Number.isFinite(total) ? total : 0
};
}
function extractOutputText(raw) {
function extractOutputTextFromResponses(raw) {
if (typeof raw.output_text === "string" && raw.output_text.trim().length > 0) {
return raw.output_text;
}
@@ -51,7 +65,55 @@ function extractOutputText(raw) {
return nested.output_text;
}
}
throw new http_1.ApiError("OPENAI_OUTPUT_PARSE_FAILED", "Не удалось извлечь output_text из Responses API ответа.", 502, raw);
throw new http_1.ApiError("OPENAI_OUTPUT_PARSE_FAILED", "Failed to extract output_text from /responses payload.", 502, raw);
}
function extractOutputTextFromChatCompletions(raw) {
const choices = raw.choices;
if (!Array.isArray(choices) || choices.length === 0) {
throw new http_1.ApiError("OPENAI_OUTPUT_PARSE_FAILED", "Missing choices in /chat/completions payload.", 502, raw);
}
const first = choices[0];
if (!first || typeof first !== "object") {
throw new http_1.ApiError("OPENAI_OUTPUT_PARSE_FAILED", "Invalid first choice in /chat/completions payload.", 502, raw);
}
const message = first.message;
if (!message || typeof message !== "object") {
throw new http_1.ApiError("OPENAI_OUTPUT_PARSE_FAILED", "Missing message in /chat/completions payload.", 502, raw);
}
const content = message.content;
if (typeof content === "string" && content.trim().length > 0) {
return content;
}
if (Array.isArray(content)) {
const textParts = content
.map((item) => {
if (!item || typeof item !== "object") {
return "";
}
const block = item;
return typeof block.text === "string" ? block.text : "";
})
.filter((item) => item.trim().length > 0);
if (textParts.length > 0) {
return textParts.join("\n");
}
}
throw new http_1.ApiError("OPENAI_OUTPUT_PARSE_FAILED", "Failed to extract text from /chat/completions payload.", 502, raw);
}
function shouldFallbackToChatCompletions(error) {
if (!(error instanceof http_1.ApiError)) {
return false;
}
if (error.code !== "OPENAI_REQUEST_FAILED") {
return false;
}
const details = (error.details ?? {});
const status = Number(details.status ?? 0);
if ([404, 405, 501].includes(status)) {
return true;
}
const message = String(error.message ?? "").toLowerCase();
return message.includes("/responses") || message.includes("responses");
}
function loadSchemaForTransport(schemaVersion) {
const schemaFile = schemaVersion === "v1"
@@ -64,19 +126,54 @@ function loadSchemaForTransport(schemaVersion) {
const schemaPath = path_1.default.resolve(config_1.SCHEMAS_DIR, schemaFile);
return JSON.parse(fs_1.default.readFileSync(schemaPath, "utf-8"));
}
function buildBaseUrlCandidates(config) {
const base = (config.baseUrl ?? config_1.DEFAULT_OPENAI_BASE_URL).replace(/\/+$/, "");
const provider = resolveProvider(config);
if (provider !== "local") {
return [base];
}
const hasVersionSuffix = /\/v\d+$/i.test(base);
if (hasVersionSuffix) {
return [base];
}
return Array.from(new Set([base, `${base}/v1`]));
}
class OpenAIResponsesClient {
async listModels(config) {
const payload = await this.getModels(config);
const data = Array.isArray(payload.data) ? payload.data : [];
const ids = data
.map((item) => {
if (!item || typeof item !== "object") {
return "";
}
return String(item.id ?? "").trim();
})
.filter((item) => item.length > 0);
return Array.from(new Set(ids));
}
async testConnection(config) {
const payload = {
const provider = resolveProvider(config);
if (provider === "local") {
try {
await this.getModels(config);
}
catch {
// Some local providers do not expose /models consistently; fallback to a tiny chat call.
await this.postChatCompletions(config, {
model: config.model,
messages: [{ role: "user", content: "ping" }],
max_tokens: 4,
temperature: 0
});
}
return { ok: true, model: config.model };
}
await this.postResponses(config, {
model: config.model,
input: [
{
role: "user",
content: [{ type: "input_text", text: "ping" }]
}
],
input: [{ role: "user", content: [{ type: "input_text", text: "ping" }] }],
max_output_tokens: 16
};
await this.post(config, payload);
});
return { ok: true, model: config.model };
}
async normalize(config, prompt) {
@@ -91,7 +188,7 @@ class OpenAIResponsesClient {
const developerPrompt = prompt.controlledRetryInstruction
? `${prompt.developerPrompt}\n\n${prompt.controlledRetryInstruction}`
: prompt.developerPrompt;
const payload = {
const responsesPayload = {
model: config.model,
temperature: config.temperature ?? 0,
max_output_tokens: config.maxOutputTokens ?? 700,
@@ -109,7 +206,7 @@ class OpenAIResponsesClient {
content: [
{
type: "input_text",
text: `${prompt.domainPrompt}\n\nПользовательский вопрос:\n${prompt.userQuestion}`
text: `${prompt.domainPrompt}\n\nUser question:\n${prompt.userQuestion}`
}
]
}
@@ -123,44 +220,133 @@ class OpenAIResponsesClient {
}
}
};
const raw = await this.post(config, payload);
const outputText = extractOutputText(raw);
const provider = resolveProvider(config);
if (provider === "openai") {
const raw = await this.postResponses(config, responsesPayload);
return {
raw,
outputText: extractOutputTextFromResponses(raw),
usage: extractUsage(raw)
};
}
// local provider: prefer /responses if available, fallback to /chat/completions
try {
const raw = await this.postResponses(config, responsesPayload);
return {
raw,
outputText: extractOutputTextFromResponses(raw),
usage: extractUsage(raw)
};
}
catch (error) {
if (!shouldFallbackToChatCompletions(error)) {
throw error;
}
}
const chatPayload = {
model: config.model,
temperature: config.temperature ?? 0,
max_tokens: config.maxOutputTokens ?? 700,
response_format: { type: "json_object" },
messages: [
{
role: "system",
content: `${prompt.systemPrompt}\n\n${developerPrompt}`
},
{
role: "user",
content: `${prompt.domainPrompt}\n\nUser question:\n${prompt.userQuestion}\n\n` +
`Return only JSON that matches schema: ${schemaName}.`
}
]
};
const raw = await this.postChatCompletions(config, chatPayload);
return {
raw,
outputText,
outputText: extractOutputTextFromChatCompletions(raw),
usage: extractUsage(raw)
};
}
async post(config, payload) {
if (!config.apiKey || config.apiKey.trim().length < 10) {
throw new http_1.ApiError("OPENAI_API_KEY_MISSING", "API ключ OpenAI не задан или слишком короткий.", 400);
async getModels(config) {
return this.requestJson(config, "/models", "GET");
}
async postResponses(config, payload) {
return this.requestJson(config, "/responses", "POST", payload);
}
async postChatCompletions(config, payload) {
return this.requestJson(config, "/chat/completions", "POST", payload);
}
async requestJson(config, routePath, method, payload) {
const apiKey = resolveApiKey(config);
const baseCandidates = buildBaseUrlCandidates(config);
const canFallbackToAlternativeBase = resolveProvider(config) === "local" && baseCandidates.length > 1;
let lastNetworkError = null;
const headers = {
Authorization: `Bearer ${apiKey}`
};
if (method === "POST") {
headers["Content-Type"] = "application/json";
}
const url = `${(config.baseUrl ?? config_1.DEFAULT_OPENAI_BASE_URL).replace(/\/$/, "")}/responses`;
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${config.apiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
for (let index = 0; index < baseCandidates.length; index += 1) {
const base = baseCandidates[index];
const isLastCandidate = index === baseCandidates.length - 1;
const url = `${base}${routePath}`;
let response;
try {
response = await fetch(url, {
method,
headers,
body: method === "POST" ? JSON.stringify(payload ?? {}) : undefined
});
}
catch (error) {
lastNetworkError = error;
if (!isLastCandidate) {
continue;
}
throw new http_1.ApiError("OPENAI_REQUEST_FAILED", "Model endpoint is unreachable.", 502, {
route: routePath,
url,
reason: error instanceof Error ? error.message : String(error)
});
}
if (!response.ok && canFallbackToAlternativeBase && !isLastCandidate && [404, 405].includes(response.status)) {
continue;
}
const text = await response.text();
let data = {};
if (text.trim().length > 0) {
try {
data = JSON.parse(text);
}
catch {
if (!response.ok && canFallbackToAlternativeBase && !isLastCandidate && [404, 405].includes(response.status)) {
continue;
}
throw new http_1.ApiError("OPENAI_NON_JSON_RESPONSE", "Model endpoint returned non-JSON response.", 502, {
route: routePath,
url,
status: response.status,
body: text.slice(0, 500)
});
}
}
if (!response.ok) {
const errorObj = (data.error ?? {});
throw new http_1.ApiError("OPENAI_REQUEST_FAILED", String(errorObj.message ?? `Model endpoint failed: ${response.status}`), response.status, {
route: routePath,
url,
status: response.status,
type: errorObj.type ?? null,
code: errorObj.code ?? null
});
}
return data;
}
throw new http_1.ApiError("OPENAI_REQUEST_FAILED", "Model endpoint is unreachable.", 502, {
route: routePath,
reason: lastNetworkError instanceof Error ? lastNetworkError.message : String(lastNetworkError ?? "unknown")
});
const text = await response.text();
let data;
try {
data = JSON.parse(text);
}
catch {
throw new http_1.ApiError("OPENAI_NON_JSON_RESPONSE", "OpenAI вернул не-JSON ответ.", 502, { status: response.status, body: text.slice(0, 500) });
}
if (!response.ok) {
const errorObj = (data.error ?? {});
throw new http_1.ApiError("OPENAI_REQUEST_FAILED", String(errorObj.message ?? `OpenAI request failed with status ${response.status}`), response.status, {
status: response.status,
type: errorObj.type ?? null,
code: errorObj.code ?? null
});
}
return data;
}
}
exports.OpenAIResponsesClient = OpenAIResponsesClient;