АРЧ АП11 - Оркестрация: дожать агентный прогон по выбору компании и возрасту активности в 1С
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user