Очеловечить MCP ответы и broad-eval recap

This commit is contained in:
dctouch 2026-05-01 22:38:23 +03:00
parent 4c00d8c854
commit da3a148918
6 changed files with 295 additions and 73 deletions

View File

@ -53,7 +53,11 @@ function isInternalMechanicsLine(value) {
text.includes("catalog_") || text.includes("catalog_") ||
text.includes("scope is not implemented yet") || text.includes("scope is not implemented yet") ||
text.includes("needs more scope before execution") || text.includes("needs more scope before execution") ||
text.includes("mcp_execution_performed")); text.includes("mcp_execution_performed")
|| text.includes("confirmed 1c metadata surface")
|| text.includes("metadata surface family scores")
|| text.includes("available metadata object sets")
|| text.includes("selected metadata"));
} }
function isMcpTransportFailureLine(value) { function isMcpTransportFailureLine(value) {
const text = value.toLowerCase(); const text = value.toLowerCase();
@ -344,16 +348,16 @@ function headlineFor(mode, pilot) {
return `По движениям${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`; return `По движениям${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
} }
if (isCatalogDrilldownPilot(pilot) && mode === "confirmed_with_bounded_inference") { if (isCatalogDrilldownPilot(pilot) && mode === "confirmed_with_bounded_inference") {
return "По метаданным 1С удалось углубиться в контур справочников и связанных объектов; это уже не общий обзор схемы, а следующий безопасный catalog drilldown."; return "По схеме 1С удалось углубиться в контур справочников и связанных объектов; это следующий безопасный шаг по проверенной схеме, а не бизнес-обороты.";
} }
if (pilot.derived_metadata_surface && mode === "confirmed_with_bounded_inference") { if (pilot.derived_metadata_surface && mode === "confirmed_with_bounded_inference") {
if (pilot.derived_metadata_surface.ambiguity_detected) { if (pilot.derived_metadata_surface.ambiguity_detected) {
return "По метаданным 1С найдены конкурирующие schema-поверхности; перед следующим шагом нужно удержать неоднозначность явно."; return "По схеме 1С найдены несколько конкурирующих контуров; перед следующим шагом нужно явно выбрать нужный тип данных.";
} }
if (pilot.derived_metadata_surface.downstream_route_family) { if (pilot.derived_metadata_surface.downstream_route_family) {
return "По метаданным 1С найдена схема и заземлена вероятная поверхность для следующего безопасного шага."; return "По схеме 1С найдены подходящие объекты; можно безопасно выбрать следующий контур проверки.";
} }
return "По метаданным 1С найдена доступная схема для дальнейшего безопасного поиска."; return "По схеме 1С найдены доступные объекты для дальнейшего безопасного поиска.";
} }
if (askedMonthlyBreakdown && pilot.derived_bidirectional_value_flow && mode === "confirmed_with_bounded_inference") { if (askedMonthlyBreakdown && pilot.derived_bidirectional_value_flow && mode === "confirmed_with_bounded_inference") {
return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто и помесячная раскладка могут называться только как расчет по найденным строкам и проверенному периоду."; return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто и помесячная раскладка могут называться только как расчет по найденным строкам и проверенному периоду.";
@ -392,7 +396,7 @@ function headlineFor(mode, pilot) {
return "Точный факт не подтвержден, но есть ограниченная оценка по найденной активности в 1С."; return "Точный факт не подтвержден, но есть ограниченная оценка по найденной активности в 1С.";
} }
if (mode === "needs_clarification" && isMetadataLaneChoiceClarification(pilot)) { if (mode === "needs_clarification" && isMetadataLaneChoiceClarification(pilot)) {
return "По подтвержденной metadata-поверхности видно несколько конкурирующих data-lane, и без явного выбора дальше идти нельзя."; return "По проверенной схеме 1С видно несколько возможных контуров, и без явного выбора дальше идти нельзя.";
} }
if (mode === "needs_clarification" && isMovementLaneClarification(pilot)) { if (mode === "needs_clarification" && isMovementLaneClarification(pilot)) {
const need = clarificationNeedRu(pilot); const need = clarificationNeedRu(pilot);
@ -468,11 +472,11 @@ function nextStepFor(mode, pilot) {
if (mode === "confirmed_with_bounded_inference" && pilot.derived_metadata_surface) { if (mode === "confirmed_with_bounded_inference" && pilot.derived_metadata_surface) {
const surface = pilot.derived_metadata_surface; const surface = pilot.derived_metadata_surface;
if (surface.ambiguity_detected && surface.ambiguity_entity_sets.length > 0) { if (surface.ambiguity_detected && surface.ambiguity_entity_sets.length > 0) {
return `Следующим шагом лучше сузить surface до одного семейства: ${surface.ambiguity_entity_sets.join(", ")}.`; return `Следующим шагом лучше выбрать один контур схемы: ${surface.ambiguity_entity_sets.join(", ")}.`;
} }
const routeLabel = metadataRouteFamilyLabelRu(surface.downstream_route_family); const routeLabel = metadataRouteFamilyLabelRu(surface.downstream_route_family);
if (surface.selected_entity_set && routeLabel) { if (surface.selected_entity_set && routeLabel) {
return `Следующим шагом могу пойти в ${routeLabel} по surface «${surface.selected_entity_set}» и уже искать подтвержденные данные, а не только схему.`; return `Следующим шагом могу пойти в ${routeLabel} по типу «${surface.selected_entity_set}» и уже искать подтвержденные данные, а не только схему.`;
} }
} }
if (mode === "checked_sources_only" && pilot.query_limitations.length > 0) { if (mode === "checked_sources_only" && pilot.query_limitations.length > 0) {
@ -580,19 +584,18 @@ function derivedMetadataConfirmedLine(pilot) {
} }
const scope = surface.metadata_scope ? ` по области "${surface.metadata_scope}"` : ""; const scope = surface.metadata_scope ? ` по области "${surface.metadata_scope}"` : "";
const entitySets = surface.available_entity_sets.length > 0 const entitySets = surface.available_entity_sets.length > 0
? ` Типы объектов: ${surface.available_entity_sets.join(", ")}.` ? ` Тип объектов: ${surface.available_entity_sets.join(", ")}.`
: ""; : "";
const objects = surface.matched_objects.length > 0 const objects = surface.matched_objects.length > 0
? ` Найденные объекты: ${surface.matched_objects.slice(0, 8).join(", ")}.` ? ` Найденные объекты: ${surface.matched_objects.slice(0, 8).join(", ")}.`
: ""; : "";
const selectedEntitySet = surface.selected_entity_set ? ` Выбранное family: ${surface.selected_entity_set}.` : "";
const selectedObjects = surface.selected_surface_objects.length > 0 const selectedObjects = surface.selected_surface_objects.length > 0
? ` Выбранные surface-объекты: ${surface.selected_surface_objects.slice(0, 6).join(", ")}.` ? ` Для следующего шага подходят: ${surface.selected_surface_objects.slice(0, 6).join(", ")}.`
: ""; : "";
const fields = surface.available_fields.length > 0 const fields = surface.available_fields.length > 0
? ` Доступные поля/секции: ${surface.available_fields.slice(0, 12).join(", ")}.` ? ` Доступные поля/секции: ${surface.available_fields.slice(0, 12).join(", ")}.`
: ""; : "";
return `Подтвержденная metadata-поверхность 1С${scope}: ${surface.matched_rows} строк metadata-ответа.${entitySets}${objects}${selectedEntitySet}${selectedObjects}${fields}`.replace(/\s+/g, " ").trim(); return `В схеме 1С${scope} найдены подтвержденные объекты: ${surface.matched_rows}.${entitySets}${objects}${selectedObjects}${fields}`.replace(/\s+/g, " ").trim();
} }
function derivedMetadataInferenceLine(pilot) { function derivedMetadataInferenceLine(pilot) {
const surface = pilot.derived_metadata_surface; const surface = pilot.derived_metadata_surface;
@ -600,13 +603,13 @@ function derivedMetadataInferenceLine(pilot) {
return null; return null;
} }
if (surface.ambiguity_detected && surface.ambiguity_entity_sets.length > 0) { if (surface.ambiguity_detected && surface.ambiguity_entity_sets.length > 0) {
return `По подтвержденной metadata-поверхности видно несколько конкурирующих family: ${surface.ambiguity_entity_sets.join(", ")}. Следующий data-lane пока нельзя выбрать без явного сужения.`; return `По проверенной схеме видно несколько возможных контуров: ${surface.ambiguity_entity_sets.join(", ")}. Следующий шаг пока нельзя выбрать без явного сужения.`;
} }
const routeLabel = metadataRouteFamilyLabelRu(surface.downstream_route_family); const routeLabel = metadataRouteFamilyLabelRu(surface.downstream_route_family);
if (!surface.selected_entity_set || !routeLabel) { if (!surface.selected_entity_set || !routeLabel) {
return null; return null;
} }
return `По подтвержденной metadata-поверхности следующий проверяемый шаг можно ограниченно оценить как ${routeLabel} через family «${surface.selected_entity_set}». Это еще не выполненный data-fetch, а только grounded выбор следующего контура.`; return `Следующий проверяемый шаг можно вести в ${routeLabel} через тип «${surface.selected_entity_set}». Это пока выбор контура по схеме 1С, а не уже полученные бизнес-строки.`;
} }
function derivedEntityResolutionConfirmedLine(pilot) { function derivedEntityResolutionConfirmedLine(pilot) {
const resolution = pilot.derived_entity_resolution; const resolution = pilot.derived_entity_resolution;
@ -651,6 +654,15 @@ function derivedRankedValueFlowConfirmedLine(pilot) {
const leader = ranking.ranked_values[0]; const leader = ranking.ranked_values[0];
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : ""; const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне"; const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне";
if (ranking.ranked_values.length === 1) {
const singleLead = ranking.value_flow_direction === "outgoing_supplier_payout"
? "В проверенных исходящих платежах найден один контрагент"
: "В проверенных входящих поступлениях найден один контрагент";
const limitCaveat = ranking.coverage_limited_by_probe_limit
? " Лимит строк проверки достигнут; сравнение с другими контрагентами может быть неполным."
: " Других контрагентов в этом проверенном срезе не найдено, поэтому это не полноценный сравнительный рейтинг.";
return `${singleLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${limitCaveat}`;
}
const directionLead = ranking.ranking_need === "bottom_asc" const directionLead = ranking.ranking_need === "bottom_asc"
? ranking.value_flow_direction === "outgoing_supplier_payout" ? ranking.value_flow_direction === "outgoing_supplier_payout"
? "Меньше всего заплатили контрагенту" ? "Меньше всего заплатили контрагенту"
@ -783,8 +795,13 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
: derivedEntityResolutionLine : derivedEntityResolutionLine
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine] ? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
: derivedMetadataLine : derivedMetadataLine
? [...pilot.evidence.confirmed_facts, derivedMetadataLine] ? [derivedMetadataLine]
: pilot.evidence.confirmed_facts; : pilot.evidence.confirmed_facts;
const unknownLines = pilot.derived_metadata_surface
? pilot.derived_metadata_surface.available_fields.length > 0
? userFacingUnknowns(pilot.evidence.unknown_facts)
: ["Детальный список полей этих объектов этим шагом не получен."]
: rankedValueFlowUnknownLines(pilot);
return { return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION, schema_version: exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryAnswerAdapter", policy_owner: "assistantMcpDiscoveryAnswerAdapter",
@ -792,7 +809,7 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
headline: headlineFor(mode, pilot), headline: headlineFor(mode, pilot),
confirmed_lines: uniqueStrings(confirmedLines), confirmed_lines: uniqueStrings(confirmedLines),
inference_lines: uniqueStrings(inferenceLines), inference_lines: uniqueStrings(inferenceLines),
unknown_lines: rankedValueFlowUnknownLines(pilot), unknown_lines: unknownLines,
limitation_lines: userFacingLimitations([...pilot.query_limitations, ...pilot.evidence.query_limitations]), limitation_lines: userFacingLimitations([...pilot.query_limitations, ...pilot.evidence.query_limitations]),
next_step_line: nextStepFor(mode, pilot), next_step_line: nextStepFor(mode, pilot),
internal_mechanics_allowed: false, internal_mechanics_allowed: false,

View File

@ -88,31 +88,55 @@ function buildDiscoveryRecapFactLine(input) {
const objectsPart = objects.length > 0 ? `, нашли объекты ${objects.slice(0, 4).join(", ")}` : ""; const objectsPart = objects.length > 0 ? `, нашли объекты ${objects.slice(0, 4).join(", ")}` : "";
const entitySetsPart = entitySets.length > 0 ? `, видны типы ${entitySets.slice(0, 4).join(", ")}` : ""; const entitySetsPart = entitySets.length > 0 ? `, видны типы ${entitySets.slice(0, 4).join(", ")}` : "";
const fieldsPart = fields.length > 0 ? `, доступны поля/секции ${fields.slice(0, 5).join(", ")}` : ""; const fieldsPart = fields.length > 0 ? `, доступны поля/секции ${fields.slice(0, 5).join(", ")}` : "";
return `смотрели metadata-поверхность 1С${scopePart}${periodPart}: ${rows} подтвержденных строк${objectsPart}${entitySetsPart}${fieldsPart}`.trim(); return `смотрели схему 1С${scopePart}${periodPart}: ${rows} подтвержденных строк${objectsPart}${entitySetsPart}${fieldsPart}`.trim();
} }
if (!input.counterparty) { const rankedFlow = toRecordObject(pilot?.derived_ranked_value_flow);
if (rankedFlow) {
const rankedValues = Array.isArray(rankedFlow.ranked_values) ? rankedFlow.ranked_values : [];
const leader = toRecordObject(rankedValues[0]);
const leaderName = toNonEmptyString(leader?.axis_value);
const leaderAmount = toNonEmptyString(leader?.total_amount_human_ru);
const leaderRows = toNonEmptyString(leader?.rows_with_amount);
const organization = toNonEmptyString(rankedFlow.organization_scope) ?? input.organization;
const period = toNonEmptyString(rankedFlow.period_scope) ?? input.scopedDate;
const organizationPart = organization ? ` по компании «${organization}»` : "";
const periodPartForRanking = period ? ` за период ${period}` : periodPart;
if (leaderName && leaderAmount) {
const rowsPart = leaderRows ? ` по ${leaderRows} строкам` : "";
const rankingKind = rankedValues.length > 1 ? "строили рейтинг клиентов" : "видели единственного клиента в проверенном срезе";
return `${rankingKind}${organizationPart}${periodPartForRanking}: ${leaderName}${leaderAmount}${rowsPart}`.trim();
}
}
const subjectPart = input.counterparty
? `контрагенту «${input.counterparty}»`
: input.organization
? `компании «${input.organization}»`
: null;
if (!subjectPart) {
return null; return null;
} }
if (pilotScope === "counterparty_lifecycle_query_documents_v1") { if (pilotScope === "counterparty_lifecycle_query_documents_v1") {
const activityPeriod = toRecordObject(pilot?.derived_activity_period); const activityPeriod = toRecordObject(pilot?.derived_activity_period);
const duration = toNonEmptyString(activityPeriod?.duration_human_ru); const duration = toNonEmptyString(activityPeriod?.duration_human_ru);
return duration return duration
? `смотрели подтвержденную активность по контрагенту «${input.counterparty}»${periodPart} и оценили период взаимодействия примерно как ${duration}` ? `смотрели подтвержденную активность по ${subjectPart}${periodPart} и оценили период взаимодействия примерно как ${duration}`
: `смотрели подтвержденную активность по контрагенту «${input.counterparty}»${periodPart}`; : `смотрели подтвержденную активность по ${subjectPart}${periodPart}`;
} }
if (pilotScope === "counterparty_supplier_payout_query_movements_v1") { if (pilotScope === "counterparty_supplier_payout_query_movements_v1") {
const flow = toRecordObject(pilot?.derived_value_flow); const flow = toRecordObject(pilot?.derived_value_flow);
const amount = toNonEmptyString(flow?.total_amount_human_ru); const amount = toNonEmptyString(flow?.total_amount_human_ru);
const flowPeriodPart = periodPartForRecap(toNonEmptyString(flow?.period_scope) ?? input.scopedDate);
return amount return amount
? `считали исходящие платежи/списания по контрагенту «${input.counterparty}»${periodPart}: ${amount}` ? `считали исходящие платежи/списания по ${subjectPart}${flowPeriodPart}: ${amount}`
: `считали исходящие платежи/списания по контрагенту «${input.counterparty}»${periodPart}`; : `считали исходящие платежи/списания по ${subjectPart}${flowPeriodPart}`;
} }
if (pilotScope === "counterparty_value_flow_query_movements_v1") { if (pilotScope === "counterparty_value_flow_query_movements_v1") {
const flow = toRecordObject(pilot?.derived_value_flow); const flow = toRecordObject(pilot?.derived_value_flow);
const amount = toNonEmptyString(flow?.total_amount_human_ru); const amount = toNonEmptyString(flow?.total_amount_human_ru);
const flowPeriodPart = periodPartForRecap(toNonEmptyString(flow?.period_scope) ?? input.scopedDate);
return amount return amount
? `смотрели денежный поток по контрагенту «${input.counterparty}»${periodPart}: ${amount}` ? `смотрели денежный поток по ${subjectPart}${flowPeriodPart}: ${amount}`
: `смотрели денежный поток по контрагенту «${input.counterparty}»${periodPart}`; : `смотрели денежный поток по ${subjectPart}${flowPeriodPart}`;
} }
if (pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1") { if (pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1") {
const flow = toRecordObject(pilot?.derived_bidirectional_value_flow); const flow = toRecordObject(pilot?.derived_bidirectional_value_flow);
@ -121,10 +145,11 @@ function buildDiscoveryRecapFactLine(input) {
const incomingAmount = toNonEmptyString(incoming?.total_amount_human_ru); const incomingAmount = toNonEmptyString(incoming?.total_amount_human_ru);
const outgoingAmount = toNonEmptyString(outgoing?.total_amount_human_ru); const outgoingAmount = toNonEmptyString(outgoing?.total_amount_human_ru);
const netAmount = toNonEmptyString(flow?.net_amount_human_ru); const netAmount = toNonEmptyString(flow?.net_amount_human_ru);
const flowPeriodPart = periodPartForRecap(toNonEmptyString(flow?.period_scope) ?? input.scopedDate);
if (incomingAmount && outgoingAmount && netAmount) { if (incomingAmount && outgoingAmount && netAmount) {
return `считали нетто по деньгам с контрагентом «${input.counterparty}»${periodPart}: получили ${incomingAmount}, заплатили ${outgoingAmount}, расчетное нетто ${netAmount}`; return `считали нетто по деньгам по ${subjectPart}${flowPeriodPart}: получили ${incomingAmount}, заплатили ${outgoingAmount}, расчетное нетто ${netAmount}`;
} }
return `считали нетто по деньгам с контрагентом «${input.counterparty}»${periodPart}`; return `считали нетто по деньгам по ${subjectPart}${flowPeriodPart}`;
} }
return null; return null;
} }
@ -177,6 +202,7 @@ function buildRecapFactLine(input) {
const discoveryFact = buildDiscoveryRecapFactLine({ const discoveryFact = buildDiscoveryRecapFactLine({
debug: input.debug, debug: input.debug,
counterparty: input.counterparty, counterparty: input.counterparty,
organization: input.organization,
scopedDate scopedDate
}); });
if (discoveryFact) { if (discoveryFact) {
@ -259,7 +285,7 @@ function collectRecentRecapFacts(input) {
} }
seen.add(fact); seen.add(fact);
facts.push(fact); facts.push(fact);
if (facts.length >= 3) { if (facts.length >= (input.limit ?? 3)) {
break; break;
} }
} }
@ -309,6 +335,13 @@ function buildAddressMemoryRecapReply(input) {
"Могу продолжить по нему без переписывания контекста: поступления, платежи, нетто, документы или пояснение границ ответа." "Могу продолжить по нему без переписывания контекста: поступления, платежи, нетто, документы или пояснение границ ответа."
].join(" "); ].join(" ");
} }
if (recapFacts.length > 0) {
return [
"Да, помню. В предыдущем проверенном контуре мы уже выяснили:",
...recapFacts.map((fact) => `- ${fact}.`),
"Могу продолжить от этого места: углубиться в данные, документы, движения или границы подтверждения."
].join("\n");
}
if (organization || scopedDate) { if (organization || scopedDate) {
const organizationPart = organization ? ` по компании «${organization}»` : ""; const organizationPart = organization ? ` по компании «${organization}»` : "";
const datePart = scopedDate ? ` на ${scopedDate}` : ""; const datePart = scopedDate ? ` на ${scopedDate}` : "";
@ -326,16 +359,32 @@ function buildBroadBusinessEvaluationReply(input) {
sessionItems: input.sessionItems, sessionItems: input.sessionItems,
item: null, item: null,
organization, organization,
toNonEmptyString: input.toNonEmptyString toNonEmptyString: input.toNonEmptyString,
limit: 5
}); });
const organizationPart = organization ? ` по компании «${organization}»` : ""; const organizationPart = organization ? ` по компании «${organization}»` : "";
if (recapFacts.length > 0) { if (recapFacts.length > 0) {
const moneyFactCount = recapFacts.filter((fact) => /(?:денежн|нетто|поступлен|платеж|рейтинг|клиент|выруч|оборот|заплатили|получили)/iu.test(fact)).length;
const hasRankingFact = recapFacts.some((fact) => /(?:рейтинг|клиент|единственного клиента)/iu.test(fact));
const hasNetFact = recapFacts.some((fact) => /нетто/iu.test(fact));
const auditLines = [
moneyFactCount > 0
? "- Денежный контур уже выглядит операционно значимым: есть подтвержденные поступления, платежи или клиентские срезы."
: "- Операционная активность подтверждена, но денежный контур пока раскрыт слабо.",
hasRankingFact
? "- По клиентской базе уже есть точечные лидеры, но это еще не полноценная управленческая сегментация всей базы."
: "- Ключевых клиентов и концентрацию выручки стоит добрать отдельным рейтингом.",
hasNetFact
? "- По нетто можно обсуждать направление денежного потока, но прибыль и маржу этим не доказываем."
: "- Прибыль, маржа и качество операционки пока не доказаны: нужны расходы, себестоимость и задолженность."
];
return [ return [
`Коротко: по тому, что мы уже подтвердили в 1С${organizationPart}, компания выглядит операционно живой, но это пока только частичная оценка бизнеса.`, `Коротко: по уже подтвержденным срезам 1С${organizationPart} компания выглядит операционно живой; это предварительная оценка бизнеса, а для взрослого вывода еще нужны прибыль, маржа и долги.`,
"Сейчас я опираюсь на такие подтвержденные факты:", "Что уже видно:",
...recapFacts.map((fact) => `- ${ensureSentence(fact)}`), ...recapFacts.map((fact) => `- ${ensureSentence(fact)}`),
"Это еще не полная диагностика всего бизнеса и не вывод о прибыли: я честно суммирую только те контуры, которые мы уже проверили в диалоге.", "Предварительный LLM-аудит:",
"Если хочешь, следующим шагом могу сузить оценку до денежного потока, долгов, НДС или ключевых контрагентов." ...auditLines,
"Что добрать для полной оценки: обороты по годам, топ клиентов, входящие/исходящие деньги, дебиторку/кредиторку, НДС и признаки маржинальности."
].join("\n"); ].join("\n");
} }
return [ return [

View File

@ -81,6 +81,10 @@ function isInternalMechanicsLine(value: string): boolean {
text.includes("scope is not implemented yet") || text.includes("scope is not implemented yet") ||
text.includes("needs more scope before execution") || text.includes("needs more scope before execution") ||
text.includes("mcp_execution_performed") text.includes("mcp_execution_performed")
|| text.includes("confirmed 1c metadata surface")
|| text.includes("metadata surface family scores")
|| text.includes("available metadata object sets")
|| text.includes("selected metadata")
); );
} }
@ -444,16 +448,16 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
return `По движениям${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`; return `По движениям${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
} }
if (isCatalogDrilldownPilot(pilot) && mode === "confirmed_with_bounded_inference") { if (isCatalogDrilldownPilot(pilot) && mode === "confirmed_with_bounded_inference") {
return "По метаданным 1С удалось углубиться в контур справочников и связанных объектов; это уже не общий обзор схемы, а следующий безопасный catalog drilldown."; return "По схеме 1С удалось углубиться в контур справочников и связанных объектов; это следующий безопасный шаг по проверенной схеме, а не бизнес-обороты.";
} }
if (pilot.derived_metadata_surface && mode === "confirmed_with_bounded_inference") { if (pilot.derived_metadata_surface && mode === "confirmed_with_bounded_inference") {
if (pilot.derived_metadata_surface.ambiguity_detected) { if (pilot.derived_metadata_surface.ambiguity_detected) {
return "По метаданным 1С найдены конкурирующие schema-поверхности; перед следующим шагом нужно удержать неоднозначность явно."; return "По схеме 1С найдены несколько конкурирующих контуров; перед следующим шагом нужно явно выбрать нужный тип данных.";
} }
if (pilot.derived_metadata_surface.downstream_route_family) { if (pilot.derived_metadata_surface.downstream_route_family) {
return "По метаданным 1С найдена схема и заземлена вероятная поверхность для следующего безопасного шага."; return "По схеме 1С найдены подходящие объекты; можно безопасно выбрать следующий контур проверки.";
} }
return "По метаданным 1С найдена доступная схема для дальнейшего безопасного поиска."; return "По схеме 1С найдены доступные объекты для дальнейшего безопасного поиска.";
} }
if (askedMonthlyBreakdown && pilot.derived_bidirectional_value_flow && mode === "confirmed_with_bounded_inference") { if (askedMonthlyBreakdown && pilot.derived_bidirectional_value_flow && mode === "confirmed_with_bounded_inference") {
return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто и помесячная раскладка могут называться только как расчет по найденным строкам и проверенному периоду."; return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто и помесячная раскладка могут называться только как расчет по найденным строкам и проверенному периоду.";
@ -492,7 +496,7 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
return "Точный факт не подтвержден, но есть ограниченная оценка по найденной активности в 1С."; return "Точный факт не подтвержден, но есть ограниченная оценка по найденной активности в 1С.";
} }
if (mode === "needs_clarification" && isMetadataLaneChoiceClarification(pilot)) { if (mode === "needs_clarification" && isMetadataLaneChoiceClarification(pilot)) {
return "По подтвержденной metadata-поверхности видно несколько конкурирующих data-lane, и без явного выбора дальше идти нельзя."; return "По проверенной схеме 1С видно несколько возможных контуров, и без явного выбора дальше идти нельзя.";
} }
if (mode === "needs_clarification" && isMovementLaneClarification(pilot)) { if (mode === "needs_clarification" && isMovementLaneClarification(pilot)) {
const need = clarificationNeedRu(pilot); const need = clarificationNeedRu(pilot);
@ -573,11 +577,11 @@ function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
if (mode === "confirmed_with_bounded_inference" && pilot.derived_metadata_surface) { if (mode === "confirmed_with_bounded_inference" && pilot.derived_metadata_surface) {
const surface = pilot.derived_metadata_surface; const surface = pilot.derived_metadata_surface;
if (surface.ambiguity_detected && surface.ambiguity_entity_sets.length > 0) { if (surface.ambiguity_detected && surface.ambiguity_entity_sets.length > 0) {
return `Следующим шагом лучше сузить surface до одного семейства: ${surface.ambiguity_entity_sets.join(", ")}.`; return `Следующим шагом лучше выбрать один контур схемы: ${surface.ambiguity_entity_sets.join(", ")}.`;
} }
const routeLabel = metadataRouteFamilyLabelRu(surface.downstream_route_family); const routeLabel = metadataRouteFamilyLabelRu(surface.downstream_route_family);
if (surface.selected_entity_set && routeLabel) { if (surface.selected_entity_set && routeLabel) {
return `Следующим шагом могу пойти в ${routeLabel} по surface «${surface.selected_entity_set}» и уже искать подтвержденные данные, а не только схему.`; return `Следующим шагом могу пойти в ${routeLabel} по типу «${surface.selected_entity_set}» и уже искать подтвержденные данные, а не только схему.`;
} }
} }
if (mode === "checked_sources_only" && pilot.query_limitations.length > 0) { if (mode === "checked_sources_only" && pilot.query_limitations.length > 0) {
@ -692,22 +696,21 @@ function derivedMetadataConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecution
const scope = surface.metadata_scope ? ` по области "${surface.metadata_scope}"` : ""; const scope = surface.metadata_scope ? ` по области "${surface.metadata_scope}"` : "";
const entitySets = const entitySets =
surface.available_entity_sets.length > 0 surface.available_entity_sets.length > 0
? ` Типы объектов: ${surface.available_entity_sets.join(", ")}.` ? ` Тип объектов: ${surface.available_entity_sets.join(", ")}.`
: ""; : "";
const objects = const objects =
surface.matched_objects.length > 0 surface.matched_objects.length > 0
? ` Найденные объекты: ${surface.matched_objects.slice(0, 8).join(", ")}.` ? ` Найденные объекты: ${surface.matched_objects.slice(0, 8).join(", ")}.`
: ""; : "";
const selectedEntitySet = surface.selected_entity_set ? ` Выбранное family: ${surface.selected_entity_set}.` : "";
const selectedObjects = const selectedObjects =
surface.selected_surface_objects.length > 0 surface.selected_surface_objects.length > 0
? ` Выбранные surface-объекты: ${surface.selected_surface_objects.slice(0, 6).join(", ")}.` ? ` Для следующего шага подходят: ${surface.selected_surface_objects.slice(0, 6).join(", ")}.`
: ""; : "";
const fields = const fields =
surface.available_fields.length > 0 surface.available_fields.length > 0
? ` Доступные поля/секции: ${surface.available_fields.slice(0, 12).join(", ")}.` ? ` Доступные поля/секции: ${surface.available_fields.slice(0, 12).join(", ")}.`
: ""; : "";
return `Подтвержденная metadata-поверхность 1С${scope}: ${surface.matched_rows} строк metadata-ответа.${entitySets}${objects}${selectedEntitySet}${selectedObjects}${fields}`.replace(/\s+/g, " ").trim(); return `В схеме 1С${scope} найдены подтвержденные объекты: ${surface.matched_rows}.${entitySets}${objects}${selectedObjects}${fields}`.replace(/\s+/g, " ").trim();
} }
function derivedMetadataInferenceLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null { function derivedMetadataInferenceLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
@ -716,13 +719,13 @@ function derivedMetadataInferenceLine(pilot: AssistantMcpDiscoveryPilotExecution
return null; return null;
} }
if (surface.ambiguity_detected && surface.ambiguity_entity_sets.length > 0) { if (surface.ambiguity_detected && surface.ambiguity_entity_sets.length > 0) {
return `По подтвержденной metadata-поверхности видно несколько конкурирующих family: ${surface.ambiguity_entity_sets.join(", ")}. Следующий data-lane пока нельзя выбрать без явного сужения.`; return `По проверенной схеме видно несколько возможных контуров: ${surface.ambiguity_entity_sets.join(", ")}. Следующий шаг пока нельзя выбрать без явного сужения.`;
} }
const routeLabel = metadataRouteFamilyLabelRu(surface.downstream_route_family); const routeLabel = metadataRouteFamilyLabelRu(surface.downstream_route_family);
if (!surface.selected_entity_set || !routeLabel) { if (!surface.selected_entity_set || !routeLabel) {
return null; return null;
} }
return `По подтвержденной metadata-поверхности следующий проверяемый шаг можно ограниченно оценить как ${routeLabel} через family «${surface.selected_entity_set}». Это еще не выполненный data-fetch, а только grounded выбор следующего контура.`; return `Следующий проверяемый шаг можно вести в ${routeLabel} через тип «${surface.selected_entity_set}». Это пока выбор контура по схеме 1С, а не уже полученные бизнес-строки.`;
} }
function derivedEntityResolutionConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null { function derivedEntityResolutionConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
@ -774,6 +777,16 @@ function derivedRankedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotEx
const leader = ranking.ranked_values[0]; const leader = ranking.ranked_values[0];
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : ""; const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне"; const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне";
if (ranking.ranked_values.length === 1) {
const singleLead =
ranking.value_flow_direction === "outgoing_supplier_payout"
? "В проверенных исходящих платежах найден один контрагент"
: "В проверенных входящих поступлениях найден один контрагент";
const limitCaveat = ranking.coverage_limited_by_probe_limit
? " Лимит строк проверки достигнут; сравнение с другими контрагентами может быть неполным."
: " Других контрагентов в этом проверенном срезе не найдено, поэтому это не полноценный сравнительный рейтинг.";
return `${singleLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${limitCaveat}`;
}
const directionLead = const directionLead =
ranking.ranking_need === "bottom_asc" ranking.ranking_need === "bottom_asc"
? ranking.value_flow_direction === "outgoing_supplier_payout" ? ranking.value_flow_direction === "outgoing_supplier_payout"
@ -926,8 +939,13 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
: derivedEntityResolutionLine : derivedEntityResolutionLine
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine] ? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
: derivedMetadataLine : derivedMetadataLine
? [...pilot.evidence.confirmed_facts, derivedMetadataLine] ? [derivedMetadataLine]
: pilot.evidence.confirmed_facts; : pilot.evidence.confirmed_facts;
const unknownLines = pilot.derived_metadata_surface
? pilot.derived_metadata_surface.available_fields.length > 0
? userFacingUnknowns(pilot.evidence.unknown_facts)
: ["Детальный список полей этих объектов этим шагом не получен."]
: rankedValueFlowUnknownLines(pilot);
return { return {
schema_version: ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION, schema_version: ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
@ -936,7 +954,7 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
headline: headlineFor(mode, pilot), headline: headlineFor(mode, pilot),
confirmed_lines: uniqueStrings(confirmedLines), confirmed_lines: uniqueStrings(confirmedLines),
inference_lines: uniqueStrings(inferenceLines), inference_lines: uniqueStrings(inferenceLines),
unknown_lines: rankedValueFlowUnknownLines(pilot), unknown_lines: unknownLines,
limitation_lines: userFacingLimitations([...pilot.query_limitations, ...pilot.evidence.query_limitations]), limitation_lines: userFacingLimitations([...pilot.query_limitations, ...pilot.evidence.query_limitations]),
next_step_line: nextStepFor(mode, pilot), next_step_line: nextStepFor(mode, pilot),
internal_mechanics_allowed: false, internal_mechanics_allowed: false,

View File

@ -103,6 +103,7 @@ function readDiscoveryMetadataScope(debug: Record<string, unknown> | null): stri
function buildDiscoveryRecapFactLine(input: { function buildDiscoveryRecapFactLine(input: {
debug: Record<string, unknown> | null; debug: Record<string, unknown> | null;
counterparty: string | null; counterparty: string | null;
organization: string | null;
scopedDate: string | null; scopedDate: string | null;
}): string | null { }): string | null {
if (!input.debug) { if (!input.debug) {
@ -138,31 +139,55 @@ function buildDiscoveryRecapFactLine(input: {
entitySets.length > 0 ? `, видны типы ${entitySets.slice(0, 4).join(", ")}` : ""; entitySets.length > 0 ? `, видны типы ${entitySets.slice(0, 4).join(", ")}` : "";
const fieldsPart = const fieldsPart =
fields.length > 0 ? `, доступны поля/секции ${fields.slice(0, 5).join(", ")}` : ""; fields.length > 0 ? `, доступны поля/секции ${fields.slice(0, 5).join(", ")}` : "";
return `смотрели metadata-поверхность 1С${scopePart}${periodPart}: ${rows} подтвержденных строк${objectsPart}${entitySetsPart}${fieldsPart}`.trim(); return `смотрели схему 1С${scopePart}${periodPart}: ${rows} подтвержденных строк${objectsPart}${entitySetsPart}${fieldsPart}`.trim();
} }
if (!input.counterparty) { const rankedFlow = toRecordObject(pilot?.derived_ranked_value_flow);
if (rankedFlow) {
const rankedValues = Array.isArray(rankedFlow.ranked_values) ? rankedFlow.ranked_values : [];
const leader = toRecordObject(rankedValues[0]);
const leaderName = toNonEmptyString(leader?.axis_value);
const leaderAmount = toNonEmptyString(leader?.total_amount_human_ru);
const leaderRows = toNonEmptyString(leader?.rows_with_amount);
const organization = toNonEmptyString(rankedFlow.organization_scope) ?? input.organization;
const period = toNonEmptyString(rankedFlow.period_scope) ?? input.scopedDate;
const organizationPart = organization ? ` по компании «${organization}»` : "";
const periodPartForRanking = period ? ` за период ${period}` : periodPart;
if (leaderName && leaderAmount) {
const rowsPart = leaderRows ? ` по ${leaderRows} строкам` : "";
const rankingKind = rankedValues.length > 1 ? "строили рейтинг клиентов" : "видели единственного клиента в проверенном срезе";
return `${rankingKind}${organizationPart}${periodPartForRanking}: ${leaderName}${leaderAmount}${rowsPart}`.trim();
}
}
const subjectPart = input.counterparty
? `контрагенту «${input.counterparty}»`
: input.organization
? `компании «${input.organization}»`
: null;
if (!subjectPart) {
return null; return null;
} }
if (pilotScope === "counterparty_lifecycle_query_documents_v1") { if (pilotScope === "counterparty_lifecycle_query_documents_v1") {
const activityPeriod = toRecordObject(pilot?.derived_activity_period); const activityPeriod = toRecordObject(pilot?.derived_activity_period);
const duration = toNonEmptyString(activityPeriod?.duration_human_ru); const duration = toNonEmptyString(activityPeriod?.duration_human_ru);
return duration return duration
? `смотрели подтвержденную активность по контрагенту «${input.counterparty}»${periodPart} и оценили период взаимодействия примерно как ${duration}` ? `смотрели подтвержденную активность по ${subjectPart}${periodPart} и оценили период взаимодействия примерно как ${duration}`
: `смотрели подтвержденную активность по контрагенту «${input.counterparty}»${periodPart}`; : `смотрели подтвержденную активность по ${subjectPart}${periodPart}`;
} }
if (pilotScope === "counterparty_supplier_payout_query_movements_v1") { if (pilotScope === "counterparty_supplier_payout_query_movements_v1") {
const flow = toRecordObject(pilot?.derived_value_flow); const flow = toRecordObject(pilot?.derived_value_flow);
const amount = toNonEmptyString(flow?.total_amount_human_ru); const amount = toNonEmptyString(flow?.total_amount_human_ru);
const flowPeriodPart = periodPartForRecap(toNonEmptyString(flow?.period_scope) ?? input.scopedDate);
return amount return amount
? `считали исходящие платежи/списания по контрагенту «${input.counterparty}»${periodPart}: ${amount}` ? `считали исходящие платежи/списания по ${subjectPart}${flowPeriodPart}: ${amount}`
: `считали исходящие платежи/списания по контрагенту «${input.counterparty}»${periodPart}`; : `считали исходящие платежи/списания по ${subjectPart}${flowPeriodPart}`;
} }
if (pilotScope === "counterparty_value_flow_query_movements_v1") { if (pilotScope === "counterparty_value_flow_query_movements_v1") {
const flow = toRecordObject(pilot?.derived_value_flow); const flow = toRecordObject(pilot?.derived_value_flow);
const amount = toNonEmptyString(flow?.total_amount_human_ru); const amount = toNonEmptyString(flow?.total_amount_human_ru);
const flowPeriodPart = periodPartForRecap(toNonEmptyString(flow?.period_scope) ?? input.scopedDate);
return amount return amount
? `смотрели денежный поток по контрагенту «${input.counterparty}»${periodPart}: ${amount}` ? `смотрели денежный поток по ${subjectPart}${flowPeriodPart}: ${amount}`
: `смотрели денежный поток по контрагенту «${input.counterparty}»${periodPart}`; : `смотрели денежный поток по ${subjectPart}${flowPeriodPart}`;
} }
if (pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1") { if (pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1") {
const flow = toRecordObject(pilot?.derived_bidirectional_value_flow); const flow = toRecordObject(pilot?.derived_bidirectional_value_flow);
@ -171,10 +196,11 @@ function buildDiscoveryRecapFactLine(input: {
const incomingAmount = toNonEmptyString(incoming?.total_amount_human_ru); const incomingAmount = toNonEmptyString(incoming?.total_amount_human_ru);
const outgoingAmount = toNonEmptyString(outgoing?.total_amount_human_ru); const outgoingAmount = toNonEmptyString(outgoing?.total_amount_human_ru);
const netAmount = toNonEmptyString(flow?.net_amount_human_ru); const netAmount = toNonEmptyString(flow?.net_amount_human_ru);
const flowPeriodPart = periodPartForRecap(toNonEmptyString(flow?.period_scope) ?? input.scopedDate);
if (incomingAmount && outgoingAmount && netAmount) { if (incomingAmount && outgoingAmount && netAmount) {
return `считали нетто по деньгам с контрагентом «${input.counterparty}»${periodPart}: получили ${incomingAmount}, заплатили ${outgoingAmount}, расчетное нетто ${netAmount}`; return `считали нетто по деньгам по ${subjectPart}${flowPeriodPart}: получили ${incomingAmount}, заплатили ${outgoingAmount}, расчетное нетто ${netAmount}`;
} }
return `считали нетто по деньгам с контрагентом «${input.counterparty}»${periodPart}`; return `считали нетто по деньгам по ${subjectPart}${flowPeriodPart}`;
} }
return null; return null;
} }
@ -253,6 +279,7 @@ function buildRecapFactLine(input: {
const discoveryFact = buildDiscoveryRecapFactLine({ const discoveryFact = buildDiscoveryRecapFactLine({
debug: input.debug, debug: input.debug,
counterparty: input.counterparty, counterparty: input.counterparty,
organization: input.organization,
scopedDate scopedDate
}); });
if (discoveryFact) { if (discoveryFact) {
@ -300,6 +327,7 @@ function collectRecentRecapFacts(input: {
item: string | null; item: string | null;
organization: string | null; organization: string | null;
toNonEmptyString: (value: unknown) => string | null; toNonEmptyString: (value: unknown) => string | null;
limit?: number;
}): string[] { }): string[] {
const sessionItems = Array.isArray(input.sessionItems) ? input.sessionItems : []; const sessionItems = Array.isArray(input.sessionItems) ? input.sessionItems : [];
if (sessionItems.length === 0) { if (sessionItems.length === 0) {
@ -342,7 +370,7 @@ function collectRecentRecapFacts(input: {
} }
seen.add(fact); seen.add(fact);
facts.push(fact); facts.push(fact);
if (facts.length >= 3) { if (facts.length >= (input.limit ?? 3)) {
break; break;
} }
} }
@ -402,6 +430,14 @@ export function buildAddressMemoryRecapReply(input: {
].join(" "); ].join(" ");
} }
if (recapFacts.length > 0) {
return [
"Да, помню. В предыдущем проверенном контуре мы уже выяснили:",
...recapFacts.map((fact) => `- ${fact}.`),
"Могу продолжить от этого места: углубиться в данные, документы, движения или границы подтверждения."
].join("\n");
}
if (organization || scopedDate) { if (organization || scopedDate) {
const organizationPart = organization ? ` по компании «${organization}»` : ""; const organizationPart = organization ? ` по компании «${organization}»` : "";
const datePart = scopedDate ? ` на ${scopedDate}` : ""; const datePart = scopedDate ? ` на ${scopedDate}` : "";
@ -426,17 +462,35 @@ export function buildBroadBusinessEvaluationReply(input: {
sessionItems: input.sessionItems, sessionItems: input.sessionItems,
item: null, item: null,
organization, organization,
toNonEmptyString: input.toNonEmptyString toNonEmptyString: input.toNonEmptyString,
limit: 5
}); });
const organizationPart = organization ? ` по компании «${organization}»` : ""; const organizationPart = organization ? ` по компании «${organization}»` : "";
if (recapFacts.length > 0) { if (recapFacts.length > 0) {
const moneyFactCount = recapFacts.filter((fact) =>
/(?:денежн|нетто|поступлен|платеж|рейтинг|клиент|выруч|оборот|заплатили|получили)/iu.test(fact)
).length;
const hasRankingFact = recapFacts.some((fact) => /(?:рейтинг|клиент|единственного клиента)/iu.test(fact));
const hasNetFact = recapFacts.some((fact) => /нетто/iu.test(fact));
const auditLines = [
moneyFactCount > 0
? "- Денежный контур уже выглядит операционно значимым: есть подтвержденные поступления, платежи или клиентские срезы."
: "- Операционная активность подтверждена, но денежный контур пока раскрыт слабо.",
hasRankingFact
? "- По клиентской базе уже есть точечные лидеры, но это еще не полноценная управленческая сегментация всей базы."
: "- Ключевых клиентов и концентрацию выручки стоит добрать отдельным рейтингом.",
hasNetFact
? "- По нетто можно обсуждать направление денежного потока, но прибыль и маржу этим не доказываем."
: "- Прибыль, маржа и качество операционки пока не доказаны: нужны расходы, себестоимость и задолженность."
];
return [ return [
`Коротко: по тому, что мы уже подтвердили в 1С${organizationPart}, компания выглядит операционно живой, но это пока только частичная оценка бизнеса.`, `Коротко: по уже подтвержденным срезам 1С${organizationPart} компания выглядит операционно живой; это предварительная оценка бизнеса, а для взрослого вывода еще нужны прибыль, маржа и долги.`,
"Сейчас я опираюсь на такие подтвержденные факты:", "Что уже видно:",
...recapFacts.map((fact) => `- ${ensureSentence(fact)}`), ...recapFacts.map((fact) => `- ${ensureSentence(fact)}`),
"Это еще не полная диагностика всего бизнеса и не вывод о прибыли: я честно суммирую только те контуры, которые мы уже проверили в диалоге.", "Предварительный LLM-аудит:",
"Если хочешь, следующим шагом могу сузить оценку до денежного потока, долгов, НДС или ключевых контрагентов." ...auditLines,
"Что добрать для полной оценки: обороты по годам, топ клиентов, входящие/исходящие деньги, дебиторку/кредиторку, НДС и признаки маржинальности."
].join("\n"); ].join("\n");
} }

View File

@ -407,6 +407,53 @@ describe("assistant MCP discovery answer adapter", () => {
expect(draft.unknown_lines[0]).toContain("\u041f\u043e\u043b\u043d\u044b\u0439 \u0440\u0435\u0439\u0442\u0438\u043d\u0433"); expect(draft.unknown_lines[0]).toContain("\u041f\u043e\u043b\u043d\u044b\u0439 \u0440\u0435\u0439\u0442\u0438\u043d\u0433");
}); });
it("does not overclaim a comparative ranking when only one counterparty is present", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: "top_desc",
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2021",
seeded_ranking_need: "top_desc"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildDeps([
{
Period: "2021-01-15T00:00:00",
Amount: 8560025,
Counterparty: "Группа СВК",
Organization: "ООО Альтернатива Плюс"
}
])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
const text = draft.confirmed_lines.join("\n");
expect(text).toContain("найден один контрагент");
expect(text).toContain("Группа СВК");
expect(text).toContain("не полноценный сравнительный рейтинг");
expect(text).not.toContain("Больше всего денег принёс");
});
it("asks for both organization and period when an open total still misses both axes", async () => { it("asks for both organization and period when an open total still misses both axes", async () => {
const planner = planAssistantMcpDiscovery({ const planner = planAssistantMcpDiscovery({
dataNeedGraph: { dataNeedGraph: {
@ -501,7 +548,7 @@ describe("assistant MCP discovery answer adapter", () => {
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("needs_clarification"); expect(draft.answer_mode).toBe("needs_clarification");
expect(draft.headline).toContain("data-lane"); expect(draft.headline).toContain("контуров");
expect(draft.next_step_line).toContain("по документам"); expect(draft.next_step_line).toContain("по документам");
expect(draft.next_step_line).toContain("по движениям/регистрам"); expect(draft.next_step_line).toContain("по движениям/регистрам");
expect(draft.must_not_claim).toContain("Do not claim rows were checked when mcp_execution_performed=false."); expect(draft.must_not_claim).toContain("Do not claim rows were checked when mcp_execution_performed=false.");
@ -537,7 +584,7 @@ describe("assistant MCP discovery answer adapter", () => {
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("needs_clarification"); expect(draft.answer_mode).toBe("needs_clarification");
expect(draft.headline).toContain("data-lane"); expect(draft.headline).toContain("контуров");
expect(draft.next_step_line).toContain("по документам"); expect(draft.next_step_line).toContain("по документам");
expect(draft.next_step_line).toContain("по движениям/регистрам"); expect(draft.next_step_line).toContain("по движениям/регистрам");
expect(draft.next_step_line).not.toContain("Уточните контрагента"); expect(draft.next_step_line).not.toContain("Уточните контрагента");
@ -671,13 +718,15 @@ describe("assistant MCP discovery answer adapter", () => {
const confirmedText = draft.confirmed_lines.join("\n"); const confirmedText = draft.confirmed_lines.join("\n");
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
expect(draft.headline).toContain("заземлена вероятная поверхность"); expect(draft.headline).toContain("схеме 1С");
expect(confirmedText).toContain("Подтвержденная metadata-поверхность 1С"); expect(confirmedText).toContain("В схеме 1С");
expect(confirmedText).toContain("Документ.СчетФактураВыданный"); expect(confirmedText).toContain("Документ.СчетФактураВыданный");
expect(confirmedText).toContain("Выбранное family: Документ"); expect(confirmedText).toContain("Для следующего шага подходят");
expect(confirmedText).toContain("Дата"); expect(confirmedText).toContain("Дата");
expect(draft.inference_lines.join("\n")).toContain("контур документов"); expect(draft.inference_lines.join("\n")).toContain("контур документов");
expect(draft.next_step_line).toContain("surface «Документ»"); expect(draft.next_step_line).toContain("типу «Документ»");
expect(confirmedText).not.toContain("Confirmed 1C metadata surface");
expect(confirmedText).not.toContain("Metadata surface family scores");
expect(draft.must_not_claim).toContain("Do not present metadata surface as confirmed business data rows."); expect(draft.must_not_claim).toContain("Do not present metadata surface as confirmed business data rows.");
expect(draft.must_not_claim).toContain("Do not present the inferred next checked lane as already executed data retrieval."); expect(draft.must_not_claim).toContain("Do not present the inferred next checked lane as already executed data retrieval.");
}); });
@ -715,7 +764,42 @@ describe("assistant MCP discovery answer adapter", () => {
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
expect(draft.headline).toContain("углубиться"); expect(draft.headline).toContain("углубиться");
expect(draft.headline).toContain("справочников"); expect(draft.headline).toContain("справочников");
expect(draft.headline).toContain("catalog drilldown"); expect(draft.headline).not.toContain("catalog drilldown");
});
it("renders catalog metadata without leaking internal surface scoring", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "inspect_catalog",
explicit_entity_candidates: ["контрагент"],
unsupported_but_understood_family: "1c_metadata_surface"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildMetadataDeps([
{ FullName: "Справочник.ДоговорыКонтрагентов", MetaType: "Справочник" },
{ FullName: "Справочник.Контрагенты", MetaType: "Справочник" }
])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
const userText = [
draft.headline,
...draft.confirmed_lines,
...draft.inference_lines,
...draft.unknown_lines,
draft.next_step_line ?? ""
].join("\n");
expect(userText).toContain("Справочник.ДоговорыКонтрагентов");
expect(userText).toContain("Справочник.Контрагенты");
expect(userText).toContain("Детальный список полей");
expect(userText).not.toContain("Confirmed 1C metadata surface");
expect(userText).not.toContain("Metadata surface family scores");
expect(userText).not.toContain("surface «");
expect(userText).not.toContain("catalog drilldown");
}); });
it("keeps metadata answer honest when schema surface stays ambiguous", async () => { it("keeps metadata answer honest when schema surface stays ambiguous", async () => {
@ -744,8 +828,8 @@ describe("assistant MCP discovery answer adapter", () => {
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.headline).toContain("конкурирующие schema-поверхности"); expect(draft.headline).toContain("конкурирующих контуров");
expect(draft.inference_lines.join("\n")).toContain("несколько конкурирующих family"); expect(draft.inference_lines.join("\n")).toContain("несколько возможных контуров");
expect(draft.unknown_lines).toContain( expect(draft.unknown_lines).toContain(
"Exact downstream metadata surface remains ambiguous across: Документ, РегистрНакопления" "Exact downstream metadata surface remains ambiguous across: Документ, РегистрНакопления"
); );

View File

@ -439,7 +439,7 @@ describe("assistantMemoryRecapPolicy", () => {
expect(context.contextualMemoryRecapFollowup).toBe(true); expect(context.contextualMemoryRecapFollowup).toBe(true);
expect(reply).toContain("НДС"); expect(reply).toContain("НДС");
expect(reply).toContain("metadata-поверхность 1С"); expect(reply).toContain("схему 1С");
expect(reply).toContain("amount"); expect(reply).toContain("amount");
expect(reply).toContain("accumulation_register"); expect(reply).toContain("accumulation_register");
}); });