АРЧ АП11 - Архитектура после регресса: Архитектура: сохранить свежую дату при inventory root restore и очистить data-scope ответы от грязных лейблов

This commit is contained in:
2026-04-18 09:12:39 +03:00
parent a5ea9adf53
commit 0431595542
24 changed files with 1030 additions and 58 deletions
@@ -60,6 +60,19 @@ function hasUnicodeLikelyCounterpartyAfterBy(text) {
]);
return !stopWords.has(token);
}
function hasUnicodeCounterpartyActivityLifecycleSignal(text) {
const normalized = String(text ?? "").toLowerCase();
if (!normalized) {
return false;
}
const hasActivityAgeCue = /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+\u043b\u0435\u0442\s+\u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u0438|\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+\u043b\u0435\u0442\s+\u0432\s+\u0431\u0430\u0437\u0435|\u0432\u043e\u0437\u0440\u0430\u0441\u0442\s+\u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u0438|\u043f\u0435\u0440\u0432(?:\u0430\u044f|\u044b\u0439|\u043e\u0435)\s+(?:\u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u044c|\u043f\u043b\u0430\u0442\u0435\u0436|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0435|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442)|\u043f\u043e\u0441\u043b\u0435\u0434\u043d(?:\u044f\u044f|\u0438\u0439|\u0435\u0435)\s+\u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u044c|\u0441\s+\u043a\u0430\u043a\u043e\u0433\u043e\s+\u0433\u043e\u0434\u0430\s+\u0430\u043a\u0442\u0438\u0432)/iu.test(normalized);
if (!hasActivityAgeCue) {
return false;
}
const hasOneCLexeme = /(?:\u0432\s+\u0431\u0430\u0437\u0435\s+1[\u0441c]|\u0432\s+1[\u0441c]\s+\u0431\u0430\u0437\u0435|\u0438\u0437\s+1[\u0441c])/iu.test(normalized);
const hasBusinessAnchor = /(?:\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043d\u0430\u0448\u0435\u0439\s+\u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438|\u043d\u0430\u0448\u0435\u0439\s+\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u0438|\u043e\u043e\u043e|\u0430\u043e|\u0437\u0430\u043e|\u0438\u043f)/iu.test(normalized);
return hasOneCLexeme || hasBusinessAnchor || hasUnicodeLikelyCounterpartyAfterBy(normalized);
}
function resolveCounterpartyAddressIntent(text, deps) {
if (hasUnicodeOpenItemsAccountSignal(text)) {
return {
@@ -107,6 +120,13 @@ function resolveCounterpartyAddressIntent(text, deps) {
reasons: ["counterparty_item_flow_signal_detected"]
};
}
if (hasUnicodeCounterpartyActivityLifecycleSignal(text)) {
return {
intent: "counterparty_activity_lifecycle",
confidence: "high",
reasons: ["counterparty_activity_lifecycle_signal_detected"]
};
}
if (deps.hasOpenContractsListSignal(text)) {
return {
intent: "open_contracts_confirmed_as_of_date",
@@ -1401,9 +1401,7 @@ function stripOrganizationLegalForm(value) {
.trim();
}
function sameOrganizationEntityReference(left, right) {
const leftNorm = stripOrganizationLegalForm(left);
const rightNorm = stripOrganizationLegalForm(right);
return Boolean(leftNorm && rightNorm && leftNorm === rightNorm);
return (0, assistantOrganizationMatcher_1.organizationsLikelySameEntity)(left, right);
}
function applyPreExecutionOrganizationScopeGrounding(input) {
const activeOrganization = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(input.activeOrganization ?? null);
@@ -287,10 +287,36 @@ function buildInventoryRootFollowupContext(followupContext) {
if (!followupContext || !followupContext.root_intent || !followupContext.root_filters) {
return followupContext;
}
const rootFilters = followupContext.root_filters && typeof followupContext.root_filters === "object"
? { ...followupContext.root_filters }
: {};
const previousFilters = followupContext.previous_filters && typeof followupContext.previous_filters === "object"
? followupContext.previous_filters
: {};
const previousAsOfDate = toNonEmptyString(previousFilters.as_of_date);
const previousPeriodFrom = toNonEmptyString(previousFilters.period_from);
const previousPeriodTo = toNonEmptyString(previousFilters.period_to);
const previousOrganization = toNonEmptyString(previousFilters.organization);
const previousWarehouse = toNonEmptyString(previousFilters.warehouse);
if (previousAsOfDate) {
rootFilters.as_of_date = previousAsOfDate;
}
if (previousPeriodFrom) {
rootFilters.period_from = previousPeriodFrom;
}
if (previousPeriodTo) {
rootFilters.period_to = previousPeriodTo;
}
if (!toNonEmptyString(rootFilters.organization) && previousOrganization) {
rootFilters.organization = previousOrganization;
}
if (!toNonEmptyString(rootFilters.warehouse) && previousWarehouse) {
rootFilters.warehouse = previousWarehouse;
}
return {
...followupContext,
previous_intent: followupContext.root_intent,
previous_filters: { ...followupContext.root_filters },
previous_filters: rootFilters,
previous_anchor_type: followupContext.root_anchor_type ?? followupContext.previous_anchor_type,
previous_anchor_value: followupContext.root_anchor_value ?? followupContext.previous_anchor_value,
current_frame_kind: "inventory_root"
@@ -22,8 +22,9 @@ function createAssistantBoundaryPolicy(deps) {
function buildAssistantDataScopeContractReply(scopeProbe = null) {
const organizations = Array.isArray(scopeProbe?.organizations)
? scopeProbe.organizations
.map((item) => String(item ?? "").trim())
.map((item) => normalizeSelectedOrganization(item, deps.normalizeOrganizationScopeValue))
.filter((item) => item.length > 0)
.filter((item, index, array) => array.indexOf(item) === index)
: [];
if (organizations.length === 1) {
return [
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.normalizeOrganizationScopeValue = normalizeOrganizationScopeValue;
exports.normalizeOrganizationScopeSearchText = normalizeOrganizationScopeSearchText;
exports.scoreOrganizationMentionInMessage = scoreOrganizationMentionInMessage;
exports.organizationsLikelySameEntity = organizationsLikelySameEntity;
exports.mergeKnownOrganizations = mergeKnownOrganizations;
exports.resolveOrganizationSelectionFromMessage = resolveOrganizationSelectionFromMessage;
const ORGANIZATION_SCOPE_STOPWORDS = new Set([
@@ -53,7 +54,9 @@ const ORGANIZATION_SCOPE_STOPWORDS = new Set([
]);
function normalizeScopeLabel(value) {
return String(value ?? "")
.replace(/\\/g, " ")
.replace(/[“”«»]/g, '"')
.replace(/([\p{L}])"(?=[\p{L}])/gu, "$1в")
.replace(/\s+/g, " ")
.trim();
}
@@ -104,6 +107,52 @@ function organizationTokenVariants(token) {
}
return Array.from(variants);
}
function isSingleInsertionOrDeletionAway(left, right) {
const longer = left.length >= right.length ? left : right;
const shorter = left.length >= right.length ? right : left;
if (longer.length - shorter.length !== 1) {
return false;
}
let longIndex = 0;
let shortIndex = 0;
let mismatchUsed = false;
while (longIndex < longer.length && shortIndex < shorter.length) {
if (longer[longIndex] === shorter[shortIndex]) {
longIndex += 1;
shortIndex += 1;
continue;
}
if (mismatchUsed) {
return false;
}
mismatchUsed = true;
longIndex += 1;
}
return true;
}
function organizationTokensLookEquivalent(left, right) {
if (!left || !right) {
return false;
}
if (left === right) {
return true;
}
if (left.length >= 5 && right.length >= 5 && (left.startsWith(right) || right.startsWith(left))) {
return true;
}
const leftCompact = left.replace(/\s+/g, "");
const rightCompact = right.replace(/\s+/g, "");
if (!leftCompact || !rightCompact) {
return false;
}
if (leftCompact === rightCompact) {
return true;
}
if (leftCompact.length >= 6 && rightCompact.length >= 6 && isSingleInsertionOrDeletionAway(leftCompact, rightCompact)) {
return true;
}
return false;
}
function scoreOrganizationMentionInMessage(message, organization) {
const messageNorm = normalizeOrganizationScopeSearchText(message);
const organizationNorm = normalizeOrganizationScopeSearchText(organization);
@@ -163,20 +212,62 @@ function scoreOrganizationMentionInMessage(message, organization) {
}
return score;
}
function organizationsLikelySameEntity(left, right) {
const leftNorm = normalizeOrganizationScopeSearchText(left);
const rightNorm = normalizeOrganizationScopeSearchText(right);
if (!leftNorm || !rightNorm) {
return false;
}
if (leftNorm === rightNorm) {
return true;
}
const leftTokens = tokenizeOrganizationScope(leftNorm);
const rightTokens = tokenizeOrganizationScope(rightNorm);
if (leftTokens.length === 0 || rightTokens.length === 0) {
return false;
}
const leftCompact = leftTokens.join("");
const rightCompact = rightTokens.join("");
if (leftCompact && rightCompact) {
if (leftCompact === rightCompact) {
return true;
}
if (leftCompact.length >= 8 &&
rightCompact.length >= 8 &&
isSingleInsertionOrDeletionAway(leftCompact, rightCompact)) {
return true;
}
}
const leftCovered = leftTokens.every((leftToken) => rightTokens.some((rightToken) => organizationTokensLookEquivalent(leftToken, rightToken)));
if (!leftCovered) {
return false;
}
const rightCovered = rightTokens.every((rightToken) => leftTokens.some((leftToken) => organizationTokensLookEquivalent(leftToken, rightToken)));
return rightCovered;
}
function mergeKnownOrganizations(values, limit = 50) {
const dedup = new Map();
const dedup = [];
for (const raw of Array.isArray(values) ? values : []) {
const normalized = normalizeOrganizationScopeValue(raw);
if (!normalized) {
continue;
}
const key = normalizeOrganizationScopeSearchText(normalized);
if (!key || dedup.has(key)) {
if (!key) {
continue;
}
dedup.set(key, normalized);
const existingIndex = dedup.findIndex((item) => organizationsLikelySameEntity(item, normalized));
if (existingIndex >= 0) {
const existing = dedup[existingIndex];
const existingKey = normalizeOrganizationScopeSearchText(existing);
if (key.length > existingKey.length || normalized.length > existing.length) {
dedup[existingIndex] = normalized;
}
continue;
}
dedup.push(normalized);
}
return Array.from(dedup.values()).slice(0, limit);
return dedup.slice(0, limit);
}
function resolveOrganizationSelectionFromMessage(userMessage, knownOrganizations) {
const known = mergeKnownOrganizations(Array.isArray(knownOrganizations) ? knownOrganizations : []);
+8 -4
View File
@@ -41,6 +41,7 @@ exports.evaluateCoverageForTests = evaluateCoverageForTests;
exports.extractSubjectTokensForTests = extractSubjectTokensForTests;
exports.resolveAssistantOrchestrationDecision = resolveAssistantOrchestrationDecision;
exports.resolveSessionOrganizationScopeContextForTests = resolveSessionOrganizationScopeContextForTests;
exports.buildRootScopedCarryoverFiltersForTests = buildRootScopedCarryoverFiltersForTests;
exports.extractOrganizationFactsFromRowsForTests = extractOrganizationFactsFromRowsForTests;
exports.resolveOrganizationNamesByRefsForTests = resolveOrganizationNamesByRefsForTests;
exports.resolveLivingAssistantModeDecision = resolveLivingAssistantModeDecision;
@@ -2751,13 +2752,13 @@ function hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMes
function buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame) {
const candidateFilters = inventoryRootFrame?.filters && typeof inventoryRootFrame.filters === "object"
? inventoryRootFrame.filters
: previousFilters;
: {};
const nextFilters = {};
const organization = toNonEmptyString(candidateFilters?.organization) ?? toNonEmptyString(previousFilters?.organization);
const warehouse = toNonEmptyString(candidateFilters?.warehouse) ?? toNonEmptyString(previousFilters?.warehouse);
const asOfDate = toNonEmptyString(candidateFilters?.as_of_date) ?? toNonEmptyString(previousFilters?.as_of_date);
const periodFrom = toNonEmptyString(candidateFilters?.period_from) ?? toNonEmptyString(previousFilters?.period_from);
const periodTo = toNonEmptyString(candidateFilters?.period_to) ?? toNonEmptyString(previousFilters?.period_to);
const asOfDate = toNonEmptyString(previousFilters?.as_of_date) ?? toNonEmptyString(candidateFilters?.as_of_date);
const periodFrom = toNonEmptyString(previousFilters?.period_from) ?? toNonEmptyString(candidateFilters?.period_from);
const periodTo = toNonEmptyString(previousFilters?.period_to) ?? toNonEmptyString(candidateFilters?.period_to);
if (organization) {
nextFilters.organization = organization;
}
@@ -4557,6 +4558,9 @@ function mergeFollowupContextWithOrganizationScope(followupContext, organization
function resolveSessionOrganizationScopeContextForTests(userMessage, items, addressNavigationState = null) {
return resolveSessionOrganizationScopeContext(userMessage, items, addressNavigationState);
}
function buildRootScopedCarryoverFiltersForTests(previousFilters, inventoryRootFrame) {
return buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame);
}
function normalizeGuidValue(value) {
const source = normalizeScopeLabel(value);
if (!source) {
+66 -13
View File
@@ -16,14 +16,14 @@ const FALLBACK_REGISTRY = {
{
group_code: "vat",
group_title: "НДС",
description: "Срезы и расчеты НДС на базе данных 1С.",
description: "Срезы и расчёты НДС на базе данных 1С",
risk_level: "high",
maturity_status: "partial",
supported_operations: ["vat_period_snapshot", "vat_payable_forecast"],
unsupported_operations: ["submit_tax_declaration"],
required_entities: ["period", "organization"],
optional_entities: ["counterparty"],
typical_queries: ["Сколько НДС к уплате за период?"],
typical_queries: ["Сколько НДС к уплате за период?", "Покажи срез НДС на дату"],
related_routes: [],
safe_alternatives: ["Показать движения по 68/19 за период"],
one_c_hints: ["РегистрБухгалтерии.Хозрасчетный"]
@@ -31,22 +31,71 @@ const FALLBACK_REGISTRY = {
{
group_code: "counterparties",
group_title: "Контрагенты",
description: "Документы, операции, договоры и срезы по контрагентам.",
description: "Документы, операции, договоры и активность по контрагентам",
risk_level: "medium",
maturity_status: "production_ready",
supported_operations: ["list_documents_by_counterparty", "list_contracts_by_counterparty"],
unsupported_operations: ["edit_counterparty_card"],
required_entities: ["counterparty_scope_or_contract"],
optional_entities: ["period", "organization"],
typical_queries: ["Покажи документы по контрагенту"],
typical_queries: ["Покажи документы по контрагенту", "Какие операции были по банку с контрагентом?"],
related_routes: [],
safe_alternatives: ["Уточнить ИНН/наименование контрагента"],
safe_alternatives: ["Уточнить ИНН или наименование контрагента"],
one_c_hints: ["Справочник.Контрагенты"]
},
{
group_code: "settlements",
group_title: "Долги и расчёты",
description: "Сальдо, хвосты, незакрытые авансы и аналитика по расчётам",
risk_level: "high",
maturity_status: "production_ready",
supported_operations: ["receivables_confirmed_as_of_date", "open_items_by_counterparty_or_contract"],
unsupported_operations: ["close_period"],
required_entities: ["period_or_date"],
optional_entities: ["organization", "account", "counterparty"],
typical_queries: ["Кто нам должен на дату?", "Хвосты покажи по счёту 60 за период"],
related_routes: [],
safe_alternatives: ["Уточнить период, счёт или организацию"],
one_c_hints: ["РегистрБухгалтерии.Хозрасчетный"]
},
{
group_code: "cash",
group_title: "Деньги",
description: "Остатки и движение по денежным счетам и кассе",
risk_level: "medium",
maturity_status: "production_ready",
supported_operations: ["account_balance_snapshot", "bank_operations_by_counterparty"],
unsupported_operations: ["post_bank_statement"],
required_entities: ["date_or_period"],
optional_entities: ["organization", "account", "counterparty"],
typical_queries: ["Какой остаток по счёту 51 на дату?", "Покажи движение денег за месяц"],
related_routes: [],
safe_alternatives: ["Уточнить счёт или период"],
one_c_hints: ["РегистрБухгалтерии.Хозрасчетный"]
},
{
group_code: "inventory",
group_title: "Склад и товары",
description: "Подтверждённые остатки, происхождение и документы по товарным позициям",
risk_level: "medium",
maturity_status: "production_ready",
supported_operations: [
"inventory_on_hand_as_of_date",
"inventory_purchase_provenance_for_item",
"inventory_purchase_documents_for_item"
],
unsupported_operations: ["write_off_inventory"],
required_entities: ["date_or_period"],
optional_entities: ["organization", "warehouse", "item"],
typical_queries: ["Какие товары сейчас лежат на складе?", "Кто поставил эту позицию?"],
related_routes: [],
safe_alternatives: ["Уточнить организацию, дату или позицию"],
one_c_hints: ["РегистрБухгалтерии.Хозрасчетный"]
},
{
group_code: "boundaries",
group_title: "Ограничения",
description: "Операции, которые ассистент не выполняет.",
description: "Операции, которые ассистент не выполняет в этом рантайме",
risk_level: "high",
maturity_status: "production_ready",
supported_operations: ["explain_boundary", "suggest_safe_next_step"],
@@ -55,7 +104,7 @@ const FALLBACK_REGISTRY = {
optional_entities: [],
typical_queries: ["Можешь настроить 1С?"],
related_routes: [],
safe_alternatives: ["Сформировать план диагностики для 1С/ИТ-админа"],
safe_alternatives: ["Сформировать безопасный план диагностики для 1С или ИТ-админа"],
one_c_hints: []
}
]
@@ -150,16 +199,20 @@ function loadCapabilitiesRegistry() {
}
function buildCapabilityContractReplyFromRegistry() {
const registry = loadCapabilitiesRegistry();
const topGroups = registry.groups.slice(0, 6);
const topGroups = registry.groups.filter((group) => group.group_code !== "boundaries").slice(0, 6);
const groupLines = topGroups.map((group, index) => {
const ops = group.supported_operations.slice(0, 3).join(", ");
return `${index + 1}. ${group.group_title}: ${group.description}${ops ? ` (например: ${ops})` : ""}.`;
const examples = group.typical_queries
.slice(0, 2)
.map((query) => query.trim())
.filter((query) => query.length > 0)
.join("; ");
return `${index + 1}. ${group.group_title}: ${group.description}${examples ? `. Например: ${examples}` : "."}`;
});
return [
"Я ассистент по анализу данных 1С в режиме чтения.",
"Что умею по группам:",
"Могу помочь с вопросами по данным 1С в режиме чтения: НДС, контрагенты, долги, деньги и склад.",
"По основным группам:",
...groupLines,
"Если хотите, раскрою любую группу точечно и дам готовую формулировку запроса.",
"Если нужно, подскажу, как лучше сформулировать запрос под вашу задачу.",
"Что не делаю: не настраиваю 1С, не меняю конфигурацию, не создаю и не провожу документы, не выполняю админ-действия на сервере."
].join("\n");
}