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

498 lines
31 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.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+мы)/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 buildRecapFactLine(input) {
const detectedIntent = String(input.debug?.detected_intent ?? "");
const scopedDate = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(input.debug).scopedDate;
const discoveryFact = buildDiscoveryRecapFactLine({
debug: input.debug,
counterparty: input.counterparty,
organization: input.organization,
scopedDate
});
if (discoveryFact) {
return discoveryFact;
}
const itemPart = input.item ? `по позиции «${input.item}»` : null;
const counterpartyPart = input.counterparty ? `по контрагенту «${input.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 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}»` : "";
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} компания выглядит операционно живой; это предварительная оценка бизнеса, а для взрослого вывода еще нужны прибыль, маржа и долги.`,
"Что уже видно:",
...recapFacts.map((fact) => `- ${ensureSentence(fact)}`),
"Предварительный LLM-аудит:",
...auditLines,
"Что добрать для полной оценки: обороты по годам, топ клиентов, входящие/исходящие деньги, дебиторку/кредиторку, НДС и признаки маржинальности."
].join("\n");
}
return [
`Коротко: по нынешнему контексту 1С${organizationPart} я вижу признаки операционной активности, но для содержательной оценки бизнеса нужно еще несколько опорных срезов.`,
"Если хочешь, я быстро доберу основу для такой оценки: денежный поток, дебиторка/кредиторка, НДС или ключевые контрагенты."
].join(" ");
}
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
};
}