АДРЕСНЫЙ РЕЖИМ -Step-5 - feat(assistant): стабилизация свободного LLM-роутинга, прическа маршрутов chat/address, прототип прогноза НДС

This commit is contained in:
2026-04-08 23:40:12 +03:00
parent 71762af575
commit df29798fa2
47 changed files with 11439 additions and 651 deletions
@@ -8,7 +8,7 @@ const iconv_lite_1 = __importDefault(require("iconv-lite"));
const ACCOUNT_PATTERN = /(?:сч[её]т|счет|account)[^0-9]{0,12}(\d{2}(?:[.,]\d{1,2})?)/i;
const LIMIT_PATTERN = /(?:\btop\b|\blimit\b|первые|топ)[\s\-–—_:№#]*?(\d{1,3})/iu;
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 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;
@@ -298,47 +298,46 @@ function extractYearRangePeriod(text) {
};
}
function cleanupAnchorValue(value) {
const normalized = String(value ?? "").trim();
if (!normalized) {
const stripOuterQuotes = (text) => String(text ?? "")
.replace(/^['"«»“”„`’‘]+|['"«»“”„`’‘]+$/gu, "")
.trim();
let cleaned = stripOuterQuotes(String(value ?? "").trim());
if (!cleaned) {
return "";
}
// Remove trailing as-of qualifiers often captured by broad contract/counterparty regexes:
// "<anchor> на 2020-07-31", "<anchor> на дату 31.07.2020", "<anchor> as of 2020-07-31".
const asOfTailPattern = /\s+(?:на\s+(?:дат[ауеы]\s+)?\d{1,4}[./-]\d{1,2}(?:[./-]\d{1,4})?|as\s+of\s+\d{1,4}[./-]\d{1,2}(?:[./-]\d{1,4})?)(?:\s+|$)[\s\S]*$/iu;
if (asOfTailPattern.test(normalized)) {
return normalized.replace(asOfTailPattern, "").trim();
}
const asOfTruncatedTailPattern = /\s+на\s+дат[ауеы]\s+\d{1,2}(?:\s+|$)[\s\S]*$/iu;
if (asOfTruncatedTailPattern.test(normalized)) {
return normalized.replace(asOfTruncatedTailPattern, "").trim();
}
const asOfReportDateTailPattern = /\s+на\s+дат[ауеы]\s+(?:отчетност[ьи]|отч[её]тн(?:ую|ой)?\s+дат[ауеы]|конец(?:\s+период[а-яё]*)?)\s+\d{1,4}[./-]\d{1,2}(?:[./-]\d{1,4})?(?:\s+|$)[\s\S]*$/iu;
const periodEndTailPattern = /\s+на\s+конец(?:\s+период[а-яё]*)?\s+(?:\d{1,4}[./-]\d{1,2}(?:[./-]\d{1,4})?|\d{4}|[a-zа-яё]+\s+\d{4})(?:\s+|$)[\s\S]*$/iu;
if (periodEndTailPattern.test(normalized)) {
return normalized.replace(periodEndTailPattern, "").trim();
}
// Remove trailing period qualifiers that can be swallowed by broad anchor regexes:
// "<counterparty> с 2020-07-01 по 2020-07-31", "<counterparty> from 2020-07-01 to 2020-07-31"
const periodTailPattern = /\s+(?:с\s+\d{1,4}[./-]\d{1,2}[./-]\d{1,4}|from\s+\d{1,4}[./-]\d{1,2}[./-]\d{1,4}|between\s+\d{1,4}[./-]\d{1,2}[./-]\d{1,4}|за\s+период)(?:\s+|$)[\s\S]*$/iu;
if (periodTailPattern.test(normalized)) {
return normalized.replace(periodTailPattern, "").trim();
}
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|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();
for (const tailPattern of [
asOfTailPattern,
asOfTruncatedTailPattern,
asOfReportDateTailPattern,
periodEndTailPattern,
periodTailPattern,
allTimeTailPattern,
allTimeTailPatternEn
]) {
if (tailPattern.test(cleaned)) {
cleaned = stripOuterQuotes(cleaned.replace(tailPattern, "").trim());
}
}
const trailingYearTailPattern = /\s+(?:year\s+)?(20\d{2})(?:\s*(?:г(?:од|ода)?\.?|year))?(?:\s+|$)[\s\S]*$/iu;
let cleaned = normalized;
if (trailingYearTailPattern.test(normalized)) {
cleaned = normalized.replace(trailingYearTailPattern, "").trim();
if (trailingYearTailPattern.test(cleaned)) {
cleaned = stripOuterQuotes(cleaned.replace(trailingYearTailPattern, "").trim());
}
return cleaned
cleaned = cleaned
.replace(/\s+(?:from|to|between|and)(?:\s+|$)[\s\S]*$/iu, "")
.replace(/\s+(?:с|по|за)(?:\s+|$)[\s\S]*$/iu, "")
.trim();
return stripOuterQuotes(cleaned);
}
function cleanupContractAnchorValue(value) {
let normalized = cleanupAnchorValue(value);
@@ -395,6 +394,8 @@ function extractLooseByAnchorValue(text) {
"партнера",
"договору",
"договора",
"контракту",
"контракта",
"счету",
"счёту",
"дате",
@@ -442,6 +443,7 @@ function extractLooseByAnchorValue(text) {
"linked",
"нему",
"ней",
"нее",
"ним",
"этому",
"тому",
@@ -502,10 +504,38 @@ function isLikelyCounterpartyToken(rawToken) {
"каких",
"какому",
"какую",
"кто",
"что",
"чего",
"где",
"когда",
"почему",
"зачем",
"сколько",
"чьи",
"чья",
"чей",
"чью",
"самый",
"самая",
"самое",
"самые",
"крупный",
"крупная",
"крупное",
"крупные",
"жирный",
"жирная",
"жирное",
"жирные",
"больше",
"меньше",
"платит",
"платят",
"денег",
"деньги",
"объем",
"объём",
"док",
"доки",
"документ",
@@ -634,6 +664,13 @@ function isLowQualityCounterpartyAnchorValue(rawValue) {
if (tokens.length === 0) {
return true;
}
const questionCue = /(?:кто|что|какой|какая|какие|какого|сколько|где|когда|почему|зачем|which|who|what|how\s+many)/iu.test(value) ||
/[?]/u.test(String(rawValue ?? ""));
const rankingCue = /(?:больше|меньше|сам(?:ый|ая|ое|ые)|крупн|жирн|максим|миним)/iu.test(value);
const paymentCue = /(?:плат(?:ит|ят|еж|ёж|ежн|ежей|ежа)|денег|деньг|money|payment)/iu.test(value);
if (questionCue && (rankingCue || paymentCue)) {
return true;
}
const meaningfulTokens = tokens.filter((token) => isLikelyCounterpartyToken(token));
return meaningfulTokens.length === 0;
}
@@ -671,7 +708,9 @@ function isLowQualityContractAnchorValue(rawValue) {
"период",
"периоду",
"договор",
"договору"
"договору",
"контракт",
"контракту"
]);
const meaningfulTokens = tokens.filter((token) => !lowQualityTokens.has(token));
return meaningfulTokens.length === 0;
+59 -17
View File
@@ -54,7 +54,8 @@ const OPEN_CONTRACTS_HINTS = [
"незакрыт",
"не закрыт",
"открыт",
"договор"
"договор",
"контракт"
];
const OPEN_ITEMS_HINTS = [
"open items",
@@ -124,7 +125,10 @@ const DOCUMENTS_BY_CONTRACT_HINTS = [
"доки по договору",
"док по договору",
"документы договор",
"договор"
"договор",
"документы по контракту",
"доки по контракту",
"контракт"
];
const BANK_OPERATIONS_BY_CONTRACT_HINTS = [
"bank operations by contract",
@@ -134,7 +138,10 @@ const BANK_OPERATIONS_BY_CONTRACT_HINTS = [
"bank ops by contract",
"банковские операции по договору",
"платежи по договору",
"выписка по договору"
"выписка по договору",
"банковские операции по контракту",
"платежи по контракту",
"выписка по контракту"
];
const BANK_OPERATION_CORE_HINTS = [
"банк",
@@ -257,6 +264,14 @@ const COUNTERPARTY_ACTIVITY_LIFECYCLE_HINTS = [
"регулярные поставщики",
"эпизодические поставщики",
"давно не использовались поставщики",
"всех заков",
"кто был активен",
"потом отвалился",
"ровно один раз",
"и пропал",
"самые старые по сотрудничеству",
"разбей поставщиков на регуляр и разовые",
"кто новые в этом году",
"active customers",
"customer activity list",
"counterparty lifecycle"
@@ -314,14 +329,22 @@ const CONTRACT_USAGE_AND_VALUE_HINTS = [
"договоры по обороту",
"договоры по сумме оборота",
"топ договоров по обороту",
"контракты по обороту",
"контракты по сумме оборота",
"топ контрактов по обороту",
"договоры с минимальным бюджетом",
"договоры с самым маленьким бюджетом",
"контракты с минимальным бюджетом",
"контракты с самым маленьким бюджетом",
"активные договоры по бюджету",
"активные контракты по бюджету",
"контрагенты с несколькими договорами",
"несколько договоров у контрагента",
"мультидоговорные контрагенты",
"какие договоры активны",
"какие контракты активны",
"рабочие договоры",
"рабочие контракты",
"contracts by turnover",
"contracts by budget"
];
@@ -331,6 +354,10 @@ const CONTRACT_LIST_BY_COUNTERPARTY_HINTS = [
"список договоров по",
"покажи договоры по",
"выведи договоры по",
"контракты по",
"список контрактов по",
"покажи контракты по",
"выведи контракты по",
"contracts by counterparty",
"list contracts by counterparty",
"show contracts by counterparty"
@@ -550,10 +577,10 @@ function hasCounterpartyPopulationAndRolesSignal(text) {
return false;
}
function hasLifecycleSegmentationSignal(text) {
return /(?:вперв|нов(?:ые|ых|ые\s+контрагент|ые\s+клиент|ые\s+заказчик)|исчез|ушед|ушл|только\s+один\s+раз|однораз|дольше\s+всех|долгожив|регулярн|эпизодич|давно\s+не\s+использ|неиспольз|потом\s+перестал)/iu.test(text);
return /(?:вперв|нов(?:ые|ых|ые\s+контрагент|ые\s+клиент|ые\s+заказчик)|исчез|ушед|ушл|пропал|отвал|только\s+один\s+раз|ровно\s+один\s+раз|однораз|дольше\s+всех|долгожив|самые\s+старые|старые\s+по\s+сотрудничеству|регуляр|эпизодич|разов(?:ые|ой|ые\s+поставщик)|давно\s+не\s+использ|неиспольз|потом\s+перестал)/iu.test(text);
}
function hasCounterpartyActivityLifecycleSignal(text) {
if (hasDocumentSignal(text) || hasBankOperationSignal(text)) {
if ((hasDocumentSignal(text) || hasBankOperationSignal(text)) && !hasLifecycleSegmentationSignal(text)) {
return false;
}
if (hasAny(text, COUNTERPARTY_ACTIVITY_LIFECYCLE_HINTS)) {
@@ -567,6 +594,7 @@ function hasCounterpartyActivityLifecycleSignal(text) {
const hasTimeWindowLexeme = /(?:за\s+вс[её]\s+время|all\s+time|\b(?:19|20)\d{2}\b|(?:^|[^\d])\d{2}\s*(?:г(?:од|ода)?|г)(?:[^\p{L}\p{N}]|$)|в\s+конкретн(?:ом|ый)\s+год|за\s+год|в\s+году)/iu.test(text);
const hasListVerb = /(?:какие|кто|покажи|выведи|список|list|show)/iu.test(text);
const hasRosterQualifier = /(?:у\s+нас|вообще|в\s+баз[еы]|какие\s+есть|кто\s+есть|who\s+are)/iu.test(text);
const hasImplicitCounterpartyQuestion = /(?:кто\s+с\s+нами|кто\s+у\s+нас|всех?\s+зак(?:ов|а|и)?|все\s+заки|кто\s+нов(?:ые|ых|ый)\b|кто\s+был\s+активен|самые\s+старые\s+по\s+сотрудничеству)/iu.test(text);
const hasListWithWindow = hasCounterpartyLexeme && hasListVerb && hasTimeWindowLexeme;
if (hasListWithWindow) {
return true;
@@ -577,22 +605,28 @@ function hasCounterpartyActivityLifecycleSignal(text) {
if (hasCounterpartyLexeme && hasLifecycleSegmentationSignal(text)) {
return true;
}
if (hasImplicitCounterpartyQuestion && (hasLifecycleSegmentationSignal(text) || hasTimeWindowLexeme || hasActivityLexeme)) {
return true;
}
if (!hasCounterpartyLexeme && hasListVerb && hasLifecycleSegmentationSignal(text) && /\bкто\b/iu.test(text)) {
return true;
}
return hasCounterpartyLexeme && hasActivityLexeme && (hasTimeWindowLexeme || hasListVerb);
}
function hasContractUsageOverviewSignal(text) {
if (hasAny(text, CONTRACT_USAGE_OVERVIEW_HINTS)) {
return true;
}
if (/(?:сколько\s+(?:всего\s+)?договор(?:ов|а)?(?:\s+заведен[оы])?|договорн(?:ая|ой)\s+баз[аы]).*(?:сколько|used|использ)/iu.test(text)) {
if (/(?:сколько\s+(?:всего\s+)?(?:договор|контракт)(?:ов|а)?(?:\s+заведен[оы])?|(?:договорн(?:ая|ой)|контрактн(?:ая|ой))\s+баз[аы]).*(?:сколько|used|использ)/iu.test(text)) {
return true;
}
if (/(?:сколько\s+из\s+договор(?:ов|а)?\s+(?:реально\s+)?использ(?:ован[оы]|овал(?:и|ось)?))/iu.test(text)) {
if (/(?:сколько\s+из\s+(?:договор|контракт)(?:ов|а)?\s+(?:реально\s+)?использ(?:ован[оы]|овал(?:и|ось)?))/iu.test(text)) {
return true;
}
if (/(?:total\s+vs\s+used|used\s+vs\s+total).*(?:договор|contract)?/iu.test(text)) {
if (/(?:total\s+vs\s+used|used\s+vs\s+total).*(?:договор|контракт|contract)?/iu.test(text)) {
return true;
}
if (/(?:какие\s+договор(?:ы|а)?).*(?:давно\s+не\s+использ|неиспольз|протух|мертв|мёртв|stale|unused)/iu.test(text)) {
if (/(?:какие\s+(?:договор|контракт)(?:ы|а)?).*(?:давно\s+не\s+использ|неиспольз|протух|мертв|мёртв|stale|unused)/iu.test(text)) {
return true;
}
return false;
@@ -606,17 +640,19 @@ function hasCustomerRevenueAndPaymentsSignal(text) {
}
const hasFuzzyCustomerLexeme = hasFuzzyLexeme(text, ["клиент", "заказчик", "покупател", "customer", "client"]);
const hasFuzzySupplierLexeme = hasFuzzyLexeme(text, ["поставщик", "supplier", "vendor"]);
const hasCounterpartyLexeme = /(?:контрагент(?:ов|а|ы)?|counterpart(?:y|ies)|компан(?:и|ия|ии|ию)|организац(?:и|ия|ии|ию)|partner(?:s)?)/iu.test(text);
const hasSpecificCounterpartyAnchor = hasLooseByAnchorMention(text) ||
hasHeuristicCounterpartyAnchor(text) ||
/(?:по\s+(?:клиент(?:у|а)?|заказчик(?:у|а)?|покупател(?:ю|я)|customer|client)\s+[a-zа-яё0-9])/iu.test(text);
const asksWhoPays = /(?:кто\s+(?:нам\s+)?(?:(?:больше|чаще)\s+)?плат(?:ит|ят)?)/iu.test(text);
const asksCustomerGroup = /(?:клиент(?:ов|а|ы)?|заказчик(?:ов|а|и)?|покупател(?:ей|я|и)?|customer(?:s)?|client(?:s)?)/iu.test(text) ||
hasFuzzyCustomerLexeme ||
/(?:кто\s+нам\s+(?:больше|чаще)|кто\s+платит)/iu.test(text);
asksWhoPays;
const asksCounterpartySource = /(?:с\s+каких|от\s+каких|от\s+кого|from\s+which|from\s+who)/iu.test(text);
const asksIncomingFlow = /(?:приход|поступлен|входящ|зачислен|inflow|incoming)/iu.test(text);
const asksDealBudgetRanking = /(?:сделк|deal|бюджет)/iu.test(text) &&
/(?:топ|top|сам(?:ый|ая|ое|ые)|крупн|мален|жирн|мелк|больше\s+всего|чаще\s+всего|наибольш|максимальн|минимальн)/iu.test(text);
const asksValue = /(?:доходн|выручк|приход|поступлен|входящ|зачислен|оплат|чек|сделк|бюджет|занес|занёс|принес|принёс|revenue|inflow|deal)/iu.test(text);
const asksValue = /(?:доходн|выручк|приход|поступлен|входящ|зачислен|оплат|плат(?:еж|ёж|ежн|ежей|ежа|ит|ят)|деньг|денег|чек|сделк|бюджет|занес|занёс|принес|принёс|revenue|inflow|deal)/iu.test(text);
const asksRankOrTop = /(?:топ|top|сам(?:ый|ая|ое|ые)|крупн|мален|жирн|мелк|больше\s+всего|чаще\s+всего|наибольш|максимальн)/iu.test(text);
const asksCountOnly = /(?:сколько|скока|скок)\s+/iu.test(text) && !asksValue;
if (asksCountOnly) {
@@ -628,6 +664,12 @@ function hasCustomerRevenueAndPaymentsSignal(text) {
if (asksCustomerGroup && (asksValue || asksRankOrTop)) {
return true;
}
if (!hasFuzzySupplierLexeme && hasCounterpartyLexeme && asksRankOrTop && (asksValue || asksWhoPays)) {
return true;
}
if (!hasFuzzySupplierLexeme && asksWhoPays && (asksRankOrTop || hasCounterpartyLexeme)) {
return true;
}
if (asksCounterpartySource && asksValue) {
return true;
}
@@ -668,19 +710,19 @@ function hasContractUsageAndValueSignal(text) {
if (hasAny(text, CONTRACT_USAGE_AND_VALUE_HINTS)) {
return true;
}
if (!/(?:договор(?:ов|а|ы)?|contract(?:s)?)/iu.test(text)) {
if (!/(?:договор(?:ов|а|ы)?|контракт(?:ов|а|ы|у|ом|е)?|contract(?:s)?)/iu.test(text)) {
return false;
}
if (hasContractUsageOverviewSignal(text)) {
return false;
}
const asksStructure = /(?:нескольк(?:ими|их|ие|о)?\s+договор|мультидоговор|контрагент(?:ов|ы)?.*нескольк(?:ими|их|ие|о)\s+договор|какие\s+договор(?:ы|а)?\s+активн|рабоч(?:ие|их)\s+договор)/iu.test(text);
const asksStructure = /(?:нескольк(?:ими|их|ие|о)?\s+(?:договор|контракт)|мультидоговор|контрагент(?:ов|ы)?.*нескольк(?:ими|их|ие|о)\s+(?:договор|контракт)|какие\s+(?:договор|контракт)(?:ы|а)?\s+активн|рабоч(?:ие|их)\s+(?:договор|контракт))/iu.test(text);
const asksValue = /(?:оборот|бюджет|сумм|стоим|value|turnover|amount|revenue|крупн|мелк|миним|максим)/iu.test(text);
const asksRank = /(?:топ|top|ранк|rank|сам(?:ый|ая|ое|ые))/iu.test(text);
return asksStructure || asksValue || asksRank;
}
function hasContractListByCounterpartySignal(text) {
const hasContractLexeme = /(?:договор(?:а|у|ом|е|ы)?|contracts?|contract)/iu.test(text);
const hasContractLexeme = /(?:договор(?:а|у|ом|е|ы)?|контракт(?:а|у|ом|е|ы)?|contracts?|contract)/iu.test(text);
if (!hasContractLexeme) {
return false;
}
@@ -715,7 +757,7 @@ function hasDocumentsByAccountDrilldownSignal(text) {
return hasAccountLexeme && hasDocLexeme && (hasDrilldownVerb || hasSameDate);
}
function hasOpenContractsListSignal(text) {
const hasContractLexeme = text.includes("договор") || text.includes("contract") || text.includes("dogovor");
const hasContractLexeme = text.includes("договор") || text.includes("контракт") || text.includes("contract") || text.includes("dogovor");
const hasOpenLexeme = /(?:незакрыт|не\s+закрыт|открыт|open|unclosed)/iu.test(text);
if (!hasContractLexeme || !hasOpenLexeme) {
return false;
@@ -1059,7 +1101,7 @@ function resolveAddressIntent(userMessage) {
};
}
if (hasAny(text, OPEN_ITEMS_HINTS) &&
(text.includes("контраг") || text.includes("договор") || text.includes("counterparty") || text.includes("contract"))) {
(text.includes("контраг") || text.includes("договор") || text.includes("контракт") || text.includes("counterparty") || text.includes("contract"))) {
return {
intent: "open_items_by_counterparty_or_contract",
confidence: "medium",
@@ -1192,7 +1234,7 @@ function resolveAddressIntent(userMessage) {
reasons: ["generic_lookup_with_loose_anchor_fallback"]
};
}
if (hasAny(text, OPEN_CONTRACTS_HINTS) && (text.includes("договор") || text.includes("contract"))) {
if (hasAny(text, OPEN_CONTRACTS_HINTS) && (text.includes("договор") || text.includes("контракт") || text.includes("contract"))) {
return {
intent: "list_open_contracts",
confidence: "medium",
@@ -21,6 +21,10 @@ const ADDRESS_ACTION_TOKENS = [
"вывед",
"кто",
"кому",
"какой",
"какая",
"какое",
"какую",
"какие",
"каких",
"что по",
@@ -67,6 +71,7 @@ const ADDRESS_ENTITY_TOKENS = [
"клиент",
"покупател",
"партнер",
"контракт",
"банк",
"выписк",
"операц",
@@ -229,7 +234,17 @@ function hasLooseByAnchorMention(text) {
"активности",
"пассивности",
"наименее",
"минимум"
"минимум",
"запрос",
"запросу",
"запроса",
"запросом",
"запросе",
"вопрос",
"вопросу",
"вопроса",
"вопросом",
"вопросе"
]);
return !stopWords.has(token);
}
@@ -306,7 +321,17 @@ function hasLikelyCounterpartyToken(text) {
"пассивный",
"наименее",
"минимум",
"реже"
"реже",
"запрос",
"запросу",
"запроса",
"запросом",
"запросе",
"вопрос",
"вопросу",
"вопроса",
"вопросом",
"вопросе"
]);
const tokens = String(text ?? "")
.split(/[^a-zа-яё0-9._-]+/iu)
@@ -366,11 +391,11 @@ function detectAddressQuestionMode(userMessage) {
reasons: ["loose_by_anchor_detected", ...(hasFollowupSignal ? ["address_followup_signal_detected"] : [])]
};
}
if ((hasAddressEntity || hasAccountCode) && !hasDeepReasoning) {
if (hasAccountCode && !hasDeepReasoning) {
return {
mode: "address_query",
confidence: "medium",
reasons: ["address_entity_detected"]
reasons: ["account_code_detected"]
};
}
if (!hasDeepReasoning && hasDocsOrBankSignal(text) && (hasLooseByAnchor || hasLikelyCounterpartyToken(text))) {
+43 -14
View File
@@ -1358,12 +1358,15 @@ class AddressQueryService {
rowsMatched: historicalFilteredRows.length
});
const historicalFactual = (0, composeStage_1.composeFactualReply)(intent.intent, historicalFilteredRows, { userMessage });
const historicalPrefix = "В последних доступных записях якорь не подтвердился; показаны найденные строки по историческому окну.";
const historicalPrefix = "Найдены данные в историческом срезе базы по вашему запросу.";
const historicalSuggestion = intent.intent === "list_documents_by_counterparty"
? "\nЕсли нужно, могу дополнительно показать платежи и договоры по этому контрагенту."
: "";
const historicalLimitations = [...filters.warnings, "historical_window_sort_recovery_applied"];
const historicalReasons = [...baseReasons, "historical_window_sort_recovery_applied"];
return {
handled: true,
reply_text: `${historicalPrefix}\n${historicalFactual.text}`,
reply_text: `${historicalPrefix}\n${historicalFactual.text}${historicalSuggestion}`,
reply_type: (0, composeStage_1.inferReplyType)(historicalFactual.responseType),
response_type: historicalFactual.responseType,
debug: {
@@ -1419,12 +1422,15 @@ class AddressQueryService {
const documentBankFallbackRows = applyIntentSpecificFilter(intent.intent, normalizedRows);
if (documentBankFallbackRows.length > 0) {
const fallbackFactual = (0, composeStage_1.composeFactualReply)(intent.intent, documentBankFallbackRows, { userMessage });
const fallbackPrefix = "По вашему запросу показываю найденные документы и операции в доступном срезе базы.";
const fallbackSuggestion = intent.intent === "list_documents_by_counterparty"
? "\nЕсли нужно, могу дополнительно сузить период или показать только платежи."
: "";
const fallbackLimitations = [...filters.warnings, "anchor_not_matched_fallback_rows"];
const fallbackReasons = [...baseReasons, "anchor_not_matched_fallback_rows"];
return {
handled: true,
reply_text: "Точный якорь не подтвердился в текущем окне live-данных; показаны ближайшие доступные документы/операции по выбранному типу.\n" +
fallbackFactual.text,
reply_text: `${fallbackPrefix}\n${fallbackFactual.text}${fallbackSuggestion}`,
reply_type: (0, composeStage_1.inferReplyType)(fallbackFactual.responseType),
response_type: fallbackFactual.responseType,
debug: {
@@ -1484,9 +1490,20 @@ class AddressQueryService {
const isFollowupAnchorCarryover = Array.isArray(filters.warnings) &&
(filters.warnings.includes("counterparty_from_followup_context") ||
filters.warnings.includes("contract_from_followup_context"));
const anchorMismatchByCounterparty = isAnchorMismatch && String(matchFailureReason ?? "").includes("counterparty_anchor_not_matched");
const anchorMismatchByContract = isAnchorMismatch && String(matchFailureReason ?? "").includes("contract_anchor_not_matched");
const isLowQualityPartyAnchor = (anchor.anchor_type === "counterparty" || anchor.anchor_type === "contract") &&
isLikelyLowQualityPartyAnchor(anchor.anchor_value_raw);
const anchorMismatchCategory = isFollowupAnchorCarryover || !isLowQualityPartyAnchor ? "empty_match" : "missing_anchor";
const requestedPeriodFrom = typeof filters.extracted_filters.period_from === "string" ? filters.extracted_filters.period_from : null;
const requestedPeriodTo = typeof filters.extracted_filters.period_to === "string" ? filters.extracted_filters.period_to : null;
const requestedPeriodHint = requestedPeriodFrom && requestedPeriodTo ? ` (период ${requestedPeriodFrom}..${requestedPeriodTo} сохранен)` : "";
const anchorMismatchCategory = isFollowupAnchorCarryover
? "empty_match"
: anchorMismatchByCounterparty || anchorMismatchByContract
? "missing_anchor"
: !isLowQualityPartyAnchor
? "empty_match"
: "missing_anchor";
const category = isAnchorMismatch
? anchorMismatchCategory
: isRecipeFilteredOut
@@ -1495,18 +1512,26 @@ class AddressQueryService {
? "recipe_visibility_gap"
: "empty_match";
const reasonText = isAnchorMismatch
? anchorMismatchCategory === "missing_anchor"
? "якорь контрагента/договора не найден в материализованных live-строках"
: "по указанному якорю и фильтрам в live-выборке нет строк"
? anchorMismatchByCounterparty
? "контрагент по указанному имени/алиасу не найден в materialized live-строках"
: anchorMismatchByContract
? "договор по указанному номеру/названию не найден в materialized live-строках"
: anchorMismatchCategory === "missing_anchor"
? "якорь контрагента/договора не найден в materialized live-строках"
: "по указанному якорю и фильтрам в live-выборке нет строк"
: isRecipeFilteredOut
? "строки по якорю найдены, но отфильтрованы intent-specific recipe"
: isVisibilityGapCandidate
? "в текущем live recipe нет достаточной document/bank видимости после фильтрации"
: "по выбранным фильтрам в live-выборке нет строк";
const nextStep = isAnchorMismatch
? anchorMismatchCategory === "missing_anchor"
? "уточните контрагента точным именем или добавьте ИНН/договор"
: "уточните период или снимите часть фильтров"
? anchorMismatchByCounterparty
? `уточните точное имя контрагента или добавьте ИНН${requestedPeriodHint}`
: anchorMismatchByContract
? `уточните номер/наименование договора${requestedPeriodHint}`
: anchorMismatchCategory === "missing_anchor"
? "уточните контрагента точным именем или добавьте ИНН/договор"
: "уточните период или снимите часть фильтров"
: isRecipeFilteredOut
? "сузьте период, уточните контрагента или документный тип"
: isVisibilityGapCandidate
@@ -1514,9 +1539,13 @@ class AddressQueryService {
: "уточните период, контрагента, договор или снимите часть фильтров";
const limitations = isAnchorMismatch
? [
anchorMismatchCategory === "missing_anchor"
? "anchor_not_matched_after_materialization"
: "no_rows_for_anchor_after_materialization"
anchorMismatchByCounterparty
? "counterparty_anchor_not_matched_after_materialization"
: anchorMismatchByContract
? "contract_anchor_not_matched_after_materialization"
: anchorMismatchCategory === "missing_anchor"
? "anchor_not_matched_after_materialization"
: "no_rows_for_anchor_after_materialization"
]
: isRecipeFilteredOut
? ["rows_filtered_out_by_recipe_after_anchor_match"]
@@ -169,13 +169,13 @@ function detectCounterpartyProfileFocus(userMessage) {
const hasCustomerToken = /(?:заказчик(?:ов|а)?|клиент(?:ов|а)?|customer(?:s)?|client(?:s)?)/iu.test(text);
const hasMixedToken = /(?:смешан|проч(?:их|ие)|mixed)/iu.test(text);
const asksRoles = /(?:заказчик(?:ов|а)?|поставщик(?:ов|а)?|смешан|проч(?:их|ие)|типы?\s+контрагент|разбей|раздели|roles?|split)/iu.test(text);
if (hasSupplierToken && !hasCustomerToken && !hasMixedToken && !asksTotal) {
if (hasSupplierToken && !hasCustomerToken && !hasMixedToken) {
return "suppliers_only";
}
if (hasCustomerToken && !hasSupplierToken && !hasMixedToken && !asksTotal) {
if (hasCustomerToken && !hasSupplierToken && !hasMixedToken) {
return "customers_only";
}
if (hasMixedToken && !hasSupplierToken && !hasCustomerToken && !asksTotal) {
if (hasMixedToken && !hasSupplierToken && !hasCustomerToken) {
return "mixed_only";
}
if (asksTotal && !asksRoles) {
@@ -614,7 +614,17 @@ function composeFactualReply(intent, rows, options = {}) {
const focus = detectCounterpartyProfileFocus(options.userMessage);
const includeTotal = focus === "full_profile" || focus === "total_only";
const includeRoles = focus === "full_profile" || focus === "roles_only";
const directLead = focus === "suppliers_only"
? `Поставщиков (только supplier-роль): ${supplierOnly}.`
: focus === "customers_only"
? `Заказчиков (только customer-роль): ${customerOnly}.`
: focus === "mixed_only"
? `Смешанных контрагентов (и customer, и supplier): ${mixedActive}.`
: includeTotal && totalCounterparties > 0
? `Всего уникальных контрагентов в базе: ${totalCounterparties}.`
: `Активных контрагентов по операциям: ${activeCounterparties}.`;
const lines = [
directLead,
"Профиль контрагентов собран (catalog + bank-doc activity aggregate).",
`Строк агрегата: ${rows.length}.`
];
@@ -693,17 +703,17 @@ function composeFactualReply(intent, rows, options = {}) {
? `в ${requestedYear} году`
: "в выбранном периоде";
const lines = [
`Активные заказчики ${scopeLabel}: ${counterparties.length}.`,
"Собран профиль активности заказчиков (bank-doc activity aggregate).",
`Строк агрегата: ${rows.length}.`
];
if (counterparties.length === 0) {
lines.push("Активных заказчиков по выбранному окну не найдено.");
lines.push("По выбранному окну активности заказчики не найдены.");
return {
responseType: "FACTUAL_SUMMARY",
text: lines.join("\n")
};
}
lines.push(`Активные заказчики ${scopeLabel}: ${counterparties.length}.`);
const visible = counterparties.slice(0, 120);
lines.push(...visible.map((item, index) => {
const suffix = item.lastPeriod ? ` | последняя активность: ${item.lastPeriod}` : "";
@@ -734,7 +744,11 @@ function composeFactualReply(intent, rows, options = {}) {
const usedContracts = sumMarker("CT_USED");
const unusedContracts = totalContracts > 0 ? Math.max(0, totalContracts - Math.min(usedContracts, totalContracts)) : null;
const usedShare = totalContracts > 0 ? formatPercent(Math.min(usedContracts, totalContracts), totalContracts) : null;
const usageLead = totalContracts > 0
? `Использованных договоров: ${usedContracts} из ${totalContracts}${usedShare ? ` (${usedShare})` : ""}.`
: `Использованных договоров (есть factual связь с операциями): ${usedContracts}.`;
const lines = [
usageLead,
"Профиль договорной базы собран (catalog + usage aggregate).",
`Строк агрегата: ${rows.length}.`
];
@@ -830,9 +844,10 @@ function composeFactualReply(intent, rows, options = {}) {
}
if (focus === "top_by_ops") {
const visible = rankedByOps.slice(0, limit);
lines.push(isSupplier
const heading = isSupplier
? `Топ-${visible.length} поставщиков по количеству исходящих платежных операций:`
: `Топ-${visible.length} заказчиков по количеству входящих платежных операций:`);
: `Топ-${visible.length} заказчиков по количеству входящих платежных операций:`;
lines.unshift(heading);
lines.push(...visible.map((item, index) => `${index + 1}. ${item.name} | операций: ${item.ops} | сумма: ${item.total} | макс: ${item.maxSingle}`));
return {
responseType: "FACTUAL_LIST",
@@ -841,9 +856,10 @@ function composeFactualReply(intent, rows, options = {}) {
}
if (focus === "top_by_max_single") {
const visible = rankedByMaxSingle.slice(0, limit);
lines.push(isSupplier
const heading = isSupplier
? `Топ-${visible.length} поставщиков по максимальной разовой выплате:`
: `Топ-${visible.length} заказчиков по максимальной сумме одной входящей операции:`);
: `Топ-${visible.length} заказчиков по максимальной сумме одной входящей операции:`;
lines.unshift(heading);
lines.push(...visible.map((item, index) => `${index + 1}. ${item.name} | max single: ${item.maxSingle} | сумма: ${item.total} | операций: ${item.ops}`));
return {
responseType: "FACTUAL_LIST",
@@ -852,9 +868,10 @@ function composeFactualReply(intent, rows, options = {}) {
}
if (focus === "top_by_avg_check_min_ops") {
const visible = rankedByAvgCheck.slice(0, limit);
lines.push(isSupplier
const heading = isSupplier
? `Топ-${visible.length} поставщиков по среднему чеку (минимум ${minOpsForAvgCheck} операций):`
: `Топ-${visible.length} заказчиков по среднему чеку (минимум ${minOpsForAvgCheck} входящих операций):`);
: `Топ-${visible.length} заказчиков по среднему чеку (минимум ${minOpsForAvgCheck} входящих операций):`;
lines.unshift(heading);
if (visible.length === 0) {
lines.push(`Контрагентов с минимум ${minOpsForAvgCheck} операций не найдено.`);
}
@@ -868,9 +885,10 @@ function composeFactualReply(intent, rows, options = {}) {
}
if (focus === "top_deals") {
const visible = rankedDealsTop.slice(0, limit);
lines.push(isSupplier
const heading = isSupplier
? `Топ-${visible.length} самых крупных разовых выплат поставщикам:`
: `Топ-${visible.length} самых крупных разовых сделок по поступлениям:`);
: `Топ-${visible.length} самых крупных разовых сделок по поступлениям:`;
lines.unshift(heading);
lines.push(...visible.map((item, index) => `${index + 1}. ${item.period ?? "n/a"} | ${item.counterparty} | ${item.registrator} | ${item.amount}`));
return {
responseType: "FACTUAL_LIST",
@@ -879,9 +897,10 @@ function composeFactualReply(intent, rows, options = {}) {
}
if (focus === "bottom_deals") {
const visible = rankedDealsBottom.slice(0, limit);
lines.push(isSupplier
const heading = isSupplier
? `Топ-${visible.length} самых маленьких разовых выплат:`
: `Топ-${visible.length} самых маленьких разовых сделок по поступлениям:`);
: `Топ-${visible.length} самых маленьких разовых сделок по поступлениям:`;
lines.unshift(heading);
if (activeOnlyForBottomDeals) {
lines.push("Фильтр: только активные контрагенты (минимум 3 операции).");
}
@@ -892,9 +911,10 @@ function composeFactualReply(intent, rows, options = {}) {
};
}
const visible = rankedByTotal.slice(0, limit);
lines.push(isSupplier
const heading = isSupplier
? `Топ-${visible.length} поставщиков по сумме выплат:`
: `Топ-${visible.length} заказчиков по сумме поступлений:`);
: `Топ-${visible.length} заказчиков по сумме поступлений:`;
lines.unshift(heading);
lines.push(...visible.map((item, index) => {
const avgCheck = item.ops > 0 ? (item.total / item.ops).toFixed(2) : "0";
return `${index + 1}. ${item.name} | сумма: ${item.total} | операций: ${item.ops} | средний чек: ${avgCheck} | макс: ${item.maxSingle}`;
@@ -943,9 +963,10 @@ function composeFactualReply(intent, rows, options = {}) {
.filter((item) => item.docs > 0 && item.turnover > 0)
.sort((a, b) => a.turnover - b.turnover || b.docs - a.docs || a.contract.localeCompare(b.contract));
const lines = [
`Активных договоров: ${contractRows.length}.`,
"Собран профиль договоров по обороту/бюджету (bank-doc contract aggregate).",
`Строк источника: ${rows.length}.`,
`Активных договоров: ${contractRows.length}.`
`Договорных агрегатов: ${contractRows.length}.`
];
if (contractRows.length === 0) {
lines.push("В выбранном окне не найдено операций, связанных с договорами.");
@@ -956,7 +977,8 @@ function composeFactualReply(intent, rows, options = {}) {
}
if (focus === "top_by_docs") {
const visible = rankedByDocs.slice(0, limit);
lines.push(`Топ-${visible.length} договоров по количеству операций:`);
const heading = `Топ-${visible.length} договоров по количеству операций:`;
lines.unshift(heading);
lines.push(...visible.map((item, index) => `${index + 1}. ${item.contract} | операций: ${item.docs} | оборот: ${item.turnover} | контрагентов: ${item.counterparties.size}`));
return {
responseType: "FACTUAL_LIST",
@@ -965,7 +987,8 @@ function composeFactualReply(intent, rows, options = {}) {
}
if (focus === "bottom_by_turnover_active") {
const visible = rankedBottomActive.slice(0, limit);
lines.push(`Топ-${visible.length} активных договоров с минимальным бюджетом (оборотом):`);
const heading = `Топ-${visible.length} активных договоров с минимальным бюджетом (оборотом):`;
lines.unshift(heading);
lines.push(...visible.map((item, index) => `${index + 1}. ${item.contract} | оборот: ${item.turnover} | операций: ${item.docs} | последняя активность: ${item.lastPeriod ?? "n/a"}`));
return {
responseType: "FACTUAL_LIST",
@@ -973,7 +996,8 @@ function composeFactualReply(intent, rows, options = {}) {
};
}
const visible = rankedByTurnover.slice(0, limit);
lines.push(`Топ-${visible.length} договоров по сумме оборота:`);
const heading = `Топ-${visible.length} договоров по сумме оборота:`;
lines.unshift(heading);
lines.push(...visible.map((item, index) => `${index + 1}. ${item.contract} | оборот: ${item.turnover} | операций: ${item.docs} | контрагентов: ${item.counterparties.size} | последняя активность: ${item.lastPeriod ?? "n/a"}`));
return {
responseType: "FACTUAL_LIST",
@@ -1073,8 +1097,7 @@ function composeFactualReply(intent, rows, options = {}) {
}
if (intent === "list_documents_by_counterparty") {
const lines = [
"Собран список документов по контрагенту (live address lane).",
`Строк отобрано: ${rows.length}.`,
`Найдено документов по контрагенту: ${rows.length}.`,
...formatTopRows(rows, rows.length)
];
return {
@@ -1084,6 +1107,7 @@ function composeFactualReply(intent, rows, options = {}) {
}
if (intent === "list_documents_by_contract") {
const lines = [
`Найдено документов по договору: ${rows.length}.`,
"Собран список документов по договору (live address lane).",
`Строк отобрано: ${rows.length}.`,
...formatTopRows(rows, rows.length)
@@ -1095,6 +1119,7 @@ function composeFactualReply(intent, rows, options = {}) {
}
if (intent === "bank_operations_by_counterparty") {
const lines = [
`Найдено банковских операций по контрагенту: ${rows.length}.`,
"Собран список банковских операций по контрагенту (live address lane).",
`Строк отобрано: ${rows.length}.`,
...formatTopRows(rows, rows.length)
@@ -1106,6 +1131,7 @@ function composeFactualReply(intent, rows, options = {}) {
}
if (intent === "bank_operations_by_contract") {
const lines = [
`Найдено банковских операций по договору: ${rows.length}.`,
"Собран список банковских операций по договору (live address lane).",
`Строк отобрано: ${rows.length}.`,
...formatTopRows(rows, rows.length)
@@ -24,6 +24,9 @@ function hasAllTimeHint(text) {
function hasSameDateHint(text) {
return /(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|та\s+же\s+дата|same\s+date|as\s+of\s+same\s+date|the\s+same\s+date)/iu.test(String(text ?? ""));
}
function hasExplicitPeriodLiteral(text) {
return /\b(?:19|20)\d{2}(?:[./-](?:0?[1-9]|1[0-2]))?\b/.test(String(text ?? ""));
}
function hasOpenItemsHint(text) {
return /(?:open\s+items|unclosed\s+items|хвост|висят|незакрыт|не\s+закрыт|открыт|долг|задолж|позиц)/iu.test(String(text ?? ""));
}
@@ -93,6 +96,29 @@ const FOLLOWUP_LOW_QUALITY_COUNTERPARTY_TOKENS = new Set([
"что",
"все",
"всё",
"кроме",
"помимо",
"этого",
"этот",
"эта",
"эту",
"этом",
"это",
"эти",
"этих",
"документ",
"документа",
"документы",
"документов",
"договор",
"договора",
"контрагент",
"контрагента",
"еще",
"ещё",
"другие",
"другое",
"остальное",
"год",
"года",
"году",
@@ -313,6 +339,17 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
}
return { filters: merged, reasons };
}
if (intent === "counterparty_activity_lifecycle" &&
hasAddressFollowupContextSignal(userMessage) &&
!hasExplicitPeriodLiteral(userMessage)) {
const currentPeriodFrom = toNonEmptyString(merged.period_from);
const currentPeriodTo = toNonEmptyString(merged.period_to);
const todayIso = new Date().toISOString().slice(0, 10);
if (!currentPeriodFrom && currentPeriodTo === todayIso) {
delete merged.period_to;
reasons.push("period_to_cleared_for_lifecycle_followup");
}
}
const currentHasPeriod = hasExplicitPeriodWindow(merged);
const previousHasPeriod = hasExplicitPeriodWindow(previous);
if (!currentHasPeriod && previousHasPeriod && hasAddressFollowupContextSignal(userMessage)) {
@@ -439,23 +476,29 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
}
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 mode = detectedMode.mode === "address_query"
? detectedMode
: detectedIntent.intent !== "unknown"
? {
mode: "address_query",
confidence: "medium",
reasons: [...detectedMode.reasons, "address_mode_from_resolved_intent"]
}
: 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 intent = deriveIntentWithFollowupContext(detectedIntent, userMessage, followupContext);
const extractedFilters = (0, addressFilterExtractor_1.extractAddressFilters)(userMessage, intent.intent);
const followupMerged = mergeFollowupFilters(extractedFilters.extracted_filters, intent.intent, userMessage, followupContext);
File diff suppressed because it is too large Load Diff
@@ -167,6 +167,90 @@ function buildBaseUrlCandidates(config) {
return Array.from(new Set([base, `${base}/v1`]));
}
class OpenAIResponsesClient {
async chat(config, prompt) {
const responsesPayload = {
model: config.model,
temperature: prompt.temperature ?? config.temperature ?? 0.2,
max_output_tokens: prompt.maxOutputTokens ?? config.maxOutputTokens ?? 400,
input: [
...(String(prompt.systemPrompt ?? "").trim().length > 0
? [
{
role: "system",
content: [{ type: "input_text", text: String(prompt.systemPrompt ?? "").trim() }]
}
]
: []),
...(String(prompt.developerPrompt ?? "").trim().length > 0
? [
{
role: "developer",
content: [{ type: "input_text", text: String(prompt.developerPrompt ?? "").trim() }]
}
]
: []),
{
role: "user",
content: [{ type: "input_text", text: String(prompt.userMessage ?? "") }]
}
]
};
const provider = resolveProvider(config);
if (provider === "openai") {
const raw = await this.postResponses(config, responsesPayload);
return {
raw,
outputText: extractOutputTextFromResponses(raw),
usage: extractUsage(raw)
};
}
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: prompt.temperature ?? config.temperature ?? 0.2,
max_tokens: prompt.maxOutputTokens ?? config.maxOutputTokens ?? 400,
messages: [
...(String(prompt.systemPrompt ?? "").trim().length > 0
? [
{
role: "system",
content: String(prompt.systemPrompt ?? "").trim()
}
]
: []),
...(String(prompt.developerPrompt ?? "").trim().length > 0
? [
{
role: "developer",
content: String(prompt.developerPrompt ?? "").trim()
}
]
: []),
{
role: "user",
content: String(prompt.userMessage ?? "")
}
]
};
const raw = await this.postChatCompletions(config, chatPayload);
return {
raw,
outputText: extractOutputTextFromChatCompletions(raw),
usage: extractUsage(raw)
};
}
async listModels(config) {
const payload = await this.getModels(config);
const data = Array.isArray(payload.data) ? payload.data : [];