АРЧ АП11 - Архитектура: вынести data scope и organization history из assistantService в отдельный owner
This commit is contained in:
parent
cd0b78d1de
commit
4b83e2b2de
|
|
@ -14,7 +14,7 @@ The goal is to turn it from a god-service into a thinner coordinator.
|
||||||
|
|
||||||
Approximate size:
|
Approximate size:
|
||||||
|
|
||||||
- `5198` lines
|
- `5178` lines
|
||||||
|
|
||||||
It currently mixes concerns from:
|
It currently mixes concerns from:
|
||||||
|
|
||||||
|
|
@ -114,13 +114,13 @@ Current owner:
|
||||||
|
|
||||||
Current references:
|
Current references:
|
||||||
|
|
||||||
- [assistantService.ts:3974](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:3974)
|
- [assistantService.ts:4284](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:4284)
|
||||||
- [assistantService.ts:4412](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:4412)
|
- [assistantService.ts:4977](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:4977)
|
||||||
- [assistantService.ts:6052](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:6052)
|
|
||||||
|
|
||||||
Target owner:
|
Target owner:
|
||||||
|
|
||||||
- `assistantBoundaryPolicy`
|
- `assistantBoundaryPolicy`
|
||||||
|
- `assistantDataScopePolicy`
|
||||||
|
|
||||||
Expected artifact:
|
Expected artifact:
|
||||||
|
|
||||||
|
|
@ -201,17 +201,19 @@ This order is chosen because route and transition pressure are currently the mai
|
||||||
|
|
||||||
This extraction is materially underway and no longer just a proposal.
|
This extraction is materially underway and no longer just a proposal.
|
||||||
|
|
||||||
Current active owner creation and wiring in [assistantService.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:4283):
|
Current active owner creation and wiring in [assistantService.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:4188):
|
||||||
|
|
||||||
- provider owner near `4283`
|
- provider owner near `4188`
|
||||||
- meta and memory owners near `4296-4301`
|
- meta and memory owners near `4201-4206`
|
||||||
- route owner near `4306`
|
- route owner near `4211`
|
||||||
- transition owner near `4343`
|
- transition owner near `4250`
|
||||||
- boundary owner near `4997`
|
- data-scope owner near `4284`
|
||||||
|
- boundary owner near `4977`
|
||||||
|
|
||||||
What is already true:
|
What is already true:
|
||||||
|
|
||||||
- route, transition, boundary, meta, memory, and provider policies have explicit external owners;
|
- route, transition, boundary, meta, memory, and provider policies have explicit external owners;
|
||||||
|
- data-scope probing and organization-history extraction now also have an explicit owner;
|
||||||
- runtime already delegates important decisions to those owners.
|
- runtime already delegates important decisions to those owners.
|
||||||
|
|
||||||
What is still not fully true:
|
What is still not fully true:
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ This is enough to build targeted semantic packs that are not single-domain toy s
|
||||||
|
|
||||||
## Honest Phase Status
|
## Honest Phase Status
|
||||||
|
|
||||||
Estimated overall turnaround completion: `~85%`
|
Estimated overall turnaround completion: `~86%`
|
||||||
|
|
||||||
### Phase 0. Shared Baseline
|
### Phase 0. Shared Baseline
|
||||||
|
|
||||||
|
|
@ -191,15 +191,16 @@ Remaining debt:
|
||||||
|
|
||||||
### Phase 5. AssistantService Extraction
|
### Phase 5. AssistantService Extraction
|
||||||
|
|
||||||
Status: `76%`
|
Status: `79%`
|
||||||
|
|
||||||
Reason:
|
Reason:
|
||||||
|
|
||||||
- major policy categories have real owners outside the coordinator.
|
- major policy categories have real owners outside the coordinator.
|
||||||
|
- data-scope probing and organization-history extraction are now delegated to a dedicated owner.
|
||||||
|
|
||||||
Remaining debt:
|
Remaining debt:
|
||||||
|
|
||||||
- `assistantService.ts` is still about `5198` lines;
|
- `assistantService.ts` is still about `5178` lines;
|
||||||
- runtime uses extracted owners, but legacy bodies and fallback branches still live in the coordinator file;
|
- runtime uses extracted owners, but legacy bodies and fallback branches still live in the coordinator file;
|
||||||
- code review still sometimes requires reading `assistantService` together with extracted owners.
|
- code review still sometimes requires reading `assistantService` together with extracted owners.
|
||||||
|
|
||||||
|
|
@ -240,6 +241,7 @@ Compared with the pre-turnaround baseline, the system is now materially better i
|
||||||
- temporal honesty is now evaluated as an explicit invariant;
|
- temporal honesty is now evaluated as an explicit invariant;
|
||||||
- factual-negative answers can remain truthful instead of collapsing into generic technical refusals;
|
- factual-negative answers can remain truthful instead of collapsing into generic technical refusals;
|
||||||
- meta questions and memory recap are no longer purely incidental side effects of route logic;
|
- meta questions and memory recap are no longer purely incidental side effects of route logic;
|
||||||
|
- organization data-scope probing is no longer owned only by coordinator-local helper bodies;
|
||||||
- architecture regressions can now be localized to route, transition, truth gate, coverage/evidence, boundary, or meta/memory layers.
|
- architecture regressions can now be localized to route, transition, truth gate, coverage/evidence, boundary, or meta/memory layers.
|
||||||
|
|
||||||
## What Still Remains The Main Architectural Debt
|
## What Still Remains The Main Architectural Debt
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,436 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.createAssistantDataScopePolicy = createAssistantDataScopePolicy;
|
||||||
|
// @ts-nocheck
|
||||||
|
const assistantOrganizationMatcher_1 = require("./assistantOrganizationMatcher");
|
||||||
|
const DATA_SCOPE_CACHE_TTL_MS = 60_000;
|
||||||
|
function normalizeScopeLabel(value) {
|
||||||
|
return String(value ?? "")
|
||||||
|
.replace(/[“”«»]/g, '"')
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
function normalizeScopeKey(value) {
|
||||||
|
return normalizeScopeLabel(value).toLowerCase().replace(/ё/g, "е");
|
||||||
|
}
|
||||||
|
function normalizeGuidValue(value) {
|
||||||
|
const source = normalizeScopeLabel(value);
|
||||||
|
if (!source) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const match = source.match(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i);
|
||||||
|
return match ? String(match[0]).toLowerCase() : null;
|
||||||
|
}
|
||||||
|
function extractGuidValuesFromText(value) {
|
||||||
|
const source = normalizeScopeLabel(value);
|
||||||
|
if (!source) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const matches = source.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi);
|
||||||
|
if (!matches || matches.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return Array.from(new Set(matches.map((item) => String(item).toLowerCase())));
|
||||||
|
}
|
||||||
|
function hasOrganizationKeyHint(key) {
|
||||||
|
return /(?:организац|organization|company|контор|org)\b/i.test(String(key ?? ""));
|
||||||
|
}
|
||||||
|
function hasNameKeyHint(key) {
|
||||||
|
return /(?:представ|наимен|name|title|display|presentation|description)\b/i.test(String(key ?? ""));
|
||||||
|
}
|
||||||
|
function hasGuidKeyHint(key) {
|
||||||
|
return /(?:идентифик|guid|uuid|key|ref|ссылк|\bid\b)\b/i.test(String(key ?? ""));
|
||||||
|
}
|
||||||
|
function looksLikeOrganizationTypeMarker(value) {
|
||||||
|
const normalized = normalizeScopeKey(value);
|
||||||
|
return /(?:справочникссылка\.\s*организац|catalogref\.\s*organization|organization|company|организац)/i.test(normalized);
|
||||||
|
}
|
||||||
|
function isPlausibleOrganizationName(value) {
|
||||||
|
const candidate = normalizeScopeLabel(value);
|
||||||
|
if (!candidate) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/^(?:период|регистратор|счетдт|счеткт|amount|period|registrator|accountdt|accountkt)$/i.test(candidate)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/^[0-9._:/\\-]+$/i.test(candidate)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i.test(candidate)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/(?:справочникссылка|документссылка|плансчетовссылка|standardodata|recordtype|cmp:)/i.test(candidate)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /[A-Za-z\u0400-\u04FF]/u.test(candidate);
|
||||||
|
}
|
||||||
|
function appendOrganizationFactsFromValue(value, hints, bucket, depth = 0) {
|
||||||
|
if (depth > 4 || value === null || value === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof value === "string") {
|
||||||
|
for (const guid of extractGuidValuesFromText(value)) {
|
||||||
|
if (hints.guidHint || hints.organizationHint || hints.nameHint) {
|
||||||
|
bucket.refs.push(guid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((hints.organizationHint || hints.nameHint) && isPlausibleOrganizationName(value)) {
|
||||||
|
const normalized = normalizeScopeLabel(value);
|
||||||
|
if (normalized) {
|
||||||
|
bucket.names.push(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const item of value) {
|
||||||
|
appendOrganizationFactsFromValue(item, hints, bucket, depth + 1);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof value !== "object") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const entries = Object.entries(value);
|
||||||
|
let objectIsOrganization = false;
|
||||||
|
let hasObjectRefMarker = false;
|
||||||
|
let hasGuidLikeField = false;
|
||||||
|
let hasTypeMarker = false;
|
||||||
|
for (const [rawKey, rawVal] of entries) {
|
||||||
|
const key = normalizeScopeKey(rawKey);
|
||||||
|
if ((key.includes("objectref") || key.includes("_objectref")) && rawVal === true) {
|
||||||
|
hasObjectRefMarker = true;
|
||||||
|
}
|
||||||
|
if (typeof rawVal === "string" && normalizeGuidValue(rawVal)) {
|
||||||
|
hasGuidLikeField = true;
|
||||||
|
}
|
||||||
|
if (hasOrganizationKeyHint(key)) {
|
||||||
|
objectIsOrganization = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ((key.includes("типобъекта") || key.includes("type")) && typeof rawVal === "string" && looksLikeOrganizationTypeMarker(rawVal)) {
|
||||||
|
objectIsOrganization = true;
|
||||||
|
hasTypeMarker = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!objectIsOrganization && hasObjectRefMarker && hasGuidLikeField) {
|
||||||
|
const hasNameLikeValue = entries.some(([rawKey, rawVal]) => {
|
||||||
|
if (typeof rawVal !== "string") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const key = normalizeScopeKey(rawKey);
|
||||||
|
return hasNameKeyHint(key) || isPlausibleOrganizationName(rawVal);
|
||||||
|
});
|
||||||
|
if (hasTypeMarker || hasNameLikeValue) {
|
||||||
|
objectIsOrganization = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [rawKey, rawVal] of entries) {
|
||||||
|
if (String(rawKey ?? "").startsWith("__")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const key = normalizeScopeKey(rawKey);
|
||||||
|
const childHints = {
|
||||||
|
organizationHint: hints.organizationHint || objectIsOrganization || hasOrganizationKeyHint(key),
|
||||||
|
nameHint: hints.nameHint || objectIsOrganization || hasNameKeyHint(key),
|
||||||
|
guidHint: hints.guidHint || objectIsOrganization || hasGuidKeyHint(key)
|
||||||
|
};
|
||||||
|
if (typeof rawVal === "string") {
|
||||||
|
const guid = normalizeGuidValue(rawVal);
|
||||||
|
if (guid && childHints.guidHint) {
|
||||||
|
bucket.refs.push(guid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appendOrganizationFactsFromValue(rawVal, childHints, bucket, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function buildResolvedDataScopeProbe(status, activeMcpChannel, organizations) {
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
channel: activeMcpChannel,
|
||||||
|
organizations: Array.from(new Set(Array.isArray(organizations) ? organizations : [])).slice(0, 20),
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function createAssistantDataScopePolicy(deps) {
|
||||||
|
const dataScopeProbeCache = new Map();
|
||||||
|
function parseOrganizationsFromDataScopeAssistantText(text) {
|
||||||
|
const source = deps.repairAddressMojibake(String(text ?? ""));
|
||||||
|
if (!source) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const extracted = [];
|
||||||
|
const singleMatch = source.match(/доступна\s+организация:\s*([^.\n]+)/iu);
|
||||||
|
if (singleMatch) {
|
||||||
|
const value = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(singleMatch[1]);
|
||||||
|
if (value) {
|
||||||
|
extracted.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const multiMatch = source.match(/доступны\s+организац(?:ии|ия)\s*(?:\(\d+\))?:\s*([^.\n]+)/iu);
|
||||||
|
if (multiMatch) {
|
||||||
|
const parts = String(multiMatch[1] ?? "")
|
||||||
|
.split(",")
|
||||||
|
.map((item) => (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(item))
|
||||||
|
.filter((item) => Boolean(item));
|
||||||
|
extracted.push(...parts);
|
||||||
|
}
|
||||||
|
return Array.from(new Set(extracted));
|
||||||
|
}
|
||||||
|
function extractKnownOrganizationsFromHistory(items) {
|
||||||
|
const collected = [];
|
||||||
|
for (let index = (Array.isArray(items) ? items.length : 0) - 1; index >= 0; index -= 1) {
|
||||||
|
const item = Array.isArray(items) ? items[index] : null;
|
||||||
|
if (!item || typeof item !== "object" || item.role !== "assistant") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const debug = item.debug;
|
||||||
|
if (debug && typeof debug === "object") {
|
||||||
|
const directFromProbe = Array.isArray(debug.living_chat_data_scope_probe_organizations)
|
||||||
|
? debug.living_chat_data_scope_probe_organizations
|
||||||
|
: [];
|
||||||
|
const knownFromDebug = Array.isArray(debug.assistant_known_organizations)
|
||||||
|
? debug.assistant_known_organizations
|
||||||
|
: [];
|
||||||
|
const directFromCandidates = Array.isArray(debug.organization_candidates) ? debug.organization_candidates : [];
|
||||||
|
const directFromResolved = [
|
||||||
|
(0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(debug.assistant_active_organization),
|
||||||
|
(0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(debug.living_chat_selected_organization),
|
||||||
|
(0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(debug.extracted_filters?.organization),
|
||||||
|
(0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(debug.address_root_frame_context?.organization)
|
||||||
|
].filter((value) => Boolean(value));
|
||||||
|
if (directFromProbe.length > 0 ||
|
||||||
|
knownFromDebug.length > 0 ||
|
||||||
|
directFromCandidates.length > 0 ||
|
||||||
|
directFromResolved.length > 0) {
|
||||||
|
collected.push(...directFromProbe, ...knownFromDebug, ...directFromCandidates, ...directFromResolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const parsedFromText = parseOrganizationsFromDataScopeAssistantText(item.text);
|
||||||
|
if (parsedFromText.length > 0) {
|
||||||
|
collected.push(...parsedFromText);
|
||||||
|
}
|
||||||
|
if (collected.length >= 20) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)(collected, 20);
|
||||||
|
}
|
||||||
|
function findLastAssistantActiveOrganization(items) {
|
||||||
|
for (let index = (Array.isArray(items) ? items.length : 0) - 1; index >= 0; index -= 1) {
|
||||||
|
const item = Array.isArray(items) ? items[index] : null;
|
||||||
|
if (!item || typeof item !== "object" || item.role !== "assistant") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const debug = item.debug;
|
||||||
|
if (!debug || typeof debug !== "object") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const direct = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(debug.assistant_active_organization);
|
||||||
|
if (direct) {
|
||||||
|
return direct;
|
||||||
|
}
|
||||||
|
const selected = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(debug.living_chat_selected_organization);
|
||||||
|
if (selected) {
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function extractOrganizationFactsFromRows(rows) {
|
||||||
|
const names = [];
|
||||||
|
const refs = [];
|
||||||
|
const pairs = [];
|
||||||
|
for (const row of Array.isArray(rows) ? rows : []) {
|
||||||
|
if (!row || typeof row !== "object") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const rowNames = [];
|
||||||
|
const rowRefs = [];
|
||||||
|
for (const [rawKey, rawValue] of Object.entries(row)) {
|
||||||
|
if (String(rawKey ?? "").startsWith("__")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const key = normalizeScopeKey(rawKey);
|
||||||
|
appendOrganizationFactsFromValue(rawValue, {
|
||||||
|
organizationHint: hasOrganizationKeyHint(key),
|
||||||
|
nameHint: hasNameKeyHint(key),
|
||||||
|
guidHint: hasGuidKeyHint(key)
|
||||||
|
}, { names: rowNames, refs: rowRefs });
|
||||||
|
}
|
||||||
|
const dedupRowNames = Array.from(new Set(rowNames))
|
||||||
|
.filter((item) => isPlausibleOrganizationName(item))
|
||||||
|
.slice(0, 20);
|
||||||
|
const dedupRowRefs = Array.from(new Set(rowRefs))
|
||||||
|
.map((item) => String(item ?? "").toLowerCase())
|
||||||
|
.filter((item) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(item))
|
||||||
|
.slice(0, 20);
|
||||||
|
if (dedupRowNames.length === 0 && dedupRowRefs.length === 0) {
|
||||||
|
const fallbackBucket = { names: [], refs: [] };
|
||||||
|
appendOrganizationFactsFromValue(row, {
|
||||||
|
organizationHint: true,
|
||||||
|
nameHint: true,
|
||||||
|
guidHint: true
|
||||||
|
}, fallbackBucket);
|
||||||
|
for (const value of fallbackBucket.names) {
|
||||||
|
if (isPlausibleOrganizationName(value)) {
|
||||||
|
dedupRowNames.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const value of fallbackBucket.refs) {
|
||||||
|
const normalized = String(value ?? "").toLowerCase();
|
||||||
|
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(normalized)) {
|
||||||
|
dedupRowRefs.push(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
names.push(...dedupRowNames);
|
||||||
|
refs.push(...dedupRowRefs);
|
||||||
|
if (dedupRowRefs.length > 0 && dedupRowNames.length > 0) {
|
||||||
|
for (const ref of dedupRowRefs) {
|
||||||
|
for (const name of dedupRowNames) {
|
||||||
|
pairs.push({ ref, name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
names: Array.from(new Set(names)).slice(0, 20),
|
||||||
|
refs: Array.from(new Set(refs)).slice(0, 20),
|
||||||
|
pairs: Array.from(new Set(pairs.map((item) => `${item.ref}||${item.name}`)))
|
||||||
|
.map((token) => {
|
||||||
|
const [ref, name] = token.split("||");
|
||||||
|
return { ref, name };
|
||||||
|
})
|
||||||
|
.slice(0, 100)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function resolveOrganizationNamesByRefs(refs, facts) {
|
||||||
|
const refSet = new Set((Array.isArray(refs) ? refs : [])
|
||||||
|
.map((item) => String(item ?? "").toLowerCase())
|
||||||
|
.filter((item) => item.length > 0));
|
||||||
|
if (refSet.size === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const names = [];
|
||||||
|
for (const pair of Array.isArray(facts?.pairs) ? facts?.pairs : []) {
|
||||||
|
const ref = String(pair?.ref ?? "").toLowerCase();
|
||||||
|
const name = normalizeScopeLabel(pair?.name ?? "");
|
||||||
|
if (!ref || !name || !refSet.has(ref)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
names.push(name);
|
||||||
|
}
|
||||||
|
return Array.from(new Set(names)).slice(0, 20);
|
||||||
|
}
|
||||||
|
async function resolveAssistantDataScopeProbe() {
|
||||||
|
const cacheKey = `${deps.mcpProxyUrl}|${deps.activeMcpChannel}`;
|
||||||
|
const now = Date.now();
|
||||||
|
const cached = dataScopeProbeCache.get(cacheKey);
|
||||||
|
if (cached && Number(cached.expiresAt ?? 0) > now) {
|
||||||
|
return cached.value;
|
||||||
|
}
|
||||||
|
if (String(process.env.NODE_ENV ?? "").toLowerCase() === "test") {
|
||||||
|
return {
|
||||||
|
status: "skipped_test_env",
|
||||||
|
channel: deps.activeMcpChannel,
|
||||||
|
organizations: [],
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const catalogQueryCandidates = [
|
||||||
|
"ВЫБРАТЬ ПЕРВЫЕ 20 Организации.Наименование КАК Организация ИЗ Справочник.Организации КАК Организации",
|
||||||
|
"ВЫБРАТЬ ПЕРВЫЕ 20 Организации.НаименованиеПолное КАК Организация ИЗ Справочник.Организации КАК Организации",
|
||||||
|
"ВЫБРАТЬ ПЕРВЫЕ 100 Организации.Ссылка КАК Организация, ПРЕДСТАВЛЕНИЕ(Организации.Ссылка) КАК ОрганизацияПредставление ИЗ Справочник.Организации КАК Организации"
|
||||||
|
];
|
||||||
|
const movementProbeCandidates = [
|
||||||
|
"ВЫБРАТЬ ПЕРВЫЕ 60 Движения.Организация КАК Организация ИЗ РегистрБухгалтерии.Хозрасчетный КАК Движения УПОРЯДОЧИТЬ ПО Движения.Период УБЫВ",
|
||||||
|
"ВЫБРАТЬ ПЕРВЫЕ 60 Движения.Организация КАК Организация ИЗ РегистрБухгалтерии.Хозрасчетный КАК Движения"
|
||||||
|
];
|
||||||
|
let lastError = null;
|
||||||
|
const catalogFacts = { names: [], refs: [], pairs: [] };
|
||||||
|
for (const queryText of catalogQueryCandidates) {
|
||||||
|
const probe = await deps.executeAddressMcpQuery({
|
||||||
|
query: queryText,
|
||||||
|
limit: 100
|
||||||
|
});
|
||||||
|
if (probe.error) {
|
||||||
|
lastError = String(probe.error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const facts = extractOrganizationFactsFromRows(Array.isArray(probe.rows) ? probe.rows : []);
|
||||||
|
catalogFacts.names.push(...facts.names);
|
||||||
|
catalogFacts.refs.push(...facts.refs);
|
||||||
|
catalogFacts.pairs.push(...facts.pairs);
|
||||||
|
if (facts.names.length > 0) {
|
||||||
|
const resolved = buildResolvedDataScopeProbe("resolved", deps.activeMcpChannel, facts.names);
|
||||||
|
dataScopeProbeCache.set(cacheKey, {
|
||||||
|
expiresAt: now + DATA_SCOPE_CACHE_TTL_MS,
|
||||||
|
value: resolved
|
||||||
|
});
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const movementFacts = { names: [], refs: [], pairs: [] };
|
||||||
|
for (const queryText of movementProbeCandidates) {
|
||||||
|
const probe = await deps.executeAddressMcpQuery({
|
||||||
|
query: queryText,
|
||||||
|
limit: 60
|
||||||
|
});
|
||||||
|
if (probe.error) {
|
||||||
|
lastError = String(probe.error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const facts = extractOrganizationFactsFromRows(Array.isArray(probe.rows) ? probe.rows : []);
|
||||||
|
movementFacts.names.push(...facts.names);
|
||||||
|
movementFacts.refs.push(...facts.refs);
|
||||||
|
movementFacts.pairs.push(...facts.pairs);
|
||||||
|
if (facts.names.length > 0) {
|
||||||
|
const resolved = buildResolvedDataScopeProbe("resolved_from_activity", deps.activeMcpChannel, facts.names);
|
||||||
|
dataScopeProbeCache.set(cacheKey, {
|
||||||
|
expiresAt: now + DATA_SCOPE_CACHE_TTL_MS,
|
||||||
|
value: resolved
|
||||||
|
});
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const movementRefs = Array.from(new Set(movementFacts.refs))
|
||||||
|
.map((item) => String(item ?? "").toLowerCase())
|
||||||
|
.filter((item) => item.length > 0);
|
||||||
|
if (movementRefs.length > 0) {
|
||||||
|
const namesFromCatalogPairs = resolveOrganizationNamesByRefs(movementRefs, {
|
||||||
|
names: Array.from(new Set(catalogFacts.names)),
|
||||||
|
refs: Array.from(new Set(catalogFacts.refs)),
|
||||||
|
pairs: catalogFacts.pairs
|
||||||
|
});
|
||||||
|
if (namesFromCatalogPairs.length > 0) {
|
||||||
|
const resolved = buildResolvedDataScopeProbe("resolved_from_ref_lookup", deps.activeMcpChannel, namesFromCatalogPairs);
|
||||||
|
dataScopeProbeCache.set(cacheKey, {
|
||||||
|
expiresAt: now + DATA_SCOPE_CACHE_TTL_MS,
|
||||||
|
value: resolved
|
||||||
|
});
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const fallback = {
|
||||||
|
status: lastError ? "unresolved_with_error" : "unresolved",
|
||||||
|
channel: deps.activeMcpChannel,
|
||||||
|
organizations: [],
|
||||||
|
error: lastError
|
||||||
|
};
|
||||||
|
dataScopeProbeCache.set(cacheKey, {
|
||||||
|
expiresAt: now + DATA_SCOPE_CACHE_TTL_MS,
|
||||||
|
value: fallback
|
||||||
|
});
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
parseOrganizationsFromDataScopeAssistantText,
|
||||||
|
extractKnownOrganizationsFromHistory,
|
||||||
|
findLastAssistantActiveOrganization,
|
||||||
|
extractOrganizationFactsFromRows,
|
||||||
|
resolveOrganizationNamesByRefs,
|
||||||
|
resolveAssistantDataScopeProbe
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -65,6 +65,7 @@ const capabilitiesRegistry_1 = __importStar(require("./capabilitiesRegistry"));
|
||||||
const assistantCanon_1 = __importStar(require("./assistantCanon"));
|
const assistantCanon_1 = __importStar(require("./assistantCanon"));
|
||||||
const assistantAddressAttemptRuntimeAdapter_1 = __importStar(require("./assistantAddressAttemptRuntimeAdapter"));
|
const assistantAddressAttemptRuntimeAdapter_1 = __importStar(require("./assistantAddressAttemptRuntimeAdapter"));
|
||||||
const assistantCoverageGrounding_1 = __importStar(require("./assistantCoverageGrounding"));
|
const assistantCoverageGrounding_1 = __importStar(require("./assistantCoverageGrounding"));
|
||||||
|
const assistantDataScopePolicy_1 = __importStar(require("./assistantDataScopePolicy"));
|
||||||
const assistantDeepTurnAttemptRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnAttemptRuntimeAdapter"));
|
const assistantDeepTurnAttemptRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnAttemptRuntimeAdapter"));
|
||||||
const assistantBoundaryPolicy_1 = __importStar(require("./assistantBoundaryPolicy"));
|
const assistantBoundaryPolicy_1 = __importStar(require("./assistantBoundaryPolicy"));
|
||||||
const assistantLivingModePolicy_1 = __importStar(require("./assistantLivingModePolicy"));
|
const assistantLivingModePolicy_1 = __importStar(require("./assistantLivingModePolicy"));
|
||||||
|
|
@ -74,14 +75,14 @@ const assistantProviderExecutionPolicy_1 = __importStar(require("./assistantProv
|
||||||
const assistantRoutePolicy_1 = __importStar(require("./assistantRoutePolicy"));
|
const assistantRoutePolicy_1 = __importStar(require("./assistantRoutePolicy"));
|
||||||
const assistantTransitionPolicy_1 = __importStar(require("./assistantTransitionPolicy"));
|
const assistantTransitionPolicy_1 = __importStar(require("./assistantTransitionPolicy"));
|
||||||
const assistantOrganizationScopeRuntimeAdapter_1 = __importStar(require("./assistantOrganizationScopeRuntimeAdapter"));
|
const assistantOrganizationScopeRuntimeAdapter_1 = __importStar(require("./assistantOrganizationScopeRuntimeAdapter"));
|
||||||
|
const assistantOrganizationMatcher_1 = __importStar(require("./assistantOrganizationMatcher"));
|
||||||
const assistantTurnAttemptRuntimeAdapter_1 = __importStar(require("./assistantTurnAttemptRuntimeAdapter"));
|
const assistantTurnAttemptRuntimeAdapter_1 = __importStar(require("./assistantTurnAttemptRuntimeAdapter"));
|
||||||
const assistantTurnRuntimeDepsAdapter_1 = __importStar(require("./assistantTurnRuntimeDepsAdapter"));
|
const assistantTurnRuntimeDepsAdapter_1 = __importStar(require("./assistantTurnRuntimeDepsAdapter"));
|
||||||
const assistantTurnRuntimeInputBuilder_1 = __importStar(require("./assistantTurnRuntimeInputBuilder"));
|
const assistantTurnRuntimeInputBuilder_1 = __importStar(require("./assistantTurnRuntimeInputBuilder"));
|
||||||
const assistantUserTurnBootstrapRuntimeAdapter_1 = __importStar(require("./assistantUserTurnBootstrapRuntimeAdapter"));
|
const assistantUserTurnBootstrapRuntimeAdapter_1 = __importStar(require("./assistantUserTurnBootstrapRuntimeAdapter"));
|
||||||
const assistantQueryPlanning_1 = __importStar(require("./assistantQueryPlanning"));
|
const assistantQueryPlanning_1 = __importStar(require("./assistantQueryPlanning"));
|
||||||
const iconv_lite_1 = __importDefault(require("iconv-lite"));
|
const iconv_lite_1 = __importDefault(require("iconv-lite"));
|
||||||
const DATA_SCOPE_CACHE_TTL_MS = 60_000;
|
const normalizeOrganizationScopeValue = assistantOrganizationMatcher_1.normalizeOrganizationScopeValue;
|
||||||
const dataScopeProbeCache = new Map();
|
|
||||||
function retrievalSummaryForRoute(route) {
|
function retrievalSummaryForRoute(route) {
|
||||||
if (route === "store_canonical")
|
if (route === "store_canonical")
|
||||||
return "Canonical accounting data path selected.";
|
return "Canonical accounting data path selected.";
|
||||||
|
|
@ -4225,105 +4226,6 @@ function hasLivingChatSignal(text) {
|
||||||
function buildAssistantCapabilityContractReply() {
|
function buildAssistantCapabilityContractReply() {
|
||||||
return (0, capabilitiesRegistry_1.buildCapabilityContractReplyFromRegistry)();
|
return (0, capabilitiesRegistry_1.buildCapabilityContractReplyFromRegistry)();
|
||||||
}
|
}
|
||||||
function normalizeScopeLabel(value) {
|
|
||||||
const repaired = repairAddressMojibake(String(value ?? ""));
|
|
||||||
let normalized = compactWhitespace(repaired.trim());
|
|
||||||
for (let index = 0; index < 2; index += 1) {
|
|
||||||
const first = normalized[0];
|
|
||||||
const last = normalized[normalized.length - 1];
|
|
||||||
const wrappedInQuotes = (first === "\"" && last === "\"") ||
|
|
||||||
(first === "'" && last === "'") ||
|
|
||||||
(first === "«" && last === "»");
|
|
||||||
if (!wrappedInQuotes) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
normalized = compactWhitespace(normalized.slice(1, -1).trim());
|
|
||||||
}
|
|
||||||
if (!normalized) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (/^(?:null|undefined|nan|0|не\s*заполнено)$/i.test(normalized)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
function normalizeScopeKey(value) {
|
|
||||||
return repairAddressMojibake(String(value ?? "")).toLowerCase().replace(/ё/g, "е");
|
|
||||||
}
|
|
||||||
const ORGANIZATION_SCOPE_STOPWORDS = new Set([
|
|
||||||
"ооо",
|
|
||||||
"ao",
|
|
||||||
"ао",
|
|
||||||
"зао",
|
|
||||||
"ип",
|
|
||||||
"llc",
|
|
||||||
"ltd",
|
|
||||||
"company",
|
|
||||||
"компания",
|
|
||||||
"организация",
|
|
||||||
"организации",
|
|
||||||
"контора",
|
|
||||||
"конторы",
|
|
||||||
"фирма",
|
|
||||||
"фирмы",
|
|
||||||
"по",
|
|
||||||
"для",
|
|
||||||
"над",
|
|
||||||
"под",
|
|
||||||
"без",
|
|
||||||
"с",
|
|
||||||
"со",
|
|
||||||
"в",
|
|
||||||
"во",
|
|
||||||
"на",
|
|
||||||
"и",
|
|
||||||
"или",
|
|
||||||
"а",
|
|
||||||
"но",
|
|
||||||
"не",
|
|
||||||
"мы",
|
|
||||||
"нам",
|
|
||||||
"наш",
|
|
||||||
"наша",
|
|
||||||
"наше",
|
|
||||||
"наши",
|
|
||||||
"ты",
|
|
||||||
"тебе",
|
|
||||||
"твой",
|
|
||||||
"сейчас",
|
|
||||||
"щас",
|
|
||||||
"тут",
|
|
||||||
"вот",
|
|
||||||
"давай",
|
|
||||||
"го",
|
|
||||||
"погнали",
|
|
||||||
"тогда",
|
|
||||||
"обсудим",
|
|
||||||
"обсуждать",
|
|
||||||
"работать",
|
|
||||||
"работаем",
|
|
||||||
"работаешь",
|
|
||||||
"работаете",
|
|
||||||
"можем",
|
|
||||||
"можно",
|
|
||||||
"какая",
|
|
||||||
"какой",
|
|
||||||
"какие",
|
|
||||||
"чья",
|
|
||||||
"чье",
|
|
||||||
"чьи"
|
|
||||||
]);
|
|
||||||
function normalizeOrganizationScopeValue(value) {
|
|
||||||
const normalized = normalizeScopeLabel(value);
|
|
||||||
if (!normalized) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const unwrapped = normalized
|
|
||||||
.replace(/^"+|"+$/g, "")
|
|
||||||
.replace(/^'+|'+$/g, "")
|
|
||||||
.trim();
|
|
||||||
return unwrapped ? unwrapped : null;
|
|
||||||
}
|
|
||||||
const assistantProviderExecutionPolicy = (0, assistantProviderExecutionPolicy_1.createAssistantProviderExecutionPolicy)();
|
const assistantProviderExecutionPolicy = (0, assistantProviderExecutionPolicy_1.createAssistantProviderExecutionPolicy)();
|
||||||
const assistantLivingModePolicy = (0, assistantLivingModePolicy_1.createAssistantLivingModePolicy)({
|
const assistantLivingModePolicy = (0, assistantLivingModePolicy_1.createAssistantLivingModePolicy)({
|
||||||
featureAssistantLivingChatRouterV1: config_1.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1,
|
featureAssistantLivingChatRouterV1: config_1.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1,
|
||||||
|
|
@ -4420,6 +4322,78 @@ const assistantTransitionPolicy = (0, assistantTransitionPolicy_1.createAssistan
|
||||||
extractDisplayedAddressEntityCandidates,
|
extractDisplayedAddressEntityCandidates,
|
||||||
resolveDisplayedAddressEntityMention
|
resolveDisplayedAddressEntityMention
|
||||||
});
|
});
|
||||||
|
const assistantDataScopePolicy = (0, assistantDataScopePolicy_1.createAssistantDataScopePolicy)({
|
||||||
|
activeMcpChannel: config_1.ASSISTANT_MCP_CHANNEL,
|
||||||
|
mcpProxyUrl: config_1.ASSISTANT_MCP_PROXY_URL,
|
||||||
|
executeAddressMcpQuery: addressMcpClient_1.executeAddressMcpQuery,
|
||||||
|
repairAddressMojibake
|
||||||
|
});
|
||||||
|
function normalizeScopeKey(value) {
|
||||||
|
return repairAddressMojibake(String(value ?? "")).toLowerCase().replace(/ё/g, "е");
|
||||||
|
}
|
||||||
|
const ORGANIZATION_SCOPE_STOPWORDS = new Set([
|
||||||
|
"ооо",
|
||||||
|
"ao",
|
||||||
|
"ао",
|
||||||
|
"зао",
|
||||||
|
"ип",
|
||||||
|
"llc",
|
||||||
|
"ltd",
|
||||||
|
"company",
|
||||||
|
"компания",
|
||||||
|
"организация",
|
||||||
|
"организации",
|
||||||
|
"контора",
|
||||||
|
"конторы",
|
||||||
|
"фирма",
|
||||||
|
"фирмы",
|
||||||
|
"по",
|
||||||
|
"для",
|
||||||
|
"над",
|
||||||
|
"под",
|
||||||
|
"без",
|
||||||
|
"с",
|
||||||
|
"со",
|
||||||
|
"в",
|
||||||
|
"во",
|
||||||
|
"на",
|
||||||
|
"и",
|
||||||
|
"или",
|
||||||
|
"а",
|
||||||
|
"но",
|
||||||
|
"не",
|
||||||
|
"мы",
|
||||||
|
"нам",
|
||||||
|
"наш",
|
||||||
|
"наша",
|
||||||
|
"наше",
|
||||||
|
"наши",
|
||||||
|
"ты",
|
||||||
|
"тебе",
|
||||||
|
"твой",
|
||||||
|
"сейчас",
|
||||||
|
"щас",
|
||||||
|
"тут",
|
||||||
|
"вот",
|
||||||
|
"давай",
|
||||||
|
"го",
|
||||||
|
"погнали",
|
||||||
|
"тогда",
|
||||||
|
"обсудим",
|
||||||
|
"обсуждать",
|
||||||
|
"работать",
|
||||||
|
"работаем",
|
||||||
|
"работаешь",
|
||||||
|
"работаете",
|
||||||
|
"можем",
|
||||||
|
"можно",
|
||||||
|
"какая",
|
||||||
|
"какой",
|
||||||
|
"какие",
|
||||||
|
"чья",
|
||||||
|
"чье",
|
||||||
|
"чьи"
|
||||||
|
]);
|
||||||
function normalizeOrganizationScopeSearchText(value) {
|
function normalizeOrganizationScopeSearchText(value) {
|
||||||
const source = normalizeScopeKey(value);
|
const source = normalizeScopeKey(value);
|
||||||
return source
|
return source
|
||||||
|
|
@ -4679,9 +4653,9 @@ function resolveSessionOrganizationScopeContext(userMessage, items, addressNavig
|
||||||
userMessage,
|
userMessage,
|
||||||
items,
|
items,
|
||||||
addressNavigationState,
|
addressNavigationState,
|
||||||
extractKnownOrganizationsFromHistory,
|
extractKnownOrganizationsFromHistory: assistantDataScopePolicy.extractKnownOrganizationsFromHistory,
|
||||||
resolveOrganizationSelectionFromMessage,
|
resolveOrganizationSelectionFromMessage,
|
||||||
findLastAssistantActiveOrganization,
|
findLastAssistantActiveOrganization: assistantDataScopePolicy.findLastAssistantActiveOrganization,
|
||||||
normalizeOrganizationScopeValue
|
normalizeOrganizationScopeValue
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -4935,12 +4909,13 @@ function buildResolvedDataScopeProbe(status, organizations) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function extractOrganizationFactsFromRowsForTests(rows) {
|
function extractOrganizationFactsFromRowsForTests(rows) {
|
||||||
return extractOrganizationFactsFromRows(rows);
|
return assistantDataScopePolicy.extractOrganizationFactsFromRows(rows);
|
||||||
}
|
}
|
||||||
function resolveOrganizationNamesByRefsForTests(refs, facts) {
|
function resolveOrganizationNamesByRefsForTests(refs, facts) {
|
||||||
return resolveOrganizationNamesByRefs(refs, facts);
|
return assistantDataScopePolicy.resolveOrganizationNamesByRefs(refs, facts);
|
||||||
}
|
}
|
||||||
async function resolveAssistantDataScopeProbe() {
|
async function resolveAssistantDataScopeProbe() {
|
||||||
|
return assistantDataScopePolicy.resolveAssistantDataScopeProbe();
|
||||||
const cacheKey = `${config_1.ASSISTANT_MCP_PROXY_URL}|${config_1.ASSISTANT_MCP_CHANNEL}`;
|
const cacheKey = `${config_1.ASSISTANT_MCP_PROXY_URL}|${config_1.ASSISTANT_MCP_CHANNEL}`;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const cached = dataScopeProbeCache.get(cacheKey);
|
const cached = dataScopeProbeCache.get(cacheKey);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,550 @@
|
||||||
|
// @ts-nocheck
|
||||||
|
import {
|
||||||
|
mergeKnownOrganizations,
|
||||||
|
normalizeOrganizationScopeValue
|
||||||
|
} from "./assistantOrganizationMatcher";
|
||||||
|
|
||||||
|
const DATA_SCOPE_CACHE_TTL_MS = 60_000;
|
||||||
|
|
||||||
|
export interface AssistantDataScopeProbe {
|
||||||
|
status: string;
|
||||||
|
channel: string;
|
||||||
|
organizations: string[];
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantOrganizationFacts {
|
||||||
|
names: string[];
|
||||||
|
refs: string[];
|
||||||
|
pairs: Array<{
|
||||||
|
ref: string;
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantDataScopePolicyDeps {
|
||||||
|
activeMcpChannel: string;
|
||||||
|
mcpProxyUrl: string;
|
||||||
|
executeAddressMcpQuery: (input: { query: string; limit: number }) => Promise<{
|
||||||
|
rows?: unknown[];
|
||||||
|
error?: string | null;
|
||||||
|
}>;
|
||||||
|
repairAddressMojibake: (value: unknown) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantDataScopePolicy {
|
||||||
|
parseOrganizationsFromDataScopeAssistantText: (text: unknown) => string[];
|
||||||
|
extractKnownOrganizationsFromHistory: (items: unknown[]) => string[];
|
||||||
|
findLastAssistantActiveOrganization: (items: unknown[]) => string | null;
|
||||||
|
extractOrganizationFactsFromRows: (rows: unknown[]) => AssistantOrganizationFacts;
|
||||||
|
resolveOrganizationNamesByRefs: (refs: unknown[], facts: AssistantOrganizationFacts | null | undefined) => string[];
|
||||||
|
resolveAssistantDataScopeProbe: () => Promise<AssistantDataScopeProbe>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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, "е");
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeGuidValue(value: unknown): string | null {
|
||||||
|
const source = normalizeScopeLabel(value);
|
||||||
|
if (!source) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const match = source.match(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i);
|
||||||
|
return match ? String(match[0]).toLowerCase() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractGuidValuesFromText(value: unknown): string[] {
|
||||||
|
const source = normalizeScopeLabel(value);
|
||||||
|
if (!source) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const matches = source.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi);
|
||||||
|
if (!matches || matches.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return Array.from(new Set(matches.map((item) => String(item).toLowerCase())));
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasOrganizationKeyHint(key: unknown): boolean {
|
||||||
|
return /(?:организац|organization|company|контор|org)\b/i.test(String(key ?? ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasNameKeyHint(key: unknown): boolean {
|
||||||
|
return /(?:представ|наимен|name|title|display|presentation|description)\b/i.test(String(key ?? ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasGuidKeyHint(key: unknown): boolean {
|
||||||
|
return /(?:идентифик|guid|uuid|key|ref|ссылк|\bid\b)\b/i.test(String(key ?? ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
function looksLikeOrganizationTypeMarker(value: unknown): boolean {
|
||||||
|
const normalized = normalizeScopeKey(value);
|
||||||
|
return /(?:справочникссылка\.\s*организац|catalogref\.\s*organization|organization|company|организац)/i.test(
|
||||||
|
normalized
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlausibleOrganizationName(value: unknown): boolean {
|
||||||
|
const candidate = normalizeScopeLabel(value);
|
||||||
|
if (!candidate) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/^(?:период|регистратор|счетдт|счеткт|amount|period|registrator|accountdt|accountkt)$/i.test(candidate)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/^[0-9._:/\\-]+$/i.test(candidate)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i.test(candidate)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/(?:справочникссылка|документссылка|плансчетовссылка|standardodata|recordtype|cmp:)/i.test(candidate)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /[A-Za-z\u0400-\u04FF]/u.test(candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendOrganizationFactsFromValue(
|
||||||
|
value: unknown,
|
||||||
|
hints: { organizationHint: boolean; nameHint: boolean; guidHint: boolean },
|
||||||
|
bucket: { names: string[]; refs: string[] },
|
||||||
|
depth = 0
|
||||||
|
): void {
|
||||||
|
if (depth > 4 || value === null || value === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "string") {
|
||||||
|
for (const guid of extractGuidValuesFromText(value)) {
|
||||||
|
if (hints.guidHint || hints.organizationHint || hints.nameHint) {
|
||||||
|
bucket.refs.push(guid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((hints.organizationHint || hints.nameHint) && isPlausibleOrganizationName(value)) {
|
||||||
|
const normalized = normalizeScopeLabel(value);
|
||||||
|
if (normalized) {
|
||||||
|
bucket.names.push(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const item of value) {
|
||||||
|
appendOrganizationFactsFromValue(item, hints, bucket, depth + 1);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value !== "object") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = Object.entries(value);
|
||||||
|
let objectIsOrganization = false;
|
||||||
|
let hasObjectRefMarker = false;
|
||||||
|
let hasGuidLikeField = false;
|
||||||
|
let hasTypeMarker = false;
|
||||||
|
|
||||||
|
for (const [rawKey, rawVal] of entries) {
|
||||||
|
const key = normalizeScopeKey(rawKey);
|
||||||
|
if ((key.includes("objectref") || key.includes("_objectref")) && rawVal === true) {
|
||||||
|
hasObjectRefMarker = true;
|
||||||
|
}
|
||||||
|
if (typeof rawVal === "string" && normalizeGuidValue(rawVal)) {
|
||||||
|
hasGuidLikeField = true;
|
||||||
|
}
|
||||||
|
if (hasOrganizationKeyHint(key)) {
|
||||||
|
objectIsOrganization = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ((key.includes("типобъекта") || key.includes("type")) && typeof rawVal === "string" && looksLikeOrganizationTypeMarker(rawVal)) {
|
||||||
|
objectIsOrganization = true;
|
||||||
|
hasTypeMarker = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!objectIsOrganization && hasObjectRefMarker && hasGuidLikeField) {
|
||||||
|
const hasNameLikeValue = entries.some(([rawKey, rawVal]) => {
|
||||||
|
if (typeof rawVal !== "string") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const key = normalizeScopeKey(rawKey);
|
||||||
|
return hasNameKeyHint(key) || isPlausibleOrganizationName(rawVal);
|
||||||
|
});
|
||||||
|
if (hasTypeMarker || hasNameLikeValue) {
|
||||||
|
objectIsOrganization = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [rawKey, rawVal] of entries) {
|
||||||
|
if (String(rawKey ?? "").startsWith("__")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const key = normalizeScopeKey(rawKey);
|
||||||
|
const childHints = {
|
||||||
|
organizationHint: hints.organizationHint || objectIsOrganization || hasOrganizationKeyHint(key),
|
||||||
|
nameHint: hints.nameHint || objectIsOrganization || hasNameKeyHint(key),
|
||||||
|
guidHint: hints.guidHint || objectIsOrganization || hasGuidKeyHint(key)
|
||||||
|
};
|
||||||
|
if (typeof rawVal === "string") {
|
||||||
|
const guid = normalizeGuidValue(rawVal);
|
||||||
|
if (guid && childHints.guidHint) {
|
||||||
|
bucket.refs.push(guid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appendOrganizationFactsFromValue(rawVal, childHints, bucket, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildResolvedDataScopeProbe(status: string, activeMcpChannel: string, organizations: unknown[]): AssistantDataScopeProbe {
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
channel: activeMcpChannel,
|
||||||
|
organizations: Array.from(new Set(Array.isArray(organizations) ? organizations : [])).slice(0, 20) as string[],
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAssistantDataScopePolicy(deps: AssistantDataScopePolicyDeps): AssistantDataScopePolicy {
|
||||||
|
const dataScopeProbeCache = new Map<string, { expiresAt: number; value: AssistantDataScopeProbe }>();
|
||||||
|
|
||||||
|
function parseOrganizationsFromDataScopeAssistantText(text: unknown): string[] {
|
||||||
|
const source = deps.repairAddressMojibake(String(text ?? ""));
|
||||||
|
if (!source) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const extracted: string[] = [];
|
||||||
|
const singleMatch = source.match(/доступна\s+организация:\s*([^.\n]+)/iu);
|
||||||
|
if (singleMatch) {
|
||||||
|
const value = normalizeOrganizationScopeValue(singleMatch[1]);
|
||||||
|
if (value) {
|
||||||
|
extracted.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const multiMatch = source.match(/доступны\s+организац(?:ии|ия)\s*(?:\(\d+\))?:\s*([^.\n]+)/iu);
|
||||||
|
if (multiMatch) {
|
||||||
|
const parts = String(multiMatch[1] ?? "")
|
||||||
|
.split(",")
|
||||||
|
.map((item) => normalizeOrganizationScopeValue(item))
|
||||||
|
.filter((item): item is string => Boolean(item));
|
||||||
|
extracted.push(...parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(new Set(extracted));
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractKnownOrganizationsFromHistory(items: unknown[]): string[] {
|
||||||
|
const collected: unknown[] = [];
|
||||||
|
for (let index = (Array.isArray(items) ? items.length : 0) - 1; index >= 0; index -= 1) {
|
||||||
|
const item = Array.isArray(items) ? items[index] : null;
|
||||||
|
if (!item || typeof item !== "object" || (item as { role?: string }).role !== "assistant") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const debug = (item as { debug?: Record<string, unknown> }).debug;
|
||||||
|
if (debug && typeof debug === "object") {
|
||||||
|
const directFromProbe = Array.isArray(debug.living_chat_data_scope_probe_organizations)
|
||||||
|
? debug.living_chat_data_scope_probe_organizations
|
||||||
|
: [];
|
||||||
|
const knownFromDebug = Array.isArray(debug.assistant_known_organizations)
|
||||||
|
? debug.assistant_known_organizations
|
||||||
|
: [];
|
||||||
|
const directFromCandidates = Array.isArray(debug.organization_candidates) ? debug.organization_candidates : [];
|
||||||
|
const directFromResolved = [
|
||||||
|
normalizeOrganizationScopeValue(debug.assistant_active_organization),
|
||||||
|
normalizeOrganizationScopeValue(debug.living_chat_selected_organization),
|
||||||
|
normalizeOrganizationScopeValue(debug.extracted_filters?.organization),
|
||||||
|
normalizeOrganizationScopeValue(debug.address_root_frame_context?.organization)
|
||||||
|
].filter((value): value is string => Boolean(value));
|
||||||
|
|
||||||
|
if (
|
||||||
|
directFromProbe.length > 0 ||
|
||||||
|
knownFromDebug.length > 0 ||
|
||||||
|
directFromCandidates.length > 0 ||
|
||||||
|
directFromResolved.length > 0
|
||||||
|
) {
|
||||||
|
collected.push(...directFromProbe, ...knownFromDebug, ...directFromCandidates, ...directFromResolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedFromText = parseOrganizationsFromDataScopeAssistantText((item as { text?: unknown }).text);
|
||||||
|
if (parsedFromText.length > 0) {
|
||||||
|
collected.push(...parsedFromText);
|
||||||
|
}
|
||||||
|
if (collected.length >= 20) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergeKnownOrganizations(collected, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findLastAssistantActiveOrganization(items: unknown[]): string | null {
|
||||||
|
for (let index = (Array.isArray(items) ? items.length : 0) - 1; index >= 0; index -= 1) {
|
||||||
|
const item = Array.isArray(items) ? items[index] : null;
|
||||||
|
if (!item || typeof item !== "object" || (item as { role?: string }).role !== "assistant") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const debug = (item as { debug?: Record<string, unknown> }).debug;
|
||||||
|
if (!debug || typeof debug !== "object") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const direct = normalizeOrganizationScopeValue(debug.assistant_active_organization);
|
||||||
|
if (direct) {
|
||||||
|
return direct;
|
||||||
|
}
|
||||||
|
const selected = normalizeOrganizationScopeValue(debug.living_chat_selected_organization);
|
||||||
|
if (selected) {
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractOrganizationFactsFromRows(rows: unknown[]): AssistantOrganizationFacts {
|
||||||
|
const names: string[] = [];
|
||||||
|
const refs: string[] = [];
|
||||||
|
const pairs: Array<{ ref: string; name: string }> = [];
|
||||||
|
|
||||||
|
for (const row of Array.isArray(rows) ? rows : []) {
|
||||||
|
if (!row || typeof row !== "object") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowNames: string[] = [];
|
||||||
|
const rowRefs: string[] = [];
|
||||||
|
for (const [rawKey, rawValue] of Object.entries(row)) {
|
||||||
|
if (String(rawKey ?? "").startsWith("__")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const key = normalizeScopeKey(rawKey);
|
||||||
|
appendOrganizationFactsFromValue(
|
||||||
|
rawValue,
|
||||||
|
{
|
||||||
|
organizationHint: hasOrganizationKeyHint(key),
|
||||||
|
nameHint: hasNameKeyHint(key),
|
||||||
|
guidHint: hasGuidKeyHint(key)
|
||||||
|
},
|
||||||
|
{ names: rowNames, refs: rowRefs }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dedupRowNames = Array.from(new Set(rowNames))
|
||||||
|
.filter((item) => isPlausibleOrganizationName(item))
|
||||||
|
.slice(0, 20);
|
||||||
|
const dedupRowRefs = Array.from(new Set(rowRefs))
|
||||||
|
.map((item) => String(item ?? "").toLowerCase())
|
||||||
|
.filter((item) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(item))
|
||||||
|
.slice(0, 20);
|
||||||
|
|
||||||
|
if (dedupRowNames.length === 0 && dedupRowRefs.length === 0) {
|
||||||
|
const fallbackBucket = { names: [] as string[], refs: [] as string[] };
|
||||||
|
appendOrganizationFactsFromValue(
|
||||||
|
row,
|
||||||
|
{
|
||||||
|
organizationHint: true,
|
||||||
|
nameHint: true,
|
||||||
|
guidHint: true
|
||||||
|
},
|
||||||
|
fallbackBucket
|
||||||
|
);
|
||||||
|
for (const value of fallbackBucket.names) {
|
||||||
|
if (isPlausibleOrganizationName(value)) {
|
||||||
|
dedupRowNames.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const value of fallbackBucket.refs) {
|
||||||
|
const normalized = String(value ?? "").toLowerCase();
|
||||||
|
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(normalized)) {
|
||||||
|
dedupRowRefs.push(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
names.push(...dedupRowNames);
|
||||||
|
refs.push(...dedupRowRefs);
|
||||||
|
if (dedupRowRefs.length > 0 && dedupRowNames.length > 0) {
|
||||||
|
for (const ref of dedupRowRefs) {
|
||||||
|
for (const name of dedupRowNames) {
|
||||||
|
pairs.push({ ref, name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
names: Array.from(new Set(names)).slice(0, 20),
|
||||||
|
refs: Array.from(new Set(refs)).slice(0, 20),
|
||||||
|
pairs: Array.from(new Set(pairs.map((item) => `${item.ref}||${item.name}`)))
|
||||||
|
.map((token) => {
|
||||||
|
const [ref, name] = token.split("||");
|
||||||
|
return { ref, name };
|
||||||
|
})
|
||||||
|
.slice(0, 100)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveOrganizationNamesByRefs(
|
||||||
|
refs: unknown[],
|
||||||
|
facts: AssistantOrganizationFacts | null | undefined
|
||||||
|
): string[] {
|
||||||
|
const refSet = new Set(
|
||||||
|
(Array.isArray(refs) ? refs : [])
|
||||||
|
.map((item) => String(item ?? "").toLowerCase())
|
||||||
|
.filter((item) => item.length > 0)
|
||||||
|
);
|
||||||
|
if (refSet.size === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const names: string[] = [];
|
||||||
|
for (const pair of Array.isArray(facts?.pairs) ? facts?.pairs : []) {
|
||||||
|
const ref = String(pair?.ref ?? "").toLowerCase();
|
||||||
|
const name = normalizeScopeLabel(pair?.name ?? "");
|
||||||
|
if (!ref || !name || !refSet.has(ref)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
names.push(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(new Set(names)).slice(0, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveAssistantDataScopeProbe(): Promise<AssistantDataScopeProbe> {
|
||||||
|
const cacheKey = `${deps.mcpProxyUrl}|${deps.activeMcpChannel}`;
|
||||||
|
const now = Date.now();
|
||||||
|
const cached = dataScopeProbeCache.get(cacheKey);
|
||||||
|
if (cached && Number(cached.expiresAt ?? 0) > now) {
|
||||||
|
return cached.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String(process.env.NODE_ENV ?? "").toLowerCase() === "test") {
|
||||||
|
return {
|
||||||
|
status: "skipped_test_env",
|
||||||
|
channel: deps.activeMcpChannel,
|
||||||
|
organizations: [],
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const catalogQueryCandidates = [
|
||||||
|
"ВЫБРАТЬ ПЕРВЫЕ 20 Организации.Наименование КАК Организация ИЗ Справочник.Организации КАК Организации",
|
||||||
|
"ВЫБРАТЬ ПЕРВЫЕ 20 Организации.НаименованиеПолное КАК Организация ИЗ Справочник.Организации КАК Организации",
|
||||||
|
"ВЫБРАТЬ ПЕРВЫЕ 100 Организации.Ссылка КАК Организация, ПРЕДСТАВЛЕНИЕ(Организации.Ссылка) КАК ОрганизацияПредставление ИЗ Справочник.Организации КАК Организации"
|
||||||
|
];
|
||||||
|
const movementProbeCandidates = [
|
||||||
|
"ВЫБРАТЬ ПЕРВЫЕ 60 Движения.Организация КАК Организация ИЗ РегистрБухгалтерии.Хозрасчетный КАК Движения УПОРЯДОЧИТЬ ПО Движения.Период УБЫВ",
|
||||||
|
"ВЫБРАТЬ ПЕРВЫЕ 60 Движения.Организация КАК Организация ИЗ РегистрБухгалтерии.Хозрасчетный КАК Движения"
|
||||||
|
];
|
||||||
|
|
||||||
|
let lastError: string | null = null;
|
||||||
|
const catalogFacts: AssistantOrganizationFacts = { names: [], refs: [], pairs: [] };
|
||||||
|
for (const queryText of catalogQueryCandidates) {
|
||||||
|
const probe = await deps.executeAddressMcpQuery({
|
||||||
|
query: queryText,
|
||||||
|
limit: 100
|
||||||
|
});
|
||||||
|
if (probe.error) {
|
||||||
|
lastError = String(probe.error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const facts = extractOrganizationFactsFromRows(Array.isArray(probe.rows) ? probe.rows : []);
|
||||||
|
catalogFacts.names.push(...facts.names);
|
||||||
|
catalogFacts.refs.push(...facts.refs);
|
||||||
|
catalogFacts.pairs.push(...facts.pairs);
|
||||||
|
if (facts.names.length > 0) {
|
||||||
|
const resolved = buildResolvedDataScopeProbe("resolved", deps.activeMcpChannel, facts.names);
|
||||||
|
dataScopeProbeCache.set(cacheKey, {
|
||||||
|
expiresAt: now + DATA_SCOPE_CACHE_TTL_MS,
|
||||||
|
value: resolved
|
||||||
|
});
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const movementFacts: AssistantOrganizationFacts = { names: [], refs: [], pairs: [] };
|
||||||
|
for (const queryText of movementProbeCandidates) {
|
||||||
|
const probe = await deps.executeAddressMcpQuery({
|
||||||
|
query: queryText,
|
||||||
|
limit: 60
|
||||||
|
});
|
||||||
|
if (probe.error) {
|
||||||
|
lastError = String(probe.error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const facts = extractOrganizationFactsFromRows(Array.isArray(probe.rows) ? probe.rows : []);
|
||||||
|
movementFacts.names.push(...facts.names);
|
||||||
|
movementFacts.refs.push(...facts.refs);
|
||||||
|
movementFacts.pairs.push(...facts.pairs);
|
||||||
|
if (facts.names.length > 0) {
|
||||||
|
const resolved = buildResolvedDataScopeProbe("resolved_from_activity", deps.activeMcpChannel, facts.names);
|
||||||
|
dataScopeProbeCache.set(cacheKey, {
|
||||||
|
expiresAt: now + DATA_SCOPE_CACHE_TTL_MS,
|
||||||
|
value: resolved
|
||||||
|
});
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const movementRefs = Array.from(new Set(movementFacts.refs))
|
||||||
|
.map((item) => String(item ?? "").toLowerCase())
|
||||||
|
.filter((item) => item.length > 0);
|
||||||
|
if (movementRefs.length > 0) {
|
||||||
|
const namesFromCatalogPairs = resolveOrganizationNamesByRefs(movementRefs, {
|
||||||
|
names: Array.from(new Set(catalogFacts.names)),
|
||||||
|
refs: Array.from(new Set(catalogFacts.refs)),
|
||||||
|
pairs: catalogFacts.pairs
|
||||||
|
});
|
||||||
|
if (namesFromCatalogPairs.length > 0) {
|
||||||
|
const resolved = buildResolvedDataScopeProbe(
|
||||||
|
"resolved_from_ref_lookup",
|
||||||
|
deps.activeMcpChannel,
|
||||||
|
namesFromCatalogPairs
|
||||||
|
);
|
||||||
|
dataScopeProbeCache.set(cacheKey, {
|
||||||
|
expiresAt: now + DATA_SCOPE_CACHE_TTL_MS,
|
||||||
|
value: resolved
|
||||||
|
});
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallback: AssistantDataScopeProbe = {
|
||||||
|
status: lastError ? "unresolved_with_error" : "unresolved",
|
||||||
|
channel: deps.activeMcpChannel,
|
||||||
|
organizations: [],
|
||||||
|
error: lastError
|
||||||
|
};
|
||||||
|
dataScopeProbeCache.set(cacheKey, {
|
||||||
|
expiresAt: now + DATA_SCOPE_CACHE_TTL_MS,
|
||||||
|
value: fallback
|
||||||
|
});
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
parseOrganizationsFromDataScopeAssistantText,
|
||||||
|
extractKnownOrganizationsFromHistory,
|
||||||
|
findLastAssistantActiveOrganization,
|
||||||
|
extractOrganizationFactsFromRows,
|
||||||
|
resolveOrganizationNamesByRefs,
|
||||||
|
resolveAssistantDataScopeProbe
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,7 @@ import * as capabilitiesRegistry_1 from "./capabilitiesRegistry";
|
||||||
import * as assistantCanon_1 from "./assistantCanon";
|
import * as assistantCanon_1 from "./assistantCanon";
|
||||||
import * as assistantAddressAttemptRuntimeAdapter_1 from "./assistantAddressAttemptRuntimeAdapter";
|
import * as assistantAddressAttemptRuntimeAdapter_1 from "./assistantAddressAttemptRuntimeAdapter";
|
||||||
import * as assistantCoverageGrounding_1 from "./assistantCoverageGrounding";
|
import * as assistantCoverageGrounding_1 from "./assistantCoverageGrounding";
|
||||||
|
import * as assistantDataScopePolicy_1 from "./assistantDataScopePolicy";
|
||||||
import * as assistantDeepTurnAttemptRuntimeAdapter_1 from "./assistantDeepTurnAttemptRuntimeAdapter";
|
import * as assistantDeepTurnAttemptRuntimeAdapter_1 from "./assistantDeepTurnAttemptRuntimeAdapter";
|
||||||
import * as assistantBoundaryPolicy_1 from "./assistantBoundaryPolicy";
|
import * as assistantBoundaryPolicy_1 from "./assistantBoundaryPolicy";
|
||||||
import * as assistantLivingModePolicy_1 from "./assistantLivingModePolicy";
|
import * as assistantLivingModePolicy_1 from "./assistantLivingModePolicy";
|
||||||
|
|
@ -35,8 +36,7 @@ import * as assistantTurnRuntimeInputBuilder_1 from "./assistantTurnRuntimeInput
|
||||||
import * as assistantUserTurnBootstrapRuntimeAdapter_1 from "./assistantUserTurnBootstrapRuntimeAdapter";
|
import * as assistantUserTurnBootstrapRuntimeAdapter_1 from "./assistantUserTurnBootstrapRuntimeAdapter";
|
||||||
import * as assistantQueryPlanning_1 from "./assistantQueryPlanning";
|
import * as assistantQueryPlanning_1 from "./assistantQueryPlanning";
|
||||||
import iconv from "iconv-lite";
|
import iconv from "iconv-lite";
|
||||||
const DATA_SCOPE_CACHE_TTL_MS = 60_000;
|
const normalizeOrganizationScopeValue = assistantOrganizationMatcher_1.normalizeOrganizationScopeValue;
|
||||||
const dataScopeProbeCache = new Map();
|
|
||||||
function retrievalSummaryForRoute(route) {
|
function retrievalSummaryForRoute(route) {
|
||||||
if (route === "store_canonical")
|
if (route === "store_canonical")
|
||||||
return "Canonical accounting data path selected.";
|
return "Canonical accounting data path selected.";
|
||||||
|
|
@ -4185,105 +4185,6 @@ function hasLivingChatSignal(text) {
|
||||||
function buildAssistantCapabilityContractReply() {
|
function buildAssistantCapabilityContractReply() {
|
||||||
return (0, capabilitiesRegistry_1.buildCapabilityContractReplyFromRegistry)();
|
return (0, capabilitiesRegistry_1.buildCapabilityContractReplyFromRegistry)();
|
||||||
}
|
}
|
||||||
function normalizeScopeLabel(value) {
|
|
||||||
const repaired = repairAddressMojibake(String(value ?? ""));
|
|
||||||
let normalized = compactWhitespace(repaired.trim());
|
|
||||||
for (let index = 0; index < 2; index += 1) {
|
|
||||||
const first = normalized[0];
|
|
||||||
const last = normalized[normalized.length - 1];
|
|
||||||
const wrappedInQuotes = (first === "\"" && last === "\"") ||
|
|
||||||
(first === "'" && last === "'") ||
|
|
||||||
(first === "«" && last === "»");
|
|
||||||
if (!wrappedInQuotes) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
normalized = compactWhitespace(normalized.slice(1, -1).trim());
|
|
||||||
}
|
|
||||||
if (!normalized) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (/^(?:null|undefined|nan|0|не\s*заполнено)$/i.test(normalized)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
function normalizeScopeKey(value) {
|
|
||||||
return repairAddressMojibake(String(value ?? "")).toLowerCase().replace(/ё/g, "е");
|
|
||||||
}
|
|
||||||
const ORGANIZATION_SCOPE_STOPWORDS = new Set([
|
|
||||||
"ооо",
|
|
||||||
"ao",
|
|
||||||
"ао",
|
|
||||||
"зао",
|
|
||||||
"ип",
|
|
||||||
"llc",
|
|
||||||
"ltd",
|
|
||||||
"company",
|
|
||||||
"компания",
|
|
||||||
"организация",
|
|
||||||
"организации",
|
|
||||||
"контора",
|
|
||||||
"конторы",
|
|
||||||
"фирма",
|
|
||||||
"фирмы",
|
|
||||||
"по",
|
|
||||||
"для",
|
|
||||||
"над",
|
|
||||||
"под",
|
|
||||||
"без",
|
|
||||||
"с",
|
|
||||||
"со",
|
|
||||||
"в",
|
|
||||||
"во",
|
|
||||||
"на",
|
|
||||||
"и",
|
|
||||||
"или",
|
|
||||||
"а",
|
|
||||||
"но",
|
|
||||||
"не",
|
|
||||||
"мы",
|
|
||||||
"нам",
|
|
||||||
"наш",
|
|
||||||
"наша",
|
|
||||||
"наше",
|
|
||||||
"наши",
|
|
||||||
"ты",
|
|
||||||
"тебе",
|
|
||||||
"твой",
|
|
||||||
"сейчас",
|
|
||||||
"щас",
|
|
||||||
"тут",
|
|
||||||
"вот",
|
|
||||||
"давай",
|
|
||||||
"го",
|
|
||||||
"погнали",
|
|
||||||
"тогда",
|
|
||||||
"обсудим",
|
|
||||||
"обсуждать",
|
|
||||||
"работать",
|
|
||||||
"работаем",
|
|
||||||
"работаешь",
|
|
||||||
"работаете",
|
|
||||||
"можем",
|
|
||||||
"можно",
|
|
||||||
"какая",
|
|
||||||
"какой",
|
|
||||||
"какие",
|
|
||||||
"чья",
|
|
||||||
"чье",
|
|
||||||
"чьи"
|
|
||||||
]);
|
|
||||||
function normalizeOrganizationScopeValue(value) {
|
|
||||||
const normalized = normalizeScopeLabel(value);
|
|
||||||
if (!normalized) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const unwrapped = normalized
|
|
||||||
.replace(/^"+|"+$/g, "")
|
|
||||||
.replace(/^'+|'+$/g, "")
|
|
||||||
.trim();
|
|
||||||
return unwrapped ? unwrapped : null;
|
|
||||||
}
|
|
||||||
const assistantProviderExecutionPolicy = (0, assistantProviderExecutionPolicy_1.createAssistantProviderExecutionPolicy)();
|
const assistantProviderExecutionPolicy = (0, assistantProviderExecutionPolicy_1.createAssistantProviderExecutionPolicy)();
|
||||||
const assistantLivingModePolicy = (0, assistantLivingModePolicy_1.createAssistantLivingModePolicy)({
|
const assistantLivingModePolicy = (0, assistantLivingModePolicy_1.createAssistantLivingModePolicy)({
|
||||||
featureAssistantLivingChatRouterV1: config_1.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1,
|
featureAssistantLivingChatRouterV1: config_1.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1,
|
||||||
|
|
@ -4380,6 +4281,78 @@ const assistantTransitionPolicy = (0, assistantTransitionPolicy_1.createAssistan
|
||||||
extractDisplayedAddressEntityCandidates,
|
extractDisplayedAddressEntityCandidates,
|
||||||
resolveDisplayedAddressEntityMention
|
resolveDisplayedAddressEntityMention
|
||||||
});
|
});
|
||||||
|
const assistantDataScopePolicy = (0, assistantDataScopePolicy_1.createAssistantDataScopePolicy)({
|
||||||
|
activeMcpChannel: config_1.ASSISTANT_MCP_CHANNEL,
|
||||||
|
mcpProxyUrl: config_1.ASSISTANT_MCP_PROXY_URL,
|
||||||
|
executeAddressMcpQuery: addressMcpClient_1.executeAddressMcpQuery,
|
||||||
|
repairAddressMojibake
|
||||||
|
});
|
||||||
|
function normalizeScopeKey(value) {
|
||||||
|
return repairAddressMojibake(String(value ?? "")).toLowerCase().replace(/ё/g, "е");
|
||||||
|
}
|
||||||
|
const ORGANIZATION_SCOPE_STOPWORDS = new Set([
|
||||||
|
"ооо",
|
||||||
|
"ao",
|
||||||
|
"ао",
|
||||||
|
"зао",
|
||||||
|
"ип",
|
||||||
|
"llc",
|
||||||
|
"ltd",
|
||||||
|
"company",
|
||||||
|
"компания",
|
||||||
|
"организация",
|
||||||
|
"организации",
|
||||||
|
"контора",
|
||||||
|
"конторы",
|
||||||
|
"фирма",
|
||||||
|
"фирмы",
|
||||||
|
"по",
|
||||||
|
"для",
|
||||||
|
"над",
|
||||||
|
"под",
|
||||||
|
"без",
|
||||||
|
"с",
|
||||||
|
"со",
|
||||||
|
"в",
|
||||||
|
"во",
|
||||||
|
"на",
|
||||||
|
"и",
|
||||||
|
"или",
|
||||||
|
"а",
|
||||||
|
"но",
|
||||||
|
"не",
|
||||||
|
"мы",
|
||||||
|
"нам",
|
||||||
|
"наш",
|
||||||
|
"наша",
|
||||||
|
"наше",
|
||||||
|
"наши",
|
||||||
|
"ты",
|
||||||
|
"тебе",
|
||||||
|
"твой",
|
||||||
|
"сейчас",
|
||||||
|
"щас",
|
||||||
|
"тут",
|
||||||
|
"вот",
|
||||||
|
"давай",
|
||||||
|
"го",
|
||||||
|
"погнали",
|
||||||
|
"тогда",
|
||||||
|
"обсудим",
|
||||||
|
"обсуждать",
|
||||||
|
"работать",
|
||||||
|
"работаем",
|
||||||
|
"работаешь",
|
||||||
|
"работаете",
|
||||||
|
"можем",
|
||||||
|
"можно",
|
||||||
|
"какая",
|
||||||
|
"какой",
|
||||||
|
"какие",
|
||||||
|
"чья",
|
||||||
|
"чье",
|
||||||
|
"чьи"
|
||||||
|
]);
|
||||||
function normalizeOrganizationScopeSearchText(value) {
|
function normalizeOrganizationScopeSearchText(value) {
|
||||||
const source = normalizeScopeKey(value);
|
const source = normalizeScopeKey(value);
|
||||||
return source
|
return source
|
||||||
|
|
@ -4638,9 +4611,9 @@ function resolveSessionOrganizationScopeContext(userMessage, items, addressNavig
|
||||||
userMessage,
|
userMessage,
|
||||||
items,
|
items,
|
||||||
addressNavigationState,
|
addressNavigationState,
|
||||||
extractKnownOrganizationsFromHistory,
|
extractKnownOrganizationsFromHistory: assistantDataScopePolicy.extractKnownOrganizationsFromHistory,
|
||||||
resolveOrganizationSelectionFromMessage,
|
resolveOrganizationSelectionFromMessage,
|
||||||
findLastAssistantActiveOrganization,
|
findLastAssistantActiveOrganization: assistantDataScopePolicy.findLastAssistantActiveOrganization,
|
||||||
normalizeOrganizationScopeValue
|
normalizeOrganizationScopeValue
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -4894,12 +4867,13 @@ function buildResolvedDataScopeProbe(status, organizations) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export function extractOrganizationFactsFromRowsForTests(rows) {
|
export function extractOrganizationFactsFromRowsForTests(rows) {
|
||||||
return extractOrganizationFactsFromRows(rows);
|
return assistantDataScopePolicy.extractOrganizationFactsFromRows(rows);
|
||||||
}
|
}
|
||||||
export function resolveOrganizationNamesByRefsForTests(refs, facts) {
|
export function resolveOrganizationNamesByRefsForTests(refs, facts) {
|
||||||
return resolveOrganizationNamesByRefs(refs, facts);
|
return assistantDataScopePolicy.resolveOrganizationNamesByRefs(refs, facts);
|
||||||
}
|
}
|
||||||
async function resolveAssistantDataScopeProbe() {
|
async function resolveAssistantDataScopeProbe() {
|
||||||
|
return assistantDataScopePolicy.resolveAssistantDataScopeProbe();
|
||||||
const cacheKey = `${config_1.ASSISTANT_MCP_PROXY_URL}|${config_1.ASSISTANT_MCP_CHANNEL}`;
|
const cacheKey = `${config_1.ASSISTANT_MCP_PROXY_URL}|${config_1.ASSISTANT_MCP_CHANNEL}`;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const cached = dataScopeProbeCache.get(cacheKey);
|
const cached = dataScopeProbeCache.get(cacheKey);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue