АРЧ АП11 - Оркестрация: дожать агентный прогон по выбору компании и возрасту активности в 1С

This commit is contained in:
2026-04-17 16:40:08 +03:00
parent 7be037e225
commit cd0b78d1de
31 changed files with 1909 additions and 1187 deletions
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.contractCandidatesFromRows = contractCandidatesFromRows;
exports.composeFactualReply = composeFactualReply;
exports.inferReplyType = inferReplyType;
const assistantOrganizationMatcher_1 = require("../assistantOrganizationMatcher");
function uniqueStrings(values) {
return Array.from(new Set(values
.map((item) => item.trim())
@@ -581,6 +582,39 @@ function extractCounterpartyName(row) {
}
return null;
}
function normalizeCounterpartyLookupText(value) {
return String(value ?? "")
.toLowerCase()
.replace(/ё/g, "е")
.replace(/[^a-zа-я0-9]+/giu, " ")
.replace(/\s+/g, " ")
.trim();
}
function counterpartyLookupMatches(candidate, hint) {
const normalizedCandidate = normalizeCounterpartyLookupText(candidate);
const normalizedHint = normalizeCounterpartyLookupText(hint);
if (!normalizedCandidate || !normalizedHint) {
return false;
}
if (normalizedCandidate === normalizedHint) {
return true;
}
if (normalizedCandidate.includes(normalizedHint) || normalizedHint.includes(normalizedCandidate)) {
return true;
}
const hintTokens = normalizedHint.split(" ").filter((token) => token.length >= 3);
if (hintTokens.length === 0) {
return false;
}
return hintTokens.every((token) => normalizedCandidate.includes(token));
}
function hasCounterpartyActivityAgeQuestion(userMessage) {
const text = normalizeQuestionText(userMessage);
if (!text) {
return false;
}
return /(?:сколько\s+лет\s+активности|сколько\s+лет\s+в\s+базе|возраст\s+активности|перв(?:ая|ый|ое)\s+(?:активность|платеж|поступление|документ)|последн(?:яя|ий|ее)\s+активность|с\s+какого\s+года\s+актив)/iu.test(text);
}
function hasCounterpartyItemFlowQuestion(userMessage) {
const text = String(userMessage ?? "").trim().toLowerCase();
if (!text) {
@@ -2386,9 +2420,39 @@ function composeFactualReply(intent, rows, options = {}) {
};
}
if (intent === "counterparty_activity_lifecycle") {
const activityFirstRows = rows.filter((row) => String(row.registrator ?? "").trim().toUpperCase() === "CP_CUSTOMER_ACTIVITY_FIRST");
const activityRows = rows.filter((row) => String(row.registrator ?? "").trim().toUpperCase() === "CP_CUSTOMER_ACTIVITY");
const activityYearRows = rows.filter((row) => String(row.registrator ?? "").trim().toUpperCase() === "CP_CUSTOMER_ACTIVITY_YEAR");
const byCounterparty = new Map();
for (const row of activityFirstRows) {
const name = extractCounterpartyName(row);
if (!name) {
continue;
}
const opsCount = Math.max(0, Math.trunc(row.amount ?? 0));
const year = extractYearFromIso(row.period);
const current = byCounterparty.get(name);
if (!current) {
byCounterparty.set(name, {
name,
opsCount,
lastPeriod: row.period,
firstPeriod: row.period,
firstObservedActivity: row.period,
years: new Set(year !== null ? [year] : [])
});
continue;
}
if (!current.firstObservedActivity || (row.period ?? "") < current.firstObservedActivity) {
current.firstObservedActivity = row.period;
}
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
current.firstPeriod = row.period;
}
if (year !== null) {
current.years.add(year);
}
}
for (const row of activityYearRows) {
const name = extractCounterpartyName(row);
if (!name) {
@@ -2403,6 +2467,7 @@ function composeFactualReply(intent, rows, options = {}) {
opsCount,
lastPeriod: row.period,
firstPeriod: row.period,
firstObservedActivity: null,
years: new Set(year !== null ? [year] : [])
});
continue;
@@ -2432,6 +2497,7 @@ function composeFactualReply(intent, rows, options = {}) {
opsCount,
lastPeriod: row.period,
firstPeriod: row.period,
firstObservedActivity: row.period,
years: new Set(year !== null ? [year] : [])
});
continue;
@@ -2454,6 +2520,7 @@ function composeFactualReply(intent, rows, options = {}) {
const focus = detectCounterpartyLifecycleFocus(options.userMessage);
const requestedYear = extractRequestedYearFromQuestion(options.userMessage);
const longevityQuestion = hasCounterpartyLifecycleLongevityQuestion(options.userMessage);
const activityAgeQuestion = hasCounterpartyActivityAgeQuestion(options.userMessage);
const rankingLimit = detectRankingLimit(options.userMessage, 10);
const counterparties = counterpartiesRaw.sort((left, right) => {
if (longevityQuestion) {
@@ -2472,6 +2539,105 @@ function composeFactualReply(intent, rows, options = {}) {
: requestedYear
? `в ${requestedYear} году`
: "в выбранном периоде";
if (activityAgeQuestion) {
const focusedCounterparty = counterparties.find((item) => counterpartyLookupMatches(item.name, options.counterpartyHint)) ?? null;
if (focusedCounterparty) {
const firstObservedActivity = focusedCounterparty.firstObservedActivity ?? focusedCounterparty.firstPeriod;
const lastObservedActivity = focusedCounterparty.lastPeriod;
const firstTimestamp = toUtcDayTimestamp(firstObservedActivity);
const lastTimestamp = toUtcDayTimestamp(lastObservedActivity);
const observedDays = firstTimestamp !== null && lastTimestamp !== null && lastTimestamp >= firstTimestamp
? Math.floor((lastTimestamp - firstTimestamp) / 86_400_000)
: null;
const observedAgeLabel = observedDays !== null
? formatAgeYearsMonthsDays(observedDays)
: focusedCounterparty.years.size > 0
? `${focusedCounterparty.years.size} г.`
: null;
const directLine = observedAgeLabel && firstObservedActivity && lastObservedActivity
? `По активности в базе 1С контрагент ${focusedCounterparty.name} наблюдается минимум ${observedAgeLabel}.`
: `По активности в базе 1С контрагент ${focusedCounterparty.name} найден в подтвержденных движениях.`;
const lines = [directLine];
if (firstObservedActivity) {
lines.push(`Первая подтвержденная активность: ${formatDateRu(firstObservedActivity)}.`);
}
if (lastObservedActivity) {
lines.push(`Последняя подтвержденная активность: ${formatDateRu(lastObservedActivity)}.`);
}
lines.push(`Подтвержденных операций в агрегате: ${focusedCounterparty.opsCount}.`);
if (focusedCounterparty.years.size > 0) {
const years = Array.from(focusedCounterparty.years).sort((a, b) => a - b);
lines.push(`Годы с активностью в базе: ${years.join(", ")}.`);
}
lines.push("Это возраст активности в 1С по подтвержденным движениям, а не дата регистрации юрлица.");
return {
responseType: "FACTUAL_SUMMARY",
text: lines.join("\n")
};
}
const organizationHint = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.organizationHint ?? null);
if (organizationHint && counterparties.length > 0) {
const organizationFirstObservedActivity = counterparties.reduce((earliest, item) => {
const candidate = item.firstObservedActivity ?? item.firstPeriod ?? null;
if (!candidate) {
return earliest;
}
if (!earliest || candidate < earliest) {
return candidate;
}
return earliest;
}, null);
const organizationLastObservedActivity = counterparties.reduce((latest, item) => {
const candidate = item.lastPeriod ?? item.firstPeriod ?? item.firstObservedActivity ?? null;
if (!candidate) {
return latest;
}
if (!latest || candidate > latest) {
return candidate;
}
return latest;
}, null);
const organizationYears = new Set();
let organizationOpsCount = 0;
for (const item of counterparties) {
organizationOpsCount += item.opsCount;
for (const year of item.years) {
organizationYears.add(year);
}
}
const firstTimestamp = toUtcDayTimestamp(organizationFirstObservedActivity);
const lastTimestamp = toUtcDayTimestamp(organizationLastObservedActivity);
const observedDays = firstTimestamp !== null && lastTimestamp !== null && lastTimestamp >= firstTimestamp
? Math.floor((lastTimestamp - firstTimestamp) / 86_400_000)
: null;
const observedAgeLabel = observedDays !== null
? formatAgeYearsMonthsDays(observedDays)
: organizationYears.size > 0
? `${organizationYears.size} г.`
: null;
const lines = [
observedAgeLabel && organizationFirstObservedActivity && organizationLastObservedActivity
? `По активности организации ${organizationHint} в базе 1С наблюдается минимум ${observedAgeLabel}.`
: `По активности организации ${organizationHint} в базе 1С найдены подтвержденные движения.`
];
if (organizationFirstObservedActivity) {
lines.push(`Первая подтвержденная активность: ${formatDateRu(organizationFirstObservedActivity)}.`);
}
if (organizationLastObservedActivity) {
lines.push(`Последняя подтвержденная активность: ${formatDateRu(organizationLastObservedActivity)}.`);
}
lines.push(`Подтвержденных операций в агрегате: ${organizationOpsCount}.`);
if (organizationYears.size > 0) {
const years = Array.from(organizationYears).sort((a, b) => a - b);
lines.push(`Годы с активностью в базе: ${years.join(", ")}.`);
}
lines.push("Это возраст активности организации в 1С по подтвержденным движениям, а не дата регистрации юрлица.");
return {
responseType: "FACTUAL_SUMMARY",
text: lines.join("\n")
};
}
}
const lines = longevityQuestion
? [
`Заказчиков с самым длинным горизонтом сотрудничества (по годам): ${counterparties.length}.`,
@@ -425,7 +425,7 @@ function shouldRestoreInventoryRootFrame(userMessage, intent, extractedFilters,
/(?:покажи|показать|выведи|раскрой|еще\s+раз|ещ[её]\s+раз|снова|опять|верни|вернись|повтори|тот\s+же|этот\s+же|same|again)/iu.test(normalized);
const canReenterInventoryRoot = comingFromInventoryDrilldown ||
rootContextOnly ||
(currentFrameKind === "inventory_root" && hasSamePeriodHint(normalized)) ||
(currentFrameKind === "inventory_root" && (hasSamePeriodHint(normalized) || hasInventoryRootRestatementCue)) ||
(currentFrameKind === "generic" && hasInventoryRootRestatementCue && hasSamePeriodHint(normalized));
if (!canReenterInventoryRoot) {
return false;
@@ -537,7 +537,8 @@ function hasAddressFollowupContextSignal(text) {
return tokenCount <= 6;
}
function isValueCounterpartyIntent(intent) {
return (intent === "customer_revenue_and_payments" ||
return (intent === "counterparty_activity_lifecycle" ||
intent === "customer_revenue_and_payments" ||
intent === "supplier_payouts_profile" ||
intent === "contract_usage_and_value");
}