Очеловечить MCP ответы и broad-eval recap
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user