NODEDC_1C/llm_normalizer/backend/dist/services/assistantMemoryRecapPolicy.js

841 lines
50 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 readSessionAssistantText(item, toNonEmptyString) {
const record = toRecordObject(item);
if (!record) {
return null;
}
const direct = toNonEmptyString(record.text) ??
toNonEmptyString(record.chatText) ??
toNonEmptyString(record.assistant_message) ??
toNonEmptyString(record.answer) ??
toNonEmptyString(record.output);
if (direct) {
return direct;
}
const humanReadable = toRecordObject(record.human_readable);
const technicalJson = toRecordObject(record.technical_json);
return (toNonEmptyString(humanReadable?.answer) ??
toNonEmptyString(humanReadable?.assistant_message) ??
toNonEmptyString(technicalJson?.assistant_message));
}
function readSessionItemTraceId(item, toNonEmptyString) {
const record = toRecordObject(item);
const debug = toRecordObject(record?.debug) ?? toRecordObject(toRecordObject(record?.technical_json)?.debug);
return toNonEmptyString(debug?.trace_id) ?? toNonEmptyString(record?.trace_id);
}
function findPriorAssistantAnswerForDebug(input) {
const sessionItems = Array.isArray(input.sessionItems) ? input.sessionItems : [];
if (sessionItems.length === 0) {
return null;
}
const targetTraceId = input.toNonEmptyString(input.addressDebug?.trace_id);
const targetItemKey = normalizeRecapIdentity(input.item);
let fallbackText = null;
for (let index = sessionItems.length - 1; index >= 0; index -= 1) {
const record = toRecordObject(sessionItems[index]);
if (!record || record.role !== "assistant") {
continue;
}
const text = readSessionAssistantText(record, input.toNonEmptyString);
if (!text) {
continue;
}
const traceId = readSessionItemTraceId(record, input.toNonEmptyString);
if (targetTraceId && traceId === targetTraceId) {
return text;
}
const debug = toRecordObject(record.debug) ?? toRecordObject(toRecordObject(record.technical_json)?.debug);
const detectedIntent = String(debug?.detected_intent ?? "");
const debugContext = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(debug, input.toNonEmptyString);
const itemMatches = targetItemKey && normalizeRecapIdentity(debugContext.item) === targetItemKey;
if (detectedIntent === input.detectedIntent && itemMatches && !fallbackText) {
fallbackText = text;
}
}
return fallbackText;
}
function cleanExtractedCounterpartyLabel(value, itemLabel) {
let text = String(value ?? "")
.replace(/\*\*/g, "")
.replace(/[«»"]/g, "")
.split("|")[0]
.split("\n")[0]
.trim();
text = text.replace(/^[\s:.-]+/u, "").replace(/[\s,;:.]+$/u, "").trim();
if (!text || /(?:не выделен|не найден|не определен|не подтвержден)/iu.test(text)) {
return null;
}
if (itemLabel && normalizeRecapIdentity(text) === normalizeRecapIdentity(itemLabel)) {
return null;
}
return text;
}
function extractBuyerFromSaleTraceAnswer(answerText, itemLabel) {
if (!answerText) {
return null;
}
const patterns = [
/покупатель\s+определ[её]н\s*:\s*([^\n]+)/iu,
/отгружал[^\n:]*покупател[^\n:]*:\s*([^\n]+)/iu,
/покупател[^\n:]*:\s*([^\n]+)/iu,
/контрагент\s*:\s*([^|\n]+)/iu
];
for (const pattern of patterns) {
const match = answerText.match(pattern);
const label = match?.[1] ? cleanExtractedCounterpartyLabel(match[1], itemLabel) : null;
if (label) {
return label;
}
}
return null;
}
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") {
const priorAnswerText = findPriorAssistantAnswerForDebug({
sessionItems: input.sessionItems,
addressDebug: input.addressDebug,
item: contextFacts.item,
detectedIntent,
toNonEmptyString: input.toNonEmptyString
});
const buyerLabel = extractBuyerFromSaleTraceAnswer(priorAnswerText, contextFacts.item);
if (buyerLabel) {
return [
`Нет, в данных это не ошибка: «${itemLabel}» здесь товар/номенклатура, а не контрагент.`,
`Контрагент-покупатель в предыдущем ответе был «${buyerLabel}».`,
"В строках вида «товар: ... | контрагент: ...» эти поля надо читать отдельно; я не должен трактовать название товара как контрагента."
].join(" ");
}
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
};
}