Очеловечить MCP ответы и broad-eval recap
This commit is contained in:
parent
4c00d8c854
commit
da3a148918
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 [
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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: Документ, РегистрНакопления"
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue