ARCH: восстановить память discovery-ответов в living chat
This commit is contained in:
parent
cd3315e06d
commit
b542b65b81
|
|
@ -1,8 +1,10 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.readAssistantMcpDiscoveryPilotScope = readAssistantMcpDiscoveryPilotScope;
|
||||||
exports.formatIsoDateForReply = formatIsoDateForReply;
|
exports.formatIsoDateForReply = formatIsoDateForReply;
|
||||||
exports.readAddressDebugFilters = readAddressDebugFilters;
|
exports.readAddressDebugFilters = readAddressDebugFilters;
|
||||||
exports.readAddressDebugItem = readAddressDebugItem;
|
exports.readAddressDebugItem = readAddressDebugItem;
|
||||||
|
exports.readAddressDebugCounterparty = readAddressDebugCounterparty;
|
||||||
exports.readAddressDebugOrganization = readAddressDebugOrganization;
|
exports.readAddressDebugOrganization = readAddressDebugOrganization;
|
||||||
exports.readAddressDebugScopedDate = readAddressDebugScopedDate;
|
exports.readAddressDebugScopedDate = readAddressDebugScopedDate;
|
||||||
exports.readAddressDebugTemporalScope = readAddressDebugTemporalScope;
|
exports.readAddressDebugTemporalScope = readAddressDebugTemporalScope;
|
||||||
|
|
@ -41,6 +43,48 @@ function toRecordObject(value) {
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
function readAssistantMcpDiscoveryEntry(debug) {
|
||||||
|
const entry = toRecordObject(debug?.assistant_mcp_discovery_entry_point_v1);
|
||||||
|
return fallbackToNonEmptyString(entry?.schema_version) === "assistant_mcp_discovery_runtime_entry_point_v1"
|
||||||
|
? entry
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
function readAssistantMcpDiscoveryTurnMeaning(debug) {
|
||||||
|
const entry = readAssistantMcpDiscoveryEntry(debug);
|
||||||
|
const turnInput = toRecordObject(entry?.turn_input);
|
||||||
|
return toRecordObject(turnInput?.turn_meaning_ref);
|
||||||
|
}
|
||||||
|
function readAssistantMcpDiscoveryBridge(debug) {
|
||||||
|
return toRecordObject(readAssistantMcpDiscoveryEntry(debug)?.bridge);
|
||||||
|
}
|
||||||
|
function readAssistantMcpDiscoveryPilotScope(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||||
|
const bridge = readAssistantMcpDiscoveryBridge(debug);
|
||||||
|
const pilot = toRecordObject(bridge?.pilot);
|
||||||
|
return toNonEmptyString(pilot?.pilot_scope);
|
||||||
|
}
|
||||||
|
function formatDiscoveryDateScopeForReply(value) {
|
||||||
|
const text = fallbackToNonEmptyString(value);
|
||||||
|
if (!text) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return formatIsoDateForReply(text) ?? text;
|
||||||
|
}
|
||||||
|
function hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||||
|
if (!debug || debug.mcp_discovery_response_applied !== true) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const entry = readAssistantMcpDiscoveryEntry(debug);
|
||||||
|
const bridge = readAssistantMcpDiscoveryBridge(debug);
|
||||||
|
const bridgeStatus = toNonEmptyString(bridge?.bridge_status);
|
||||||
|
const answerDraft = toRecordObject(bridge?.answer_draft);
|
||||||
|
const answerMode = toNonEmptyString(answerDraft?.answer_mode);
|
||||||
|
return Boolean(entry &&
|
||||||
|
toNonEmptyString(entry.entry_status) === "bridge_executed" &&
|
||||||
|
bridgeStatus === "answer_draft_ready" &&
|
||||||
|
(bridge?.business_fact_answer_allowed === true ||
|
||||||
|
answerMode === "confirmed_with_bounded_inference" ||
|
||||||
|
answerMode === "bounded_inference_only"));
|
||||||
|
}
|
||||||
function mergeKnownOrganizationsDefault(values) {
|
function mergeKnownOrganizationsDefault(values) {
|
||||||
return (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)(values);
|
return (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)(values);
|
||||||
}
|
}
|
||||||
|
|
@ -65,17 +109,43 @@ function readAddressDebugItem(debug, toNonEmptyString = fallbackToNonEmptyString
|
||||||
? toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw)
|
? toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw)
|
||||||
: null));
|
: null));
|
||||||
}
|
}
|
||||||
|
function readAddressDebugCounterparty(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||||
|
const extractedFilters = readAddressDebugFilters(debug);
|
||||||
|
if (toNonEmptyString(extractedFilters?.counterparty)) {
|
||||||
|
return toNonEmptyString(extractedFilters?.counterparty);
|
||||||
|
}
|
||||||
|
if (String(debug?.anchor_type ?? "") === "counterparty") {
|
||||||
|
return toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw);
|
||||||
|
}
|
||||||
|
const discoveryMeaning = readAssistantMcpDiscoveryTurnMeaning(debug);
|
||||||
|
const explicitEntities = Array.isArray(discoveryMeaning?.explicit_entity_candidates)
|
||||||
|
? discoveryMeaning?.explicit_entity_candidates
|
||||||
|
: [];
|
||||||
|
for (const entity of explicitEntities) {
|
||||||
|
const text = toNonEmptyString(entity);
|
||||||
|
if (text) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
function readAddressDebugOrganization(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
function readAddressDebugOrganization(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||||
const extractedFilters = readAddressDebugFilters(debug);
|
const extractedFilters = readAddressDebugFilters(debug);
|
||||||
const rootFrameContext = toRecordObject(debug?.address_root_frame_context);
|
const rootFrameContext = toRecordObject(debug?.address_root_frame_context);
|
||||||
return toNonEmptyString(extractedFilters?.organization) ?? toNonEmptyString(rootFrameContext?.organization);
|
const discoveryMeaning = readAssistantMcpDiscoveryTurnMeaning(debug);
|
||||||
|
return (toNonEmptyString(extractedFilters?.organization) ??
|
||||||
|
toNonEmptyString(rootFrameContext?.organization) ??
|
||||||
|
toNonEmptyString(discoveryMeaning?.explicit_organization_scope) ??
|
||||||
|
toNonEmptyString(debug?.assistant_active_organization) ??
|
||||||
|
toNonEmptyString(debug?.living_chat_selected_organization));
|
||||||
}
|
}
|
||||||
function readAddressDebugScopedDate(debug) {
|
function readAddressDebugScopedDate(debug) {
|
||||||
const extractedFilters = readAddressDebugFilters(debug);
|
const extractedFilters = readAddressDebugFilters(debug);
|
||||||
const rootFrameContext = toRecordObject(debug?.address_root_frame_context);
|
const rootFrameContext = toRecordObject(debug?.address_root_frame_context);
|
||||||
return (formatIsoDateForReply(extractedFilters?.as_of_date) ??
|
return (formatIsoDateForReply(extractedFilters?.as_of_date) ??
|
||||||
formatIsoDateForReply(rootFrameContext?.as_of_date) ??
|
formatIsoDateForReply(rootFrameContext?.as_of_date) ??
|
||||||
formatIsoDateForReply(extractedFilters?.period_to));
|
formatIsoDateForReply(extractedFilters?.period_to) ??
|
||||||
|
formatDiscoveryDateScopeForReply(readAssistantMcpDiscoveryTurnMeaning(debug)?.explicit_date_scope));
|
||||||
}
|
}
|
||||||
function readAddressDebugTemporalScope(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
function readAddressDebugTemporalScope(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||||
const extractedFilters = readAddressDebugFilters(debug);
|
const extractedFilters = readAddressDebugFilters(debug);
|
||||||
|
|
@ -110,6 +180,13 @@ function resolveAddressDebugAnchorContext(debug, toNonEmptyString = fallbackToNo
|
||||||
anchorValue: counterparty
|
anchorValue: counterparty
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const discoveryCounterparty = readAddressDebugCounterparty(debug, toNonEmptyString);
|
||||||
|
if (discoveryCounterparty) {
|
||||||
|
return {
|
||||||
|
anchorType: "counterparty",
|
||||||
|
anchorValue: discoveryCounterparty
|
||||||
|
};
|
||||||
|
}
|
||||||
const account = toNonEmptyString(extractedFilters?.account);
|
const account = toNonEmptyString(extractedFilters?.account);
|
||||||
if (account) {
|
if (account) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -149,6 +226,7 @@ function resolveNavigationSessionContextState(addressNavigationState, toNonEmpty
|
||||||
function resolveAddressDebugContextFacts(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
function resolveAddressDebugContextFacts(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||||
return {
|
return {
|
||||||
item: readAddressDebugItem(debug, toNonEmptyString),
|
item: readAddressDebugItem(debug, toNonEmptyString),
|
||||||
|
counterparty: readAddressDebugCounterparty(debug, toNonEmptyString),
|
||||||
organization: readAddressDebugOrganization(debug, toNonEmptyString),
|
organization: readAddressDebugOrganization(debug, toNonEmptyString),
|
||||||
scopedDate: readAddressDebugScopedDate(debug)
|
scopedDate: readAddressDebugScopedDate(debug)
|
||||||
};
|
};
|
||||||
|
|
@ -538,13 +616,12 @@ function isGroundedAddressDebug(debug, toNonEmptyString = fallbackToNonEmptyStri
|
||||||
if (!debug || typeof debug !== "object") {
|
if (!debug || typeof debug !== "object") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const executionLane = toNonEmptyString(debug.execution_lane);
|
|
||||||
if (executionLane !== "address_query") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const answerGroundingCheck = toRecordObject(debug.answer_grounding_check);
|
const answerGroundingCheck = toRecordObject(debug.answer_grounding_check);
|
||||||
const groundingStatus = toNonEmptyString(answerGroundingCheck?.status);
|
const groundingStatus = toNonEmptyString(answerGroundingCheck?.status);
|
||||||
return groundingStatus === "grounded";
|
if (groundingStatus === "grounded" && toNonEmptyString(debug.execution_lane) === "address_query") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString);
|
||||||
}
|
}
|
||||||
function isGroundedInventoryContextDebug(debug, toNonEmptyString) {
|
function isGroundedInventoryContextDebug(debug, toNonEmptyString) {
|
||||||
if (!isGroundedAddressDebug(debug, toNonEmptyString)) {
|
if (!isGroundedAddressDebug(debug, toNonEmptyString)) {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,62 @@ function toNonEmptyString(value) {
|
||||||
const text = String(value).trim();
|
const text = String(value).trim();
|
||||||
return text.length > 0 ? text : null;
|
return text.length > 0 ? text : null;
|
||||||
}
|
}
|
||||||
|
function toRecordObject(value) {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
function periodPartForRecap(scopedDate) {
|
||||||
|
if (!scopedDate) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return /^\d{2}\.\d{2}\.\d{4}$/.test(scopedDate) ? ` на ${scopedDate}` : ` за период ${scopedDate}`;
|
||||||
|
}
|
||||||
|
function buildDiscoveryRecapFactLine(input) {
|
||||||
|
if (!input.debug || !input.counterparty) {
|
||||||
|
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 === "counterparty_lifecycle_query_documents_v1") {
|
||||||
|
const activityPeriod = toRecordObject(pilot?.derived_activity_period);
|
||||||
|
const duration = toNonEmptyString(activityPeriod?.duration_human_ru);
|
||||||
|
return duration
|
||||||
|
? `смотрели подтвержденную активность по контрагенту «${input.counterparty}»${periodPart} и оценили период взаимодействия примерно как ${duration}`
|
||||||
|
: `смотрели подтвержденную активность по контрагенту «${input.counterparty}»${periodPart}`;
|
||||||
|
}
|
||||||
|
if (pilotScope === "counterparty_supplier_payout_query_movements_v1") {
|
||||||
|
const flow = toRecordObject(pilot?.derived_value_flow);
|
||||||
|
const amount = toNonEmptyString(flow?.total_amount_human_ru);
|
||||||
|
return amount
|
||||||
|
? `считали исходящие платежи/списания по контрагенту «${input.counterparty}»${periodPart}: ${amount}`
|
||||||
|
: `считали исходящие платежи/списания по контрагенту «${input.counterparty}»${periodPart}`;
|
||||||
|
}
|
||||||
|
if (pilotScope === "counterparty_value_flow_query_movements_v1") {
|
||||||
|
const flow = toRecordObject(pilot?.derived_value_flow);
|
||||||
|
const amount = toNonEmptyString(flow?.total_amount_human_ru);
|
||||||
|
return amount
|
||||||
|
? `смотрели денежный поток по контрагенту «${input.counterparty}»${periodPart}: ${amount}`
|
||||||
|
: `смотрели денежный поток по контрагенту «${input.counterparty}»${periodPart}`;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
if (incomingAmount && outgoingAmount && netAmount) {
|
||||||
|
return `считали нетто по деньгам с контрагентом «${input.counterparty}»${periodPart}: получили ${incomingAmount}, заплатили ${outgoingAmount}, расчетное нетто ${netAmount}`;
|
||||||
|
}
|
||||||
|
return `считали нетто по деньгам с контрагентом «${input.counterparty}»${periodPart}`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
function collectMessageSamples(input) {
|
function collectMessageSamples(input) {
|
||||||
const values = [
|
const values = [
|
||||||
input.rawUserMessage,
|
input.rawUserMessage,
|
||||||
|
|
@ -60,7 +116,16 @@ function normalizeRecapIdentity(value) {
|
||||||
function buildRecapFactLine(input) {
|
function buildRecapFactLine(input) {
|
||||||
const detectedIntent = String(input.debug?.detected_intent ?? "");
|
const detectedIntent = String(input.debug?.detected_intent ?? "");
|
||||||
const scopedDate = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(input.debug).scopedDate;
|
const scopedDate = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(input.debug).scopedDate;
|
||||||
|
const discoveryFact = buildDiscoveryRecapFactLine({
|
||||||
|
debug: input.debug,
|
||||||
|
counterparty: input.counterparty,
|
||||||
|
scopedDate
|
||||||
|
});
|
||||||
|
if (discoveryFact) {
|
||||||
|
return discoveryFact;
|
||||||
|
}
|
||||||
const itemPart = input.item ? `по позиции «${input.item}»` : null;
|
const itemPart = input.item ? `по позиции «${input.item}»` : null;
|
||||||
|
const counterpartyPart = input.counterparty ? `по контрагенту «${input.counterparty}»` : null;
|
||||||
const organizationPart = input.organization ? `по компании «${input.organization}»` : null;
|
const organizationPart = input.organization ? `по компании «${input.organization}»` : null;
|
||||||
const datePart = scopedDate ? ` на ${scopedDate}` : "";
|
const datePart = scopedDate ? ` на ${scopedDate}` : "";
|
||||||
if (detectedIntent === "inventory_on_hand_as_of_date") {
|
if (detectedIntent === "inventory_on_hand_as_of_date") {
|
||||||
|
|
@ -87,6 +152,9 @@ function buildRecapFactLine(input) {
|
||||||
if (detectedIntent === "counterparty_activity_lifecycle" && organizationPart) {
|
if (detectedIntent === "counterparty_activity_lifecycle" && organizationPart) {
|
||||||
return `смотрели активность в базе 1С ${organizationPart}`.trim();
|
return `смотрели активность в базе 1С ${organizationPart}`.trim();
|
||||||
}
|
}
|
||||||
|
if (detectedIntent === "list_documents_by_counterparty" && counterpartyPart) {
|
||||||
|
return `поднимали документы ${counterpartyPart}${datePart}`.trim();
|
||||||
|
}
|
||||||
if (detectedIntent === "list_documents_by_counterparty" && organizationPart) {
|
if (detectedIntent === "list_documents_by_counterparty" && organizationPart) {
|
||||||
return `поднимали документы ${organizationPart}${datePart}`.trim();
|
return `поднимали документы ${organizationPart}${datePart}`.trim();
|
||||||
}
|
}
|
||||||
|
|
@ -125,6 +193,7 @@ function collectRecentRecapFacts(input) {
|
||||||
const fact = buildRecapFactLine({
|
const fact = buildRecapFactLine({
|
||||||
debug: item.debug,
|
debug: item.debug,
|
||||||
item: debugItem,
|
item: debugItem,
|
||||||
|
counterparty: debugContext.counterparty,
|
||||||
organization: debugOrganization
|
organization: debugOrganization
|
||||||
});
|
});
|
||||||
if (!fact || seen.has(fact)) {
|
if (!fact || seen.has(fact)) {
|
||||||
|
|
@ -141,6 +210,7 @@ function collectRecentRecapFacts(input) {
|
||||||
function buildAddressMemoryRecapReply(input) {
|
function buildAddressMemoryRecapReply(input) {
|
||||||
const contextFacts = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(input.addressDebug, input.toNonEmptyString);
|
const contextFacts = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(input.addressDebug, input.toNonEmptyString);
|
||||||
const item = contextFacts.item;
|
const item = contextFacts.item;
|
||||||
|
const counterparty = contextFacts.counterparty;
|
||||||
const organization = input.organization ?? contextFacts.organization;
|
const organization = input.organization ?? contextFacts.organization;
|
||||||
const scopedDate = contextFacts.scopedDate;
|
const scopedDate = contextFacts.scopedDate;
|
||||||
const recapFacts = collectRecentRecapFacts({
|
const recapFacts = collectRecentRecapFacts({
|
||||||
|
|
@ -166,6 +236,21 @@ function buildAddressMemoryRecapReply(input) {
|
||||||
"Могу продолжить по ней без переписывания сущности: кто поставил, когда купили, по каким документам или кому продали."
|
"Могу продолжить по ней без переписывания сущности: кто поставил, когда купили, по каким документам или кому продали."
|
||||||
].join(" ");
|
].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 (organization || scopedDate) {
|
if (organization || scopedDate) {
|
||||||
const organizationPart = organization ? ` по компании «${organization}»` : "";
|
const organizationPart = organization ? ` по компании «${organization}»` : "";
|
||||||
const datePart = scopedDate ? ` на ${scopedDate}` : "";
|
const datePart = scopedDate ? ` на ${scopedDate}` : "";
|
||||||
|
|
@ -179,7 +264,35 @@ function buildAddressMemoryRecapReply(input) {
|
||||||
function buildSelectedObjectAnswerInspectionReply(input) {
|
function buildSelectedObjectAnswerInspectionReply(input) {
|
||||||
const contextFacts = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(input.addressDebug, input.toNonEmptyString);
|
const contextFacts = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(input.addressDebug, input.toNonEmptyString);
|
||||||
const itemLabel = contextFacts.item ?? "эта позиция";
|
const itemLabel = contextFacts.item ?? "эта позиция";
|
||||||
|
const counterpartyLabel = contextFacts.counterparty;
|
||||||
const detectedIntent = String(input.addressDebug?.detected_intent ?? "");
|
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") {
|
if (detectedIntent === "inventory_sale_trace_for_item") {
|
||||||
return [
|
return [
|
||||||
`Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а сама позиция, по которой мы смотрели продажу.`,
|
`Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а сама позиция, по которой мы смотрели продажу.`,
|
||||||
|
|
|
||||||
|
|
@ -3865,11 +3865,7 @@ function findLastGroundedAddressAnswerDebug(items) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const debug = item.debug;
|
const debug = item.debug;
|
||||||
if (debug.execution_lane !== "address_query") {
|
if ((0, assistantContinuityPolicy_1.isGroundedAddressDebug)(debug, toNonEmptyString)) {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const groundingStatus = toNonEmptyString(debug.answer_grounding_check?.status);
|
|
||||||
if (groundingStatus === "grounded") {
|
|
||||||
return debug;
|
return debug;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ export interface AssistantContinuitySnapshot {
|
||||||
|
|
||||||
export interface AssistantAddressDebugContextFacts {
|
export interface AssistantAddressDebugContextFacts {
|
||||||
item: string | null;
|
item: string | null;
|
||||||
|
counterparty: string | null;
|
||||||
organization: string | null;
|
organization: string | null;
|
||||||
scopedDate: string | null;
|
scopedDate: string | null;
|
||||||
}
|
}
|
||||||
|
|
@ -107,6 +108,68 @@ function toRecordObject(value: unknown): Record<string, unknown> | null {
|
||||||
return value as Record<string, unknown>;
|
return value as Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readAssistantMcpDiscoveryEntry(
|
||||||
|
debug: Record<string, unknown> | null
|
||||||
|
): Record<string, unknown> | null {
|
||||||
|
const entry = toRecordObject(debug?.assistant_mcp_discovery_entry_point_v1);
|
||||||
|
return fallbackToNonEmptyString(entry?.schema_version) === "assistant_mcp_discovery_runtime_entry_point_v1"
|
||||||
|
? entry
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readAssistantMcpDiscoveryTurnMeaning(
|
||||||
|
debug: Record<string, unknown> | null
|
||||||
|
): Record<string, unknown> | null {
|
||||||
|
const entry = readAssistantMcpDiscoveryEntry(debug);
|
||||||
|
const turnInput = toRecordObject(entry?.turn_input);
|
||||||
|
return toRecordObject(turnInput?.turn_meaning_ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readAssistantMcpDiscoveryBridge(
|
||||||
|
debug: Record<string, unknown> | null
|
||||||
|
): Record<string, unknown> | null {
|
||||||
|
return toRecordObject(readAssistantMcpDiscoveryEntry(debug)?.bridge);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readAssistantMcpDiscoveryPilotScope(
|
||||||
|
debug: Record<string, unknown> | null,
|
||||||
|
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||||
|
): string | null {
|
||||||
|
const bridge = readAssistantMcpDiscoveryBridge(debug);
|
||||||
|
const pilot = toRecordObject(bridge?.pilot);
|
||||||
|
return toNonEmptyString(pilot?.pilot_scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDiscoveryDateScopeForReply(value: unknown): string | null {
|
||||||
|
const text = fallbackToNonEmptyString(value);
|
||||||
|
if (!text) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return formatIsoDateForReply(text) ?? text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasGroundedDiscoveryBusinessAnswer(
|
||||||
|
debug: Record<string, unknown> | null,
|
||||||
|
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||||
|
): boolean {
|
||||||
|
if (!debug || debug.mcp_discovery_response_applied !== true) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const entry = readAssistantMcpDiscoveryEntry(debug);
|
||||||
|
const bridge = readAssistantMcpDiscoveryBridge(debug);
|
||||||
|
const bridgeStatus = toNonEmptyString(bridge?.bridge_status);
|
||||||
|
const answerDraft = toRecordObject(bridge?.answer_draft);
|
||||||
|
const answerMode = toNonEmptyString(answerDraft?.answer_mode);
|
||||||
|
return Boolean(
|
||||||
|
entry &&
|
||||||
|
toNonEmptyString(entry.entry_status) === "bridge_executed" &&
|
||||||
|
bridgeStatus === "answer_draft_ready" &&
|
||||||
|
(bridge?.business_fact_answer_allowed === true ||
|
||||||
|
answerMode === "confirmed_with_bounded_inference" ||
|
||||||
|
answerMode === "bounded_inference_only")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function mergeKnownOrganizationsDefault(values: unknown[]): string[] {
|
function mergeKnownOrganizationsDefault(values: unknown[]): string[] {
|
||||||
return mergeKnownOrganizationsFromMatcher(values);
|
return mergeKnownOrganizationsFromMatcher(values);
|
||||||
}
|
}
|
||||||
|
|
@ -141,13 +204,44 @@ export function readAddressDebugItem(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function readAddressDebugCounterparty(
|
||||||
|
debug: Record<string, unknown> | null,
|
||||||
|
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||||
|
): string | null {
|
||||||
|
const extractedFilters = readAddressDebugFilters(debug);
|
||||||
|
if (toNonEmptyString(extractedFilters?.counterparty)) {
|
||||||
|
return toNonEmptyString(extractedFilters?.counterparty);
|
||||||
|
}
|
||||||
|
if (String(debug?.anchor_type ?? "") === "counterparty") {
|
||||||
|
return toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw);
|
||||||
|
}
|
||||||
|
const discoveryMeaning = readAssistantMcpDiscoveryTurnMeaning(debug);
|
||||||
|
const explicitEntities = Array.isArray(discoveryMeaning?.explicit_entity_candidates)
|
||||||
|
? discoveryMeaning?.explicit_entity_candidates
|
||||||
|
: [];
|
||||||
|
for (const entity of explicitEntities) {
|
||||||
|
const text = toNonEmptyString(entity);
|
||||||
|
if (text) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function readAddressDebugOrganization(
|
export function readAddressDebugOrganization(
|
||||||
debug: Record<string, unknown> | null,
|
debug: Record<string, unknown> | null,
|
||||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||||
): string | null {
|
): string | null {
|
||||||
const extractedFilters = readAddressDebugFilters(debug);
|
const extractedFilters = readAddressDebugFilters(debug);
|
||||||
const rootFrameContext = toRecordObject(debug?.address_root_frame_context);
|
const rootFrameContext = toRecordObject(debug?.address_root_frame_context);
|
||||||
return toNonEmptyString(extractedFilters?.organization) ?? toNonEmptyString(rootFrameContext?.organization);
|
const discoveryMeaning = readAssistantMcpDiscoveryTurnMeaning(debug);
|
||||||
|
return (
|
||||||
|
toNonEmptyString(extractedFilters?.organization) ??
|
||||||
|
toNonEmptyString(rootFrameContext?.organization) ??
|
||||||
|
toNonEmptyString(discoveryMeaning?.explicit_organization_scope) ??
|
||||||
|
toNonEmptyString(debug?.assistant_active_organization) ??
|
||||||
|
toNonEmptyString(debug?.living_chat_selected_organization)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readAddressDebugScopedDate(debug: Record<string, unknown> | null): string | null {
|
export function readAddressDebugScopedDate(debug: Record<string, unknown> | null): string | null {
|
||||||
|
|
@ -156,7 +250,8 @@ export function readAddressDebugScopedDate(debug: Record<string, unknown> | null
|
||||||
return (
|
return (
|
||||||
formatIsoDateForReply(extractedFilters?.as_of_date) ??
|
formatIsoDateForReply(extractedFilters?.as_of_date) ??
|
||||||
formatIsoDateForReply(rootFrameContext?.as_of_date) ??
|
formatIsoDateForReply(rootFrameContext?.as_of_date) ??
|
||||||
formatIsoDateForReply(extractedFilters?.period_to)
|
formatIsoDateForReply(extractedFilters?.period_to) ??
|
||||||
|
formatDiscoveryDateScopeForReply(readAssistantMcpDiscoveryTurnMeaning(debug)?.explicit_date_scope)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -202,6 +297,13 @@ export function resolveAddressDebugAnchorContext(
|
||||||
anchorValue: counterparty
|
anchorValue: counterparty
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const discoveryCounterparty = readAddressDebugCounterparty(debug, toNonEmptyString);
|
||||||
|
if (discoveryCounterparty) {
|
||||||
|
return {
|
||||||
|
anchorType: "counterparty",
|
||||||
|
anchorValue: discoveryCounterparty
|
||||||
|
};
|
||||||
|
}
|
||||||
const account = toNonEmptyString(extractedFilters?.account);
|
const account = toNonEmptyString(extractedFilters?.account);
|
||||||
if (account) {
|
if (account) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -250,6 +352,7 @@ export function resolveAddressDebugContextFacts(
|
||||||
): AssistantAddressDebugContextFacts {
|
): AssistantAddressDebugContextFacts {
|
||||||
return {
|
return {
|
||||||
item: readAddressDebugItem(debug, toNonEmptyString),
|
item: readAddressDebugItem(debug, toNonEmptyString),
|
||||||
|
counterparty: readAddressDebugCounterparty(debug, toNonEmptyString),
|
||||||
organization: readAddressDebugOrganization(debug, toNonEmptyString),
|
organization: readAddressDebugOrganization(debug, toNonEmptyString),
|
||||||
scopedDate: readAddressDebugScopedDate(debug)
|
scopedDate: readAddressDebugScopedDate(debug)
|
||||||
};
|
};
|
||||||
|
|
@ -897,13 +1000,12 @@ export function isGroundedAddressDebug(
|
||||||
if (!debug || typeof debug !== "object") {
|
if (!debug || typeof debug !== "object") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const executionLane = toNonEmptyString(debug.execution_lane);
|
|
||||||
if (executionLane !== "address_query") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const answerGroundingCheck = toRecordObject(debug.answer_grounding_check);
|
const answerGroundingCheck = toRecordObject(debug.answer_grounding_check);
|
||||||
const groundingStatus = toNonEmptyString(answerGroundingCheck?.status);
|
const groundingStatus = toNonEmptyString(answerGroundingCheck?.status);
|
||||||
return groundingStatus === "grounded";
|
if (groundingStatus === "grounded" && toNonEmptyString(debug.execution_lane) === "address_query") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isGroundedInventoryContextDebug(
|
function isGroundedInventoryContextDebug(
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isGroundedAddressDebug,
|
isGroundedAddressDebug,
|
||||||
|
readAssistantMcpDiscoveryPilotScope,
|
||||||
resolveAddressDebugContextFacts,
|
resolveAddressDebugContextFacts,
|
||||||
resolveAssistantContinuitySnapshot
|
resolveAssistantContinuitySnapshot
|
||||||
} from "./assistantContinuityPolicy";
|
} from "./assistantContinuityPolicy";
|
||||||
|
|
@ -54,6 +55,69 @@ function toNonEmptyString(value: unknown): string | null {
|
||||||
return text.length > 0 ? text : null;
|
return text.length > 0 ? text : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toRecordObject(value: unknown): Record<string, unknown> | null {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function periodPartForRecap(scopedDate: string | null): string {
|
||||||
|
if (!scopedDate) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return /^\d{2}\.\d{2}\.\d{4}$/.test(scopedDate) ? ` на ${scopedDate}` : ` за период ${scopedDate}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDiscoveryRecapFactLine(input: {
|
||||||
|
debug: Record<string, unknown> | null;
|
||||||
|
counterparty: string | null;
|
||||||
|
scopedDate: string | null;
|
||||||
|
}): string | null {
|
||||||
|
if (!input.debug || !input.counterparty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const pilotScope = 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 === "counterparty_lifecycle_query_documents_v1") {
|
||||||
|
const activityPeriod = toRecordObject(pilot?.derived_activity_period);
|
||||||
|
const duration = toNonEmptyString(activityPeriod?.duration_human_ru);
|
||||||
|
return duration
|
||||||
|
? `смотрели подтвержденную активность по контрагенту «${input.counterparty}»${periodPart} и оценили период взаимодействия примерно как ${duration}`
|
||||||
|
: `смотрели подтвержденную активность по контрагенту «${input.counterparty}»${periodPart}`;
|
||||||
|
}
|
||||||
|
if (pilotScope === "counterparty_supplier_payout_query_movements_v1") {
|
||||||
|
const flow = toRecordObject(pilot?.derived_value_flow);
|
||||||
|
const amount = toNonEmptyString(flow?.total_amount_human_ru);
|
||||||
|
return amount
|
||||||
|
? `считали исходящие платежи/списания по контрагенту «${input.counterparty}»${periodPart}: ${amount}`
|
||||||
|
: `считали исходящие платежи/списания по контрагенту «${input.counterparty}»${periodPart}`;
|
||||||
|
}
|
||||||
|
if (pilotScope === "counterparty_value_flow_query_movements_v1") {
|
||||||
|
const flow = toRecordObject(pilot?.derived_value_flow);
|
||||||
|
const amount = toNonEmptyString(flow?.total_amount_human_ru);
|
||||||
|
return amount
|
||||||
|
? `смотрели денежный поток по контрагенту «${input.counterparty}»${periodPart}: ${amount}`
|
||||||
|
: `смотрели денежный поток по контрагенту «${input.counterparty}»${periodPart}`;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
if (incomingAmount && outgoingAmount && netAmount) {
|
||||||
|
return `считали нетто по деньгам с контрагентом «${input.counterparty}»${periodPart}: получили ${incomingAmount}, заплатили ${outgoingAmount}, расчетное нетто ${netAmount}`;
|
||||||
|
}
|
||||||
|
return `считали нетто по деньгам с контрагентом «${input.counterparty}»${periodPart}`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function collectMessageSamples(input: ResolveAssistantRouteMemorySignalsInput): string[] {
|
function collectMessageSamples(input: ResolveAssistantRouteMemorySignalsInput): string[] {
|
||||||
const values = [
|
const values = [
|
||||||
input.rawUserMessage,
|
input.rawUserMessage,
|
||||||
|
|
@ -120,11 +184,21 @@ function normalizeRecapIdentity(value: unknown): string {
|
||||||
function buildRecapFactLine(input: {
|
function buildRecapFactLine(input: {
|
||||||
debug: Record<string, unknown> | null;
|
debug: Record<string, unknown> | null;
|
||||||
item: string | null;
|
item: string | null;
|
||||||
|
counterparty: string | null;
|
||||||
organization: string | null;
|
organization: string | null;
|
||||||
}): string | null {
|
}): string | null {
|
||||||
const detectedIntent = String(input.debug?.detected_intent ?? "");
|
const detectedIntent = String(input.debug?.detected_intent ?? "");
|
||||||
const scopedDate = resolveAddressDebugContextFacts(input.debug).scopedDate;
|
const scopedDate = resolveAddressDebugContextFacts(input.debug).scopedDate;
|
||||||
|
const discoveryFact = buildDiscoveryRecapFactLine({
|
||||||
|
debug: input.debug,
|
||||||
|
counterparty: input.counterparty,
|
||||||
|
scopedDate
|
||||||
|
});
|
||||||
|
if (discoveryFact) {
|
||||||
|
return discoveryFact;
|
||||||
|
}
|
||||||
const itemPart = input.item ? `по позиции «${input.item}»` : null;
|
const itemPart = input.item ? `по позиции «${input.item}»` : null;
|
||||||
|
const counterpartyPart = input.counterparty ? `по контрагенту «${input.counterparty}»` : null;
|
||||||
const organizationPart = input.organization ? `по компании «${input.organization}»` : null;
|
const organizationPart = input.organization ? `по компании «${input.organization}»` : null;
|
||||||
const datePart = scopedDate ? ` на ${scopedDate}` : "";
|
const datePart = scopedDate ? ` на ${scopedDate}` : "";
|
||||||
if (detectedIntent === "inventory_on_hand_as_of_date") {
|
if (detectedIntent === "inventory_on_hand_as_of_date") {
|
||||||
|
|
@ -151,6 +225,9 @@ function buildRecapFactLine(input: {
|
||||||
if (detectedIntent === "counterparty_activity_lifecycle" && organizationPart) {
|
if (detectedIntent === "counterparty_activity_lifecycle" && organizationPart) {
|
||||||
return `смотрели активность в базе 1С ${organizationPart}`.trim();
|
return `смотрели активность в базе 1С ${organizationPart}`.trim();
|
||||||
}
|
}
|
||||||
|
if (detectedIntent === "list_documents_by_counterparty" && counterpartyPart) {
|
||||||
|
return `поднимали документы ${counterpartyPart}${datePart}`.trim();
|
||||||
|
}
|
||||||
if (detectedIntent === "list_documents_by_counterparty" && organizationPart) {
|
if (detectedIntent === "list_documents_by_counterparty" && organizationPart) {
|
||||||
return `поднимали документы ${organizationPart}${datePart}`.trim();
|
return `поднимали документы ${organizationPart}${datePart}`.trim();
|
||||||
}
|
}
|
||||||
|
|
@ -196,6 +273,7 @@ function collectRecentRecapFacts(input: {
|
||||||
const fact = buildRecapFactLine({
|
const fact = buildRecapFactLine({
|
||||||
debug: item.debug,
|
debug: item.debug,
|
||||||
item: debugItem,
|
item: debugItem,
|
||||||
|
counterparty: debugContext.counterparty,
|
||||||
organization: debugOrganization
|
organization: debugOrganization
|
||||||
});
|
});
|
||||||
if (!fact || seen.has(fact)) {
|
if (!fact || seen.has(fact)) {
|
||||||
|
|
@ -219,6 +297,7 @@ export function buildAddressMemoryRecapReply(input: {
|
||||||
}): string {
|
}): string {
|
||||||
const contextFacts = resolveAddressDebugContextFacts(input.addressDebug, input.toNonEmptyString);
|
const contextFacts = resolveAddressDebugContextFacts(input.addressDebug, input.toNonEmptyString);
|
||||||
const item = contextFacts.item;
|
const item = contextFacts.item;
|
||||||
|
const counterparty = contextFacts.counterparty;
|
||||||
const organization = input.organization ?? contextFacts.organization;
|
const organization = input.organization ?? contextFacts.organization;
|
||||||
const scopedDate = contextFacts.scopedDate;
|
const scopedDate = contextFacts.scopedDate;
|
||||||
const recapFacts = collectRecentRecapFacts({
|
const recapFacts = collectRecentRecapFacts({
|
||||||
|
|
@ -246,6 +325,22 @@ export function buildAddressMemoryRecapReply(input: {
|
||||||
].join(" ");
|
].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 (organization || scopedDate) {
|
if (organization || scopedDate) {
|
||||||
const organizationPart = organization ? ` по компании «${organization}»` : "";
|
const organizationPart = organization ? ` по компании «${organization}»` : "";
|
||||||
const datePart = scopedDate ? ` на ${scopedDate}` : "";
|
const datePart = scopedDate ? ` на ${scopedDate}` : "";
|
||||||
|
|
@ -264,7 +359,39 @@ export function buildSelectedObjectAnswerInspectionReply(input: {
|
||||||
}): string {
|
}): string {
|
||||||
const contextFacts = resolveAddressDebugContextFacts(input.addressDebug, input.toNonEmptyString);
|
const contextFacts = resolveAddressDebugContextFacts(input.addressDebug, input.toNonEmptyString);
|
||||||
const itemLabel = contextFacts.item ?? "эта позиция";
|
const itemLabel = contextFacts.item ?? "эта позиция";
|
||||||
|
const counterpartyLabel = contextFacts.counterparty;
|
||||||
const detectedIntent = String(input.addressDebug?.detected_intent ?? "");
|
const detectedIntent = String(input.addressDebug?.detected_intent ?? "");
|
||||||
|
const pilotScope = 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") {
|
if (detectedIntent === "inventory_sale_trace_for_item") {
|
||||||
return [
|
return [
|
||||||
|
|
|
||||||
|
|
@ -3822,11 +3822,7 @@ function findLastGroundedAddressAnswerDebug(items) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const debug = item.debug;
|
const debug = item.debug;
|
||||||
if (debug.execution_lane !== "address_query") {
|
if ((0, assistantContinuityPolicy_1.isGroundedAddressDebug)(debug, toNonEmptyString)) {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const groundingStatus = toNonEmptyString(debug.answer_grounding_check?.status);
|
|
||||||
if (groundingStatus === "grounded") {
|
|
||||||
return debug;
|
return debug;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,11 +108,45 @@ describe("assistantContinuityPolicy organization authority", () => {
|
||||||
|
|
||||||
expect(facts).toEqual({
|
expect(facts).toEqual({
|
||||||
item: "Рабочая станция",
|
item: "Рабочая станция",
|
||||||
|
counterparty: null,
|
||||||
organization: 'ООО "Альтернатива Плюс"',
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
scopedDate: "31.03.2020"
|
scopedDate: "31.03.2020"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("reads counterparty, organization and period from grounded MCP discovery fallback", () => {
|
||||||
|
const facts = resolveAddressDebugContextFacts({
|
||||||
|
execution_lane: "living_chat",
|
||||||
|
mcp_discovery_response_applied: true,
|
||||||
|
assistant_active_organization: "ООО Альтернатива Плюс",
|
||||||
|
assistant_mcp_discovery_entry_point_v1: {
|
||||||
|
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||||
|
entry_status: "bridge_executed",
|
||||||
|
turn_input: {
|
||||||
|
turn_meaning_ref: {
|
||||||
|
explicit_entity_candidates: ["Группа СВК"],
|
||||||
|
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(facts).toEqual({
|
||||||
|
item: null,
|
||||||
|
counterparty: "Группа СВК",
|
||||||
|
organization: "ООО Альтернатива Плюс",
|
||||||
|
scopedDate: "2020"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("resolves navigation session context through one shared helper", () => {
|
it("resolves navigation session context through one shared helper", () => {
|
||||||
const state = resolveNavigationSessionContextState({
|
const state = resolveNavigationSessionContextState({
|
||||||
session_context: {
|
session_context: {
|
||||||
|
|
|
||||||
|
|
@ -366,6 +366,56 @@ describe("assistant living chat runtime adapter", () => {
|
||||||
expect(executeLlmChat).not.toHaveBeenCalled();
|
expect(executeLlmChat).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("builds deterministic memory recap for prior grounded MCP discovery counterparty context", async () => {
|
||||||
|
const executeLlmChat = vi.fn(async () => "raw-llm");
|
||||||
|
const input = buildRuntimeInput({
|
||||||
|
userMessage: "а ты помнишь, что мы выяснили по свк?",
|
||||||
|
modeDecision: { mode: "chat", reason: "memory_recap_followup_detected" },
|
||||||
|
sessionItems: [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
debug: {
|
||||||
|
execution_lane: "living_chat",
|
||||||
|
mcp_discovery_response_applied: true,
|
||||||
|
assistant_mcp_discovery_entry_point_v1: {
|
||||||
|
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||||
|
entry_status: "bridge_executed",
|
||||||
|
turn_input: {
|
||||||
|
turn_meaning_ref: {
|
||||||
|
explicit_entity_candidates: ["Группа СВК"],
|
||||||
|
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference"
|
||||||
|
},
|
||||||
|
pilot: {
|
||||||
|
pilot_scope: "counterparty_supplier_payout_query_movements_v1",
|
||||||
|
derived_value_flow: {
|
||||||
|
total_amount_human_ru: "43 763 351,53 руб."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
executeLlmChat
|
||||||
|
});
|
||||||
|
|
||||||
|
const output = await runAssistantLivingChatRuntime(input);
|
||||||
|
|
||||||
|
expect(output.handled).toBe(true);
|
||||||
|
expect(output.chatText).toContain("Группа СВК");
|
||||||
|
expect(output.chatText).toContain("43 763 351,53 руб.");
|
||||||
|
expect(output.debug?.living_chat_response_source).toBe("deterministic_memory_recap_contract");
|
||||||
|
expect(executeLlmChat).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("uses continuity-backed active organization for organization-fact boundary even when session scope is empty", async () => {
|
it("uses continuity-backed active organization for organization-fact boundary even when session scope is empty", async () => {
|
||||||
const executeLlmChat = vi.fn(async () => "raw-llm");
|
const executeLlmChat = vi.fn(async () => "raw-llm");
|
||||||
const input = buildRuntimeInput({
|
const input = buildRuntimeInput({
|
||||||
|
|
@ -434,4 +484,50 @@ describe("assistant living chat runtime adapter", () => {
|
||||||
expect(output.debug?.living_chat_response_source).toBe("deterministic_answer_inspection_contract");
|
expect(output.debug?.living_chat_response_source).toBe("deterministic_answer_inspection_contract");
|
||||||
expect(executeLlmChat).not.toHaveBeenCalled();
|
expect(executeLlmChat).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("builds deterministic answer inspection reply over grounded MCP discovery net answer", async () => {
|
||||||
|
const executeLlmChat = vi.fn(async () => "raw-llm");
|
||||||
|
const input = buildRuntimeInput({
|
||||||
|
userMessage: "что ты имел в виду под нетто по свк?",
|
||||||
|
modeDecision: { mode: "chat", reason: "answer_inspection_followup_detected" },
|
||||||
|
sessionItems: [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
debug: {
|
||||||
|
execution_lane: "living_chat",
|
||||||
|
mcp_discovery_response_applied: true,
|
||||||
|
assistant_mcp_discovery_entry_point_v1: {
|
||||||
|
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||||
|
entry_status: "bridge_executed",
|
||||||
|
turn_input: {
|
||||||
|
turn_meaning_ref: {
|
||||||
|
explicit_entity_candidates: ["Группа СВК"],
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference"
|
||||||
|
},
|
||||||
|
pilot: {
|
||||||
|
pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
executeLlmChat
|
||||||
|
});
|
||||||
|
|
||||||
|
const output = await runAssistantLivingChatRuntime(input);
|
||||||
|
|
||||||
|
expect(output.handled).toBe(true);
|
||||||
|
expect(output.chatText).toContain("Группа СВК");
|
||||||
|
expect(output.chatText).toContain("Нетто");
|
||||||
|
expect(output.debug?.living_chat_response_source).toBe("deterministic_answer_inspection_contract");
|
||||||
|
expect(executeLlmChat).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,50 @@ describe("assistantMemoryRecapPolicy", () => {
|
||||||
expect(signals.contextualMemoryRecapFollowupDetected).toBe(false);
|
expect(signals.contextualMemoryRecapFollowupDetected).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("detects contextual memory recap over prior grounded MCP discovery answer", () => {
|
||||||
|
const signals = policy.resolveRouteMemorySignals({
|
||||||
|
rawUserMessage: "а ты помнишь, что мы уже выяснили по свк?",
|
||||||
|
repairedRawUserMessage: "",
|
||||||
|
effectiveAddressUserMessage: "",
|
||||||
|
repairedEffectiveAddressUserMessage: "",
|
||||||
|
dataScopeMetaQuery: false,
|
||||||
|
capabilityMetaQuery: false,
|
||||||
|
dataRetrievalSignal: false,
|
||||||
|
strongDataSignal: false,
|
||||||
|
aggregateBusinessAnalyticsSignal: false,
|
||||||
|
lastGroundedAddressDebug: null,
|
||||||
|
hasPriorAddressDebug: true,
|
||||||
|
sessionItems: [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
debug: {
|
||||||
|
execution_lane: "living_chat",
|
||||||
|
mcp_discovery_response_applied: true,
|
||||||
|
assistant_mcp_discovery_entry_point_v1: {
|
||||||
|
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||||
|
entry_status: "bridge_executed",
|
||||||
|
turn_input: {
|
||||||
|
turn_meaning_ref: {
|
||||||
|
explicit_entity_candidates: ["Группа СВК"],
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(signals.contextualMemoryRecapFollowupDetected).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("builds deterministic recap summary from recent selected-object facts", () => {
|
it("builds deterministic recap summary from recent selected-object facts", () => {
|
||||||
const context = resolveAssistantLivingChatMemoryContext({
|
const context = resolveAssistantLivingChatMemoryContext({
|
||||||
modeDecisionReason: "memory_recap_followup_detected",
|
modeDecisionReason: "memory_recap_followup_detected",
|
||||||
|
|
@ -278,4 +322,114 @@ describe("assistantMemoryRecapPolicy", () => {
|
||||||
expect(reply).toContain("Рабочая станция");
|
expect(reply).toContain("Рабочая станция");
|
||||||
expect(reply).toContain("Покупатель");
|
expect(reply).toContain("Покупатель");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("builds deterministic recap summary from grounded MCP discovery counterparty context", () => {
|
||||||
|
const sessionItems = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
debug: {
|
||||||
|
execution_lane: "living_chat",
|
||||||
|
mcp_discovery_response_applied: true,
|
||||||
|
assistant_mcp_discovery_entry_point_v1: {
|
||||||
|
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||||
|
entry_status: "bridge_executed",
|
||||||
|
turn_input: {
|
||||||
|
turn_meaning_ref: {
|
||||||
|
explicit_entity_candidates: ["Группа СВК"],
|
||||||
|
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference"
|
||||||
|
},
|
||||||
|
pilot: {
|
||||||
|
pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1",
|
||||||
|
derived_bidirectional_value_flow: {
|
||||||
|
net_amount_human_ru: "3 865 501,50 руб.",
|
||||||
|
incoming_customer_revenue: {
|
||||||
|
total_amount_human_ru: "47 628 853,03 руб."
|
||||||
|
},
|
||||||
|
outgoing_supplier_payout: {
|
||||||
|
total_amount_human_ru: "43 763 351,53 руб."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const context = resolveAssistantLivingChatMemoryContext({
|
||||||
|
modeDecisionReason: "memory_recap_followup_detected",
|
||||||
|
sessionItems
|
||||||
|
});
|
||||||
|
|
||||||
|
const reply = buildAddressMemoryRecapReply({
|
||||||
|
organization: null,
|
||||||
|
addressDebug: context.lastMemoryAddressDebug,
|
||||||
|
sessionItems,
|
||||||
|
toNonEmptyString: (value: unknown) => {
|
||||||
|
const text = String(value ?? "").trim();
|
||||||
|
return text.length > 0 ? text : null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(context.contextualMemoryRecapFollowup).toBe(true);
|
||||||
|
expect(reply).toContain("Группа СВК");
|
||||||
|
expect(reply).toContain("нетто");
|
||||||
|
expect(reply).toContain("47 628 853,03 руб.");
|
||||||
|
expect(reply).toContain("43 763 351,53 руб.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds grounded answer inspection reply for MCP discovery net answer", () => {
|
||||||
|
const context = resolveAssistantLivingChatMemoryContext({
|
||||||
|
modeDecisionReason: "answer_inspection_followup_detected",
|
||||||
|
sessionItems: [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
debug: {
|
||||||
|
execution_lane: "living_chat",
|
||||||
|
mcp_discovery_response_applied: true,
|
||||||
|
assistant_mcp_discovery_entry_point_v1: {
|
||||||
|
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||||
|
entry_status: "bridge_executed",
|
||||||
|
turn_input: {
|
||||||
|
turn_meaning_ref: {
|
||||||
|
explicit_entity_candidates: ["Группа СВК"],
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference"
|
||||||
|
},
|
||||||
|
pilot: {
|
||||||
|
pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const reply = buildSelectedObjectAnswerInspectionReply({
|
||||||
|
addressDebug: context.lastAnswerInspectionAddressDebug,
|
||||||
|
toNonEmptyString: (value: unknown) => {
|
||||||
|
const text = String(value ?? "").trim();
|
||||||
|
return text.length > 0 ? text : null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(context.contextualAnswerInspectionFollowup).toBe(true);
|
||||||
|
expect(reply).toContain("Группа СВК");
|
||||||
|
expect(reply).toContain("Нетто");
|
||||||
|
expect(reply).toContain("проверенному периоду");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue