АДРЕСНЫЙ РЕЖИМ -Step-5 - feat(assistant): стабилизация свободного LLM-роутинга, прическа маршрутов chat/address, прототип прогноза НДС
This commit is contained in:
+64
-25
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
+1300
-48
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 : [];
|
||||
|
||||
Reference in New Issue
Block a user