АРЧ - Ассистент: отделить meta-followup по прошлому ответу от повторного запуска address lane

This commit is contained in:
2026-04-15 12:38:44 +03:00
parent 8866176be6
commit 70cc5a99f1
61 changed files with 4023 additions and 298 deletions
@@ -0,0 +1,221 @@
const ORGANIZATION_SCOPE_STOPWORDS = new Set([
"ооо",
"зао",
"оао",
"пао",
"ао",
"ип",
"llc",
"inc",
"ltd",
"corp",
"group",
"company",
"co",
"the",
"and",
"org",
"organization",
"компания",
"организация",
"контора",
"фирма",
"база",
"по",
"в",
"во",
"на",
"для",
"из",
"у",
"к",
"от",
"это",
"эта",
"этой",
"этот",
"сегодня",
"сейчас",
"текущая",
"текущей",
"наш",
"наша",
"нашей",
"нашу",
"наши"
]);
function normalizeScopeLabel(value: unknown): string {
return String(value ?? "")
.replace(/[“”«»]/g, '"')
.replace(/\s+/g, " ")
.trim();
}
function normalizeScopeKey(value: unknown): string {
return normalizeScopeLabel(value).toLowerCase().replace(/ё/g, "е");
}
export function normalizeOrganizationScopeValue(value: unknown): string | null {
const normalized = normalizeScopeLabel(value);
if (!normalized) {
return null;
}
let unwrapped = normalized.replace(/^\\+|\\+$/g, "").trim();
if (
(unwrapped.startsWith('"') && unwrapped.endsWith('"')) ||
(unwrapped.startsWith("'") && unwrapped.endsWith("'"))
) {
unwrapped = unwrapped.slice(1, -1).trim();
}
return unwrapped.length > 0 ? unwrapped : null;
}
export function normalizeOrganizationScopeSearchText(value: unknown): string {
return normalizeScopeKey(value)
.replace(/[^\p{L}\p{N}]+/gu, " ")
.replace(/\s+/g, " ")
.trim();
}
function tokenizeOrganizationScope(value: unknown): string[] {
const normalized = normalizeOrganizationScopeSearchText(value);
if (!normalized) {
return [];
}
return normalized
.split(" ")
.map((token) => token.trim())
.filter((token) => token.length >= 3 && !ORGANIZATION_SCOPE_STOPWORDS.has(token));
}
function organizationTokenVariants(token: string): string[] {
const source = String(token ?? "").trim().toLowerCase();
if (!source) {
return [];
}
const variants = new Set([source]);
const withoutLongEnding = source.replace(
/(?:ами|ями|ого|ему|ому|ыми|ими|иях|ях|ах|ей|ой|ом|ем|ам|ям|ую|юю|ая|яя|ое|ее|ые|ие|ов|ев|ий|ый|ой)$/iu,
""
);
if (withoutLongEnding.length >= 4) {
variants.add(withoutLongEnding);
}
const withoutShortEnding = source.replace(/[аеёиоуыэюя]$/iu, "");
if (withoutShortEnding.length >= 4) {
variants.add(withoutShortEnding);
}
return Array.from(variants);
}
export function scoreOrganizationMentionInMessage(message: unknown, organization: unknown): number {
const messageNorm = normalizeOrganizationScopeSearchText(message);
const organizationNorm = normalizeOrganizationScopeSearchText(organization);
if (!messageNorm || !organizationNorm) {
return 0;
}
if (messageNorm.includes(organizationNorm)) {
return 10_000 + organizationNorm.length;
}
const organizationTokens = tokenizeOrganizationScope(organizationNorm);
const messageTokens = tokenizeOrganizationScope(messageNorm);
if (organizationTokens.length === 0 || messageTokens.length === 0) {
return 0;
}
let matchedTokens = 0;
let score = 0;
for (const token of organizationTokens) {
const variants = organizationTokenVariants(token);
let matched = false;
let variantScore = 0;
for (const variant of variants) {
if (!variant) {
continue;
}
if (messageNorm.includes(variant)) {
matched = true;
variantScore = Math.max(variantScore, variant.length * 5);
continue;
}
const fuzzyMatched = messageTokens.some((messageToken) => {
if (messageToken === variant) {
return true;
}
if (messageToken.length >= 5 && variant.length >= 5) {
return messageToken.startsWith(variant) || variant.startsWith(messageToken);
}
return false;
});
if (fuzzyMatched) {
matched = true;
variantScore = Math.max(variantScore, Math.max(20, variant.length * 3));
}
}
if (matched) {
matchedTokens += 1;
score += variantScore > 0 ? variantScore : 10;
}
}
if (matchedTokens === 0) {
return 0;
}
if (matchedTokens === organizationTokens.length) {
score += 400;
} else {
score += matchedTokens * 50;
}
return score;
}
export function mergeKnownOrganizations(values: unknown[], limit = 50): string[] {
const dedup = new Map<string, string>();
for (const raw of Array.isArray(values) ? values : []) {
const normalized = normalizeOrganizationScopeValue(raw);
if (!normalized) {
continue;
}
const key = normalizeOrganizationScopeSearchText(normalized);
if (!key || dedup.has(key)) {
continue;
}
dedup.set(key, normalized);
}
return Array.from(dedup.values()).slice(0, limit);
}
export function resolveOrganizationSelectionFromMessage(
userMessage: string,
knownOrganizations: unknown[]
): string | null {
const known = mergeKnownOrganizations(Array.isArray(knownOrganizations) ? knownOrganizations : []);
if (!userMessage || known.length === 0) {
return null;
}
const messageNorm = normalizeOrganizationScopeSearchText(userMessage);
if (!messageNorm) {
return null;
}
const scored = known
.map((organization) => ({
organization,
score: scoreOrganizationMentionInMessage(messageNorm, organization)
}))
.filter((item) => item.score > 0)
.sort((a, b) => b.score - a.score || a.organization.length - b.organization.length);
if (scored.length === 0) {
return null;
}
const best = scored[0];
const second = scored[1];
if (best.score < 90) {
return null;
}
if (second && second.score === best.score) {
return null;
}
return best.organization;
}