736 lines
46 KiB
JavaScript
736 lines
46 KiB
JavaScript
"use strict";
|
||
// @ts-nocheck
|
||
Object.defineProperty(exports, "__esModule", { value: true });
|
||
exports.buildInventoryHistoryCapabilityFollowupReply = buildInventoryHistoryCapabilityFollowupReply;
|
||
exports.buildAddressMemoryRecapReply = buildAddressMemoryRecapReply;
|
||
exports.buildBroadBusinessEvaluationReply = buildBroadBusinessEvaluationReply;
|
||
exports.buildConversationExecutiveSummaryReply = buildConversationExecutiveSummaryReply;
|
||
exports.buildSelectedObjectAnswerInspectionReply = buildSelectedObjectAnswerInspectionReply;
|
||
exports.resolveAssistantLivingChatMemoryContext = resolveAssistantLivingChatMemoryContext;
|
||
exports.createAssistantMemoryRecapPolicy = createAssistantMemoryRecapPolicy;
|
||
const assistantContinuityPolicy_1 = require("./assistantContinuityPolicy");
|
||
function toNonEmptyString(value) {
|
||
if (value === null || value === undefined) {
|
||
return null;
|
||
}
|
||
const text = String(value).trim();
|
||
return text.length > 0 ? text : null;
|
||
}
|
||
function toRecordObject(value) {
|
||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||
return null;
|
||
}
|
||
return value;
|
||
}
|
||
function ensureSentence(value) {
|
||
const text = String(value ?? "").trim();
|
||
if (!text) {
|
||
return "";
|
||
}
|
||
return /[.!?]$/.test(text) ? text : `${text}.`;
|
||
}
|
||
function periodPartForRecap(scopedDate) {
|
||
if (!scopedDate) {
|
||
return "";
|
||
}
|
||
return /^\d{2}\.\d{2}\.\d{4}$/.test(scopedDate) ? ` на ${scopedDate}` : ` за период ${scopedDate}`;
|
||
}
|
||
function readDiscoveryMetadataScope(debug) {
|
||
const discoveryEntry = toRecordObject(debug?.assistant_mcp_discovery_entry_point_v1);
|
||
const bridge = toRecordObject(discoveryEntry?.bridge);
|
||
const pilot = toRecordObject(bridge?.pilot);
|
||
const surface = toRecordObject(pilot?.derived_metadata_surface);
|
||
const surfaceScope = toNonEmptyString(surface?.metadata_scope);
|
||
if (surfaceScope) {
|
||
return surfaceScope;
|
||
}
|
||
const turnInput = toRecordObject(discoveryEntry?.turn_input);
|
||
const turnMeaningRef = toRecordObject(turnInput?.turn_meaning_ref);
|
||
const entityCandidates = Array.isArray(turnMeaningRef?.explicit_entity_candidates)
|
||
? turnMeaningRef.explicit_entity_candidates
|
||
: [];
|
||
for (const candidate of entityCandidates) {
|
||
const text = toNonEmptyString(candidate);
|
||
if (text) {
|
||
return text;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
function buildDiscoveryRecapFactLine(input) {
|
||
if (!input.debug) {
|
||
return null;
|
||
}
|
||
const pilotScope = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryPilotScope)(input.debug, toNonEmptyString);
|
||
const discoveryEntry = toRecordObject(input.debug.assistant_mcp_discovery_entry_point_v1);
|
||
const bridge = toRecordObject(discoveryEntry?.bridge);
|
||
const pilot = toRecordObject(bridge?.pilot);
|
||
const periodPart = periodPartForRecap(input.scopedDate);
|
||
if (pilotScope === "metadata_inspection_v1") {
|
||
const metadataScope = readDiscoveryMetadataScope(input.debug);
|
||
const surface = toRecordObject(pilot?.derived_metadata_surface);
|
||
const entitySets = Array.isArray(surface?.available_entity_sets)
|
||
? surface.available_entity_sets
|
||
.map((item) => toNonEmptyString(item))
|
||
.filter((item) => Boolean(item))
|
||
: [];
|
||
const fields = Array.isArray(surface?.available_fields)
|
||
? surface.available_fields
|
||
.map((item) => toNonEmptyString(item))
|
||
.filter((item) => Boolean(item))
|
||
: [];
|
||
const objects = Array.isArray(surface?.matched_objects)
|
||
? surface.matched_objects
|
||
.map((item) => toNonEmptyString(item))
|
||
.filter((item) => Boolean(item))
|
||
: [];
|
||
const rows = Number(surface?.matched_rows ?? 0);
|
||
const scopePart = metadataScope ? ` по области «${metadataScope}»` : "";
|
||
const objectsPart = objects.length > 0 ? `, нашли объекты ${objects.slice(0, 4).join(", ")}` : "";
|
||
const entitySetsPart = entitySets.length > 0 ? `, видны типы ${entitySets.slice(0, 4).join(", ")}` : "";
|
||
const fieldsPart = fields.length > 0 ? `, доступны поля/секции ${fields.slice(0, 5).join(", ")}` : "";
|
||
return `смотрели схему 1С${scopePart}${periodPart}: ${rows} подтвержденных строк${objectsPart}${entitySetsPart}${fieldsPart}`.trim();
|
||
}
|
||
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
|
||
? `смотрели подтвержденную активность по ${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
|
||
? `считали исходящие платежи/списания по ${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
|
||
? `смотрели денежный поток по ${subjectPart}${flowPeriodPart}: ${amount}`
|
||
: `смотрели денежный поток по ${subjectPart}${flowPeriodPart}`;
|
||
}
|
||
if (pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1") {
|
||
const flow = toRecordObject(pilot?.derived_bidirectional_value_flow);
|
||
const incoming = toRecordObject(flow?.incoming_customer_revenue);
|
||
const outgoing = toRecordObject(flow?.outgoing_supplier_payout);
|
||
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 `считали нетто по деньгам по ${subjectPart}${flowPeriodPart}: получили ${incomingAmount}, заплатили ${outgoingAmount}, расчетное нетто ${netAmount}`;
|
||
}
|
||
return `считали нетто по деньгам по ${subjectPart}${flowPeriodPart}`;
|
||
}
|
||
return null;
|
||
}
|
||
function collectMessageSamples(input) {
|
||
const values = [
|
||
input.rawUserMessage,
|
||
input.repairedRawUserMessage,
|
||
input.effectiveAddressUserMessage,
|
||
input.repairedEffectiveAddressUserMessage
|
||
];
|
||
return Array.from(new Set(values
|
||
.map((item) => String(item ?? "").trim())
|
||
.filter((item) => item.length > 0)));
|
||
}
|
||
function hasSignalAcrossSamples(samples, detector) {
|
||
return samples.some((sample) => detector(sample));
|
||
}
|
||
function hasExplicitRecapPromptSignal(samples) {
|
||
return samples.some((sample) => /(?:что\s+мы\s+.*(?:обсуждали|выяснили)|что\s+уже\s+выяснили|что\s+уже\s+поняли|напомни\s+что\s+мы|executive\s+summary|финальн\w*\s+собери|итогов\w*\s+(?:резюм|summary|вывод)|по\s+всему\s+диалогу|где\s+ответы\s+были\s+подтвержден|где\s+proxy|где\s+прокси|не\s+хватил\w*\s+доказательств|ручн\w*\s+(?:смотр|провер|контрол))/iu.test(sample));
|
||
}
|
||
function buildInventoryHistoryCapabilityFollowupReply(input) {
|
||
const contextFacts = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(input.addressDebug, input.toNonEmptyString);
|
||
const organization = input.organization ?? contextFacts.organization;
|
||
const lastAsOfDate = contextFacts.scopedDate;
|
||
const organizationPart = organization ? ` по компании «${organization}»` : "";
|
||
const referenceLine = lastAsOfDate
|
||
? `Да, могу. Сейчас мы уже смотрели складской срез${organizationPart} на ${lastAsOfDate}.`
|
||
: `Да, могу показать исторические данные${organizationPart} в этом же складском контуре.`;
|
||
return [
|
||
referenceLine,
|
||
`Могу показать исторические остатки${organizationPart} за нужный месяц, дату или год.`,
|
||
"Например:",
|
||
"- `на март 2020`",
|
||
"- `на июнь 2016`",
|
||
"- `за 2017 год`",
|
||
"- `сравни июнь 2016 с текущим срезом`",
|
||
"Если хочешь, сразу покажу нужный исторический период."
|
||
].join("\n");
|
||
}
|
||
function normalizeRecapIdentity(value) {
|
||
return String(value ?? "")
|
||
.trim()
|
||
.toLowerCase()
|
||
.replace(/[«»"'`]/g, "")
|
||
.replace(/\s+/g, " ");
|
||
}
|
||
function isLowQualityRecapCounterparty(value) {
|
||
const normalized = normalizeRecapIdentity(value);
|
||
if (!normalized) {
|
||
return false;
|
||
}
|
||
const stopwordOnlyCounterparty = new Set([
|
||
"без",
|
||
"в",
|
||
"во",
|
||
"для",
|
||
"до",
|
||
"за",
|
||
"из",
|
||
"к",
|
||
"ко",
|
||
"на",
|
||
"от",
|
||
"по",
|
||
"с",
|
||
"со",
|
||
"у"
|
||
]);
|
||
if (stopwordOnlyCounterparty.has(normalized)) {
|
||
return true;
|
||
}
|
||
if (/^(?:и\s+)?(?:кто|что|где|какой|какие)\b/iu.test(normalized) ||
|
||
/(?:главн|основн|крупн|поставщик|клиент|контрагент|покупател|документ|движени|операци)/iu.test(normalized) &&
|
||
!/(?<!\p{L})(?:ооо|ип|ао|пао|зао|llc|inc|corp)(?!\p{L})/iu.test(normalized)) {
|
||
return true;
|
||
}
|
||
return /(?:\s+в|\s+по|\s+для)$/iu.test(normalized);
|
||
}
|
||
function buildRecapFactLine(input) {
|
||
const detectedIntent = String(input.debug?.detected_intent ?? "");
|
||
const scopedDate = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(input.debug).scopedDate;
|
||
const counterparty = isLowQualityRecapCounterparty(input.counterparty) ? null : input.counterparty;
|
||
const discoveryFact = buildDiscoveryRecapFactLine({
|
||
debug: input.debug,
|
||
counterparty,
|
||
organization: input.organization,
|
||
scopedDate
|
||
});
|
||
if (discoveryFact) {
|
||
return discoveryFact;
|
||
}
|
||
const itemPart = input.item ? `по позиции «${input.item}»` : null;
|
||
const counterpartyPart = counterparty ? `по контрагенту «${counterparty}»` : null;
|
||
const organizationPart = input.organization ? `по компании «${input.organization}»` : null;
|
||
const datePart = scopedDate ? ` на ${scopedDate}` : "";
|
||
if (detectedIntent === "inventory_on_hand_as_of_date") {
|
||
return `смотрели остатки${organizationPart ? ` ${organizationPart}` : ""}${datePart}`.trim();
|
||
}
|
||
if (detectedIntent === "inventory_purchase_provenance_for_item" && itemPart) {
|
||
return `разобрали, кто поставлял ${itemPart}${datePart}`.trim();
|
||
}
|
||
if (detectedIntent === "inventory_purchase_documents_for_item" && itemPart) {
|
||
return `подняли документы закупки ${itemPart}${datePart}`.trim();
|
||
}
|
||
if (detectedIntent === "inventory_sale_trace_for_item" && itemPart) {
|
||
return `разобрали, кому продавали ${itemPart}${datePart}`.trim();
|
||
}
|
||
if (detectedIntent === "inventory_purchase_to_sale_chain" && itemPart) {
|
||
return `проследили цепочку от закупки до продажи ${itemPart}${datePart}`.trim();
|
||
}
|
||
if (detectedIntent === "inventory_profitability_for_item" && itemPart) {
|
||
return `смотрели рентабельность ${itemPart}${datePart}`.trim();
|
||
}
|
||
if (detectedIntent === "inventory_aging_by_purchase_date" && itemPart) {
|
||
return `смотрели возраст остатков ${itemPart}${datePart}`.trim();
|
||
}
|
||
if (detectedIntent === "counterparty_activity_lifecycle" && organizationPart) {
|
||
return `смотрели активность в базе 1С ${organizationPart}`.trim();
|
||
}
|
||
if (detectedIntent === "list_documents_by_counterparty" && counterpartyPart) {
|
||
return `поднимали документы ${counterpartyPart}${datePart}`.trim();
|
||
}
|
||
if (detectedIntent === "list_documents_by_counterparty" && organizationPart) {
|
||
return `поднимали документы ${organizationPart}${datePart}`.trim();
|
||
}
|
||
return null;
|
||
}
|
||
function collectRecentRecapFacts(input) {
|
||
const sessionItems = Array.isArray(input.sessionItems) ? input.sessionItems : [];
|
||
if (sessionItems.length === 0) {
|
||
return [];
|
||
}
|
||
const currentItemKey = normalizeRecapIdentity(input.item);
|
||
const currentOrganizationKey = normalizeRecapIdentity(input.organization);
|
||
const facts = [];
|
||
const seen = new Set();
|
||
for (let index = sessionItems.length - 1; index >= 0; index -= 1) {
|
||
const item = sessionItems[index];
|
||
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
|
||
continue;
|
||
}
|
||
if (!(0, assistantContinuityPolicy_1.isGroundedAddressDebug)(item.debug, input.toNonEmptyString)) {
|
||
continue;
|
||
}
|
||
const debugContext = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(item.debug, input.toNonEmptyString);
|
||
const debugItem = debugContext.item;
|
||
const debugOrganization = debugContext.organization;
|
||
const itemMatches = currentItemKey ? normalizeRecapIdentity(debugItem) === currentItemKey : false;
|
||
const organizationMatches = currentOrganizationKey
|
||
? normalizeRecapIdentity(debugOrganization) === currentOrganizationKey
|
||
: false;
|
||
if (currentItemKey && !itemMatches) {
|
||
continue;
|
||
}
|
||
if (!currentItemKey && currentOrganizationKey && !organizationMatches) {
|
||
continue;
|
||
}
|
||
const fact = buildRecapFactLine({
|
||
debug: item.debug,
|
||
item: debugItem,
|
||
counterparty: debugContext.counterparty,
|
||
organization: debugOrganization
|
||
});
|
||
if (!fact || seen.has(fact)) {
|
||
continue;
|
||
}
|
||
seen.add(fact);
|
||
facts.push(fact);
|
||
if (facts.length >= (input.limit ?? 3)) {
|
||
break;
|
||
}
|
||
}
|
||
return facts.reverse();
|
||
}
|
||
function parseHumanMoney(value) {
|
||
const text = String(value ?? "")
|
||
.replace(/[^\d,.\-]/g, "")
|
||
.replace(/\s+/g, "")
|
||
.replace(",", ".");
|
||
if (!text) {
|
||
return null;
|
||
}
|
||
const parsed = Number(text);
|
||
return Number.isFinite(parsed) ? parsed : null;
|
||
}
|
||
function pushBusinessLine(target, value) {
|
||
const text = String(value ?? "").trim();
|
||
if (text && !target.includes(text)) {
|
||
target.push(text);
|
||
}
|
||
}
|
||
function collectBusinessEvaluationEvidence(input) {
|
||
const sessionItems = Array.isArray(input.sessionItems) ? input.sessionItems : [];
|
||
const currentOrganizationKey = normalizeRecapIdentity(input.organization);
|
||
const confirmedLines = [];
|
||
const interpretationLines = [];
|
||
let hasRanking = false;
|
||
let hasNet = false;
|
||
let moneySignalCount = 0;
|
||
for (let index = sessionItems.length - 1; index >= 0; index -= 1) {
|
||
const item = sessionItems[index];
|
||
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
|
||
continue;
|
||
}
|
||
if (!(0, assistantContinuityPolicy_1.isGroundedAddressDebug)(item.debug, input.toNonEmptyString)) {
|
||
continue;
|
||
}
|
||
const debugContext = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(item.debug, input.toNonEmptyString);
|
||
const debugOrganizationKey = normalizeRecapIdentity(debugContext.organization);
|
||
if (currentOrganizationKey && debugOrganizationKey && debugOrganizationKey !== currentOrganizationKey) {
|
||
continue;
|
||
}
|
||
const discoveryEntry = toRecordObject(item.debug.assistant_mcp_discovery_entry_point_v1);
|
||
const bridge = toRecordObject(discoveryEntry?.bridge);
|
||
const pilot = toRecordObject(bridge?.pilot);
|
||
const pilotScope = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryPilotScope)(item.debug, input.toNonEmptyString);
|
||
if (!pilot) {
|
||
continue;
|
||
}
|
||
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 = input.toNonEmptyString(leader?.axis_value);
|
||
const leaderAmount = input.toNonEmptyString(leader?.total_amount_human_ru);
|
||
const period = input.toNonEmptyString(rankedFlow.period_scope);
|
||
const periodPart = period ? ` за ${period}` : "";
|
||
if (leaderName && leaderAmount) {
|
||
pushBusinessLine(confirmedLines, `Топ-контрагент по подтвержденному денежному срезу${periodPart}: ${leaderName} - ${leaderAmount}.`);
|
||
hasRanking = true;
|
||
moneySignalCount += 1;
|
||
}
|
||
}
|
||
const bidirectionalFlow = toRecordObject(pilot.derived_bidirectional_value_flow);
|
||
if (bidirectionalFlow) {
|
||
const incoming = toRecordObject(bidirectionalFlow.incoming_customer_revenue);
|
||
const outgoing = toRecordObject(bidirectionalFlow.outgoing_supplier_payout);
|
||
const incomingAmount = input.toNonEmptyString(incoming?.total_amount_human_ru);
|
||
const outgoingAmount = input.toNonEmptyString(outgoing?.total_amount_human_ru);
|
||
const netAmount = input.toNonEmptyString(bidirectionalFlow.net_amount_human_ru);
|
||
const period = input.toNonEmptyString(bidirectionalFlow.period_scope);
|
||
const periodPart = period ? ` за ${period}` : "";
|
||
if (incomingAmount && outgoingAmount && netAmount) {
|
||
pushBusinessLine(confirmedLines, `Денежный поток${periodPart}: получили ${incomingAmount}, заплатили ${outgoingAmount}, расчетное нетто ${netAmount}.`);
|
||
const incomingNumber = parseHumanMoney(incomingAmount);
|
||
const outgoingNumber = parseHumanMoney(outgoingAmount);
|
||
const netNumber = parseHumanMoney(netAmount);
|
||
if (incomingNumber && outgoingNumber !== null && netNumber !== null) {
|
||
const spreadPercent = Math.abs(netNumber) / Math.max(Math.abs(incomingNumber), 1);
|
||
const spreadLabel = `${Math.round(spreadPercent * 100)}%`;
|
||
if (spreadPercent < 0.1) {
|
||
pushBusinessLine(interpretationLines, `Обороты есть, но денежный спред узкий: нетто около ${spreadLabel} от входящего потока. Это не прибыль, но сигнал, что маржу надо проверять отдельно.`);
|
||
}
|
||
else if (netNumber > 0) {
|
||
pushBusinessLine(interpretationLines, `Денежный поток в проверенном срезе положительный: нетто около ${spreadLabel} от входящего потока. Это хороший cash-flow сигнал, но не доказанная прибыль.`);
|
||
}
|
||
else {
|
||
pushBusinessLine(interpretationLines, `Денежный поток в проверенном срезе отрицательный: исходящие платежи выше входящих примерно на ${spreadLabel} от входящего потока. Нужна проверка причин и структуры расходов.`);
|
||
}
|
||
}
|
||
hasNet = true;
|
||
moneySignalCount += 1;
|
||
}
|
||
}
|
||
const valueFlow = toRecordObject(pilot.derived_value_flow);
|
||
if (valueFlow) {
|
||
const amount = input.toNonEmptyString(valueFlow.total_amount_human_ru);
|
||
const period = input.toNonEmptyString(valueFlow.period_scope);
|
||
const direction = String(valueFlow.value_flow_direction ?? "");
|
||
const periodPart = period ? ` за ${period}` : "";
|
||
if (amount) {
|
||
pushBusinessLine(confirmedLines, direction === "outgoing_supplier_payout"
|
||
? `Исходящий денежный поток${periodPart}: ${amount}.`
|
||
: `Входящий денежный поток${periodPart}: ${amount}.`);
|
||
moneySignalCount += 1;
|
||
}
|
||
}
|
||
const activityPeriod = toRecordObject(pilot.derived_activity_period);
|
||
if (pilotScope === "counterparty_lifecycle_query_documents_v1" && activityPeriod) {
|
||
const duration = input.toNonEmptyString(activityPeriod.duration_human_ru);
|
||
const first = input.toNonEmptyString(activityPeriod.first_activity_date);
|
||
const latest = input.toNonEmptyString(activityPeriod.latest_activity_date);
|
||
if (duration) {
|
||
pushBusinessLine(confirmedLines, `Подтвержденная активность в 1С: примерно ${duration}${first && latest ? ` (${first} - ${latest})` : ""}.`);
|
||
}
|
||
}
|
||
if (pilotScope === "inventory_route_template_v1") {
|
||
pushBusinessLine(confirmedLines, "Есть проверенный складской/товарный срез; его можно использовать как операционный контекст, но не как финансовую прибыль.");
|
||
}
|
||
}
|
||
if (hasRanking) {
|
||
pushBusinessLine(interpretationLines, "По клиентской базе уже виден лидер, но концентрацию выручки надо проверять отдельным рейтингом и долями, а не одной строкой.");
|
||
}
|
||
if (moneySignalCount > 0 && !hasNet) {
|
||
pushBusinessLine(interpretationLines, "Денежный контур частично подтвержден, но без входящие-вс-исходящие нельзя честно говорить о чистом денежном эффекте.");
|
||
}
|
||
return {
|
||
confirmedLines: confirmedLines.slice(0, 6),
|
||
interpretationLines: interpretationLines.slice(0, 5),
|
||
hasRanking,
|
||
hasNet,
|
||
moneySignalCount
|
||
};
|
||
}
|
||
function buildAddressMemoryRecapReply(input) {
|
||
const contextFacts = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(input.addressDebug, input.toNonEmptyString);
|
||
const item = contextFacts.item;
|
||
const counterparty = contextFacts.counterparty;
|
||
const organization = input.organization ?? contextFacts.organization;
|
||
const scopedDate = contextFacts.scopedDate;
|
||
const recapFacts = collectRecentRecapFacts({
|
||
sessionItems: input.sessionItems,
|
||
item,
|
||
organization,
|
||
toNonEmptyString: input.toNonEmptyString
|
||
});
|
||
if (item) {
|
||
if (recapFacts.length > 0) {
|
||
const datePart = scopedDate ? ` в срезе на ${scopedDate}` : "";
|
||
const organizationPart = organization ? ` по компании «${organization}»` : "";
|
||
return [
|
||
`Да, помню. По позиции «${item}»${organizationPart}${datePart} мы уже выяснили:`,
|
||
...recapFacts.map((fact) => `- ${fact}.`),
|
||
"Могу сразу продолжить по ней: поставщик, закупка, документы или продажа."
|
||
].join("\n");
|
||
}
|
||
const datePart = scopedDate ? ` в срезе на ${scopedDate}` : "";
|
||
const organizationPart = organization ? ` по компании «${organization}»` : "";
|
||
return [
|
||
`Да, помню. Мы обсуждали позицию «${item}»${organizationPart}${datePart}.`,
|
||
"Могу продолжить по ней без переписывания сущности: кто поставил, когда купили, по каким документам или кому продали."
|
||
].join(" ");
|
||
}
|
||
if (counterparty) {
|
||
const organizationPart = organization ? ` по компании «${organization}»` : "";
|
||
const periodPart = periodPartForRecap(scopedDate);
|
||
if (recapFacts.length > 0) {
|
||
return [
|
||
`Да, помню. По контрагенту «${counterparty}»${organizationPart}${periodPart} мы уже выяснили:`,
|
||
...recapFacts.map((fact) => `- ${fact}.`),
|
||
"Могу сразу продолжить по нему: поступления, платежи, нетто, помесячную раскладку или границы подтверждения."
|
||
].join("\n");
|
||
}
|
||
return [
|
||
`Да, помню. Мы уже смотрели контур по контрагенту «${counterparty}»${organizationPart}${periodPart}.`,
|
||
"Могу продолжить по нему без переписывания контекста: поступления, платежи, нетто, документы или пояснение границ ответа."
|
||
].join(" ");
|
||
}
|
||
if (recapFacts.length > 0) {
|
||
return [
|
||
"Да, помню. В предыдущем проверенном контуре мы уже выяснили:",
|
||
...recapFacts.map((fact) => `- ${fact}.`),
|
||
"Могу продолжить от этого места: углубиться в данные, документы, движения или границы подтверждения."
|
||
].join("\n");
|
||
}
|
||
if (organization || scopedDate) {
|
||
const organizationPart = organization ? ` по компании «${organization}»` : "";
|
||
const datePart = scopedDate ? ` на ${scopedDate}` : "";
|
||
return [
|
||
`Да, помню. Мы уже смотрели адресный контур${organizationPart}${datePart}.`,
|
||
"Могу кратко напомнить контекст или сразу продолжить следующий шаг по этому же сценарию."
|
||
].join(" ");
|
||
}
|
||
return "Да, помню предыдущий адресный контур. Могу кратко напомнить, что мы уже подтвердили, или сразу продолжить следующий шаг.";
|
||
}
|
||
function buildBroadBusinessEvaluationReply(input) {
|
||
const contextFacts = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(input.addressDebug, input.toNonEmptyString);
|
||
const organization = input.organization ?? contextFacts.organization;
|
||
const recapFacts = collectRecentRecapFacts({
|
||
sessionItems: input.sessionItems,
|
||
item: null,
|
||
organization,
|
||
toNonEmptyString: input.toNonEmptyString,
|
||
limit: 5
|
||
});
|
||
const organizationPart = organization ? ` по компании «${organization}»` : "";
|
||
const businessEvidence = collectBusinessEvaluationEvidence({
|
||
sessionItems: input.sessionItems,
|
||
organization,
|
||
toNonEmptyString: input.toNonEmptyString
|
||
});
|
||
const hasBusinessEvidence = businessEvidence.confirmedLines.length > 0;
|
||
if (recapFacts.length > 0 || hasBusinessEvidence) {
|
||
const moneyFactCount = recapFacts.filter((fact) => /(?:денежн|нетто|поступлен|платеж|рейтинг|клиент|выруч|оборот|заплатили|получили)/iu.test(fact)).length + businessEvidence.moneySignalCount;
|
||
const hasRankingFact = businessEvidence.hasRanking ||
|
||
recapFacts.some((fact) => /(?:рейтинг|клиент|единственного клиента)/iu.test(fact));
|
||
const hasNetFact = businessEvidence.hasNet || recapFacts.some((fact) => /нетто/iu.test(fact));
|
||
const auditLines = [
|
||
moneyFactCount > 0
|
||
? "- Денежный контур уже выглядит операционно значимым: есть подтвержденные поступления, платежи или клиентские срезы."
|
||
: "- Операционная активность подтверждена, но денежный контур пока раскрыт слабо.",
|
||
hasRankingFact
|
||
? "- По клиентской базе уже есть точечные лидеры, но это еще не полноценная управленческая сегментация всей базы."
|
||
: "- Ключевых клиентов и концентрацию выручки стоит добрать отдельным рейтингом.",
|
||
hasNetFact
|
||
? "- По нетто можно обсуждать направление денежного потока, но прибыль и маржу этим не доказываем."
|
||
: "- Прибыль, маржа и качество операционки пока не доказаны: нужны расходы, себестоимость и задолженность."
|
||
];
|
||
return [
|
||
`Коротко: по уже подтвержденным срезам 1С${organizationPart} бизнес выглядит операционно живым; это предварительная оценка бизнеса, а для взрослого вывода еще нужны прибыль, маржа и долги.`,
|
||
"Что уже видно:",
|
||
...recapFacts.map((fact) => `- ${ensureSentence(fact)}`),
|
||
...(businessEvidence.confirmedLines.length > 0
|
||
? ["Подтвержденные метрики:", ...businessEvidence.confirmedLines.map((fact) => `- ${ensureSentence(fact)}`)]
|
||
: []),
|
||
"Предварительный LLM-аудит:",
|
||
...businessEvidence.interpretationLines.map((fact) => `- ${ensureSentence(fact)}`),
|
||
...auditLines,
|
||
"Что добрать для полной оценки: обороты по годам, топ клиентов, входящие/исходящие деньги, дебиторку/кредиторку, НДС и признаки маржинальности."
|
||
].join("\n");
|
||
}
|
||
return [
|
||
`Коротко: по нынешнему контексту 1С${organizationPart} я вижу признаки операционной активности, но для содержательной оценки бизнеса нужно еще несколько опорных срезов.`,
|
||
"Если хочешь, я быстро доберу основу для такой оценки: денежный поток, дебиторка/кредиторка, НДС или ключевые контрагенты."
|
||
].join(" ");
|
||
}
|
||
function buildConversationExecutiveSummaryReply(input) {
|
||
const contextFacts = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(input.addressDebug, input.toNonEmptyString);
|
||
const organization = input.organization ?? contextFacts.organization;
|
||
const organizationPart = organization ? ` по компании «${organization}»` : "";
|
||
const recapFacts = collectRecentRecapFacts({
|
||
sessionItems: input.sessionItems,
|
||
item: null,
|
||
organization,
|
||
toNonEmptyString: input.toNonEmptyString,
|
||
limit: 8
|
||
});
|
||
const businessEvidence = collectBusinessEvaluationEvidence({
|
||
sessionItems: input.sessionItems,
|
||
organization,
|
||
toNonEmptyString: input.toNonEmptyString
|
||
});
|
||
const confirmedLines = [];
|
||
for (const fact of recapFacts) {
|
||
pushBusinessLine(confirmedLines, ensureSentence(fact));
|
||
}
|
||
for (const fact of businessEvidence.confirmedLines) {
|
||
pushBusinessLine(confirmedLines, ensureSentence(fact));
|
||
}
|
||
const proxyLines = [];
|
||
for (const line of businessEvidence.interpretationLines) {
|
||
pushBusinessLine(proxyLines, ensureSentence(line));
|
||
}
|
||
if (businessEvidence.hasNet) {
|
||
pushBusinessLine(proxyLines, "Нетто по деньгам можно использовать как cash-flow proxy, но это не бухгалтерская прибыль и не маржа.");
|
||
}
|
||
if (businessEvidence.hasRanking) {
|
||
pushBusinessLine(proxyLines, "Крупнейшие клиенты/контрагенты видны как операционный сигнал концентрации, но это не полноценный CRM-аудит.");
|
||
}
|
||
const missingLines = [
|
||
"Чистая прибыль, финрезультат и маржа не доказаны без отдельной проверки себестоимости, расходов и закрытия периода.",
|
||
"Просрочка, качество долга и due-date aging не доказаны без сроков оплаты и отдельного долгового контура.",
|
||
"Ликвидность склада, резервы, списания и устаревание нельзя считать подтвержденными без специальных складских доказательств.",
|
||
"Vendor-risk, качество закупок и юридическая надежность контрагентов остаются вне подтвержденного контура."
|
||
];
|
||
const manualLines = [
|
||
"Сверить ОСВ/финрезультат и управленческие расходы за ключевые годы.",
|
||
"Отдельно проверить старые открытые расчеты, крупные долги и документы закрытия.",
|
||
"Посмотреть концентрацию клиентов и поставщиков не только по сумме, но и по доле в периоде.",
|
||
"Сверить НДС, склад и договоры там, где ответ опирался на proxy, а не на прямой учетный факт."
|
||
];
|
||
const confirmedSection = confirmedLines.length > 0
|
||
? confirmedLines.slice(0, 10).map((line) => `- ${line}`)
|
||
: ["- Есть grounded-контекст диалога, но для строгого executive summary не хватает подтвержденных метрик в текущем окне."];
|
||
const proxySection = proxyLines.length > 0
|
||
? proxyLines.slice(0, 6).map((line) => `- ${line}`)
|
||
: ["- Proxy-выводы пока слабые: можно говорить только о направлении проверки, не о зрелой оценке бизнеса."];
|
||
return [
|
||
`Executive summary${organizationPart}: по диалогу уже можно собрать рабочую карту подтвержденного, proxy и ручного контроля, но не стоит выдавать это за полный аудит компании.`,
|
||
"Подтверждено по данным/ответам 1С:",
|
||
...confirmedSection,
|
||
"Proxy и осторожная аналитика:",
|
||
...proxySection,
|
||
"Где не хватило доказательств:",
|
||
...missingLines.map((line) => `- ${line}`),
|
||
"Что директору смотреть руками в первую очередь:",
|
||
...manualLines.map((line) => `- ${line}`)
|
||
].join("\n");
|
||
}
|
||
function buildSelectedObjectAnswerInspectionReply(input) {
|
||
const contextFacts = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(input.addressDebug, input.toNonEmptyString);
|
||
const itemLabel = contextFacts.item ?? "эта позиция";
|
||
const counterpartyLabel = contextFacts.counterparty;
|
||
const detectedIntent = String(input.addressDebug?.detected_intent ?? "");
|
||
const pilotScope = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryPilotScope)(input.addressDebug, input.toNonEmptyString);
|
||
const periodPart = periodPartForRecap(contextFacts.scopedDate);
|
||
if (counterpartyLabel && pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1") {
|
||
return [
|
||
`Да, в предыдущем ответе речь шла о двустороннем денежном потоке с контрагентом «${counterpartyLabel}»${periodPart}.`,
|
||
"Нетто там означало разницу между тем, что получили, и тем, что заплатили по найденным строкам 1С.",
|
||
"Это расчет по проверенному периоду и подтвержденным строкам, а не заявление про весь оборот вне этого окна."
|
||
].join(" ");
|
||
}
|
||
if (counterpartyLabel && pilotScope === "counterparty_supplier_payout_query_movements_v1") {
|
||
return [
|
||
`Да, в предыдущем ответе речь шла об исходящих платежах/списаниях по контрагенту «${counterpartyLabel}»${periodPart}.`,
|
||
"Это сумма по найденным строкам 1С за проверенный период, а не обещание, что за пределами этого окна больше движений не было."
|
||
].join(" ");
|
||
}
|
||
if (counterpartyLabel && pilotScope === "counterparty_value_flow_query_movements_v1") {
|
||
return [
|
||
`Да, в предыдущем ответе речь шла о денежном потоке по контрагенту «${counterpartyLabel}»${periodPart}.`,
|
||
"Это расчет по найденным движениям 1С за проверенный период, а не безусловный итог по всем временам."
|
||
].join(" ");
|
||
}
|
||
if (counterpartyLabel && pilotScope === "counterparty_lifecycle_query_documents_v1") {
|
||
return [
|
||
`Да, в предыдущем ответе речь шла об активности контрагента «${counterpartyLabel}»${periodPart}.`,
|
||
"Это оценка по подтвержденным строкам 1С, а не юридически подтвержденная дата регистрации."
|
||
].join(" ");
|
||
}
|
||
if (detectedIntent === "inventory_sale_trace_for_item") {
|
||
return [
|
||
`Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а сама позиция, по которой мы смотрели продажу.`,
|
||
"В предыдущем ответе я показывал документы выбытия по этой позиции. Покупатель в доступных данных отдельно не выделен, поэтому назвать контрагента-покупателя я там не мог.",
|
||
"Если хочешь, следующим шагом могу отдельно проверить, можно ли вытащить покупателя по связанным документам реализации."
|
||
].join(" ");
|
||
}
|
||
if (detectedIntent === "inventory_purchase_provenance_for_item" ||
|
||
detectedIntent === "inventory_purchase_documents_for_item") {
|
||
return [
|
||
`Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а сама позиция или номенклатура.`,
|
||
"В предыдущем ответе речь шла о закупке этой позиции: я перечислял поставщиков или закупочные документы по ней, а не называл саму позицию контрагентом."
|
||
].join(" ");
|
||
}
|
||
return [
|
||
`Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а выбранный объект разбора.`,
|
||
"Я сейчас уточняю именно смысл предыдущего grounded-ответа по этой позиции, а не запускаю новый адресный поиск."
|
||
].join(" ");
|
||
}
|
||
function resolveAssistantLivingChatMemoryContext(input) {
|
||
const contextualInventoryHistoryCapabilityFollowup = String(input.modeDecisionReason ?? "") === "inventory_history_capability_followup_detected";
|
||
const contextualMemoryRecapFollowup = String(input.modeDecisionReason ?? "") === "memory_recap_followup_detected";
|
||
const contextualAnswerInspectionFollowup = String(input.modeDecisionReason ?? "") === "answer_inspection_followup_detected";
|
||
const continuity = (0, assistantContinuityPolicy_1.resolveAssistantContinuitySnapshot)({
|
||
sessionItems: input.sessionItems,
|
||
toNonEmptyString
|
||
});
|
||
return {
|
||
contextualInventoryHistoryCapabilityFollowup,
|
||
contextualMemoryRecapFollowup,
|
||
contextualAnswerInspectionFollowup,
|
||
lastGroundedInventoryAddressDebug: contextualInventoryHistoryCapabilityFollowup
|
||
? continuity.lastGroundedInventoryAddressDebug
|
||
: null,
|
||
lastMemoryAddressDebug: contextualMemoryRecapFollowup
|
||
? continuity.lastGroundedItemAddressDebug ?? continuity.lastGroundedAddressDebug
|
||
: null,
|
||
lastAnswerInspectionAddressDebug: contextualAnswerInspectionFollowup
|
||
? continuity.lastGroundedItemAddressDebug ?? continuity.lastGroundedAddressDebug
|
||
: null
|
||
};
|
||
}
|
||
function createAssistantMemoryRecapPolicy(deps) {
|
||
function resolveRouteMemorySignals(input) {
|
||
const samples = collectMessageSamples(input);
|
||
const continuity = (0, assistantContinuityPolicy_1.resolveAssistantContinuitySnapshot)({
|
||
sessionItems: input.sessionItems,
|
||
toNonEmptyString
|
||
});
|
||
const groundedInventoryContext = continuity.lastGroundedInventoryAddressDebug ?? input.lastGroundedAddressDebug;
|
||
const historicalCapabilitySignal = hasSignalAcrossSamples(samples, deps.hasHistoricalCapabilityFollowupSignal);
|
||
const memoryRecapSignal = hasSignalAcrossSamples(samples, deps.hasConversationMemoryRecallFollowupSignal);
|
||
const explicitRecapPromptSignal = hasExplicitRecapPromptSignal(samples);
|
||
return {
|
||
contextualHistoricalCapabilityFollowupDetected: Boolean(input.capabilityMetaQuery &&
|
||
!input.dataScopeMetaQuery &&
|
||
!input.dataRetrievalSignal &&
|
||
historicalCapabilitySignal &&
|
||
deps.isGroundedInventoryContextDebug(groundedInventoryContext)),
|
||
contextualMemoryRecapFollowupDetected: Boolean(!input.dataScopeMetaQuery &&
|
||
!input.capabilityMetaQuery &&
|
||
!input.aggregateBusinessAnalyticsSignal &&
|
||
memoryRecapSignal &&
|
||
(explicitRecapPromptSignal || (!input.dataRetrievalSignal && !input.strongDataSignal)) &&
|
||
continuity.hasGroundedAddressContext)
|
||
};
|
||
}
|
||
return {
|
||
resolveRouteMemorySignals
|
||
};
|
||
}
|