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

This commit is contained in:
2026-05-01 22:38:23 +03:00
parent 4c00d8c854
commit da3a148918
6 changed files with 295 additions and 73 deletions
@@ -81,6 +81,10 @@ function isInternalMechanicsLine(value: string): boolean {
text.includes("scope is not implemented yet") ||
text.includes("needs more scope before execution") ||
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С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
}
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.ambiguity_detected) {
return "По метаданным 1С найдены конкурирующие schema-поверхности; перед следующим шагом нужно удержать неоднозначность явно.";
return "По схеме 1С найдены несколько конкурирующих контуров; перед следующим шагом нужно явно выбрать нужный тип данных.";
}
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") {
return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто и помесячная раскладка могут называться только как расчет по найденным строкам и проверенному периоду.";
@@ -492,7 +496,7 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
return "Точный факт не подтвержден, но есть ограниченная оценка по найденной активности в 1С.";
}
if (mode === "needs_clarification" && isMetadataLaneChoiceClarification(pilot)) {
return "По подтвержденной metadata-поверхности видно несколько конкурирующих data-lane, и без явного выбора дальше идти нельзя.";
return "По проверенной схеме 1С видно несколько возможных контуров, и без явного выбора дальше идти нельзя.";
}
if (mode === "needs_clarification" && isMovementLaneClarification(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) {
const surface = pilot.derived_metadata_surface;
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);
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) {
@@ -692,22 +696,21 @@ function derivedMetadataConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecution
const scope = surface.metadata_scope ? ` по области "${surface.metadata_scope}"` : "";
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
? ` Найденные объекты: ${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
? ` Выбранные surface-объекты: ${surface.selected_surface_objects.slice(0, 6).join(", ")}.`
? ` Для следующего шага подходят: ${surface.selected_surface_objects.slice(0, 6).join(", ")}.`
: "";
const fields =
surface.available_fields.length > 0
? ` Доступные поля/секции: ${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 {
@@ -716,13 +719,13 @@ function derivedMetadataInferenceLine(pilot: AssistantMcpDiscoveryPilotExecution
return null;
}
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);
if (!surface.selected_entity_set || !routeLabel) {
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 {
@@ -774,6 +777,16 @@ function derivedRankedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotEx
const leader = ranking.ranked_values[0];
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_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"
? ranking.value_flow_direction === "outgoing_supplier_payout"
@@ -926,8 +939,13 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
: derivedEntityResolutionLine
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
: derivedMetadataLine
? [...pilot.evidence.confirmed_facts, derivedMetadataLine]
? [derivedMetadataLine]
: 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 {
schema_version: ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
@@ -936,7 +954,7 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
headline: headlineFor(mode, pilot),
confirmed_lines: uniqueStrings(confirmedLines),
inference_lines: uniqueStrings(inferenceLines),
unknown_lines: rankedValueFlowUnknownLines(pilot),
unknown_lines: unknownLines,
limitation_lines: userFacingLimitations([...pilot.query_limitations, ...pilot.evidence.query_limitations]),
next_step_line: nextStepFor(mode, pilot),
internal_mechanics_allowed: false,
@@ -103,6 +103,7 @@ function readDiscoveryMetadataScope(debug: Record<string, unknown> | null): stri
function buildDiscoveryRecapFactLine(input: {
debug: Record<string, unknown> | null;
counterparty: string | null;
organization: string | null;
scopedDate: string | null;
}): string | null {
if (!input.debug) {
@@ -138,31 +139,55 @@ function buildDiscoveryRecapFactLine(input: {
entitySets.length > 0 ? `, видны типы ${entitySets.slice(0, 4).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;
}
if (pilotScope === "counterparty_lifecycle_query_documents_v1") {
const activityPeriod = toRecordObject(pilot?.derived_activity_period);
const duration = toNonEmptyString(activityPeriod?.duration_human_ru);
return duration
? `смотрели подтвержденную активность по контрагенту «${input.counterparty}»${periodPart} и оценили период взаимодействия примерно как ${duration}`
: `смотрели подтвержденную активность по контрагенту «${input.counterparty}»${periodPart}`;
? `смотрели подтвержденную активность по ${subjectPart}${periodPart} и оценили период взаимодействия примерно как ${duration}`
: `смотрели подтвержденную активность по ${subjectPart}${periodPart}`;
}
if (pilotScope === "counterparty_supplier_payout_query_movements_v1") {
const flow = toRecordObject(pilot?.derived_value_flow);
const amount = toNonEmptyString(flow?.total_amount_human_ru);
const flowPeriodPart = periodPartForRecap(toNonEmptyString(flow?.period_scope) ?? input.scopedDate);
return amount
? `считали исходящие платежи/списания по контрагенту «${input.counterparty}»${periodPart}: ${amount}`
: `считали исходящие платежи/списания по контрагенту «${input.counterparty}»${periodPart}`;
? `считали исходящие платежи/списания по ${subjectPart}${flowPeriodPart}: ${amount}`
: `считали исходящие платежи/списания по ${subjectPart}${flowPeriodPart}`;
}
if (pilotScope === "counterparty_value_flow_query_movements_v1") {
const flow = toRecordObject(pilot?.derived_value_flow);
const amount = toNonEmptyString(flow?.total_amount_human_ru);
const flowPeriodPart = periodPartForRecap(toNonEmptyString(flow?.period_scope) ?? input.scopedDate);
return amount
? `смотрели денежный поток по контрагенту «${input.counterparty}»${periodPart}: ${amount}`
: `смотрели денежный поток по контрагенту «${input.counterparty}»${periodPart}`;
? `смотрели денежный поток по ${subjectPart}${flowPeriodPart}: ${amount}`
: `смотрели денежный поток по ${subjectPart}${flowPeriodPart}`;
}
if (pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1") {
const flow = toRecordObject(pilot?.derived_bidirectional_value_flow);
@@ -171,10 +196,11 @@ function buildDiscoveryRecapFactLine(input: {
const incomingAmount = toNonEmptyString(incoming?.total_amount_human_ru);
const outgoingAmount = toNonEmptyString(outgoing?.total_amount_human_ru);
const netAmount = toNonEmptyString(flow?.net_amount_human_ru);
const flowPeriodPart = periodPartForRecap(toNonEmptyString(flow?.period_scope) ?? input.scopedDate);
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;
}
@@ -253,6 +279,7 @@ function buildRecapFactLine(input: {
const discoveryFact = buildDiscoveryRecapFactLine({
debug: input.debug,
counterparty: input.counterparty,
organization: input.organization,
scopedDate
});
if (discoveryFact) {
@@ -300,6 +327,7 @@ function collectRecentRecapFacts(input: {
item: string | null;
organization: string | null;
toNonEmptyString: (value: unknown) => string | null;
limit?: number;
}): string[] {
const sessionItems = Array.isArray(input.sessionItems) ? input.sessionItems : [];
if (sessionItems.length === 0) {
@@ -342,7 +370,7 @@ function collectRecentRecapFacts(input: {
}
seen.add(fact);
facts.push(fact);
if (facts.length >= 3) {
if (facts.length >= (input.limit ?? 3)) {
break;
}
}
@@ -402,6 +430,14 @@ export function buildAddressMemoryRecapReply(input: {
].join(" ");
}
if (recapFacts.length > 0) {
return [
"Да, помню. В предыдущем проверенном контуре мы уже выяснили:",
...recapFacts.map((fact) => `- ${fact}.`),
"Могу продолжить от этого места: углубиться в данные, документы, движения или границы подтверждения."
].join("\n");
}
if (organization || scopedDate) {
const organizationPart = organization ? ` по компании «${organization}»` : "";
const datePart = scopedDate ? ` на ${scopedDate}` : "";
@@ -426,17 +462,35 @@ export function buildBroadBusinessEvaluationReply(input: {
sessionItems: input.sessionItems,
item: null,
organization,
toNonEmptyString: input.toNonEmptyString
toNonEmptyString: input.toNonEmptyString,
limit: 5
});
const organizationPart = organization ? ` по компании «${organization}»` : "";
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 [
`Коротко: по тому, что мы уже подтвердили в 1С${organizationPart}, компания выглядит операционно живой, но это пока только частичная оценка бизнеса.`,
"Сейчас я опираюсь на такие подтвержденные факты:",
`Коротко: по уже подтвержденным срезам 1С${organizationPart} компания выглядит операционно живой; это предварительная оценка бизнеса, а для взрослого вывода еще нужны прибыль, маржа и долги.`,
"Что уже видно:",
...recapFacts.map((fact) => `- ${ensureSentence(fact)}`),
"Это еще не полная диагностика всего бизнеса и не вывод о прибыли: я честно суммирую только те контуры, которые мы уже проверили в диалоге.",
"Если хочешь, следующим шагом могу сузить оценку до денежного потока, долгов, НДС или ключевых контрагентов."
"Предварительный LLM-аудит:",
...auditLines,
"Что добрать для полной оценки: обороты по годам, топ клиентов, входящие/исходящие деньги, дебиторку/кредиторку, НДС и признаки маржинальности."
].join("\n");
}