ARCH: добить entity-resolution chain и очистить stale runtime

This commit is contained in:
2026-04-22 12:53:48 +03:00
parent 007369a78a
commit ce48fa83a5
15 changed files with 2024 additions and 76 deletions
@@ -73,6 +73,13 @@ function readAssistantMcpDiscoveryTurnMeaning(debug) {
const turnInput = toRecordObject(entry?.turn_input);
return toRecordObject(turnInput?.turn_meaning_ref);
}
function readAssistantMcpDiscoveryTurnMeaningMetadataAmbiguityEntitySets(debug, toNonEmptyString = fallbackToNonEmptyString) {
const values = readAssistantMcpDiscoveryTurnMeaning(debug)?.metadata_ambiguity_entity_sets;
if (!Array.isArray(values)) {
return [];
}
return values.map((item) => toNonEmptyString(item)).filter((item) => Boolean(item));
}
function readAssistantMcpDiscoveryActionFamily(debug, toNonEmptyString = fallbackToNonEmptyString) {
return toNonEmptyString(readAssistantMcpDiscoveryTurnMeaning(debug)?.asked_action_family);
}
@@ -96,14 +103,15 @@ function readAssistantMcpDiscoveryMetadataSelectedEntitySet(debug, toNonEmptyStr
return toNonEmptyString(readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.selected_entity_set);
}
function readAssistantMcpDiscoveryMetadataAmbiguityDetected(debug) {
return readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.ambiguity_detected === true;
return (readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.ambiguity_detected === true ||
readAssistantMcpDiscoveryTurnMeaningMetadataAmbiguityEntitySets(debug).length > 0);
}
function readAssistantMcpDiscoveryMetadataAmbiguityEntitySets(debug, toNonEmptyString = fallbackToNonEmptyString) {
const values = readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.ambiguity_entity_sets;
if (!Array.isArray(values)) {
return [];
if (Array.isArray(values)) {
return values.map((item) => toNonEmptyString(item)).filter((item) => Boolean(item));
}
return values.map((item) => toNonEmptyString(item)).filter((item) => Boolean(item));
return readAssistantMcpDiscoveryTurnMeaningMetadataAmbiguityEntitySets(debug, toNonEmptyString);
}
function mapAssistantMcpDiscoveryPilotScopeToAddressIntent(pilotScope, actionFamily) {
if (pilotScope === "counterparty_lifecycle_query_documents_v1") {
@@ -51,6 +51,11 @@ function modeFor(pilot) {
if (pilot.pilot_status === "skipped_needs_clarification") {
return "needs_clarification";
}
if (pilot.pilot_scope === "entity_resolution_search_v1" &&
(pilot.reason_codes.includes("pilot_entity_resolution_ambiguity_requires_clarification") ||
pilot.derived_entity_resolution?.resolution_status === "ambiguous")) {
return "needs_clarification";
}
if (pilot.evidence.answer_permission === "confirmed_answer") {
return "confirmed_with_bounded_inference";
}
@@ -73,10 +78,91 @@ function isMovementPilot(pilot) {
function isMetadataPilot(pilot) {
return pilot.pilot_scope === "metadata_inspection_v1";
}
function isEntityResolutionPilot(pilot) {
return pilot.pilot_scope === "entity_resolution_search_v1";
}
function isMetadataLaneChoiceClarification(pilot) {
return (pilot.reason_codes.includes("planner_selected_metadata_lane_clarification_recipe") ||
pilot.dry_run.reason_codes.includes("planner_selected_metadata_lane_clarification_recipe"));
}
function askedActionFamily(pilot) {
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
if (typeof action !== "string") {
return null;
}
const normalized = action.trim().toLowerCase();
return normalized.length > 0 ? normalized : null;
}
function unsupportedFamily(pilot) {
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
if (typeof unsupported !== "string") {
return null;
}
const normalized = unsupported.trim().toLowerCase();
return normalized.length > 0 ? normalized : null;
}
function firstEntityCandidate(pilot) {
const values = Array.isArray(pilot.evidence.query_plan.turn_meaning_ref?.explicit_entity_candidates)
? pilot.evidence.query_plan.turn_meaning_ref?.explicit_entity_candidates
: [];
for (const value of values) {
const text = String(value ?? "").trim();
if (text) {
return text;
}
}
return null;
}
function isMovementLaneClarification(pilot) {
return (isMovementPilot(pilot) ||
pilot.reason_codes.includes("planner_selected_movement_recipe") ||
pilot.dry_run.reason_codes.includes("planner_selected_movement_recipe") ||
askedActionFamily(pilot) === "list_movements" ||
unsupportedFamily(pilot) === "movement_evidence");
}
function isDocumentLaneClarification(pilot) {
return (isDocumentPilot(pilot) ||
pilot.reason_codes.includes("planner_selected_document_recipe") ||
pilot.dry_run.reason_codes.includes("planner_selected_document_recipe") ||
askedActionFamily(pilot) === "list_documents" ||
unsupportedFamily(pilot) === "document_evidence");
}
function laneScopeSuffix(pilot) {
const entity = firstEntityCandidate(pilot);
return entity ? ` по "${entity}"` : "";
}
function dryRunMissingAxis(pilot, axis) {
return pilot.dry_run.execution_steps.some((step) => step.missing_axis_options.some((option) => option.includes(axis)));
}
function clarificationNeedRu(pilot) {
const needsPeriod = dryRunMissingAxis(pilot, "period");
const needsOrganization = dryRunMissingAxis(pilot, "organization");
if (needsPeriod && needsOrganization) {
return { subject: "проверяемый период и организацию", verb: "нужно" };
}
if (needsPeriod) {
return { subject: "проверяемый период", verb: "нужен" };
}
if (needsOrganization) {
return { subject: "организацию", verb: "нужно" };
}
return { subject: "контекст проверки", verb: "нужно" };
}
function clarificationNextStepLine(pilot, laneLabel) {
const needsPeriod = dryRunMissingAxis(pilot, "period");
const needsOrganization = dryRunMissingAxis(pilot, "organization");
const scopeSuffix = laneScopeSuffix(pilot);
if (needsPeriod && needsOrganization) {
return `Уточните период и организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
}
if (needsPeriod) {
return `Уточните период, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
}
if (needsOrganization) {
return `Уточните организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
}
return `Уточните контекст проверки, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
}
function metadataRouteFamilyLabelRu(routeFamily) {
if (routeFamily === "document_evidence") {
return "контур документов";
@@ -92,6 +178,17 @@ function metadataRouteFamilyLabelRu(routeFamily) {
function headlineFor(mode, pilot) {
const askedMonthlyBreakdown = pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
pilot.derived_value_flow?.aggregation_axis === "month";
if (isEntityResolutionPilot(pilot) && mode === "confirmed_with_bounded_inference") {
return "По каталогу 1С найден вероятный контрагент; это заземление сущности для следующего шага, а не еще бизнес-ответ по данным.";
}
if (isEntityResolutionPilot(pilot) && mode === "needs_clarification") {
return "По каталогу 1С нашлось несколько похожих контрагентов, и без уточнения нельзя честно выбрать правильную сущность.";
}
if (isEntityResolutionPilot(pilot) &&
mode === "checked_sources_only" &&
pilot.derived_entity_resolution?.resolution_status === "not_found") {
return "По текущему каталожному поиску 1С точный контрагент пока не подтвержден.";
}
if (isMovementPilot(pilot) && mode === "confirmed_with_bounded_inference") {
return "По данным 1С найдены строки движений; ответ ограничен проверенным периодом и найденными строками.";
}
@@ -134,8 +231,13 @@ function headlineFor(mode, pilot) {
if (mode === "needs_clarification" && isMetadataLaneChoiceClarification(pilot)) {
return "По подтвержденной metadata-поверхности видно несколько конкурирующих data-lane, и без явного выбора дальше идти нельзя.";
}
if (mode === "needs_clarification" && isMetadataLaneChoiceClarification(pilot)) {
return "Уточните, в какой контур идти дальше: по документам или по движениям/регистрам.";
if (mode === "needs_clarification" && isMovementLaneClarification(pilot)) {
const need = clarificationNeedRu(pilot);
return `Могу идти дальше по движениям/регистрам${laneScopeSuffix(pilot)}, но для запуска поиска в 1С ${need.verb} ${need.subject}.`;
}
if (mode === "needs_clarification" && isDocumentLaneClarification(pilot)) {
const need = clarificationNeedRu(pilot);
return `Могу идти дальше по документам${laneScopeSuffix(pilot)}, но для запуска поиска в 1С ${need.verb} ${need.subject}.`;
}
if (mode === "needs_clarification") {
return "Нужно уточнить контекст перед поиском в 1С.";
@@ -146,9 +248,26 @@ function headlineFor(mode, pilot) {
return "Я проверил доступный контур, но подтвержденного факта для ответа не получил.";
}
function nextStepFor(mode, pilot) {
if (isEntityResolutionPilot(pilot) && mode === "needs_clarification") {
return "Уточните точное название контрагента или добавьте ИНН, и я продолжу уже по нужной сущности в 1С.";
}
if (isEntityResolutionPilot(pilot) && mode === "confirmed_with_bounded_inference") {
return "Теперь могу продолжить уже по найденному контрагенту и искать документы, движения или денежный поток.";
}
if (isEntityResolutionPilot(pilot) &&
mode === "checked_sources_only" &&
pilot.derived_entity_resolution?.resolution_status === "not_found") {
return "Дайте точное название или ИНН, и я повторю поиск по каталогу 1С более прицельно.";
}
if (mode === "needs_clarification" && isMetadataLaneChoiceClarification(pilot)) {
return "Уточните, в какой контур идти дальше: по документам или по движениям/регистрам.";
}
if (mode === "needs_clarification" && isMovementLaneClarification(pilot)) {
return clarificationNextStepLine(pilot, "движениям/регистрам");
}
if (mode === "needs_clarification" && isDocumentLaneClarification(pilot)) {
return clarificationNextStepLine(pilot, "документам");
}
if (mode === "needs_clarification") {
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
}
@@ -196,6 +315,11 @@ function buildMustNotClaim(pilot) {
claims.push("Do not claim a document/register exists outside the checked metadata probe results.");
claims.push("Do not present the inferred next checked lane as already executed data retrieval.");
}
if (isEntityResolutionPilot(pilot)) {
claims.push("Do not present catalog grounding as confirmed business activity, turnover, or document evidence.");
claims.push("Do not claim legal identity uniqueness when several catalog candidates are still plausible.");
claims.push("Do not imply that the resolved entity has already been used in a downstream data probe.");
}
if (pilot.evidence.confirmed_facts.length === 0) {
claims.push("Do not claim a confirmed business fact when confirmed_facts is empty.");
}
@@ -279,6 +403,32 @@ function derivedMetadataInferenceLine(pilot) {
}
return `По подтвержденной metadata-поверхности следующий проверяемый шаг можно ограниченно оценить как ${routeLabel} через family «${surface.selected_entity_set}». Это еще не выполненный data-fetch, а только grounded выбор следующего контура.`;
}
function derivedEntityResolutionConfirmedLine(pilot) {
const resolution = pilot.derived_entity_resolution;
if (!resolution || resolution.resolution_status !== "resolved" || !resolution.resolved_entity) {
return null;
}
const requested = resolution.requested_entity ? ` по запросу "${resolution.requested_entity}"` : "";
const confidence = resolution.confidence === "high"
? " Точность совпадения выглядит высокой."
: resolution.confidence === "medium"
? " Совпадение выглядит достаточно сильным, но это все еще catalog grounding."
: " Совпадение выглядит вероятным, но его лучше считать рабочим заземлением сущности.";
return `В текущем каталожном срезе 1С${requested} найден контрагент "${resolution.resolved_entity}".${confidence}`;
}
function derivedEntityResolutionInferenceLine(pilot) {
const resolution = pilot.derived_entity_resolution;
if (!resolution) {
return null;
}
if (resolution.resolution_status === "resolved") {
return "Сейчас подтверждено только заземление сущности по каталогу 1С; документы, движения и денежные показатели по ней еще не проверялись.";
}
if (resolution.resolution_status === "ambiguous" && resolution.ambiguity_candidates.length > 0) {
return `В checked catalog slice есть несколько близких кандидатов: ${resolution.ambiguity_candidates.join(", ")}. Без уточнения нельзя честно выбрать одного контрагента для следующего шага.`;
}
return null;
}
function derivedValueFlowConfirmedLine(pilot) {
const flow = pilot.derived_value_flow;
if (!flow) {
@@ -365,11 +515,14 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
if (pilot.evidence.inferred_facts.length > 0) {
pushReason(reasonCodes, "answer_contains_bounded_inference");
}
const derivedInferenceLine = derivedActivityInferenceLine(pilot) ?? derivedMetadataInferenceLine(pilot);
const derivedInferenceLine = derivedActivityInferenceLine(pilot) ??
derivedMetadataInferenceLine(pilot) ??
derivedEntityResolutionInferenceLine(pilot);
const inferenceLines = derivedInferenceLine
? [derivedInferenceLine]
: pilot.evidence.inferred_facts;
const derivedMetadataLine = derivedMetadataConfirmedLine(pilot);
const derivedEntityResolutionLine = derivedEntityResolutionConfirmedLine(pilot);
const derivedValueLine = derivedBidirectionalValueFlowConfirmedLine(pilot) ?? derivedValueFlowConfirmedLine(pilot);
const monthlyConfirmedLines = derivedBidirectionalValueFlowMonthlyLines(pilot).length > 0
? derivedBidirectionalValueFlowMonthlyLines(pilot)
@@ -379,9 +532,11 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
}
const confirmedLines = derivedValueLine
? [...pilot.evidence.confirmed_facts, derivedValueLine, ...monthlyConfirmedLines]
: derivedMetadataLine
? [...pilot.evidence.confirmed_facts, derivedMetadataLine]
: pilot.evidence.confirmed_facts;
: derivedEntityResolutionLine
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
: derivedMetadataLine
? [...pilot.evidence.confirmed_facts, derivedMetadataLine]
: pilot.evidence.confirmed_facts;
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryAnswerAdapter",
@@ -11,6 +11,40 @@ const DEFAULT_DEPS = {
executeAddressMcpQuery: addressMcpClient_1.executeAddressMcpQuery,
executeAddressMcpMetadata: addressMcpClient_1.executeAddressMcpMetadata
};
const ENTITY_RESOLUTION_COUNTERPARTY_LOOKUP_LIMIT = 1000;
const ENTITY_RESOLUTION_COUNTERPARTY_QUERY_TEMPLATE = `
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
ПРЕДСТАВЛЕНИЕ(Контрагенты.Ссылка) КАК Контрагент,
ПРЕДСТАВЛЕНИЕ(Контрагенты.Ссылка) КАК Counterparty,
Контрагенты.Ссылка КАК КонтрагентСсылка,
Контрагенты.Ссылка КАК CounterpartyRef,
Контрагенты.Наименование КАК Наименование
ИЗ
Справочник.Контрагенты КАК Контрагенты
`;
const ENTITY_RESOLUTION_STOPWORDS = new Set([
"ооо",
"ао",
"зао",
"ип",
"llc",
"ltd",
"company",
"контрагент",
"counterparty",
"поставщик",
"supplier",
"клиент",
"customer",
"в",
"1с",
"1c",
"найди",
"найти",
"поищи",
"search",
"find"
]);
function toNonEmptyString(value) {
if (value === null || value === undefined) {
return null;
@@ -99,7 +133,149 @@ function buildValueFlowFilters(planner) {
sort: "period_asc"
};
}
function normalizeEntityResolutionText(value) {
return String(value ?? "")
.toLowerCase()
.replace(/ё/g, "е")
.replace(/[«»"'`]/g, " ")
.replace(/[^\p{L}\p{N}\s-]+/gu, " ")
.replace(/\s+/g, " ")
.trim();
}
function tokenizeEntityResolutionText(value) {
return normalizeEntityResolutionText(value)
.split(" ")
.map((token) => token.trim())
.filter((token) => token.length >= 2 && !ENTITY_RESOLUTION_STOPWORDS.has(token));
}
function isLowQualityEntityResolutionAnchor(value) {
return tokenizeEntityResolutionText(value).length <= 0;
}
function entityResolutionCandidateName(row) {
const candidates = [
row["Контрагент"],
row["Counterparty"],
row["Наименование"],
row["name"],
row["Name"],
row["registrator"],
row["Registrator"]
];
for (const candidate of candidates) {
const text = toNonEmptyString(candidate);
if (text) {
return text;
}
}
return null;
}
function entityResolutionCandidateRef(row) {
const candidates = [row["КонтрагентСсылка"], row["CounterpartyRef"], row["ref"], row["Ref"]];
for (const candidate of candidates) {
const text = toNonEmptyString(candidate);
if (text) {
return text;
}
}
return null;
}
function scoreEntityResolutionCandidate(name, requested) {
const normalizedName = normalizeEntityResolutionText(name);
const normalizedRequested = normalizeEntityResolutionText(requested);
const requestedTokens = tokenizeEntityResolutionText(requested);
if (!normalizedName || !normalizedRequested || requestedTokens.length <= 0) {
return null;
}
let score = 0;
if (normalizedName === normalizedRequested) {
score += 10_000;
}
else if (normalizedName.includes(normalizedRequested)) {
score += 5_000;
}
else if (normalizedRequested.includes(normalizedName) && normalizedName.length >= 4) {
score += 2_000;
}
for (const token of requestedTokens) {
if (!normalizedName.includes(token)) {
return null;
}
score += Math.max(40, token.length * 20);
}
score -= Math.abs(normalizedName.length - normalizedRequested.length);
return score;
}
function deriveEntityResolution(result, requestedEntity) {
if (!result || result.error || !requestedEntity) {
return null;
}
const checkedCandidates = uniqueCandidateStrings(result.raw_rows
.map((row) => entityResolutionCandidateName(row))
.filter((value) => Boolean(value)));
const scoredCandidates = checkedCandidates
.map((name) => {
const score = scoreEntityResolutionCandidate(name, requestedEntity);
return score === null ? null : { name, score };
})
.filter((value) => Boolean(value))
.sort((left, right) => right.score - left.score || left.name.length - right.name.length || left.name.localeCompare(right.name, "ru"));
if (scoredCandidates.length <= 0) {
return {
requested_entity: requestedEntity,
resolution_status: "not_found",
resolved_entity: null,
resolved_reference: null,
matched_rows: result.rows.length,
checked_candidates: checkedCandidates.slice(0, 12),
ambiguity_candidates: [],
confidence: null,
inference_basis: "catalog_counterparty_search_rows"
};
}
const bestCandidate = scoredCandidates[0];
const bestNormalized = normalizeEntityResolutionText(bestCandidate.name);
const requestedNormalized = normalizeEntityResolutionText(requestedEntity);
const requestedTokens = tokenizeEntityResolutionText(requestedEntity);
const exactMatch = bestNormalized === requestedNormalized;
const strongContains = requestedTokens.length > 1 && bestNormalized.includes(requestedNormalized);
const topCandidates = scoredCandidates.filter((candidate) => candidate.score === bestCandidate.score);
if (topCandidates.length > 1 && !exactMatch && !strongContains) {
return {
requested_entity: requestedEntity,
resolution_status: "ambiguous",
resolved_entity: null,
resolved_reference: null,
matched_rows: result.rows.length,
checked_candidates: checkedCandidates.slice(0, 12),
ambiguity_candidates: topCandidates.map((candidate) => candidate.name).slice(0, 6),
confidence: "low",
inference_basis: "catalog_counterparty_search_rows"
};
}
const matchedRow = result.raw_rows.find((row) => normalizeEntityResolutionText(entityResolutionCandidateName(row)) === bestNormalized) ?? null;
return {
requested_entity: requestedEntity,
resolution_status: "resolved",
resolved_entity: bestCandidate.name,
resolved_reference: matchedRow ? entityResolutionCandidateRef(matchedRow) : null,
matched_rows: result.rows.length,
checked_candidates: checkedCandidates.slice(0, 12),
ambiguity_candidates: [],
confidence: exactMatch ? "high" : strongContains ? "medium" : "low",
inference_basis: "catalog_counterparty_search_rows"
};
}
function uniqueCandidateStrings(values) {
const result = [];
for (const value of values) {
pushUnique(result, value);
}
return result;
}
function isLifecyclePilotEligible(planner) {
if (planner.selected_chain_id === "lifecycle") {
return true;
}
const meaning = planner.discovery_plan.turn_meaning_ref;
const domain = String(meaning?.asked_domain_family ?? "").toLowerCase();
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
@@ -108,6 +284,9 @@ function isLifecyclePilotEligible(planner) {
(combined.includes("lifecycle") || combined.includes("activity") || combined.includes("duration") || combined.includes("age")));
}
function isDocumentEvidencePilotEligible(planner) {
if (planner.selected_chain_id === "document_evidence") {
return true;
}
const meaning = planner.discovery_plan.turn_meaning_ref;
const domain = String(meaning?.asked_domain_family ?? "").toLowerCase();
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
@@ -117,6 +296,9 @@ function isDocumentEvidencePilotEligible(planner) {
(combined.includes("document") || combined.includes("list_documents")));
}
function isMovementEvidencePilotEligible(planner) {
if (planner.selected_chain_id === "movement_evidence") {
return true;
}
const meaning = planner.discovery_plan.turn_meaning_ref;
const domain = String(meaning?.asked_domain_family ?? "").toLowerCase();
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
@@ -131,6 +313,9 @@ function isMovementEvidencePilotEligible(planner) {
combined.includes("list_movements")));
}
function isValueFlowPilotEligible(planner) {
if (planner.selected_chain_id === "value_flow") {
return true;
}
const meaning = planner.discovery_plan.turn_meaning_ref;
const domain = String(meaning?.asked_domain_family ?? "").toLowerCase();
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
@@ -144,6 +329,10 @@ function isValueFlowPilotEligible(planner) {
combined.includes("value")));
}
function isMetadataPilotEligible(planner) {
if (planner.selected_chain_id === "metadata_inspection" ||
planner.selected_chain_id === "metadata_lane_clarification") {
return true;
}
const meaning = planner.discovery_plan.turn_meaning_ref;
const domain = String(meaning?.asked_domain_family ?? "").toLowerCase();
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
@@ -158,6 +347,22 @@ function isMetadataPilotEligible(planner) {
combined.includes("inspect_registers") ||
combined.includes("inspect_fields")));
}
function isEntityResolutionPilotEligible(planner) {
if (planner.selected_chain_id === "entity_resolution") {
return true;
}
const meaning = planner.discovery_plan.turn_meaning_ref;
const domain = String(meaning?.asked_domain_family ?? "").toLowerCase();
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
const unsupported = String(meaning?.unsupported_but_understood_family ?? "").toLowerCase();
const semanticNeed = String(planner.semantic_data_need ?? "").toLowerCase();
const combined = `${domain} ${action} ${unsupported} ${semanticNeed}`;
return (planner.proposed_primitives.includes("search_business_entity") &&
(combined.includes("entity_resolution") ||
combined.includes("search_business_entity") ||
combined.includes("entity discovery") ||
combined.includes("counterparty search")));
}
function metadataScopeForPlanner(planner) {
const entityCandidate = firstEntityCandidate(planner);
if (entityCandidate) {
@@ -441,6 +646,15 @@ function summarizeMetadataRows(result) {
}
return `${result.fetched_rows} MCP metadata rows fetched`;
}
function summarizeEntityResolutionRows(result) {
if (result.error) {
return null;
}
if (result.fetched_rows <= 0) {
return "0 MCP catalog rows fetched";
}
return `${result.fetched_rows} MCP catalog rows fetched for entity search`;
}
function metadataRowText(row, keys) {
for (const key of keys) {
const text = toNonEmptyString(row[key]);
@@ -475,6 +689,18 @@ function metadataEntitySet(row) {
"kind"
]);
}
function inferMetadataEntitySetFromObjectName(objectName) {
const text = String(objectName ?? "").trim();
if (!text) {
return null;
}
const dotIndex = text.indexOf(".");
if (dotIndex <= 0) {
return null;
}
const entitySet = text.slice(0, dotIndex).trim();
return entitySet.length > 0 ? entitySet : null;
}
function metadataChildNames(value) {
if (!Array.isArray(value)) {
return [];
@@ -604,7 +830,7 @@ function deriveMetadataSurface(result, metadataScope, requestedMetaTypes) {
if (objectName) {
pushUnique(matchedObjects, objectName);
}
const entitySet = metadataEntitySet(row);
const entitySet = metadataEntitySet(row) ?? inferMetadataEntitySetFromObjectName(objectName);
if (entitySet) {
pushUnique(availableEntitySets, entitySet);
}
@@ -678,6 +904,53 @@ function buildMetadataUnknownFacts(surface, metadataScope) {
}
return ["No matching 1C metadata objects were confirmed by this MCP metadata probe"];
}
function buildEntityResolutionConfirmedFacts(resolution) {
if (!resolution || resolution.resolution_status !== "resolved" || !resolution.resolved_entity) {
return [];
}
if (resolution.requested_entity && normalizeEntityResolutionText(resolution.requested_entity) === normalizeEntityResolutionText(resolution.resolved_entity)) {
return [`В проверенном каталожном срезе 1С найден контрагент: ${resolution.resolved_entity}`];
}
return [
`В проверенном каталожном срезе 1С найден наиболее вероятный контрагент: ${resolution.resolved_entity}`
];
}
function buildEntityResolutionInferredFacts(resolution) {
if (!resolution) {
return [];
}
if (resolution.resolution_status === "resolved") {
const facts = ["Пока проверено только заземление сущности по каталогу 1С; документы, движения и денежные показатели еще не проверялись"];
if (resolution.requested_entity && resolution.resolved_entity) {
const requestedNormalized = normalizeEntityResolutionText(resolution.requested_entity);
const resolvedNormalized = normalizeEntityResolutionText(resolution.resolved_entity);
if (requestedNormalized !== resolvedNormalized) {
facts.push("Контрагент выбран как ближайшее подтвержденное совпадение имени в проверенном каталоге 1С");
}
}
return facts;
}
if (resolution.resolution_status === "ambiguous") {
return ["В проверенном каталожном срезе осталось несколько близких кандидатов, поэтому точного контрагента в 1С еще нужно уточнить"];
}
return [];
}
function buildEntityResolutionUnknownFacts(resolution, requestedEntity) {
if (!resolution) {
return ["По проверенному каталожному поиску 1С не удалось заземлить сущность контрагента"];
}
const unknownFacts = ["Документы, движения и денежные показатели по этому контрагенту еще не проверялись; пока был только каталожный поиск"];
if (resolution.resolution_status === "ambiguous" && resolution.ambiguity_candidates.length > 0) {
unknownFacts.unshift(`Точное заземление контрагента в 1С остается неоднозначным между вариантами: ${resolution.ambiguity_candidates.join(", ")}`);
return unknownFacts;
}
if (resolution.resolution_status === "not_found") {
unknownFacts.unshift(requestedEntity
? `В проверенном каталожном срезе 1С не подтвержден контрагент с именем "${requestedEntity}"`
: "В проверенном каталожном срезе 1С не подтвержден подходящий контрагент");
}
return unknownFacts;
}
function rowDateValue(row) {
const candidates = [
row["Период"],
@@ -1149,19 +1422,24 @@ function buildEmptyEvidence(planner, dryRun, probeResults, reason) {
});
}
function pilotScopeForPlanner(planner) {
if (isMetadataPilotEligible(planner)) {
return "metadata_inspection_v1";
switch (planner.selected_chain_id) {
case "metadata_lane_clarification":
case "metadata_inspection":
return "metadata_inspection_v1";
case "movement_evidence":
return "counterparty_movement_evidence_query_movements_v1";
case "value_flow":
return valueFlowPilotProfile(planner).scope;
case "document_evidence":
return "counterparty_document_evidence_query_documents_v1";
case "lifecycle":
return "counterparty_lifecycle_query_documents_v1";
case "entity_resolution":
return "entity_resolution_search_v1";
}
if (isMovementEvidencePilotEligible(planner)) {
return "counterparty_movement_evidence_query_movements_v1";
}
if (isValueFlowPilotEligible(planner)) {
return valueFlowPilotProfile(planner).scope;
}
if (isDocumentEvidencePilotEligible(planner)) {
return "counterparty_document_evidence_query_documents_v1";
}
return "counterparty_lifecycle_query_documents_v1";
}
function isLivePilotChainSupported(chainId) {
return true;
}
async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
const runtimeDeps = {
@@ -1191,6 +1469,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
@@ -1214,6 +1493,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
@@ -1226,7 +1506,15 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
const movementPilotEligible = isMovementEvidencePilotEligible(planner);
const lifecyclePilotEligible = isLifecyclePilotEligible(planner);
const valueFlowPilotEligible = isValueFlowPilotEligible(planner);
if (!metadataPilotEligible && !documentPilotEligible && !movementPilotEligible && !lifecyclePilotEligible && !valueFlowPilotEligible) {
const entityResolutionPilotEligible = isEntityResolutionPilotEligible(planner);
const livePilotChainSupported = isLivePilotChainSupported(planner.selected_chain_id);
if (!livePilotChainSupported ||
(!metadataPilotEligible &&
!documentPilotEligible &&
!movementPilotEligible &&
!lifecyclePilotEligible &&
!valueFlowPilotEligible &&
!entityResolutionPilotEligible)) {
pushReason(reasonCodes, "pilot_scope_unsupported_for_live_execution");
for (const step of dryRun.execution_steps) {
skippedPrimitives.push(step.primitive_id);
@@ -1246,6 +1534,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
@@ -1309,6 +1598,96 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
evidence,
source_rows_summary: sourceRowsSummary,
derived_metadata_surface: derivedMetadataSurface,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: queryLimitations,
reason_codes: reasonCodes
};
}
if (entityResolutionPilotEligible) {
let queryResult = null;
const requestedEntity = counterparty;
if (isLowQualityEntityResolutionAnchor(requestedEntity)) {
pushReason(reasonCodes, "pilot_entity_resolution_anchor_missing_or_low_quality");
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "Entity-resolution needs a clearer counterparty name");
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "skipped_needs_clarification",
pilot_scope: "entity_resolution_search_v1",
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: ["Entity-resolution needs a clearer counterparty name"],
reason_codes: reasonCodes
};
}
for (const step of dryRun.execution_steps) {
if (step.primitive_id !== "search_business_entity") {
skippedPrimitives.push(step.primitive_id);
probeResults.push(skippedProbeResult(step, "pilot_only_executes_search_business_entity"));
continue;
}
queryResult = await runtimeDeps.executeAddressMcpQuery({
query: ENTITY_RESOLUTION_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(ENTITY_RESOLUTION_COUNTERPARTY_LOOKUP_LIMIT)),
limit: ENTITY_RESOLUTION_COUNTERPARTY_LOOKUP_LIMIT
});
pushUnique(executedPrimitives, step.primitive_id);
probeResults.push(queryResultToProbeResult(step.primitive_id, queryResult));
if (queryResult.error) {
pushUnique(queryLimitations, queryResult.error);
pushReason(reasonCodes, "pilot_search_business_entity_mcp_error");
}
else {
pushReason(reasonCodes, "pilot_search_business_entity_mcp_executed");
}
}
const sourceRowsSummary = queryResult ? summarizeEntityResolutionRows(queryResult) : null;
const derivedEntityResolution = deriveEntityResolution(queryResult, requestedEntity);
if (derivedEntityResolution?.resolution_status === "resolved") {
pushReason(reasonCodes, "pilot_derived_entity_resolution_from_catalog_rows");
}
if (derivedEntityResolution?.resolution_status === "ambiguous") {
pushReason(reasonCodes, "pilot_entity_resolution_ambiguity_requires_clarification");
}
if (derivedEntityResolution?.resolution_status === "not_found") {
pushReason(reasonCodes, "pilot_entity_resolution_not_found_in_checked_catalog");
}
const evidence = (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({
plan: planner.discovery_plan,
probeResults,
confirmedFacts: buildEntityResolutionConfirmedFacts(derivedEntityResolution),
inferredFacts: buildEntityResolutionInferredFacts(derivedEntityResolution),
unknownFacts: buildEntityResolutionUnknownFacts(derivedEntityResolution, requestedEntity),
sourceRowsSummary,
queryLimitations,
recommendedNextProbe: "resolve_entity_reference"
});
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "executed",
pilot_scope: "entity_resolution_search_v1",
dry_run: dryRun,
mcp_execution_performed: executedPrimitives.length > 0,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: sourceRowsSummary,
derived_metadata_surface: null,
derived_entity_resolution: derivedEntityResolution,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
@@ -1336,6 +1715,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
@@ -1389,6 +1769,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
evidence,
source_rows_summary: sourceRowsSummary,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
@@ -1416,6 +1797,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
@@ -1469,6 +1851,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
evidence,
source_rows_summary: sourceRowsSummary,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
@@ -1501,6 +1884,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
@@ -1593,6 +1977,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
evidence,
source_rows_summary: sourceRowsSummary,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: derivedBidirectionalValueFlow,
@@ -1618,6 +2003,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
@@ -1690,6 +2076,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
evidence,
source_rows_summary: sourceRowsSummary,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: derivedValueFlow,
derived_bidirectional_value_flow: null,
@@ -1716,6 +2103,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
@@ -1773,6 +2161,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
evidence,
source_rows_summary: sourceRowsSummary,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: derivedActivityPeriod,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
@@ -86,6 +86,8 @@ function recipeFor(input) {
pushUnique(axes, "lane_family_choice");
return {
semanticDataNeed: "metadata lane clarification",
chainId: "metadata_lane_clarification",
chainSummary: "Preserve the ambiguous metadata surface and ask the user to choose the next data lane before running MCP probes.",
primitives: [],
axes,
reason: "planner_selected_metadata_lane_clarification_recipe"
@@ -100,6 +102,8 @@ function recipeFor(input) {
}
return {
semanticDataNeed: "counterparty value-flow evidence",
chainId: "value_flow",
chainSummary: "Resolve the business entity, query scoped movements, aggregate checked amounts, then probe coverage before answering.",
primitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
axes,
reason: requestedAggregationAxis === "month"
@@ -113,6 +117,8 @@ function recipeFor(input) {
pushUnique(axes, "evidence_basis");
return {
semanticDataNeed: "counterparty lifecycle evidence",
chainId: "lifecycle",
chainSummary: "Resolve the business entity, query supporting documents, probe coverage, then explain the evidence basis for the inferred activity window.",
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage", "explain_evidence_basis"],
axes,
reason: "planner_selected_lifecycle_recipe"
@@ -122,6 +128,8 @@ function recipeFor(input) {
pushUnique(axes, "metadata_scope");
return {
semanticDataNeed: "1C metadata evidence",
chainId: "metadata_inspection",
chainSummary: "Inspect the 1C metadata surface first, then ground the next safe lane from confirmed schema evidence.",
primitives: ["inspect_1c_metadata"],
axes,
reason: "planner_selected_metadata_recipe"
@@ -131,6 +139,8 @@ function recipeFor(input) {
pushUnique(axes, "coverage_target");
return {
semanticDataNeed: "movement evidence",
chainId: "movement_evidence",
chainSummary: "Resolve the business entity, fetch scoped movement rows, and probe coverage without pretending to have a full movement universe.",
primitives: ["resolve_entity_reference", "query_movements", "probe_coverage"],
axes,
reason: "planner_selected_movement_recipe"
@@ -140,6 +150,8 @@ function recipeFor(input) {
pushUnique(axes, "coverage_target");
return {
semanticDataNeed: "document evidence",
chainId: "document_evidence",
chainSummary: "Resolve the business entity, fetch scoped document rows, and probe coverage before stating the checked document evidence.",
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
axes,
reason: "planner_selected_document_recipe"
@@ -147,8 +159,11 @@ function recipeFor(input) {
}
if (hasEntity(meaning)) {
pushUnique(axes, "business_entity");
pushUnique(axes, "coverage_target");
return {
semanticDataNeed: "entity discovery evidence",
chainId: "entity_resolution",
chainSummary: "Search candidate business entities, resolve the most relevant 1C reference, and prove whether the entity grounding is stable enough for the next probe.",
primitives: ["search_business_entity", "resolve_entity_reference", "probe_coverage"],
axes,
reason: "planner_selected_entity_resolution_recipe"
@@ -156,6 +171,8 @@ function recipeFor(input) {
}
return {
semanticDataNeed: "unclassified 1C discovery need",
chainId: "metadata_inspection",
chainSummary: "Start with metadata inspection instead of guessing a deeper fact route when the business need is still under-specified.",
primitives: ["inspect_1c_metadata"],
axes,
reason: "planner_selected_clarification_recipe"
@@ -202,6 +219,8 @@ function planAssistantMcpDiscovery(input) {
policy_owner: "assistantMcpDiscoveryPlanner",
planner_status: plannerStatus,
semantic_data_need: semanticDataNeed,
selected_chain_id: recipe.chainId,
selected_chain_summary: recipe.chainSummary,
proposed_primitives: recipe.primitives,
required_axes: recipe.axes,
discovery_plan: plan,
@@ -79,6 +79,7 @@ function normalizeTurnMeaning(value) {
const dateScope = toNonEmptyString(value.explicit_date_scope);
const unsupported = toNonEmptyString(value.unsupported_but_understood_family);
const entities = toStringList(value.explicit_entity_candidates);
const metadataAmbiguityEntitySets = toStringList(value.metadata_ambiguity_entity_sets);
if (domain) {
result.asked_domain_family = domain;
}
@@ -91,6 +92,9 @@ function normalizeTurnMeaning(value) {
if (entities.length > 0) {
result.explicit_entity_candidates = entities;
}
if (metadataAmbiguityEntitySets.length > 0) {
result.metadata_ambiguity_entity_sets = metadataAmbiguityEntitySets;
}
if (organization) {
result.explicit_organization_scope = organization;
}
@@ -36,6 +36,22 @@ function pushUnique(target, value) {
target.push(text);
}
}
function canonicalizeEntityResolutionCandidate(value) {
return normalizeEntityResolutionCandidate(value)
.replace(/^(?:\u0441\s+\u043d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435\u043c\s+)/iu, "")
.replace(/\s+(?:\u0432\s+\u0441\u0438\u0441\u0442\u0435\u043c\u0435\s*1\u0421|\u0432\s+1c|in\s+(?:the\s+)?1c\s+system|in\s+1c)\s*$/iu, "")
.trim();
}
function pushNormalizedEntityResolutionCandidate(target, value) {
const text = toNonEmptyString(value);
if (!text) {
return;
}
const normalized = canonicalizeEntityResolutionCandidate(text);
if (normalized && !target.includes(normalized)) {
target.push(normalized);
}
}
function compactLower(value) {
return String(value ?? "")
.toLowerCase()
@@ -263,11 +279,11 @@ function hasMetadataSignal(text) {
if (/(?:\u043c\u0435\u0442\u0430\u0434\u0430\u043d|schema|catalog|metadata\s+surface|\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440[\u0430\u044b]\s+1\u0441|\u0441\u0445\u0435\u043c[\u0430\u044b]\s+1\u0441)/iu.test(text)) {
return true;
}
return (/(?:\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u044b|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b|\u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a\u0438|\u043f\u043e\u043b(?:\u0435|\u044f)|registers?|documents?|catalogs?|fields?)/iu.test(text) &&
/(?:\u0435\u0441\u0442\u044c|\u0434\u043e\u0441\u0442\u0443\u043f\u043d|\u0432\s+1\u0441|available|exist)/iu.test(text));
return (/(?:\u043e\u0431\u044a\u0435\u043a\u0442(?:\u044b|\u0430|\u043e\u0432)?|\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u044b|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b|\u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a\u0438|\u043f\u043e\u043b(?:\u0435|\u044f)|objects?|registers?|documents?|catalogs?|fields?)/iu.test(text) &&
/(?:\u0435\u0441\u0442\u044c|\u043a\u0430\u043a\u0438\u0435|\u0434\u043e\u0441\u0442\u0443\u043f\u043d|\u0432\s+1\u0441|1\u0441|available|exist|which)/iu.test(text));
}
function hasMetadataObjectHint(text) {
return /(?:\u0440\u0435\u0433\u0438\u0441\u0442\u0440(?:\u044b)?|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|\u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a(?:\u0438)?|\u043f\u043e\u043b(?:\u0435|\u044f)|registers?|documents?|catalogs?|fields?)/iu.test(text);
return /(?:\u043e\u0431\u044a\u0435\u043a\u0442(?:\u044b|\u0430|\u043e\u0432)?|\u0440\u0435\u0433\u0438\u0441\u0442\u0440(?:\u044b)?|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|\u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a(?:\u0438)?|\u043f\u043e\u043b(?:\u0435|\u044f)|objects?|registers?|documents?|catalogs?|fields?)/iu.test(text);
}
function hasDocumentEvidenceFollowupSignal(text) {
return /(?:\u043f\u043e\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u0430\u043c|\u044b)?|\u0434\u0430\u0432\u0430\u0439\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|\u0438\u0449\u0438\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|\u043f\u043e\u043a\u0430\u0436\u0438\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|(?:\u043f\u043e\u043a\u0430\u0436\u0438|\u043a\u0430\u043a\u0438\u0435|\u0441\u043f\u0438\u0441\u043e\u043a|\u0434\u0430\u0439|\u0438\u0449\u0438)\s+(?:\u0441\u0447(?:[еe]т|\u0435\u0442)[-\u2011 ]?\u0444\u0430\u043a\u0442\u0443\u0440(?:\u044b|\u0430)?|\u043d\u0430\u043a\u043b\u0430\u0434\u043d(?:\u044b\u0435|\u0430\u044f)?|\u0430\u043a\u0442(?:\u044b)?|\u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446(?:\u0438\u0438|\u0438\u044e)|invoice(?:s)?|bill(?:s)?|waybill(?:s)?)|document(?:s)?\s+(?:then|next)?|(?:then|next)\s+documents?|go\s+to\s+documents?)/iu.test(text);
@@ -278,7 +294,39 @@ function hasMovementEvidenceFollowupSignal(text) {
function hasMetadataDownstreamContinuationSignal(text) {
return /(?:\u0434\u0430\u0432\u0430\u0439\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u0438\u0434(?:\u0435|\u0451)\u043c\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u043f\u043e\u0448\u043b(?:\u0438|\u0451\u043c)\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u0439|\u0438\u0449\u0438\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u0438\u0449\u0438\s+\u0434\u0430\u043d\u043d\u044b\u0435|\u043f\u043e\u043a\u0430\u0436\u0438\s+\u0434\u0430\u043d\u043d\u044b\u0435|\u043f\u043e\u043a\u0430\u0436\u0438\s+\u0441\u0442\u0440\u043e\u043a\u0438|\u0433\u043b\u0443\u0431\u0436\u0435|\u0447\u0442\u043e\s+\u0434\u0430\u043b\u044c\u0448\u0435|continue|go\s+ahead|go\s+deeper|look\s+deeper|drill\s+down|show\s+(?:data|rows))/iu.test(text);
}
function hasEntityResolutionSignal(text) {
const hasSearchVerb = /(?:найд(?:и|ите|ем|у)|поищ(?:и|ите|ем)|найти|поиск|search|find|look\s*up)/iu.test(text);
const hasEntityNoun = /(?:контрагент(?:а|ов)?|поставщик(?:а|ов)?|клиент(?:а|ов)?|counterpart(?:y|ies)|supplier(?:s)?|customer(?:s)?)/iu.test(text);
return hasSearchVerb && hasEntityNoun;
}
function normalizeEntityResolutionCandidate(value) {
return value
.replace(/^(?:в\s*1с\s+|в\s+1c\s+|по\s+имени\s+)/iu, "")
.replace(/[?!.]+$/gu, "")
.replace(/^(?:контрагент(?:а|ов)?|поставщик(?:а|ов)?|клиент(?:а|ов)?)\s+/iu, "")
.replace(/^(?:counterpart(?:y|ies)|supplier(?:s)?|customer(?:s)?)\s+/iu, "")
.replace(/^[«"'\s]+|[»"'\s]+$/gu, "")
.replace(/\s+/g, " ")
.trim();
}
function rawEntityResolutionCandidate(text) {
const patterns = [
/(?:найд(?:и|ите|ем|у)|поищ(?:и|ите|ем)|найти|search|find|look\s*up)\s+(?:в\s*1с\s+|в\s+1c\s+)?(?:контрагент(?:а|ов)?|поставщик(?:а|ов)?|клиент(?:а|ов)?|counterpart(?:y|ies)|supplier(?:s)?|customer(?:s)?)\s+(.+)$/iu,
/(?:контрагент(?:а|ов)?|поставщик(?:а|ов)?|клиент(?:а|ов)?|counterpart(?:y|ies)|supplier(?:s)?|customer(?:s)?)\s+(.+?)\s+(?:найд(?:и|ите|ем|у)|поищ(?:и|ите|ем)|найти|search|find|look\s*up)\b/iu
];
for (const pattern of patterns) {
const match = text.match(pattern);
const candidate = normalizeEntityResolutionCandidate(match?.[1] ?? "");
if (candidate.length >= 2) {
return candidate;
}
}
return null;
}
function metadataActionFromRawText(text) {
if (/(?:\u043e\u0431\u044a\u0435\u043a\u0442(?:\u044b|\u0430|\u043e\u0432)?|objects?)/iu.test(text)) {
return "inspect_surface";
}
if (/(?:\u043f\u043e\u043b(?:\u0435|\u044f)|field)/iu.test(text)) {
return "inspect_fields";
}
@@ -293,6 +341,18 @@ function metadataActionFromRawText(text) {
}
return "inspect_catalog";
}
function metadataScopeHintFromRawText(text) {
if (/(?:\u043d\u0434\u0441|vat)/iu.test(text)) {
return "\u041d\u0414\u0421";
}
if (/(?:\u0441\u043a\u043b\u0430\u0434|inventory|stock|warehouse|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442\u0443\u0440)/iu.test(text)) {
return "\u0441\u043a\u043b\u0430\u0434";
}
if (/(?:\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|counterparty|customer|client|supplier|vendor)/iu.test(text)) {
return "\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442";
}
return null;
}
function hasExplicitDateScopeLiteral(text) {
return /(?:\b(?:19|20)\d{2}\b|\b\d{4}-\d{2}-\d{2}\b|\b\d{4}-\d{2}\b)/iu.test(text);
}
@@ -322,6 +382,10 @@ function semanticNeedFor(input) {
if (input.valueFlowSignal || /(?:turnover|revenue|payment|payout|value|net|netting|balance|cashflow)/iu.test(combined)) {
return "counterparty value-flow evidence";
}
if (input.entityResolutionSignal ||
/(?:entity_resolution|search_business_entity|resolve_entity_reference|entity\s+discovery|counterparty\s+search)/iu.test(combined)) {
return "entity discovery evidence";
}
if (/(?:movement|movements|bank_operations|movement_evidence|list_movements)/iu.test(combined)) {
return "movement evidence";
}
@@ -337,6 +401,9 @@ function shouldRunDiscovery(input) {
if (input.metadataSignal) {
return true;
}
if (input.entityResolutionSignal) {
return true;
}
if (input.valueFlowSignal && !input.explicitIntentCandidate) {
return true;
}
@@ -355,15 +422,23 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const predecomposeEntities = collectPredecomposeEntities(predecomposeContract);
const followupSeed = collectFollowupDiscoverySeed(followupContext);
const reasonCodes = [];
const rawText = compactLower(`${input.userMessage ?? ""} ${input.effectiveMessage ?? ""}`);
const rawUserText = toNonEmptyString(input.userMessage);
const rawEffectiveText = toNonEmptyString(input.effectiveMessage);
const rawSignalSourceText = `${rawUserText ?? ""} ${rawEffectiveText ?? ""}`.trim();
const rawEntitySourceText = rawUserText ?? rawEffectiveText ?? rawSignalSourceText;
const rawText = compactLower(rawSignalSourceText);
const rawLifecycleSignal = hasLifecycleSignal(rawText);
const rawBidirectionalValueFlowSignal = !rawLifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
const rawValueFlowSignal = !rawLifecycleSignal && (hasValueFlowSignal(rawText) || rawBidirectionalValueFlowSignal);
const rawMetadataSignal = !rawLifecycleSignal && !rawValueFlowSignal && hasMetadataSignal(rawText);
const rawEntityResolutionSignal = !rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
const rawPayoutSignal = rawValueFlowSignal && !rawBidirectionalValueFlowSignal && hasPayoutSignal(rawText);
const monthlyAggregationSignal = hasMonthlyAggregationSignal(rawText);
const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(rawText);
const rawDateScope = collectDateScopeFromRawText(rawText);
const rawMetadataScopeHint = rawMetadataSignal ? metadataScopeHintFromRawText(rawText) : null;
const rawEntityCandidate = rawEntityResolutionSignal ? rawEntityResolutionCandidate(rawEntitySourceText) : null;
const entityResolutionSignal = rawEntityResolutionSignal || Boolean(rawEntityCandidate);
const metadataDocumentHintSignal = hasDocumentEvidenceFollowupSignal(rawText);
const metadataMovementHintSignal = hasMovementEvidenceFollowupSignal(rawText);
const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family);
@@ -508,13 +583,26 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
unsupported: unsupported ?? seededUnsupported,
lifecycleSignal,
valueFlowSignal,
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
entityResolutionSignal
});
const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
pushUnique(entityCandidates, predecomposeEntities.counterparty);
pushUnique(entityCandidates, followupSeed.counterparty);
const entityCandidates = entityResolutionSignal ? [] : collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
if (entityResolutionSignal) {
pushNormalizedEntityResolutionCandidate(entityCandidates, rawEntityCandidate);
for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) {
pushNormalizedEntityResolutionCandidate(entityCandidates, candidate);
}
pushNormalizedEntityResolutionCandidate(entityCandidates, predecomposeEntities.counterparty);
pushNormalizedEntityResolutionCandidate(entityCandidates, followupSeed.counterparty);
}
else {
pushUnique(entityCandidates, predecomposeEntities.counterparty);
pushUnique(entityCandidates, followupSeed.counterparty);
pushUnique(entityCandidates, rawEntityCandidate);
}
if ((rawMetadataSignal || metadataFollowupSeedApplicable) && !followupSeed.counterparty) {
pushUnique(entityCandidates, followupSeed.discoveryEntity);
pushUnique(entityCandidates, rawMetadataScopeHint);
}
if (valueFlowSignal && !predecomposeEntities.counterparty && !followupSeed.counterparty) {
pushUnique(entityCandidates, predecomposeEntities.organization);
@@ -533,9 +621,11 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
? "movements"
: metadataGroundedDocumentLaneApplicable
? "documents"
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
? "metadata"
: rawDomain ?? seededDomain,
: entityResolutionSignal
? "entity_resolution"
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
? "metadata"
: rawDomain ?? seededDomain,
asked_action_family: lifecycleSignal
? "activity_duration"
: valueFlowSignal
@@ -548,9 +638,11 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
? "list_movements"
: metadataGroundedDocumentLaneApplicable
? "list_documents"
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
? metadataActionFromRawText(rawText) ?? seededAction
: rawAction ?? seededAction,
: entityResolutionSignal
? "search_business_entity"
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
? metadataActionFromRawText(rawText) ?? seededAction
: rawAction ?? seededAction,
asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis,
explicit_entity_candidates: entityCandidates,
metadata_ambiguity_entity_sets: metadataAmbiguityLaneClarificationApplicable && followupSeed.metadataAmbiguityEntitySets.length > 0
@@ -573,11 +665,13 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
? "document_evidence"
: metadataAmbiguityLaneClarificationApplicable
? "metadata_lane_choice_clarification"
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
? "1c_metadata_surface"
: followupDiscoverySeedApplicable
? seededUnsupported
: null),
: entityResolutionSignal
? "entity_resolution"
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
? "1c_metadata_surface"
: followupDiscoverySeedApplicable
? seededUnsupported
: null),
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden ||
unsupported ||
lifecycleSignal ||
@@ -585,6 +679,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
metadataGroundedMovementLaneApplicable ||
metadataGroundedDocumentLaneApplicable ||
metadataAmbiguityLaneClarificationApplicable ||
entityResolutionSignal ||
rawMetadataSignal ||
effectiveMetadataFollowupSeedApplicable ||
followupDiscoverySeedApplicable)
@@ -622,6 +717,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
lifecycleSignal,
valueFlowSignal,
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
entityResolutionSignal,
semanticDataNeed,
explicitIntentCandidate,
followupDiscoverySeedApplicable: followupDiscoverySeedApplicable ||
@@ -645,9 +741,11 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
? "raw_text"
: valueFlowSignal
? "raw_text"
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
: entityResolutionSignal
? "raw_text"
: "none";
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
? "raw_text"
: "none";
if (lifecycleSignal) {
pushReason(reasonCodes, "mcp_discovery_lifecycle_signal_detected");
}
@@ -657,6 +755,15 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (rawMetadataSignal) {
pushReason(reasonCodes, "mcp_discovery_metadata_signal_detected");
}
if (entityResolutionSignal) {
pushReason(reasonCodes, "mcp_discovery_entity_resolution_signal_detected");
}
if (rawMetadataScopeHint) {
pushReason(reasonCodes, "mcp_discovery_metadata_scope_hint_from_raw_text");
}
if (rawEntityCandidate) {
pushReason(reasonCodes, "mcp_discovery_entity_scope_from_raw_entity_search");
}
if (payoutSignal) {
pushReason(reasonCodes, "mcp_discovery_payout_signal_detected");
}