Архитектура: централизовать party-anchor carryover и referenced-entity follow-up в continuity policy и transition glue
This commit is contained in:
@@ -14,6 +14,8 @@ exports.buildRootScopedCarryoverFilters = buildRootScopedCarryoverFilters;
|
||||
exports.shouldUseNavigationTemporalCarryover = shouldUseNavigationTemporalCarryover;
|
||||
exports.applyTemporalCarryoverFilters = applyTemporalCarryoverFilters;
|
||||
exports.applyOrganizationCarryoverFilters = applyOrganizationCarryoverFilters;
|
||||
exports.applyHistoricalPartyCarryoverFilters = applyHistoricalPartyCarryoverFilters;
|
||||
exports.applyReferencedEntityCarryover = applyReferencedEntityCarryover;
|
||||
exports.buildInventoryRootFrameFromAddressDebug = buildInventoryRootFrameFromAddressDebug;
|
||||
exports.isGroundedAddressDebug = isGroundedAddressDebug;
|
||||
exports.resolveAssistantContinuitySnapshot = resolveAssistantContinuitySnapshot;
|
||||
@@ -281,6 +283,75 @@ function applyOrganizationCarryoverFilters(previousFilters, historicalOrganizati
|
||||
}
|
||||
return nextFilters;
|
||||
}
|
||||
function applyHistoricalPartyCarryoverFilters(previousFilters, shouldBackfillHistoricalPartyAnchors, historicalContract, historicalCounterparty, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
const nextFilters = previousFilters && typeof previousFilters === "object" ? { ...previousFilters } : {};
|
||||
if (!shouldBackfillHistoricalPartyAnchors) {
|
||||
return nextFilters;
|
||||
}
|
||||
if (!toNonEmptyString(nextFilters.contract)) {
|
||||
nextFilters.contract = toNonEmptyString(historicalContract) ?? undefined;
|
||||
}
|
||||
if (!toNonEmptyString(nextFilters.counterparty)) {
|
||||
nextFilters.counterparty = toNonEmptyString(historicalCounterparty) ?? undefined;
|
||||
}
|
||||
return nextFilters;
|
||||
}
|
||||
function applyReferencedEntityCarryover(previousFilters, previousAnchorType, previousAnchorValue, followupSelectionMode, resolvedEntityFromFollowup, rootScopedPivot, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
const nextFilters = previousFilters && typeof previousFilters === "object" ? { ...previousFilters } : {};
|
||||
if (rootScopedPivot || !resolvedEntityFromFollowup || typeof resolvedEntityFromFollowup !== "object") {
|
||||
return {
|
||||
previousFilters: nextFilters,
|
||||
previousAnchorType,
|
||||
previousAnchorValue,
|
||||
followupSelectionMode,
|
||||
resolvedCounterpartyFromDisplay: false
|
||||
};
|
||||
}
|
||||
const entityType = toNonEmptyString(resolvedEntityFromFollowup.entityType);
|
||||
const entityValue = toNonEmptyString(resolvedEntityFromFollowup.value);
|
||||
if (!entityType || !entityValue) {
|
||||
return {
|
||||
previousFilters: nextFilters,
|
||||
previousAnchorType,
|
||||
previousAnchorValue,
|
||||
followupSelectionMode,
|
||||
resolvedCounterpartyFromDisplay: false
|
||||
};
|
||||
}
|
||||
let resolvedCounterpartyFromDisplay = false;
|
||||
if (entityType === "counterparty") {
|
||||
nextFilters.counterparty = entityValue;
|
||||
previousAnchorType = "counterparty";
|
||||
previousAnchorValue = entityValue;
|
||||
resolvedCounterpartyFromDisplay = true;
|
||||
}
|
||||
else if (entityType === "contract") {
|
||||
nextFilters.contract = entityValue;
|
||||
previousAnchorType = "contract";
|
||||
previousAnchorValue = entityValue;
|
||||
}
|
||||
else if (entityType === "item") {
|
||||
nextFilters.item = entityValue;
|
||||
previousAnchorType = "item";
|
||||
previousAnchorValue = entityValue;
|
||||
}
|
||||
else {
|
||||
return {
|
||||
previousFilters: nextFilters,
|
||||
previousAnchorType,
|
||||
previousAnchorValue,
|
||||
followupSelectionMode,
|
||||
resolvedCounterpartyFromDisplay: false
|
||||
};
|
||||
}
|
||||
return {
|
||||
previousFilters: nextFilters,
|
||||
previousAnchorType,
|
||||
previousAnchorValue,
|
||||
followupSelectionMode: followupSelectionMode !== "switch_to_suggested_intent" ? "carry_referenced_entity" : followupSelectionMode,
|
||||
resolvedCounterpartyFromDisplay
|
||||
};
|
||||
}
|
||||
function buildInventoryRootFrameFromAddressDebug(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
if (!debug || typeof debug !== "object") {
|
||||
return null;
|
||||
|
||||
@@ -602,18 +602,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||
sourceIntentHint === "list_documents_by_contract" ||
|
||||
sourceIntentHint === "bank_operations_by_contract" ||
|
||||
sourceIntentHint === "open_items_by_counterparty_or_contract";
|
||||
if (shouldBackfillHistoricalPartyAnchors && !deps.toNonEmptyString(previousFilters.contract)) {
|
||||
const historicalContract = deps.findRecentAddressFilterValue(items, "contract");
|
||||
if (historicalContract) {
|
||||
previousFilters.contract = historicalContract;
|
||||
}
|
||||
}
|
||||
if (shouldBackfillHistoricalPartyAnchors && !deps.toNonEmptyString(previousFilters.counterparty)) {
|
||||
const historicalCounterparty = deps.findRecentAddressFilterValue(items, "counterparty");
|
||||
if (historicalCounterparty) {
|
||||
previousFilters.counterparty = historicalCounterparty;
|
||||
}
|
||||
}
|
||||
previousFilters = (0, assistantContinuityPolicy_1.applyHistoricalPartyCarryoverFilters)(previousFilters, shouldBackfillHistoricalPartyAnchors, deps.findRecentAddressFilterValue(items, "contract"), deps.findRecentAddressFilterValue(items, "counterparty"), deps.toNonEmptyString);
|
||||
const historicalOrganization = deps.findRecentAddressFilterValue(items, "organization");
|
||||
const authorityActiveOrganization = deps.normalizeOrganizationScopeValue(organizationAuthority.activeOrganization) ??
|
||||
deps.normalizeOrganizationScopeValue(organizationAuthority.continuityActiveOrganization);
|
||||
@@ -670,26 +659,14 @@ function createAssistantTransitionPolicy(deps) {
|
||||
: null);
|
||||
if (resolvedEntityFromFollowup && !rootScopedPivot) {
|
||||
displayedEntityTargetIntent = resolveDisplayedEntityRetargetIntent(userMessage, resolvedEntityFromFollowup.entityType);
|
||||
if (resolvedEntityFromFollowup.entityType === "counterparty") {
|
||||
previousFilters.counterparty = resolvedEntityFromFollowup.value;
|
||||
previousAnchorType = "counterparty";
|
||||
previousAnchor = resolvedEntityFromFollowup.value;
|
||||
resolvedCounterpartyFromDisplay = true;
|
||||
}
|
||||
else if (resolvedEntityFromFollowup.entityType === "contract") {
|
||||
previousFilters.contract = resolvedEntityFromFollowup.value;
|
||||
previousAnchorType = "contract";
|
||||
previousAnchor = resolvedEntityFromFollowup.value;
|
||||
}
|
||||
else if (resolvedEntityFromFollowup.entityType === "item") {
|
||||
previousFilters.item = resolvedEntityFromFollowup.value;
|
||||
previousAnchorType = "item";
|
||||
previousAnchor = resolvedEntityFromFollowup.value;
|
||||
}
|
||||
if (followupSelectionMode !== "switch_to_suggested_intent") {
|
||||
followupSelectionMode = "carry_referenced_entity";
|
||||
}
|
||||
}
|
||||
({
|
||||
previousFilters,
|
||||
previousAnchorType,
|
||||
previousAnchorValue: previousAnchor,
|
||||
followupSelectionMode,
|
||||
resolvedCounterpartyFromDisplay
|
||||
} = (0, assistantContinuityPolicy_1.applyReferencedEntityCarryover)(previousFilters, previousAnchorType, previousAnchor, followupSelectionMode, resolvedEntityFromFollowup, rootScopedPivot, deps.toNonEmptyString));
|
||||
if (!rootScopedPivot &&
|
||||
!deps.toNonEmptyString(previousFilters.item) &&
|
||||
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||
|
||||
@@ -436,6 +436,106 @@ export function applyOrganizationCarryoverFilters(
|
||||
return nextFilters;
|
||||
}
|
||||
|
||||
export function applyHistoricalPartyCarryoverFilters(
|
||||
previousFilters: Record<string, unknown> | null,
|
||||
shouldBackfillHistoricalPartyAnchors: boolean,
|
||||
historicalContract: unknown,
|
||||
historicalCounterparty: unknown,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): Record<string, unknown> {
|
||||
const nextFilters =
|
||||
previousFilters && typeof previousFilters === "object" ? { ...previousFilters } : {};
|
||||
if (!shouldBackfillHistoricalPartyAnchors) {
|
||||
return nextFilters;
|
||||
}
|
||||
if (!toNonEmptyString(nextFilters.contract)) {
|
||||
nextFilters.contract = toNonEmptyString(historicalContract) ?? undefined;
|
||||
}
|
||||
if (!toNonEmptyString(nextFilters.counterparty)) {
|
||||
nextFilters.counterparty = toNonEmptyString(historicalCounterparty) ?? undefined;
|
||||
}
|
||||
return nextFilters;
|
||||
}
|
||||
|
||||
export function applyReferencedEntityCarryover(
|
||||
previousFilters: Record<string, unknown> | null,
|
||||
previousAnchorType: string | null,
|
||||
previousAnchorValue: string | null,
|
||||
followupSelectionMode: string | null,
|
||||
resolvedEntityFromFollowup:
|
||||
| {
|
||||
entityType?: unknown;
|
||||
value?: unknown;
|
||||
}
|
||||
| null
|
||||
| undefined,
|
||||
rootScopedPivot: boolean,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): {
|
||||
previousFilters: Record<string, unknown>;
|
||||
previousAnchorType: string | null;
|
||||
previousAnchorValue: string | null;
|
||||
followupSelectionMode: string | null;
|
||||
resolvedCounterpartyFromDisplay: boolean;
|
||||
} {
|
||||
const nextFilters =
|
||||
previousFilters && typeof previousFilters === "object" ? { ...previousFilters } : {};
|
||||
if (rootScopedPivot || !resolvedEntityFromFollowup || typeof resolvedEntityFromFollowup !== "object") {
|
||||
return {
|
||||
previousFilters: nextFilters,
|
||||
previousAnchorType,
|
||||
previousAnchorValue,
|
||||
followupSelectionMode,
|
||||
resolvedCounterpartyFromDisplay: false
|
||||
};
|
||||
}
|
||||
|
||||
const entityType = toNonEmptyString(resolvedEntityFromFollowup.entityType);
|
||||
const entityValue = toNonEmptyString(resolvedEntityFromFollowup.value);
|
||||
if (!entityType || !entityValue) {
|
||||
return {
|
||||
previousFilters: nextFilters,
|
||||
previousAnchorType,
|
||||
previousAnchorValue,
|
||||
followupSelectionMode,
|
||||
resolvedCounterpartyFromDisplay: false
|
||||
};
|
||||
}
|
||||
|
||||
let resolvedCounterpartyFromDisplay = false;
|
||||
if (entityType === "counterparty") {
|
||||
nextFilters.counterparty = entityValue;
|
||||
previousAnchorType = "counterparty";
|
||||
previousAnchorValue = entityValue;
|
||||
resolvedCounterpartyFromDisplay = true;
|
||||
} else if (entityType === "contract") {
|
||||
nextFilters.contract = entityValue;
|
||||
previousAnchorType = "contract";
|
||||
previousAnchorValue = entityValue;
|
||||
} else if (entityType === "item") {
|
||||
nextFilters.item = entityValue;
|
||||
previousAnchorType = "item";
|
||||
previousAnchorValue = entityValue;
|
||||
} else {
|
||||
return {
|
||||
previousFilters: nextFilters,
|
||||
previousAnchorType,
|
||||
previousAnchorValue,
|
||||
followupSelectionMode,
|
||||
resolvedCounterpartyFromDisplay: false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
previousFilters: nextFilters,
|
||||
previousAnchorType,
|
||||
previousAnchorValue,
|
||||
followupSelectionMode:
|
||||
followupSelectionMode !== "switch_to_suggested_intent" ? "carry_referenced_entity" : followupSelectionMode,
|
||||
resolvedCounterpartyFromDisplay
|
||||
};
|
||||
}
|
||||
|
||||
export function buildInventoryRootFrameFromAddressDebug(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// @ts-nocheck
|
||||
import {
|
||||
applyHistoricalPartyCarryoverFilters,
|
||||
applyOrganizationCarryoverFilters,
|
||||
applyReferencedEntityCarryover,
|
||||
applyTemporalCarryoverFilters,
|
||||
buildRootScopedCarryoverFilters,
|
||||
buildInventoryRootFrameFromAddressDebug,
|
||||
@@ -759,18 +761,13 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
sourceIntentHint === "list_documents_by_contract" ||
|
||||
sourceIntentHint === "bank_operations_by_contract" ||
|
||||
sourceIntentHint === "open_items_by_counterparty_or_contract";
|
||||
if (shouldBackfillHistoricalPartyAnchors && !deps.toNonEmptyString(previousFilters.contract)) {
|
||||
const historicalContract = deps.findRecentAddressFilterValue(items, "contract");
|
||||
if (historicalContract) {
|
||||
previousFilters.contract = historicalContract;
|
||||
}
|
||||
}
|
||||
if (shouldBackfillHistoricalPartyAnchors && !deps.toNonEmptyString(previousFilters.counterparty)) {
|
||||
const historicalCounterparty = deps.findRecentAddressFilterValue(items, "counterparty");
|
||||
if (historicalCounterparty) {
|
||||
previousFilters.counterparty = historicalCounterparty;
|
||||
}
|
||||
}
|
||||
previousFilters = applyHistoricalPartyCarryoverFilters(
|
||||
previousFilters,
|
||||
shouldBackfillHistoricalPartyAnchors,
|
||||
deps.findRecentAddressFilterValue(items, "contract"),
|
||||
deps.findRecentAddressFilterValue(items, "counterparty"),
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const historicalOrganization = deps.findRecentAddressFilterValue(items, "organization");
|
||||
const authorityActiveOrganization =
|
||||
deps.normalizeOrganizationScopeValue(organizationAuthority.activeOrganization) ??
|
||||
@@ -861,24 +858,22 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
userMessage,
|
||||
resolvedEntityFromFollowup.entityType
|
||||
);
|
||||
if (resolvedEntityFromFollowup.entityType === "counterparty") {
|
||||
previousFilters.counterparty = resolvedEntityFromFollowup.value;
|
||||
previousAnchorType = "counterparty";
|
||||
previousAnchor = resolvedEntityFromFollowup.value;
|
||||
resolvedCounterpartyFromDisplay = true;
|
||||
} else if (resolvedEntityFromFollowup.entityType === "contract") {
|
||||
previousFilters.contract = resolvedEntityFromFollowup.value;
|
||||
previousAnchorType = "contract";
|
||||
previousAnchor = resolvedEntityFromFollowup.value;
|
||||
} else if (resolvedEntityFromFollowup.entityType === "item") {
|
||||
previousFilters.item = resolvedEntityFromFollowup.value;
|
||||
previousAnchorType = "item";
|
||||
previousAnchor = resolvedEntityFromFollowup.value;
|
||||
}
|
||||
if (followupSelectionMode !== "switch_to_suggested_intent") {
|
||||
followupSelectionMode = "carry_referenced_entity";
|
||||
}
|
||||
}
|
||||
({
|
||||
previousFilters,
|
||||
previousAnchorType,
|
||||
previousAnchorValue: previousAnchor,
|
||||
followupSelectionMode,
|
||||
resolvedCounterpartyFromDisplay
|
||||
} = applyReferencedEntityCarryover(
|
||||
previousFilters,
|
||||
previousAnchorType,
|
||||
previousAnchor,
|
||||
followupSelectionMode,
|
||||
resolvedEntityFromFollowup,
|
||||
rootScopedPivot,
|
||||
deps.toNonEmptyString
|
||||
));
|
||||
if (
|
||||
!rootScopedPivot &&
|
||||
!deps.toNonEmptyString(previousFilters.item) &&
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
applyHistoricalPartyCarryoverFilters,
|
||||
applyOrganizationCarryoverFilters,
|
||||
applyReferencedEntityCarryover,
|
||||
applyTemporalCarryoverFilters,
|
||||
buildRootScopedCarryoverFilters,
|
||||
hydrateInventoryRootFrameState,
|
||||
@@ -281,4 +283,45 @@ describe("assistantContinuityPolicy organization authority", () => {
|
||||
organization: "Org Existing"
|
||||
});
|
||||
});
|
||||
|
||||
it("backfills historical contract and counterparty only for party-driven follow-up families", () => {
|
||||
const filters = applyHistoricalPartyCarryoverFilters(
|
||||
{},
|
||||
true,
|
||||
"Contract Legacy",
|
||||
"Counterparty Legacy"
|
||||
);
|
||||
|
||||
expect(filters).toEqual({
|
||||
contract: "Contract Legacy",
|
||||
counterparty: "Counterparty Legacy"
|
||||
});
|
||||
});
|
||||
|
||||
it("applies referenced entity carryover into filters, anchor, and selection mode", () => {
|
||||
const carryover = applyReferencedEntityCarryover(
|
||||
{
|
||||
organization: "Org Alt"
|
||||
},
|
||||
null,
|
||||
null,
|
||||
"carry_previous_intent",
|
||||
{
|
||||
entityType: "counterparty",
|
||||
value: "SVK Group"
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
expect(carryover).toEqual({
|
||||
previousFilters: {
|
||||
organization: "Org Alt",
|
||||
counterparty: "SVK Group"
|
||||
},
|
||||
previousAnchorType: "counterparty",
|
||||
previousAnchorValue: "SVK Group",
|
||||
followupSelectionMode: "carry_referenced_entity",
|
||||
resolvedCounterpartyFromDisplay: true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -214,6 +214,47 @@ describe("assistantTransitionPolicy", () => {
|
||||
expect(contract.decision).toBe("continue_previous");
|
||||
});
|
||||
|
||||
it("retargets displayed counterparty follow-up through shared referenced-entity carryover", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => ({
|
||||
text: "1. SVK Group\n2. Gamma",
|
||||
debug: {
|
||||
detected_intent: "customer_revenue_and_payments",
|
||||
extracted_filters: {
|
||||
organization: 'ООО "Альтернатива Плюс"',
|
||||
period_from: "2017-01-01",
|
||||
period_to: "2017-12-31"
|
||||
}
|
||||
}
|
||||
}),
|
||||
hasAddressFollowupContextSignal: () => true,
|
||||
inferDisplayedEntityTypeFromIntent: () => "counterparty",
|
||||
extractDisplayedAddressEntityCandidates: () => [{ entityType: "counterparty", value: "SVK Group" }],
|
||||
resolveDisplayedAddressEntityMention: () => ({ entityType: "counterparty", value: "SVK Group" }),
|
||||
resolveAddressIntent: () => ({ intent: "unknown" })
|
||||
});
|
||||
|
||||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||
"покажи договоры по СВК",
|
||||
[],
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
expect(carryover?.followupSelectionMode).toBe("carry_referenced_entity");
|
||||
expect(carryover?.followupContext?.target_intent).toBe("list_contracts_by_counterparty");
|
||||
expect(carryover?.followupContext?.previous_anchor_type).toBe("counterparty");
|
||||
expect(carryover?.followupContext?.previous_anchor_value).toBe("SVK Group");
|
||||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||||
organization: 'ООО "Альтернатива Плюс"',
|
||||
counterparty: "SVK Group",
|
||||
period_from: "2017-01-01",
|
||||
period_to: "2017-12-31"
|
||||
});
|
||||
expect(carryover?.followupContext?.resolved_counterparty_from_display).toBe(true);
|
||||
});
|
||||
|
||||
it("retargets same-date inventory follow-up away from receivables intent", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => ({
|
||||
|
||||
Reference in New Issue
Block a user