Укрепить семантику бизнес-ответов адресного контура
This commit is contained in:
@@ -384,6 +384,15 @@ function extractMonthPeriod(text: string): { period_from?: string; period_to?: s
|
||||
return {};
|
||||
}
|
||||
|
||||
function isExactHistoricalPeriodWindow(filters: AddressFilterSet): boolean {
|
||||
return (
|
||||
typeof filters.period_from === "string" &&
|
||||
filters.period_from.trim().length > 0 &&
|
||||
typeof filters.period_to === "string" &&
|
||||
filters.period_to.trim().length > 0
|
||||
);
|
||||
}
|
||||
|
||||
function extractPeriodRange(text: string): { period_from?: string; period_to?: string } {
|
||||
const directMatch = text.match(PERIOD_RANGE_PATTERN_1) ?? text.match(PERIOD_RANGE_PATTERN_2);
|
||||
if (!directMatch) {
|
||||
@@ -810,6 +819,9 @@ function isLowQualityCounterpartyAnchorValue(rawValue: string): boolean {
|
||||
if (meaningfulNonGenericTokens.length === 0 && (hasTemporalCue || paymentCue)) {
|
||||
return true;
|
||||
}
|
||||
if (meaningfulNonGenericTokens.length === 0) {
|
||||
return true;
|
||||
}
|
||||
const meaningfulTokens = tokens.filter((token) => isLikelyCounterpartyToken(token));
|
||||
return meaningfulTokens.length === 0;
|
||||
}
|
||||
@@ -1634,6 +1646,9 @@ function resolveSemanticDateBasisHint(filters: AddressFilterSet, warnings: strin
|
||||
const hasAsOfDate = typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0;
|
||||
const hasPeriodFrom = typeof filters.period_from === "string" && filters.period_from.trim().length > 0;
|
||||
const hasPeriodTo = typeof filters.period_to === "string" && filters.period_to.trim().length > 0;
|
||||
if (warnings.includes("as_of_date_derived_from_exact_historical_period") && (hasPeriodFrom || hasPeriodTo)) {
|
||||
return hasPeriodFrom && hasPeriodTo ? "period_range" : "period_end";
|
||||
}
|
||||
if (hasPeriodFrom && hasPeriodTo) {
|
||||
return "period_range";
|
||||
}
|
||||
@@ -1938,6 +1953,16 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
||||
}
|
||||
}
|
||||
|
||||
if (isExactHistoricalPeriodWindow(filters) && !warnings.includes("exact_historical_period_window_requested")) {
|
||||
const derivedFromHistoricalPhrase =
|
||||
warnings.includes("period_derived_from_month_phrase") ||
|
||||
warnings.includes("period_derived_from_year_range_phrase") ||
|
||||
warnings.includes("period_derived_from_year_phrase");
|
||||
if (derivedFromHistoricalPhrase) {
|
||||
warnings.push("exact_historical_period_window_requested");
|
||||
}
|
||||
}
|
||||
|
||||
const vatAsOfDate = explicitAsOfDateWithCue ?? explicitAsOfDate;
|
||||
if (intent === "vat_payable_forecast" && vatAsOfDate && !periodRange.period_from && !periodRange.period_to) {
|
||||
const quarterWindow = deriveQuarterWindowForDate(vatAsOfDate);
|
||||
@@ -1954,11 +1979,14 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
||||
}
|
||||
}
|
||||
const monthPeriodWasDerived = warnings.includes("period_derived_from_month_phrase");
|
||||
const yearPeriodWasDerived =
|
||||
warnings.includes("period_derived_from_year_phrase") || warnings.includes("period_derived_from_year_range_phrase");
|
||||
if (
|
||||
intent === "vat_liability_confirmed_for_tax_period" &&
|
||||
!periodRange.period_from &&
|
||||
!periodRange.period_to &&
|
||||
!monthPeriodWasDerived
|
||||
!monthPeriodWasDerived &&
|
||||
!yearPeriodWasDerived
|
||||
) {
|
||||
const periodToForQuarter = filters.period_to ?? vatAsOfDate ?? null;
|
||||
if (periodToForQuarter) {
|
||||
@@ -1986,7 +2014,12 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
||||
warnings.includes("period_derived_from_year_range_phrase") ||
|
||||
warnings.includes("period_derived_from_year_phrase");
|
||||
const preserveDerivedPeriodWindow =
|
||||
intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date";
|
||||
usesAsOfPrimaryWindow(intent) ||
|
||||
intent === "inventory_on_hand_as_of_date" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date";
|
||||
if (periodWasDerivedHeuristically && !warnings.includes("exact_historical_period_window_requested")) {
|
||||
warnings.push("exact_historical_period_window_requested");
|
||||
}
|
||||
if (periodWasDerivedHeuristically && !periodRange.period_from && !periodRange.period_to && !preserveDerivedPeriodWindow) {
|
||||
delete filters.period_from;
|
||||
delete filters.period_to;
|
||||
@@ -2016,6 +2049,16 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
||||
if (filters.period_to) {
|
||||
filters.as_of_date = filters.period_to;
|
||||
warnings.push("as_of_date_derived_from_period_to");
|
||||
if (
|
||||
warnings.includes("period_derived_from_month_phrase") ||
|
||||
warnings.includes("period_derived_from_year_range_phrase") ||
|
||||
warnings.includes("period_derived_from_year_phrase")
|
||||
) {
|
||||
warnings.push("as_of_date_derived_from_exact_historical_period");
|
||||
if (!warnings.includes("exact_historical_period_window_requested")) {
|
||||
warnings.push("exact_historical_period_window_requested");
|
||||
}
|
||||
}
|
||||
} else if (shouldDefaultAsOfDateToToday(intent)) {
|
||||
filters.as_of_date = new Date().toISOString().slice(0, 10);
|
||||
warnings.push("as_of_date_defaulted_today");
|
||||
|
||||
@@ -2232,6 +2232,18 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
|
||||
const hasRankingCue = /(?:топ|ранк|сам(?:ый|ая|ое|ые)|больше\s+всего|наибольш|крупн|жирн|max|top|rank)/iu.test(
|
||||
normalized
|
||||
);
|
||||
const hasInventoryPurchaseToSaleDocumentChainCue =
|
||||
/(?:закупк[а-яё]*[\s\S]{0,80}склад[\s\S]{0,80}продаж|путь\s+товар[а-яё]*[\s\S]{0,80}закуп|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale|->\s*(?:склад|warehouse|stock)\s*->\s*(?:продаж|sale))/iu.test(
|
||||
normalized
|
||||
) && /(?:товар|позици|номенклатур|sku|item|product)/iu.test(normalized);
|
||||
if (hasInventoryPurchaseToSaleDocumentChainCue) {
|
||||
return unicodeBridgeResolution(
|
||||
"inventory_purchase_to_sale_chain",
|
||||
"high",
|
||||
"unicode_inventory_purchase_to_sale_chain_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
const hasSelectedObjectProfitabilityCue =
|
||||
/(?:\u043f\u043e\s+\u0432\u044b\u0431\u0440\u0430\u043d\u043d(?:\u043e\u043c\u0443|\u043e\u0439)\s+(?:\u043e\u0431\u044a\u0435\u043a\u0442\u0443|\u043f\u043e\u0437\u0438\u0446\u0438\u0438)|selected\s+object)/iu.test(
|
||||
normalized
|
||||
@@ -2247,18 +2259,6 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
|
||||
);
|
||||
}
|
||||
|
||||
const hasInventoryPurchaseToSaleDocumentChainCue =
|
||||
/(?:закупк[а-яё]*[\s\S]{0,80}склад[\s\S]{0,80}продаж|путь\s+товар[а-яё]*[\s\S]{0,80}закуп|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale|->\s*(?:склад|warehouse|stock)\s*->\s*(?:продаж|sale))/iu.test(
|
||||
normalized
|
||||
) && /(?:товар|позици|номенклатур|sku|item|product)/iu.test(normalized);
|
||||
if (hasInventoryPurchaseToSaleDocumentChainCue) {
|
||||
return unicodeBridgeResolution(
|
||||
"inventory_purchase_to_sale_chain",
|
||||
"high",
|
||||
"unicode_inventory_purchase_to_sale_chain_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
const hasOpenItemsAccountCue =
|
||||
/(?:хвост|долг|незакрыт|вис)/iu.test(normalized) &&
|
||||
/(?:сч(?:е|ё)т(?:а|у|ом|е|ов)?\s*(?:№|#)?\s*(?:60|62|76)(?:[.,]\d{1,2})?|\b(?:60|62|76)(?:[.,]\d{1,2})?\b\s*сч(?:е|ё)т)/iu.test(
|
||||
|
||||
@@ -2194,7 +2194,8 @@ function enforceStrictAccountScopeForIntent(
|
||||
|
||||
function resolveExecutionFiltersForConfirmedBalance(
|
||||
filters: AddressFilterSet,
|
||||
analysisDate: string | null
|
||||
analysisDate: string | null,
|
||||
warnings: string[] = []
|
||||
): {
|
||||
executionFilters: AddressFilterSet;
|
||||
asOfDerived: string | null;
|
||||
@@ -2208,8 +2209,10 @@ function resolveExecutionFiltersForConfirmedBalance(
|
||||
if (derivedAsOf) {
|
||||
executionFilters.as_of_date = derivedAsOf;
|
||||
}
|
||||
delete executionFilters.period_from;
|
||||
delete executionFilters.period_to;
|
||||
if (!warnings.includes("as_of_date_derived_from_exact_historical_period")) {
|
||||
delete executionFilters.period_from;
|
||||
delete executionFilters.period_to;
|
||||
}
|
||||
const limit =
|
||||
typeof executionFilters.limit === "number" && Number.isFinite(executionFilters.limit)
|
||||
? Math.max(1, Math.trunc(executionFilters.limit))
|
||||
@@ -2415,6 +2418,9 @@ function asksForUnresolvedInventorySupplierLink(userMessage: string | null | und
|
||||
}
|
||||
|
||||
function canAutoBroadenPeriodWindow(intent: AddressIntent, filters: AddressFilterSet): boolean {
|
||||
if (Array.isArray((filters as { warnings?: unknown }).warnings) && (filters as { warnings?: string[] }).warnings?.includes("exact_historical_period_window_requested")) {
|
||||
return false;
|
||||
}
|
||||
const hasRecoverableAsOfOnlyWindow =
|
||||
!hasExplicitPeriodWindow(filters) &&
|
||||
typeof filters.as_of_date === "string" &&
|
||||
@@ -3713,16 +3719,16 @@ export class AddressQueryService {
|
||||
intent.intent === "inventory_on_hand_as_of_date" && requestedResultMode === "confirmed_balance";
|
||||
const payablesConfirmedExecution =
|
||||
confirmedBalancePayablesIntent
|
||||
? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate)
|
||||
? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate, filters.warnings)
|
||||
: null;
|
||||
const receivablesConfirmedExecution = confirmedBalanceReceivablesIntent
|
||||
? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate)
|
||||
? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate, filters.warnings)
|
||||
: null;
|
||||
const vatPayableConfirmedExecution = confirmedBalanceVatPayableIntent
|
||||
? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate)
|
||||
? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate, filters.warnings)
|
||||
: null;
|
||||
const inventoryConfirmedExecution = confirmedBalanceInventoryIntent
|
||||
? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate)
|
||||
? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate, filters.warnings)
|
||||
: null;
|
||||
let executionFilters =
|
||||
inventoryConfirmedExecution?.executionFilters ??
|
||||
@@ -5145,6 +5151,7 @@ export class AddressQueryService {
|
||||
!counterpartyItemFlowQuery &&
|
||||
isDocumentOrBankAnchorIntent(intent.intent) &&
|
||||
!hasExplicitPeriodWindow(filters.extracted_filters) &&
|
||||
!filters.warnings.some((warning) => warning.startsWith("period_derived_from_")) &&
|
||||
(anchor.anchor_type === "counterparty" || anchor.anchor_type === "contract")
|
||||
) {
|
||||
const currentLimit =
|
||||
|
||||
@@ -175,6 +175,12 @@ function truthGateStatusFrom(input: ResolveAddressTruthGateInput): AssistantTrut
|
||||
return input.truthGateStatusHint;
|
||||
}
|
||||
const missingRequiredFilters = input.missingRequiredFilters ?? [];
|
||||
const reasonCodes = input.reasons ?? [];
|
||||
const heuristicOpenItemsFallback = Boolean(
|
||||
input.intent === "open_items_by_counterparty_or_contract" &&
|
||||
(reasonCodes.includes("confirmed_balance_unavailable_fallback_to_heuristic_candidates") ||
|
||||
reasonCodes.includes("open_items_account_query_override_to_movements"))
|
||||
);
|
||||
if (input.routeExpectationStatus === "mismatch") {
|
||||
return "blocked_route_expectation_failure";
|
||||
}
|
||||
@@ -190,6 +196,9 @@ function truthGateStatusFrom(input: ResolveAddressTruthGateInput): AssistantTrut
|
||||
if (input.replyType === "factual" && input.limitedReasonCategory === "empty_match") {
|
||||
return "full_confirmed";
|
||||
}
|
||||
if (heuristicOpenItemsFallback) {
|
||||
return "partial_supported";
|
||||
}
|
||||
if (
|
||||
input.limitedReasonCategory === "empty_match" ||
|
||||
input.limitedReasonCategory === "recipe_visibility_gap" ||
|
||||
|
||||
@@ -4198,12 +4198,23 @@ function composeFactualReplyBody(
|
||||
|
||||
if (intent === "open_items_by_counterparty_or_contract") {
|
||||
const counterparties = buildCounterpartyRiskAggregate(rows);
|
||||
const accountLead =
|
||||
const accountLabel =
|
||||
typeof options.accountHint === "string" && options.accountHint.trim().length > 0
|
||||
? `Проверил хвосты по счету ${options.accountHint.trim()}.`
|
||||
: "Собраны открытые позиции по взаиморасчетам.";
|
||||
? `по счету ${options.accountHint.trim()}`
|
||||
: "по взаиморасчетам";
|
||||
const exactBalanceRequested = options.requestedResultMode === "confirmed_balance";
|
||||
const periodLabel = options.asOfDate
|
||||
? `на ${formatDateRu(options.asOfDate)}`
|
||||
: options.periodFrom || options.periodTo
|
||||
? `за период ${formatDateRu(options.periodFrom ?? "...")}..${formatDateRu(options.periodTo ?? "...")}`
|
||||
: null;
|
||||
const lines = [
|
||||
accountLead,
|
||||
exactBalanceRequested
|
||||
? `Коротко: точный открытый остаток ${accountLabel}${periodLabel ? ` ${periodLabel}` : ""} не подтвержден; ниже только предварительные сигналы по движениям: ${formatNumberWithDots(rows.length)} строк, контрагентов с сигналом: ${formatNumberWithDots(counterparties.length)}.`
|
||||
: `Коротко: ${accountLabel} найдено ${formatNumberWithDots(rows.length)} строк хвостов/открытых расчетов; контрагентов с сигналом: ${formatNumberWithDots(counterparties.length)}.`,
|
||||
exactBalanceRequested
|
||||
? "Это не подтвержденное сальдо и не финальный реестр открытых расчетов: текущий контур видит движения-кандидаты, но не доказывает остаток закрытия."
|
||||
: "Это shortlist для проверки, а не финальный подтвержденный реестр открытых расчетов.",
|
||||
`Строк отобрано: ${rows.length}.`,
|
||||
`Контрагентов с сигналом: ${counterparties.length}.`
|
||||
];
|
||||
@@ -4223,7 +4234,12 @@ function composeFactualReplyBody(
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: lines.join("\n")
|
||||
text: lines.join("\n"),
|
||||
semantics: {
|
||||
result_mode: "heuristic_candidates",
|
||||
evidence_strength: counterparties.length > 0 || rows.length > 0 ? "medium" : "weak",
|
||||
balance_confirmed: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4310,7 +4326,7 @@ function composeFactualReplyBody(
|
||||
: `Найдено документов по контрагенту: ${rows.length}.`
|
||||
);
|
||||
}
|
||||
if (counterpartyLabel) {
|
||||
if (counterpartyLabel && itemFlowQuestion) {
|
||||
lines.push(`Контрагент: ${counterpartyLabel}`);
|
||||
}
|
||||
if (itemFlowQuestion) {
|
||||
@@ -4330,7 +4346,11 @@ function composeFactualReplyBody(
|
||||
lines.push(`Показаны первые 12 из ${rows.length} поставок.`);
|
||||
}
|
||||
} else {
|
||||
lines.push(...formatTopRows(rows, rows.length));
|
||||
const visibleRows = rows.slice(0, 5);
|
||||
lines.push(...formatTopRows(visibleRows, visibleRows.length));
|
||||
if (rows.length > visibleRows.length) {
|
||||
lines.push(`Показаны первые ${visibleRows.length} из ${rows.length} документов; полный список остается в подтвержденном срезе.`);
|
||||
}
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
|
||||
@@ -259,11 +259,18 @@ const FOLLOWUP_LOW_QUALITY_COUNTERPARTY_TOKENS = new Set([
|
||||
"сейчас",
|
||||
"этому",
|
||||
"этомуже",
|
||||
"этой",
|
||||
"этойже",
|
||||
"тому",
|
||||
"томуже",
|
||||
"той",
|
||||
"тойже",
|
||||
"нему",
|
||||
"ней",
|
||||
"ним",
|
||||
"цепочка",
|
||||
"цепочке",
|
||||
"цепочку",
|
||||
"неуказанному",
|
||||
"неуказанный",
|
||||
"неуказанная",
|
||||
|
||||
@@ -182,10 +182,12 @@ export function composeInventoryReply(
|
||||
const lines: string[] = [directAnswerLine];
|
||||
|
||||
if (positions.length > 0) {
|
||||
const visiblePositionsLimit = 6;
|
||||
const visiblePositions = positions.slice(0, visiblePositionsLimit);
|
||||
appendInventorySection(
|
||||
lines,
|
||||
"Позиции:",
|
||||
positions.slice(0, 20).map((item, index) =>
|
||||
visiblePositions.map((item, index) =>
|
||||
formatInventorySnapshotPositionLine(item, index, {
|
||||
formatDateRu: deps.formatDateRu,
|
||||
formatNumberWithDots: deps.formatNumberWithDots,
|
||||
@@ -193,6 +195,11 @@ export function composeInventoryReply(
|
||||
})
|
||||
)
|
||||
);
|
||||
if (positions.length > visiblePositions.length) {
|
||||
lines.push(
|
||||
`Показаны первые ${deps.formatNumberWithDots(visiblePositions.length)} из ${deps.formatNumberWithDots(positions.length)} позиций по сумме; полный список можно раскрыть отдельным запросом.`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
appendInventorySection(lines, "Позиции:", [
|
||||
"- На дату среза товары с ненулевым остатком не найдены."
|
||||
|
||||
@@ -271,6 +271,7 @@ export async function runAssistantLivingChatRuntime(
|
||||
organization: scopedOrganization,
|
||||
addressDebug: lastMemoryAddressDebug,
|
||||
sessionItems: input.sessionItems,
|
||||
userMessage,
|
||||
toNonEmptyString: input.toNonEmptyString
|
||||
});
|
||||
activeOrganization = scopedOrganization ?? activeOrganization;
|
||||
|
||||
@@ -185,12 +185,13 @@ function timeScopeNeedFor(input: {
|
||||
family: string | null;
|
||||
explicitDateScope: string | null;
|
||||
allTimeScopeHint: boolean;
|
||||
subjectScopedBidirectionalAllTime: boolean;
|
||||
}): string | null {
|
||||
if (input.explicitDateScope) {
|
||||
return "explicit_period";
|
||||
}
|
||||
if (
|
||||
input.allTimeScopeHint &&
|
||||
(input.allTimeScopeHint || input.subjectScopedBidirectionalAllTime) &&
|
||||
(input.family === "value_flow" || input.family === "movement_evidence" || input.family === "document_evidence")
|
||||
) {
|
||||
return "all_time_scope";
|
||||
@@ -515,6 +516,11 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
|
||||
const comparisonNeed = comparisonNeedFor(action);
|
||||
const rankingNeed = rankingNeedFromRawUtterance(rawUtterance) ?? seededRankingNeed;
|
||||
const allTimeScopeHint = hasAllTimeScopeHint(rawUtterance);
|
||||
const subjectScopedBidirectionalAllTime =
|
||||
businessFactFamily === "value_flow" &&
|
||||
comparisonNeed === "incoming_vs_outgoing" &&
|
||||
subjectCandidates.length > 0 &&
|
||||
!explicitDateScope;
|
||||
const directBusinessOverviewMoneyAnswerHint = hasBusinessOverviewDirectMoneyAnswerHint({
|
||||
family: businessFactFamily,
|
||||
rawUtterance,
|
||||
@@ -576,7 +582,8 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
|
||||
const timeScopeNeed = timeScopeNeedFor({
|
||||
family: businessFactFamily,
|
||||
explicitDateScope,
|
||||
allTimeScopeHint
|
||||
allTimeScopeHint,
|
||||
subjectScopedBidirectionalAllTime
|
||||
});
|
||||
if (timeScopeNeed === "period_required" && !explicitDateScope) {
|
||||
pushUnique(clarificationGaps, "period");
|
||||
@@ -618,6 +625,9 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
|
||||
if (allTimeScopeHint) {
|
||||
pushReason(reasonCodes, "data_need_graph_all_time_scope_hint");
|
||||
}
|
||||
if (subjectScopedBidirectionalAllTime) {
|
||||
pushReason(reasonCodes, "data_need_graph_subject_bidirectional_value_flow_defaults_to_all_time_scope");
|
||||
}
|
||||
if (businessFactFamily === "business_overview" && !explicitDateScope) {
|
||||
pushReason(reasonCodes, "data_need_graph_business_overview_defaults_to_all_time_scope");
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@ export interface AssistantMcpDiscoveryTurnMeaningRef {
|
||||
asked_aggregation_axis?: string | null;
|
||||
seeded_ranking_need?: string | null;
|
||||
explicit_entity_candidates?: string[];
|
||||
business_overview_separate_entity_candidates?: string[];
|
||||
previous_counterparty_value_flow_bundle?: Record<string, unknown> | null;
|
||||
previous_counterparty_document_bundle?: Record<string, unknown> | null;
|
||||
metadata_ambiguity_entity_sets?: string[];
|
||||
metadata_scope_hint?: string | null;
|
||||
explicit_organization_scope?: string | null;
|
||||
@@ -177,6 +180,7 @@ function normalizeTurnMeaning(
|
||||
const dateScope = toNonEmptyString(value.explicit_date_scope);
|
||||
const unsupported = toNonEmptyString(value.unsupported_but_understood_family);
|
||||
const entities = toStringList(value.explicit_entity_candidates);
|
||||
const businessOverviewSeparateEntities = toStringList(value.business_overview_separate_entity_candidates);
|
||||
const metadataAmbiguityEntitySets = toStringList(value.metadata_ambiguity_entity_sets);
|
||||
if (domain) {
|
||||
result.asked_domain_family = domain;
|
||||
@@ -193,6 +197,9 @@ function normalizeTurnMeaning(
|
||||
if (entities.length > 0) {
|
||||
result.explicit_entity_candidates = entities;
|
||||
}
|
||||
if (businessOverviewSeparateEntities.length > 0) {
|
||||
result.business_overview_separate_entity_candidates = businessOverviewSeparateEntities;
|
||||
}
|
||||
if (metadataAmbiguityEntitySets.length > 0) {
|
||||
result.metadata_ambiguity_entity_sets = metadataAmbiguityEntitySets;
|
||||
}
|
||||
|
||||
@@ -430,22 +430,271 @@ function businessOverviewYearRowsLine(overview: Record<string, unknown>): string
|
||||
return values.length > 0 ? `По годам: ${sentenceAmount(joined) ?? joined}.` : null;
|
||||
}
|
||||
|
||||
function firstOverviewAxisLabel(rows: unknown, amountKey = "total_amount_human_ru"): string | null {
|
||||
const first = toRecordObject(Array.isArray(rows) ? rows[0] : null);
|
||||
const label = toNonEmptyString(first?.axis_value);
|
||||
const amount = moneyText(first?.[amountKey]);
|
||||
return label && amount ? `${label} — ${sentenceAmount(amount) ?? amount}` : null;
|
||||
}
|
||||
|
||||
function businessOverviewTaxLine(overview: Record<string, unknown>): string | null {
|
||||
const tax = toRecordObject(overview.tax_position);
|
||||
if (!tax) {
|
||||
return null;
|
||||
}
|
||||
const salesVat = moneyText(tax.sales_vat_amount_human_ru);
|
||||
const purchaseVat = moneyText(tax.purchase_vat_amount_human_ru);
|
||||
const netVat = moneyText(tax.net_vat_amount_human_ru);
|
||||
if (!salesVat && !purchaseVat && !netVat) {
|
||||
return null;
|
||||
}
|
||||
const direction =
|
||||
tax.net_vat_direction === "vat_to_pay"
|
||||
? "НДС к уплате"
|
||||
: tax.net_vat_direction === "vat_to_recover_or_offset"
|
||||
? "НДС к возмещению/зачету"
|
||||
: "чистая НДС-позиция";
|
||||
return `НДС: продажи ${salesVat ?? "0 руб."}, покупки ${purchaseVat ?? "0 руб."}, ${direction} ${sentenceAmount(netVat) ?? netVat ?? "0 руб."}.`;
|
||||
}
|
||||
|
||||
function businessOverviewDebtLine(overview: Record<string, unknown>): string | null {
|
||||
const debt = toRecordObject(overview.debt_position);
|
||||
if (!debt) {
|
||||
return null;
|
||||
}
|
||||
const receivables = moneyText(toRecordObject(debt.receivables)?.total_amount_human_ru);
|
||||
const payables = moneyText(toRecordObject(debt.payables)?.total_amount_human_ru);
|
||||
const net = moneyText(debt.net_debt_position_amount_human_ru);
|
||||
if (!receivables && !payables && !net) {
|
||||
return null;
|
||||
}
|
||||
const direction =
|
||||
debt.net_debt_position_direction === "net_payable" ? "кредиторка больше дебиторки" : "дебиторка больше кредиторки";
|
||||
return `Долги: дебиторка ${receivables ?? "0 руб."}, кредиторка ${payables ?? "0 руб."}, нетто ${sentenceAmount(net) ?? net ?? "0 руб."} (${direction}).`;
|
||||
}
|
||||
|
||||
function businessOverviewInventoryLine(overview: Record<string, unknown>): string | null {
|
||||
const inventory = toRecordObject(overview.inventory_position);
|
||||
if (!inventory) {
|
||||
return null;
|
||||
}
|
||||
const amount = moneyText(inventory.total_amount_human_ru);
|
||||
const rows = Number(inventory.rows_matched);
|
||||
const quantity = Number(inventory.total_quantity);
|
||||
if (!amount && !Number.isFinite(rows)) {
|
||||
return null;
|
||||
}
|
||||
const pieces = [
|
||||
Number.isFinite(rows) ? `${rows} позиций` : null,
|
||||
amount ? `на ${sentenceAmount(amount) ?? amount}` : null,
|
||||
Number.isFinite(quantity) && quantity > 0 ? `количество ${quantity}` : null
|
||||
].filter((item): item is string => Boolean(item));
|
||||
return pieces.length > 0 ? `Склад: ${pieces.join(", ")}.` : null;
|
||||
}
|
||||
|
||||
function rowCountText(value: unknown): string | null {
|
||||
const count = Number(value);
|
||||
return Number.isFinite(count) ? String(count) : null;
|
||||
}
|
||||
|
||||
function sideRowsText(side: Record<string, unknown> | null): string | null {
|
||||
const rowsWithAmount = rowCountText(side?.rows_with_amount);
|
||||
const rowsMatched = rowCountText(side?.rows_matched);
|
||||
if (rowsWithAmount && rowsMatched) {
|
||||
return `${rowsWithAmount} из ${rowsMatched}`;
|
||||
}
|
||||
return rowsWithAmount ?? rowsMatched;
|
||||
}
|
||||
|
||||
function sideDateText(side: Record<string, unknown> | null): string | null {
|
||||
const first = toNonEmptyString(side?.first_movement_date);
|
||||
const latest = toNonEmptyString(side?.latest_movement_date);
|
||||
if (first && latest) {
|
||||
return first === latest ? `дата ${first}` : `даты ${first}..${latest}`;
|
||||
}
|
||||
return first ? `первая дата ${first}` : latest ? `последняя дата ${latest}` : null;
|
||||
}
|
||||
|
||||
function bidirectionalNetLabel(direction: unknown): string {
|
||||
if (direction === "net_outgoing") {
|
||||
return "нетто в сторону контрагента";
|
||||
}
|
||||
if (direction === "balanced") {
|
||||
return "нетто около нуля";
|
||||
}
|
||||
return "нетто в нашу сторону";
|
||||
}
|
||||
|
||||
function buildCompactBidirectionalValueFlowReply(
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract,
|
||||
draft: Record<string, unknown>
|
||||
): string | null {
|
||||
const bridge = toRecordObject(entryPoint.bridge);
|
||||
const pilot = toRecordObject(bridge?.pilot);
|
||||
const flow = toRecordObject(pilot?.derived_bidirectional_value_flow);
|
||||
if (!flow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const incoming = toRecordObject(flow.incoming_customer_revenue);
|
||||
const outgoing = toRecordObject(flow.outgoing_supplier_payout);
|
||||
const incomingAmount = moneyText(incoming?.total_amount_human_ru);
|
||||
const outgoingAmount = moneyText(outgoing?.total_amount_human_ru);
|
||||
const netAmount = moneyText(flow.net_amount_human_ru);
|
||||
if (!incomingAmount && !outgoingAmount && !netAmount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const counterparty = toNonEmptyString(flow.counterparty) ?? "запрошенному контрагенту";
|
||||
const period = toNonEmptyString(flow.period_scope);
|
||||
const periodText = period ? ` за период ${period}` : " в проверенном окне";
|
||||
const incomingRows = sideRowsText(incoming);
|
||||
const outgoingRows = sideRowsText(outgoing);
|
||||
const incomingDates = sideDateText(incoming);
|
||||
const outgoingDates = sideDateText(outgoing);
|
||||
const netLabel = bidirectionalNetLabel(flow.net_direction);
|
||||
const lines = [
|
||||
`Коротко: по контрагенту ${counterparty}${periodText} по найденным строкам 1С получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}; расчетное ${netLabel}: ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
|
||||
];
|
||||
|
||||
const basis: string[] = [];
|
||||
if (incomingRows) {
|
||||
basis.push(`входящих строк с суммой ${incomingRows}${incomingDates ? ` (${incomingDates})` : ""}`);
|
||||
}
|
||||
if (outgoingRows) {
|
||||
basis.push(`исходящих строк с суммой ${outgoingRows}${outgoingDates ? ` (${outgoingDates})` : ""}`);
|
||||
}
|
||||
if (basis.length > 0) {
|
||||
lines.push(`Основа: ${basis.join("; ")}.`);
|
||||
}
|
||||
if (flow.coverage_limited_by_probe_limit === true) {
|
||||
lines.push("Важно: часть проверки уперлась в лимит строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");
|
||||
}
|
||||
lines.push("Метод: нетто рассчитано как подтвержденные входящие строки 1С минус подтвержденные исходящие строки; это не полное бухгалтерское сальдо вне проверенного окна.");
|
||||
|
||||
const fallbackNextStep = toNonEmptyString(draft.next_step_line);
|
||||
if (fallbackNextStep) {
|
||||
lines.push(`Следующий шаг: ${localizeLine(fallbackNextStep)}`);
|
||||
}
|
||||
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
}
|
||||
|
||||
function compactComparable(value: string | null): string {
|
||||
return String(value ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/[«»"']/g, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function businessOverviewSeparateSubjectLabel(
|
||||
graph: Record<string, unknown> | null,
|
||||
turnMeaning: Record<string, unknown> | null,
|
||||
organizationScope: string | null
|
||||
): string | null {
|
||||
const candidates = uniqueStrings([
|
||||
...toStringList(turnMeaning?.business_overview_separate_entity_candidates),
|
||||
...toStringList(graph?.subject_candidates),
|
||||
...toStringList(turnMeaning?.explicit_entity_candidates)
|
||||
]);
|
||||
const organizationComparable = compactComparable(organizationScope);
|
||||
for (const candidate of candidates) {
|
||||
const text = toNonEmptyString(candidate);
|
||||
if (!text) {
|
||||
continue;
|
||||
}
|
||||
const comparable = compactComparable(text);
|
||||
if (organizationComparable && comparable === organizationComparable) {
|
||||
continue;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function sameBusinessSubject(left: string | null, right: string | null): boolean {
|
||||
const leftComparable = compactComparable(left);
|
||||
const rightComparable = compactComparable(right);
|
||||
return Boolean(leftComparable && rightComparable && leftComparable === rightComparable);
|
||||
}
|
||||
|
||||
function previousDocumentSummaryLine(
|
||||
bundle: Record<string, unknown> | null,
|
||||
separateSubject: string | null
|
||||
): string | null {
|
||||
if (!bundle || !sameBusinessSubject(toNonEmptyString(bundle.counterparty), separateSubject)) {
|
||||
return null;
|
||||
}
|
||||
const count = Number(bundle.document_count);
|
||||
if (!Number.isFinite(count) || count <= 0) {
|
||||
return null;
|
||||
}
|
||||
return `документы по цепочке: найдено ${count}`;
|
||||
}
|
||||
|
||||
function buildPreviousCounterpartyValueFlowSummary(
|
||||
flow: Record<string, unknown> | null,
|
||||
separateSubject: string | null,
|
||||
documentBundle: Record<string, unknown> | null
|
||||
): { lead: string; line: string } | null {
|
||||
if (!flow || !separateSubject || !sameBusinessSubject(toNonEmptyString(flow.counterparty), separateSubject)) {
|
||||
return null;
|
||||
}
|
||||
const incoming = toRecordObject(flow.incoming_customer_revenue);
|
||||
const outgoing = toRecordObject(flow.outgoing_supplier_payout);
|
||||
const incomingAmount = moneyText(incoming?.total_amount_human_ru);
|
||||
const outgoingAmount = moneyText(outgoing?.total_amount_human_ru);
|
||||
const netAmount = moneyText(flow.net_amount_human_ru);
|
||||
if (!incomingAmount && !outgoingAmount && !netAmount) {
|
||||
return null;
|
||||
}
|
||||
const counterparty = toNonEmptyString(flow.counterparty) ?? separateSubject;
|
||||
const netLabel = bidirectionalNetLabel(flow.net_direction);
|
||||
const lead =
|
||||
`; отдельно по ${counterparty}: получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}, ` +
|
||||
`${netLabel} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}`;
|
||||
const basis: string[] = [];
|
||||
const incomingRows = sideRowsText(incoming);
|
||||
const outgoingRows = sideRowsText(outgoing);
|
||||
const incomingDates = sideDateText(incoming);
|
||||
const outgoingDates = sideDateText(outgoing);
|
||||
if (incomingRows) {
|
||||
basis.push(`входящие строки ${incomingRows}${incomingDates ? ` (${incomingDates})` : ""}`);
|
||||
}
|
||||
if (outgoingRows) {
|
||||
basis.push(`исходящие строки ${outgoingRows}${outgoingDates ? ` (${outgoingDates})` : ""}`);
|
||||
}
|
||||
const documents = previousDocumentSummaryLine(documentBundle, counterparty);
|
||||
if (documents) {
|
||||
basis.push(documents);
|
||||
}
|
||||
const basisText = basis.length > 0 ? ` Основа: ${basis.join("; ")}.` : "";
|
||||
return {
|
||||
lead,
|
||||
line:
|
||||
`Отдельно по контрагенту ${counterparty}: подтверждено получили ${incomingAmount ?? "0 руб."}, ` +
|
||||
`заплатили ${outgoingAmount ?? "0 руб."}, расчетное ${netLabel} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.` +
|
||||
`${basisText} Это не перенос сумм компании на контрагента, а отдельный ранее подтвержденный контрагентский срез.`
|
||||
};
|
||||
}
|
||||
|
||||
function buildCompactBusinessOverviewReply(
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract,
|
||||
draft: Record<string, unknown>
|
||||
): string | null {
|
||||
const turnInput = toRecordObject(entryPoint.turn_input);
|
||||
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
|
||||
const graph = toRecordObject(turnInput?.data_need_graph);
|
||||
const bridge = toRecordObject(entryPoint.bridge);
|
||||
const pilot = toRecordObject(bridge?.pilot);
|
||||
const overview = toRecordObject(pilot?.derived_business_overview);
|
||||
const graphReasons = readStringArray(graph?.reason_codes);
|
||||
const isBusinessOverview =
|
||||
toNonEmptyString(graph?.business_fact_family) === "business_overview" ||
|
||||
toNonEmptyString(pilot?.pilot_scope) === "business_overview_route_template_v1";
|
||||
const rankingNeed = toNonEmptyString(graph?.ranking_need);
|
||||
const directMoneyAnswer = graphReasons.includes("data_need_graph_business_overview_direct_money_answer");
|
||||
if (!isBusinessOverview || !overview || (!rankingNeed && !directMoneyAnswer)) {
|
||||
if (!isBusinessOverview || !overview) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -457,8 +706,51 @@ function buildCompactBusinessOverviewReply(
|
||||
const netDirection = overview.net_direction === "net_outgoing" ? "операционное нетто в минус" : "расчетное операционное нетто";
|
||||
const period = businessOverviewPeriodText(overview);
|
||||
const limitLine = businessOverviewCoverageLimitLine(overview);
|
||||
const organizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
|
||||
const separateSubject = businessOverviewSeparateSubjectLabel(graph, turnMeaning, organizationScope);
|
||||
const previousCounterpartySummary = buildPreviousCounterpartyValueFlowSummary(
|
||||
toRecordObject(turnMeaning?.previous_counterparty_value_flow_bundle),
|
||||
separateSubject,
|
||||
toRecordObject(turnMeaning?.previous_counterparty_document_bundle)
|
||||
);
|
||||
const organizationPrefix = organizationScope ? `по компании ${organizationScope} ` : "";
|
||||
const separateSubjectLead = separateSubject
|
||||
? previousCounterpartySummary?.lead ??
|
||||
`; по контрагенту ${separateSubject} суммы компании не переношу, это отдельный контур без подтвержденного итога в этой строке`
|
||||
: "";
|
||||
const topCustomer = toRecordObject(Array.isArray(overview.top_customers) ? overview.top_customers[0] : null);
|
||||
const customerName = toNonEmptyString(topCustomer?.axis_value);
|
||||
const customerAmount = moneyText(topCustomer?.total_amount_human_ru);
|
||||
const topCustomerLead =
|
||||
customerName && customerAmount
|
||||
? `; крупнейший источник входящих денег: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}`
|
||||
: "";
|
||||
const topSupplier = firstOverviewAxisLabel(overview.top_suppliers);
|
||||
const topSupplierLead = topSupplier ? `; крупнейший получатель исходящих денег: ${topSupplier}` : "";
|
||||
const roleBoundaryLead = topCustomer || topSupplier ? "; клиент/поставщик как бизнес-роли этим денежным срезом не подтверждены" : "";
|
||||
const graphReasonCodes = toStringList(graph?.reason_codes);
|
||||
const directMoneyAnswer = graphReasonCodes.includes("data_need_graph_business_overview_direct_money_answer");
|
||||
const crossScopeExecutiveSummary = Boolean(separateSubject && previousCounterpartySummary);
|
||||
const lines: string[] = [];
|
||||
|
||||
if (crossScopeExecutiveSummary && separateSubject && previousCounterpartySummary && (incomingAmount || outgoingAmount || netAmount)) {
|
||||
lines.push(
|
||||
`Коротко: по компании ${organizationScope ?? "в выбранном контуре"} ${period} подтвержден денежный срез: получили ${incomingAmount ?? "0 руб."}, исходящие платежи/списания ${outgoingAmount ?? "0 руб."}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}${previousCounterpartySummary.lead}; можно утверждать только эти подтвержденные срезы, нельзя называть это чистой прибылью, полным оборотом или доказанной ролью главного клиента/поставщика.`
|
||||
);
|
||||
lines.push(previousCounterpartySummary.line);
|
||||
lines.push(
|
||||
`Можно утверждать: по компании подтвержден operating-flow proxy по найденным строкам 1С; по ${separateSubject} отдельно подтверждены входящие/исходящие строки, расчетное нетто и документы из предыдущего контрагентского среза.`
|
||||
);
|
||||
lines.push(
|
||||
`Нельзя утверждать: это не чистая прибыль, не полный бухгалтерский оборот вне проверенного окна и не доказательство, что ${separateSubject} является главным клиентом или поставщиком как бизнес-роль.`
|
||||
);
|
||||
if (limitLine) {
|
||||
lines.push(limitLine);
|
||||
}
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
}
|
||||
|
||||
if (rankingNeed) {
|
||||
const incomingLeader = strongestIncomingYear(overview);
|
||||
const netLeader = strongestNetYear(overview);
|
||||
@@ -469,7 +761,7 @@ function buildCompactBusinessOverviewReply(
|
||||
return null;
|
||||
}
|
||||
lines.push(
|
||||
`Коротко: самый доходный год в доступном денежном контуре 1С — ${leaderYear}: ${leaderAmount}${Number.isFinite(leaderRows) && leaderRows > 0 ? ` по ${leaderRows} строкам с суммой` : ""}.`
|
||||
`Коротко: в доступном проверенном MCP-срезе по входящим денежным строкам лидирует ${leaderYear}: ${leaderAmount}${Number.isFinite(leaderRows) && leaderRows > 0 ? ` по ${leaderRows} строкам с суммой` : ""}; это не полный бухгалтерский рейтинг доходности.`
|
||||
);
|
||||
const netYear = toNonEmptyString(netLeader?.year_bucket);
|
||||
const netYearAmount = moneyText(netLeader?.net_amount_human_ru);
|
||||
@@ -487,19 +779,62 @@ function buildCompactBusinessOverviewReply(
|
||||
}
|
||||
} else if (incomingAmount || outgoingAmount || netAmount) {
|
||||
lines.push(
|
||||
`Коротко: ${period} по подтвержденным строкам 1С получили ${incomingAmount ?? "0 руб."}; исходящие платежи/списания ${outgoingAmount ?? "0 руб."}; ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб"}.`
|
||||
`Коротко: ${organizationPrefix}${period} по подтвержденным строкам 1С получили ${incomingAmount ?? "0 руб."}; исходящие платежи/списания ${outgoingAmount ?? "0 руб."}; ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб"}${topCustomerLead}${topSupplierLead}${roleBoundaryLead}${separateSubjectLead}.`
|
||||
);
|
||||
lines.push('Метод: "заработали" здесь считаю как денежный operating-flow proxy по 1С; это не чистая прибыль и не финрезультат.');
|
||||
const topCustomer = toRecordObject(Array.isArray(overview.top_customers) ? overview.top_customers[0] : null);
|
||||
const customerName = toNonEmptyString(topCustomer?.axis_value);
|
||||
const customerAmount = moneyText(topCustomer?.total_amount_human_ru);
|
||||
if (customerName && customerAmount) {
|
||||
if (!directMoneyAnswer && customerName && customerAmount) {
|
||||
lines.push(`Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}.`);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (separateSubject) {
|
||||
lines.push(
|
||||
previousCounterpartySummary?.line ??
|
||||
`Отдельно по контрагенту ${separateSubject}: этот итог не переносит суммы компании на контрагента. Можно утверждать только разделение контура; нельзя делать вывод о выручке, долге или прибыльности ${separateSubject} без отдельного контрагентского среза документов и движений.`
|
||||
);
|
||||
}
|
||||
|
||||
if (!directMoneyAnswer && topSupplier) {
|
||||
lines.push(`Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`);
|
||||
}
|
||||
if (!directMoneyAnswer && (topCustomer || topSupplier)) {
|
||||
lines.push("Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль.");
|
||||
}
|
||||
if (!directMoneyAnswer) {
|
||||
lines.push(
|
||||
`Что подтверждено: денежный срез по компании${organizationScope ? ` ${organizationScope}` : ""}${period ? ` ${period}` : ""}${topCustomer ? ", крупнейший источник входящих денег" : ""}${topSupplier ? ", крупнейший получатель исходящих денег" : ""}.`
|
||||
);
|
||||
const taxLine = businessOverviewTaxLine(overview);
|
||||
if (taxLine) {
|
||||
lines.push(taxLine);
|
||||
}
|
||||
const debtLine = businessOverviewDebtLine(overview);
|
||||
if (debtLine) {
|
||||
lines.push(debtLine);
|
||||
}
|
||||
const inventoryLine = businessOverviewInventoryLine(overview);
|
||||
if (inventoryLine) {
|
||||
lines.push(inventoryLine);
|
||||
}
|
||||
const missingOverviewFamilies: string[] = [];
|
||||
if (!taxLine) {
|
||||
missingOverviewFamilies.push("общая НДС/налоговая позиция без отдельного точного расчета");
|
||||
}
|
||||
if (!debtLine) {
|
||||
missingOverviewFamilies.push("долги без даты среза");
|
||||
}
|
||||
if (!inventoryLine) {
|
||||
missingOverviewFamilies.push("склад без даты среза");
|
||||
}
|
||||
if (missingOverviewFamilies.length > 0) {
|
||||
lines.push(`Что не подтверждено в этом срезе: ${missingOverviewFamilies.join(", ")}.`);
|
||||
}
|
||||
lines.push(
|
||||
"Что нельзя утверждать: чистую прибыль, полноценный финрезультат, юридические бизнес-роли клиентов/поставщиков и общую налоговую позицию без отдельного точного расчета."
|
||||
);
|
||||
}
|
||||
if (limitLine) {
|
||||
lines.push(limitLine);
|
||||
}
|
||||
@@ -556,6 +891,11 @@ function buildReplyText(entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContra
|
||||
return null;
|
||||
}
|
||||
|
||||
const compactBidirectionalValueFlowReply = buildCompactBidirectionalValueFlowReply(entryPoint, draft);
|
||||
if (compactBidirectionalValueFlowReply) {
|
||||
return compactBidirectionalValueFlowReply;
|
||||
}
|
||||
|
||||
const compactBusinessOverviewReply = buildCompactBusinessOverviewReply(entryPoint, draft);
|
||||
if (compactBusinessOverviewReply) {
|
||||
return compactBusinessOverviewReply;
|
||||
|
||||
@@ -344,6 +344,19 @@ function readStateTransitionReasonCodes(input: ApplyAssistantMcpDiscoveryRespons
|
||||
.filter((item): item is string => Boolean(item));
|
||||
}
|
||||
|
||||
function hasFullConfirmedTruth(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): boolean {
|
||||
const truthGateStatus = toNonEmptyString(input.addressRuntimeMeta?.truth_gate_contract_status);
|
||||
if (truthGateStatus === "full_confirmed") {
|
||||
return true;
|
||||
}
|
||||
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
|
||||
const truthGate = toRecordObject(truthAnswerPolicy?.truth_gate);
|
||||
const sourceTruthGateStatus = toNonEmptyString(truthGate?.source_truth_gate_status);
|
||||
const coverageStatus = toNonEmptyString(truthGate?.coverage_status);
|
||||
const groundingStatus = toNonEmptyString(truthGate?.grounding_status);
|
||||
return sourceTruthGateStatus === "full_confirmed" || (coverageStatus === "full" && groundingStatus === "grounded");
|
||||
}
|
||||
|
||||
function readStringArray(value: unknown): string[] {
|
||||
return Array.isArray(value)
|
||||
? value.map((item) => toNonEmptyString(item)).filter((item): item is string => Boolean(item))
|
||||
@@ -424,6 +437,12 @@ function hasExactMatchedFactualAddressReply(
|
||||
if (hasEvidenceLaneConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
if (!(isMetadataDiscoveryTurn(entryPoint) && isInventoryExactAddressIntent(detectedIntent))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const mcpCallStatus = toNonEmptyString(input.addressRuntimeMeta?.mcp_call_status);
|
||||
const truthMode = toNonEmptyString(input.addressRuntimeMeta?.truth_mode);
|
||||
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
|
||||
@@ -472,17 +491,7 @@ function hasRuntimeAdjustedExactReply(
|
||||
if (hasEvidenceLaneConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
const truthGateStatus = toNonEmptyString(input.addressRuntimeMeta?.truth_gate_contract_status);
|
||||
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
|
||||
const truthGate = toRecordObject(truthAnswerPolicy?.truth_gate);
|
||||
const sourceTruthGateStatus = toNonEmptyString(truthGate?.source_truth_gate_status);
|
||||
const coverageStatus = toNonEmptyString(truthGate?.coverage_status);
|
||||
const groundingStatus = toNonEmptyString(truthGate?.grounding_status);
|
||||
const hasFullConfirmedTruth =
|
||||
truthGateStatus === "full_confirmed" ||
|
||||
sourceTruthGateStatus === "full_confirmed" ||
|
||||
(coverageStatus === "full" && groundingStatus === "grounded");
|
||||
if (!hasFullConfirmedTruth) {
|
||||
if (!hasFullConfirmedTruth(input)) {
|
||||
return false;
|
||||
}
|
||||
const truthAnswerShape = readTruthAnswerShape(input);
|
||||
@@ -495,6 +504,32 @@ function hasRuntimeAdjustedExactReply(
|
||||
);
|
||||
}
|
||||
|
||||
function hasRuntimeMatchedExactReply(
|
||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||
): boolean {
|
||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (!hasEffectivelyFactualAddressReply(input)) {
|
||||
return false;
|
||||
}
|
||||
if (hasMetadataDiscoveryPriority(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (hasEvidenceLaneConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (!hasFullConfirmedTruth(input)) {
|
||||
return false;
|
||||
}
|
||||
const reasonCodes = readStateTransitionReasonCodes(input);
|
||||
return (
|
||||
reasonCodes.some((reason) => reason === "route_expectation_matched") &&
|
||||
reasonCodes.some((reason) => /(?:confirmed_balance_exact|exact_.+_intent|vat_period_inspection_bridge_signal_detected)/iu.test(reason))
|
||||
);
|
||||
}
|
||||
|
||||
function hasAlignedFactualAddressReply(
|
||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||
@@ -528,6 +563,9 @@ function hasSemanticConflictWithDiscoveryTurnMeaning(
|
||||
if (hasRuntimeAdjustedExactReply(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (hasRuntimeMatchedExactReply(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
|
||||
const askedDomain = toNonEmptyString(turnMeaning?.asked_domain_family);
|
||||
@@ -619,16 +657,7 @@ function hasFullConfirmedFactualAddressReply(
|
||||
if (hasMetadataDiscoveryPriority(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
const truthGateStatus = toNonEmptyString(input.addressRuntimeMeta?.truth_gate_contract_status);
|
||||
if (truthGateStatus === "full_confirmed") {
|
||||
return true;
|
||||
}
|
||||
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
|
||||
const truthGate = toRecordObject(truthAnswerPolicy?.truth_gate);
|
||||
const sourceTruthGateStatus = toNonEmptyString(truthGate?.source_truth_gate_status);
|
||||
const coverageStatus = toNonEmptyString(truthGate?.coverage_status);
|
||||
const groundingStatus = toNonEmptyString(truthGate?.grounding_status);
|
||||
return sourceTruthGateStatus === "full_confirmed" || (coverageStatus === "full" && groundingStatus === "grounded");
|
||||
return hasFullConfirmedTruth(input);
|
||||
}
|
||||
|
||||
export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||
@@ -652,6 +681,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||
const fullConfirmedFactualAddressReply = hasFullConfirmedFactualAddressReply(input, entryPoint);
|
||||
const exactMatchedFactualAddressReply = hasExactMatchedFactualAddressReply(input, entryPoint);
|
||||
const runtimeAdjustedExactReply = hasRuntimeAdjustedExactReply(input, entryPoint);
|
||||
const runtimeMatchedExactReply = hasRuntimeMatchedExactReply(input, entryPoint);
|
||||
const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint);
|
||||
const metadataDiscoveryPriority = hasMetadataDiscoveryPriority(input, entryPoint);
|
||||
const valueFlowActionConflictWithDiscoveryTurnMeaning = hasValueFlowActionConflictWithDiscoveryTurnMeaning(
|
||||
@@ -714,6 +744,12 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||
"mcp_discovery_response_policy_keep_runtime_adjusted_exact_reply_over_stale_discovery_turn_meaning"
|
||||
);
|
||||
}
|
||||
if (runtimeMatchedExactReply) {
|
||||
pushReason(
|
||||
reasonCodes,
|
||||
"mcp_discovery_response_policy_keep_runtime_matched_exact_reply_over_stale_discovery_turn_meaning"
|
||||
);
|
||||
}
|
||||
if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") {
|
||||
pushReason(
|
||||
reasonCodes,
|
||||
@@ -742,6 +778,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||
!fullConfirmedFactualAddressReply &&
|
||||
!exactMatchedFactualAddressReply &&
|
||||
!runtimeAdjustedExactReply &&
|
||||
!runtimeMatchedExactReply &&
|
||||
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
|
||||
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
||||
candidate.eligible_for_future_hot_runtime &&
|
||||
|
||||
@@ -259,6 +259,9 @@ function pushScopedEntityCandidate(
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (target.some((existing) => sameScopedName(existing, text))) {
|
||||
return;
|
||||
}
|
||||
pushUnique(target, text);
|
||||
}
|
||||
|
||||
@@ -291,6 +294,20 @@ function sameScopedName(left: string | null, right: string | null): boolean {
|
||||
return Boolean(left && right && compactLower(left) === compactLower(right));
|
||||
}
|
||||
|
||||
function preferredScopedDisplayName(value: string | null, candidates: unknown[]): string | null {
|
||||
const anchor = toNonEmptyString(value);
|
||||
if (!anchor) {
|
||||
return null;
|
||||
}
|
||||
for (const candidate of candidates) {
|
||||
const text = candidateValue(candidate);
|
||||
if (sameScopedName(text, anchor)) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return anchor;
|
||||
}
|
||||
|
||||
function candidateValue(value: unknown): string | null {
|
||||
const direct = toNonEmptyString(value);
|
||||
if (direct && direct !== "[object Object]") {
|
||||
@@ -612,6 +629,8 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
|
||||
metadataRecommendedNextPrimitive: AssistantMcpDiscoveryMetadataRecommendedPrimitive | null;
|
||||
metadataAmbiguityDetected: boolean;
|
||||
metadataAmbiguityEntitySets: string[];
|
||||
previousBidirectionalValueFlow: Record<string, unknown> | null;
|
||||
previousDocumentSummary: Record<string, unknown> | null;
|
||||
} {
|
||||
const previousFilters = toRecordObject(followupContext?.previous_filters);
|
||||
const rootFilters = toRecordObject(followupContext?.root_filters);
|
||||
@@ -717,7 +736,9 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
|
||||
followupContext?.previous_discovery_metadata_recommended_next_primitive
|
||||
),
|
||||
metadataAmbiguityDetected: followupContext?.previous_discovery_metadata_ambiguity_detected === true,
|
||||
metadataAmbiguityEntitySets: collectEntityCandidates(followupContext?.previous_discovery_metadata_ambiguity_entity_sets)
|
||||
metadataAmbiguityEntitySets: collectEntityCandidates(followupContext?.previous_discovery_metadata_ambiguity_entity_sets),
|
||||
previousBidirectionalValueFlow: toRecordObject(followupContext?.previous_discovery_bidirectional_value_flow),
|
||||
previousDocumentSummary: toRecordObject(followupContext?.previous_discovery_document_summary)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -896,8 +917,26 @@ function hasOrganizationLevelSupplierQualityOverviewSignal(text: string): boolea
|
||||
return hasSupplierScopeCue && hasSupplierQualityCue && hasCompanyScopeCue;
|
||||
}
|
||||
|
||||
function hasCrossScopeExecutiveSummarySignal(text: string): boolean {
|
||||
return (
|
||||
/(?:\u0441\u043e\u0431\u0435\u0440\p{L}*\s+(?:\u043a\u043e\u0440\u043e\u0442\u043a\p{L}*\s+)?\u0438\u0442\u043e\u0433|\u044d\u043a\u0437\u0435\u043a\u044c\u044e\u0442\u0438\u0432\p{L}*\s+\u0441\u0430\u043c\u043c\u0430\u0440\u0438|executive\s+summary|final\s+summary)/iu.test(
|
||||
text
|
||||
) &&
|
||||
/(?:\u0447\u0442\u043e\s+(?:\u043c\u044b\s+)?\u043f\u043e\u0434\u0442\u0432\u0435\u0440\p{L}*|\u043f\u043e\s+\u043a\u043e\u043c\u043f\u0430\u043d\p{L}*|\u043f\u043e\s+\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\p{L}*|confirmed|company|organization)/iu.test(
|
||||
text
|
||||
) &&
|
||||
/(?:\u043e\u0442\u0434\u0435\u043b\u044c\u043d\p{L}*\s+\u043f\u043e|\u043f\u043e\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0433\u0440\u0443\u043f\u043f\p{L}*\s+\u0441\u0432\u043a|\u0441\u0432\u043a|counterpart(?:y|ies)?)/iu.test(
|
||||
text
|
||||
) &&
|
||||
/(?:\u0447\u0442\u043e\s+\u043c\u043e\u0436\u043d\p{L}*|\u0447\u0442\u043e\s+\u043d\u0435\u043b\u044c\u0437\p{L}*|\u0432\u044b\u0432\u043e\u0434\p{L}*|allowed|forbidden|cannot|can\s+say)/iu.test(
|
||||
text
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function hasBusinessOverviewSignal(text: string): boolean {
|
||||
if (
|
||||
hasCrossScopeExecutiveSummarySignal(text) ||
|
||||
hasOrganizationLevelEarningsOverviewSignal(text) ||
|
||||
hasOrganizationLevelDebtPositionOverviewSignal(text) ||
|
||||
hasOrganizationLevelDebtDueDateOverviewSignal(text) ||
|
||||
@@ -948,6 +987,43 @@ function hasBusinessOverviewContinuationSignal(text: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function hasExplicitVatQuestionSignal(text: string): boolean {
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
/(?:\u043d\u0434\u0441|vat)/iu.test(text) &&
|
||||
/(?:\u0437\u0430|\u043d\u0430|\u043f\u0435\u0440\u0438\u043e\u0434|\u043f\u043e\u0437\u0438\u0446|\u043a\s+\u0443\u043f\u043b\u0430\u0442|\u043a\s+\u0432\u043e\u0437\u043c\u0435\u0449|\u043e\u0441\u043d\u043e\u0432\u0430\u043d|\u043d\u0430\u043b\u043e\u0433\u043e\u0432\p{L}*\s+\u0432\u044b\u0432\u043e\u0434|tax\s+period|tax\s+position)/iu.test(
|
||||
text
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function hasBusinessOverviewSeparateCounterpartySignal(text: string): boolean {
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
/(?:\u043e\u0442\u0434\u0435\u043b\u044c\u043d\p{L}*\s+\u043f\u043e|\u043f\u043e\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|counterpart(?:y|ies)?)/iu.test(text) &&
|
||||
/(?:\u043a\u043e\u043c\u043f\u0430\u043d\p{L}*|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\p{L}*|company|organization|\u0438\u0442\u043e\u0433|summary|\u0432\u044b\u0432\u043e\u0434\p{L}*)/iu.test(text)
|
||||
);
|
||||
}
|
||||
|
||||
function businessOverviewSeparateCounterpartyCandidateFromText(text: string): string | null {
|
||||
const source = repairAddressMojibakeText(String(text ?? ""));
|
||||
const patterns = [
|
||||
/(?:\u043e\u0442\u0434\u0435\u043b\u044c\u043d\p{L}*\s+\u043f\u043e|\u043f\u043e\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*)\s+(.+?)(?:[,.;:!?]|\s+\u043a\u0430\u043a\u0438\p{L}*\b|\s+\u0447\u0442\u043e\b|$)/iu,
|
||||
/(?:\u0434\u043b\u044f|for)\s+([\p{L}\d._-]+(?:\s+[\p{L}\d._-]+){0,3})(?:[,.;:!?]|\s+\u043a\u0430\u043a\u0438\p{L}*\b|\s+\u0447\u0442\u043e\b|$)/iu
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
const candidate = normalizeFollowupCounterpartyCandidate(source.match(pattern)?.[1]);
|
||||
if (candidate && !isInvalidEntityCandidate(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function hasExplicitTopicSwitchSignal(text: string): boolean {
|
||||
return /(?:^|\s)(?:\u0442\u0435\u043f\u0435\u0440\u044c|\u0430\s+\u0442\u0435\u043f\u0435\u0440\u044c|\u0434\u0430\u043b\u044c\u0448\u0435|\u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e|\u043f\u0435\u0440\u0435\u0439\u0434[\u0451\u0435]\u043c|\u0441\u043c\u0435\u043d\u0438\u043c\s+\u0442\u0435\u043c\u0443|\u0432\u0435\u0440\u043d[\u0451\u0435]\u043c\u0441\u044f\s+\u043a|now|next|switch\s+to)(?:\s|$)/iu.test(
|
||||
text
|
||||
@@ -1456,9 +1532,16 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
const rawReferentialDocumentExclusionSignal = hasReferentialDocumentExclusionFollowupSignal(
|
||||
repairedUserText ?? rawUserText ?? ""
|
||||
);
|
||||
const rawPrimaryBusinessOverviewSignal = hasBusinessOverviewSignal(rawText);
|
||||
const explicitVatQuestionSignal = hasExplicitVatQuestionSignal(rawText);
|
||||
const explicitVatSuppressesBusinessOverviewContinuation = Boolean(
|
||||
explicitVatQuestionSignal && !rawPrimaryBusinessOverviewSignal
|
||||
);
|
||||
const businessOverviewContinuationSignal =
|
||||
hasBusinessOverviewFollowupSeed(followupSeed) && hasBusinessOverviewContinuationSignal(rawText);
|
||||
const rawBusinessOverviewSignal = hasBusinessOverviewSignal(rawText) || businessOverviewContinuationSignal;
|
||||
hasBusinessOverviewFollowupSeed(followupSeed) &&
|
||||
hasBusinessOverviewContinuationSignal(rawText) &&
|
||||
!explicitVatSuppressesBusinessOverviewContinuation;
|
||||
const rawBusinessOverviewSignal = rawPrimaryBusinessOverviewSignal || businessOverviewContinuationSignal;
|
||||
const rawLifecycleSignal = !rawBusinessOverviewSignal && hasLifecycleSignal(rawText);
|
||||
const rawBidirectionalValueFlowSignal =
|
||||
!rawBusinessOverviewSignal && !rawLifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
|
||||
@@ -1517,6 +1600,12 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
rawDomain === "business_summary" ||
|
||||
rawDomain === "business_overview" ||
|
||||
rawAction === "broad_evaluation";
|
||||
const businessOverviewSeparateCounterpartySignal = Boolean(
|
||||
businessOverviewSignal && hasBusinessOverviewSeparateCounterpartySignal(rawText)
|
||||
);
|
||||
const businessOverviewSeparateCounterpartyCandidate = businessOverviewSeparateCounterpartySignal
|
||||
? businessOverviewSeparateCounterpartyCandidateFromText(rawText)
|
||||
: null;
|
||||
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
|
||||
const currentTurnDocumentLaneSignal = rawAction === "list_documents";
|
||||
const currentTurnMovementLaneSignal = rawAction === "list_movements";
|
||||
@@ -1556,6 +1645,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
);
|
||||
const businessOverviewSuppressesFollowupCounterparty = Boolean(
|
||||
businessOverviewSignal &&
|
||||
!businessOverviewSeparateCounterpartySignal &&
|
||||
(rawBusinessOverviewSignal ||
|
||||
businessOverviewContinuationSignal ||
|
||||
broadBusinessEvaluationUnsupported ||
|
||||
@@ -1604,8 +1694,12 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
? null
|
||||
: normalizeFollowupCounterpartyCandidate(predecomposeEntities.counterparty);
|
||||
const predecomposeDateScope = collectDateScope(predecomposeContract);
|
||||
const suppressFollowupBusinessOverviewSeed = Boolean(
|
||||
explicitVatSuppressesBusinessOverviewContinuation && hasBusinessOverviewFollowupSeed(followupSeed)
|
||||
);
|
||||
const periodClarificationFollowupApplicable = Boolean(
|
||||
followupSeed.domain &&
|
||||
!suppressFollowupBusinessOverviewSeed &&
|
||||
followupSeed.loopStatus === "awaiting_clarification" &&
|
||||
followupSeed.loopPendingAxes.includes("period") &&
|
||||
!rawLifecycleSignal &&
|
||||
@@ -1618,6 +1712,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
);
|
||||
const followupDiscoverySeedApplicable = Boolean(
|
||||
followupSeed.domain &&
|
||||
!suppressFollowupBusinessOverviewSeed &&
|
||||
!rawLifecycleSignal &&
|
||||
!rawMetadataSignal &&
|
||||
(periodClarificationFollowupApplicable ||
|
||||
@@ -2005,6 +2100,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) {
|
||||
pushScopedEntityCandidate(entityCandidates, candidate, groundedFollowupEntity);
|
||||
}
|
||||
pushScopedEntityCandidate(entityCandidates, businessOverviewSeparateCounterpartyCandidate, groundedFollowupEntity);
|
||||
pushScopedEntityCandidate(entityCandidates, normalizedPredecomposeCounterparty, groundedFollowupEntity);
|
||||
pushScopedEntityCandidate(entityCandidates, rawScopedEntityCandidate, groundedFollowupEntity);
|
||||
if (!groundedFollowupEntity) {
|
||||
@@ -2017,6 +2113,20 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
}
|
||||
pushScopedEntityCandidate(entityCandidates, rawEntityCandidate, groundedFollowupEntity);
|
||||
}
|
||||
const businessOverviewSeparateCounterpartyDisplayCandidate = businessOverviewSeparateCounterpartySignal
|
||||
? preferredScopedDisplayName(businessOverviewSeparateCounterpartyCandidate, [
|
||||
groundedFollowupEntity,
|
||||
effectiveFollowupCounterparty,
|
||||
followupSeed.discoveryEntity,
|
||||
normalizedPredecomposeCounterparty,
|
||||
rawScopedEntityCandidate,
|
||||
rawEntityCandidate,
|
||||
...entityCandidates
|
||||
])
|
||||
: null;
|
||||
const businessOverviewSeparateEntityCandidates = businessOverviewSeparateCounterpartyDisplayCandidate
|
||||
? [businessOverviewSeparateCounterpartyDisplayCandidate]
|
||||
: [];
|
||||
if (
|
||||
(rawMetadataSignal || metadataFollowupSeedApplicable) &&
|
||||
!groundedFollowupEntity &&
|
||||
@@ -2107,6 +2217,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
(clarificationLoopStillNeedsPeriod ||
|
||||
businessOverviewSignal ||
|
||||
openScopeValueFlowWithoutResolvedCounterparty ||
|
||||
valueFlowGroundedDocumentFollowupApplicable ||
|
||||
valueFlowGroundedMovementFollowupApplicable ||
|
||||
(valueFlowOrganizationStaysScope && (Boolean(followupSeed.rankingNeed) || bidirectionalValueFlowSignal)))
|
||||
);
|
||||
const suppressNegatedTaxOnlyDateScope = Boolean(businessOverviewSignal && negatedTaxDateScopeOnlySignal);
|
||||
@@ -2138,11 +2250,18 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
(suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(followupSeed.dateScope))
|
||||
? null
|
||||
: followupSeed.dateScope;
|
||||
const businessOverviewRawYearOverridesPredecomposeAsOf = Boolean(
|
||||
businessOverviewSignal &&
|
||||
rawDateScope &&
|
||||
/^\d{4}$/.test(rawDateScope) &&
|
||||
normalizedPredecomposeDateScope &&
|
||||
normalizedPredecomposeDateScope.startsWith(`${rawDateScope}-`)
|
||||
);
|
||||
const explicitDateScope =
|
||||
rawAllTimeScopeSignal
|
||||
? null
|
||||
: normalizedAssistantTurnMeaningDateScope ??
|
||||
normalizedPredecomposeDateScope ??
|
||||
(businessOverviewRawYearOverridesPredecomposeAsOf ? rawDateScope : normalizedPredecomposeDateScope) ??
|
||||
rawDateScope ??
|
||||
normalizedFollowupDateScope;
|
||||
const followupDateScopeApplied = Boolean(
|
||||
@@ -2198,6 +2317,13 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
? followupSeed.rankingNeed
|
||||
: undefined,
|
||||
explicit_entity_candidates: businessOverviewSignal ? [] : entityCandidates,
|
||||
business_overview_separate_entity_candidates: businessOverviewSeparateEntityCandidates,
|
||||
previous_counterparty_value_flow_bundle:
|
||||
businessOverviewSignal && followupSeed.previousBidirectionalValueFlow
|
||||
? followupSeed.previousBidirectionalValueFlow
|
||||
: undefined,
|
||||
previous_counterparty_document_bundle:
|
||||
businessOverviewSignal && followupSeed.previousDocumentSummary ? followupSeed.previousDocumentSummary : undefined,
|
||||
metadata_ambiguity_entity_sets:
|
||||
metadataAmbiguityLaneClarificationApplicable && followupSeed.metadataAmbiguityEntitySets.length > 0
|
||||
? followupSeed.metadataAmbiguityEntitySets
|
||||
@@ -2263,6 +2389,15 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
if ((turnMeaning.explicit_entity_candidates?.length ?? 0) > 0) {
|
||||
cleanTurnMeaning.explicit_entity_candidates = turnMeaning.explicit_entity_candidates;
|
||||
}
|
||||
if ((turnMeaning.business_overview_separate_entity_candidates?.length ?? 0) > 0) {
|
||||
cleanTurnMeaning.business_overview_separate_entity_candidates = turnMeaning.business_overview_separate_entity_candidates;
|
||||
}
|
||||
if (toRecordObject(turnMeaning.previous_counterparty_value_flow_bundle)) {
|
||||
cleanTurnMeaning.previous_counterparty_value_flow_bundle = turnMeaning.previous_counterparty_value_flow_bundle;
|
||||
}
|
||||
if (toRecordObject(turnMeaning.previous_counterparty_document_bundle)) {
|
||||
cleanTurnMeaning.previous_counterparty_document_bundle = turnMeaning.previous_counterparty_document_bundle;
|
||||
}
|
||||
if ((turnMeaning.metadata_ambiguity_entity_sets?.length ?? 0) > 0) {
|
||||
cleanTurnMeaning.metadata_ambiguity_entity_sets = turnMeaning.metadata_ambiguity_entity_sets;
|
||||
}
|
||||
@@ -2478,9 +2613,21 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
if (businessOverviewContinuationSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_from_followup_context");
|
||||
}
|
||||
if (explicitVatSuppressesBusinessOverviewContinuation) {
|
||||
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_suppressed_by_explicit_vat_question");
|
||||
}
|
||||
if (businessOverviewSuppressesFollowupCounterparty) {
|
||||
pushReason(reasonCodes, "mcp_discovery_business_overview_suppressed_stale_counterparty");
|
||||
}
|
||||
if (businessOverviewSeparateCounterpartySignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_business_overview_preserved_explicit_counterparty_summary_scope");
|
||||
}
|
||||
if (businessOverviewSeparateCounterpartyCandidate) {
|
||||
pushReason(reasonCodes, "mcp_discovery_business_overview_counterparty_from_summary_text");
|
||||
}
|
||||
if (businessOverviewRawYearOverridesPredecomposeAsOf) {
|
||||
pushReason(reasonCodes, "mcp_discovery_business_overview_raw_year_overrode_predecompose_as_of_scope");
|
||||
}
|
||||
if (
|
||||
!(valueFlowOrganizationStaysScope && normalizedPredecomposeCounterparty === explicitOrganizationScope) &&
|
||||
normalizedPredecomposeCounterparty
|
||||
@@ -2515,12 +2662,19 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
if (runDiscovery && !hasTurnMeaning) {
|
||||
pushReason(reasonCodes, "mcp_discovery_turn_meaning_missing");
|
||||
}
|
||||
const dataNeedGraphTurnMeaning =
|
||||
businessOverviewSeparateCounterpartySignal && cleanTurnMeaning.explicit_entity_candidates
|
||||
? {
|
||||
...cleanTurnMeaning,
|
||||
explicit_entity_candidates: []
|
||||
}
|
||||
: cleanTurnMeaning;
|
||||
const dataNeedGraph =
|
||||
runDiscovery && hasTurnMeaning
|
||||
? buildAssistantMcpDiscoveryDataNeedGraph({
|
||||
semanticDataNeed,
|
||||
rawUtterance: rawSignalSourceText,
|
||||
turnMeaning: cleanTurnMeaning
|
||||
turnMeaning: dataNeedGraphTurnMeaning
|
||||
})
|
||||
: null;
|
||||
if (dataNeedGraph) {
|
||||
|
||||
@@ -236,6 +236,36 @@ function hasExplicitRecapPromptSignal(samples: string[]): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeMemoryCheckpointSample(value: unknown): string {
|
||||
return String(value ?? "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/ё/g, "е")
|
||||
.replace(/[«»"'`]/g, "")
|
||||
.replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
function hasMemoryCheckpointPromptSignal(samples: string[]): boolean {
|
||||
return samples.some((sample) => {
|
||||
const text = normalizeMemoryCheckpointSample(sample);
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
if (/(?:стартов\w*\s+чек\s+контекст|чек\s+контекста|context\s+check|memory\s+check)/iu.test(text)) {
|
||||
return true;
|
||||
}
|
||||
const hasSelectedStateCue =
|
||||
/(?:выбранн\w*\s+(?:компан|организац|контрагент|объект)|активн\w*\s+(?:компан|организац|контрагент|объект)|selected\s+(?:company|organization|counterparty|object)|active\s+(?:company|organization|counterparty|object))/iu.test(text);
|
||||
const hasDialogStateCue =
|
||||
/(?:в\s+текущ\w*\s+диалог|в\s+этом\s+диалог|в\s+сессии|контекст(?:е|а)?\s+диалог|current\s+(?:dialog|session|conversation))/iu.test(text);
|
||||
const hasHonestyCue =
|
||||
/(?:не\s+выдумывай\s+памят|не\s+придумывай\s+памят|скажи\s+честно|если\s+нет|no\s+fabricat|do\s+not\s+invent\s+memory)/iu.test(text);
|
||||
const asksCurrentSelection =
|
||||
/(?:есть\s+ли\s+уже|есть\s+ли\s+сейчас|что\s+выбрано|кто\s+выбран|какая\s+компан\w*\s+выбран)/iu.test(text);
|
||||
return (hasSelectedStateCue && hasDialogStateCue) || (hasDialogStateCue && hasHonestyCue) || (asksCurrentSelection && hasHonestyCue);
|
||||
});
|
||||
}
|
||||
|
||||
export function buildInventoryHistoryCapabilityFollowupReply(input: {
|
||||
organization: string | null;
|
||||
addressDebug: Record<string, unknown> | null;
|
||||
@@ -713,10 +743,32 @@ function extractBuyerFromSaleTraceAnswer(
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractRequestedMemorySubject(userMessage: unknown): string | null {
|
||||
const text = String(userMessage ?? "").trim();
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
const patterns = [
|
||||
/памят[ьи]\s+про\s+([^.;!?]+)/iu,
|
||||
/memory\s+about\s+([^.;!?]+)/iu
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
const match = text.match(pattern);
|
||||
const subject = match?.[1]
|
||||
? match[1].replace(/[«»"'`]/g, "").replace(/\s+/g, " ").trim()
|
||||
: "";
|
||||
if (subject.length >= 2 && subject.length <= 80) {
|
||||
return subject;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function buildAddressMemoryRecapReply(input: {
|
||||
organization: string | null;
|
||||
addressDebug: Record<string, unknown> | null;
|
||||
sessionItems?: unknown[];
|
||||
userMessage?: unknown;
|
||||
toNonEmptyString: (value: unknown) => string | null;
|
||||
}): string {
|
||||
const contextFacts = resolveAddressDebugContextFacts(input.addressDebug, input.toNonEmptyString);
|
||||
@@ -782,7 +834,14 @@ export function buildAddressMemoryRecapReply(input: {
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
return "Да, помню предыдущий адресный контур. Могу кратко напомнить, что мы уже подтвердили, или сразу продолжить следующий шаг.";
|
||||
const requestedMemorySubject = extractRequestedMemorySubject(input.userMessage);
|
||||
const subjectLine = requestedMemorySubject
|
||||
? ` Память про «${requestedMemorySubject}» в этом диалоге не подтверждена.`
|
||||
: " Память про конкретную компанию или контрагента в этом диалоге не подтверждена.";
|
||||
return [
|
||||
`Коротко: в текущем диалоге я не вижу выбранной компании, контрагента или позиции.${subjectLine}`,
|
||||
"Чтобы продолжить без выдуманной памяти, назови компанию, контрагента или объект, и я начну новый проверенный контур."
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
export function buildBroadBusinessEvaluationReply(input: {
|
||||
@@ -1055,6 +1114,7 @@ export function createAssistantMemoryRecapPolicy(
|
||||
deps.hasConversationMemoryRecallFollowupSignal
|
||||
);
|
||||
const explicitRecapPromptSignal = hasExplicitRecapPromptSignal(samples);
|
||||
const memoryCheckpointPromptSignal = hasMemoryCheckpointPromptSignal(samples);
|
||||
return {
|
||||
contextualHistoricalCapabilityFollowupDetected: Boolean(
|
||||
input.capabilityMetaQuery &&
|
||||
@@ -1067,9 +1127,10 @@ export function createAssistantMemoryRecapPolicy(
|
||||
!input.dataScopeMetaQuery &&
|
||||
!input.capabilityMetaQuery &&
|
||||
!input.aggregateBusinessAnalyticsSignal &&
|
||||
memoryRecapSignal &&
|
||||
(explicitRecapPromptSignal || (!input.dataRetrievalSignal && !input.strongDataSignal)) &&
|
||||
continuity.hasGroundedAddressContext
|
||||
(memoryCheckpointPromptSignal ||
|
||||
(memoryRecapSignal &&
|
||||
(explicitRecapPromptSignal || (!input.dataRetrievalSignal && !input.strongDataSignal)) &&
|
||||
continuity.hasGroundedAddressContext))
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -198,6 +198,16 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage = null) {
|
||||
const samples = [userMessage, alternateMessage].map((item) => normalizeFollowupText(item)).filter(Boolean);
|
||||
return samples.some(
|
||||
(sample) =>
|
||||
/(?:итог|summary|резюм|вывод|что\s+(?:мы\s+)?подтверд|что\s+понят|что\s+можно|что\s+нельзя|собери\s+коротк)/iu.test(
|
||||
sample
|
||||
) && /(?:контрагент|группа\s+свк|свк|отдельн)/iu.test(sample)
|
||||
);
|
||||
}
|
||||
|
||||
function parseDmyDateToIso(value) {
|
||||
const match = String(value ?? "").trim().match(/^(\d{2})\.(\d{2})\.(\d{4})$/);
|
||||
if (!match) {
|
||||
@@ -341,6 +351,61 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function readMcpDiscoveryBidirectionalValueFlow(debug) {
|
||||
const entryPoint = debug?.assistant_mcp_discovery_entry_point_v1;
|
||||
const flow = entryPoint?.bridge?.pilot?.derived_bidirectional_value_flow;
|
||||
if (!flow || typeof flow !== "object" || Array.isArray(flow)) {
|
||||
return null;
|
||||
}
|
||||
return flow;
|
||||
}
|
||||
|
||||
function readCounterpartyDocumentSummaryFromItem(item) {
|
||||
const text = deps.toNonEmptyString(item?.text);
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
const firstLine = text.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "";
|
||||
const match = firstLine.match(/Контрагент:\s*([^.\n]+)\.\s*Найдено документов:\s*(\d+)/iu);
|
||||
if (!match?.[1] || !match?.[2]) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
counterparty: deps.toNonEmptyString(match[1]),
|
||||
document_count: Number(match[2]),
|
||||
direct_answer: firstLine
|
||||
};
|
||||
}
|
||||
|
||||
function findRecentDiscoveryValueFlowBundle(items) {
|
||||
for (let index = Array.isArray(items) ? items.length - 1 : -1; index >= 0; index -= 1) {
|
||||
const item = items[index];
|
||||
const debug = item?.debug;
|
||||
if (!item || item.role !== "assistant" || !debug || typeof debug !== "object") {
|
||||
continue;
|
||||
}
|
||||
const flow = readMcpDiscoveryBidirectionalValueFlow(debug);
|
||||
if (flow) {
|
||||
return flow;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findRecentCounterpartyDocumentBundle(items) {
|
||||
for (let index = Array.isArray(items) ? items.length - 1 : -1; index >= 0; index -= 1) {
|
||||
const item = items[index];
|
||||
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
|
||||
continue;
|
||||
}
|
||||
const summary = readCounterpartyDocumentSummaryFromItem(item);
|
||||
if (summary) {
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasInventoryItemFocusHint) {
|
||||
if (
|
||||
sourceIntentHint !== "inventory_purchase_provenance_for_item" &&
|
||||
@@ -530,7 +595,10 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
llmPreDecomposeMeta
|
||||
})
|
||||
: null;
|
||||
if (assistantTurnMeaning?.stale_replay_forbidden === true) {
|
||||
if (
|
||||
assistantTurnMeaning?.stale_replay_forbidden === true &&
|
||||
!hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const latestAddressItem = deps.findLastAddressAssistantItem(items);
|
||||
@@ -638,18 +706,21 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
|
||||
? hasShortValueFlowRetargetCue(String(alternateMessage ?? ""))
|
||||
: false;
|
||||
const explicitSummaryBundleReuseSignal = hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage);
|
||||
let hasPrimaryFollowupSignal =
|
||||
deps.hasAddressFollowupContextSignal(userMessage) ||
|
||||
Boolean(debtRoleSwapPrimary) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryPurchaseDateVatBridge;
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
explicitSummaryBundleReuseSignal;
|
||||
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||
Boolean(debtRoleSwapAlternate) ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
explicitSummaryBundleReuseSignal
|
||||
: false;
|
||||
const hasPrimaryIndexReferenceSignal = deps.extractDisplayedEntityIndexMention(userMessage) !== null;
|
||||
const hasAlternateIndexReferenceSignal = deps.toNonEmptyString(alternateMessage)
|
||||
@@ -698,6 +769,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
hasInventoryRootRestatementPrimary ||
|
||||
hasInventoryRootRestatementAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
Boolean(debtRoleSwapIntent) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
@@ -718,6 +790,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
hasInventoryRootRestatementPrimary ||
|
||||
hasInventoryRootRestatementAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
Boolean(debtRoleSwapIntent) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
@@ -753,7 +826,8 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
!hasImplicitContinuationSignal &&
|
||||
!hasSuggestedIntentPivotSignal &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
!hasIndexReferenceSignal
|
||||
!hasIndexReferenceSignal &&
|
||||
!explicitSummaryBundleReuseSignal
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -769,7 +843,8 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
!hasImplicitContinuationSignal &&
|
||||
!hasSuggestedIntentPivotSignal &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
!hasIndexReferenceSignal
|
||||
!hasIndexReferenceSignal &&
|
||||
!explicitSummaryBundleReuseSignal
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -848,6 +923,9 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
carryoverSourceDebug,
|
||||
deps.toNonEmptyString
|
||||
);
|
||||
const sourceDiscoveryBidirectionalValueFlow =
|
||||
readMcpDiscoveryBidirectionalValueFlow(carryoverSourceDebug) ?? findRecentDiscoveryValueFlowBundle(items);
|
||||
const sourceDiscoveryDocumentSummary = findRecentCounterpartyDocumentBundle(items);
|
||||
const llmExplicitIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
|
||||
const llmSelectedObjectScopeDetected =
|
||||
llmPreDecomposeMeta?.predecomposeContract?.semantics?.selected_object_scope_detected === true;
|
||||
@@ -951,6 +1029,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
shortValueFlowRetargetPrimary ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
hasInventoryRootTemporalFollowupPrimary;
|
||||
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||
@@ -959,6 +1038,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
shortValueFlowRetargetAlternate ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
hasInventoryRootTemporalFollowupAlternate
|
||||
: false;
|
||||
hasStrongFollowupReference =
|
||||
@@ -972,6 +1052,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
hasInventoryRootTemporalFollowupPrimary ||
|
||||
hasInventoryRootTemporalFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
Boolean(debtRoleSwapIntent) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
@@ -1220,6 +1301,8 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
previous_discovery_metadata_ambiguity_detected: sourceDiscoveryMetadataAmbiguityDetected || undefined,
|
||||
previous_discovery_metadata_ambiguity_entity_sets:
|
||||
sourceDiscoveryMetadataAmbiguityEntitySets.length > 0 ? sourceDiscoveryMetadataAmbiguityEntitySets : undefined,
|
||||
previous_discovery_bidirectional_value_flow: sourceDiscoveryBidirectionalValueFlow ?? undefined,
|
||||
previous_discovery_document_summary: sourceDiscoveryDocumentSummary ?? undefined,
|
||||
resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined,
|
||||
root_context_only: rootScopedPivot || undefined,
|
||||
root_intent: shouldAttachInventoryRootFrame ? inventoryRootFrame?.intent ?? undefined : undefined,
|
||||
|
||||
@@ -125,21 +125,24 @@ function coverageStatusFrom(
|
||||
groundingStatus: AssistantGroundingStatus
|
||||
): AssistantCoverageStatus {
|
||||
const explicitCoverageEvidence = toAddressCoverageEvidenceContract(debug.address_coverage_evidence_v1);
|
||||
if (truthGateStatus === "full_confirmed") {
|
||||
return "full";
|
||||
}
|
||||
if (truthGateStatus === "partial_supported" || truthGateStatus === "limited_temporal_or_contextual") {
|
||||
return "partial";
|
||||
}
|
||||
if (truthGateStatus.startsWith("blocked")) {
|
||||
return "blocked";
|
||||
}
|
||||
if (toStringList(debug.missing_required_filters).length > 0 || groundingStatus === "route_mismatch_blocked" || groundingStatus === "no_grounded_answer") {
|
||||
return "blocked";
|
||||
}
|
||||
if (truthGateStatus === "partial_supported" || truthGateStatus === "limited_temporal_or_contextual") {
|
||||
return "partial";
|
||||
}
|
||||
if (explicitCoverageEvidence) {
|
||||
return explicitCoverageEvidence.coverage_status;
|
||||
}
|
||||
if (debug.balance_confirmed === false || toNonEmptyString(debug.result_mode) === "heuristic_candidates") {
|
||||
return "partial";
|
||||
}
|
||||
if (truthGateStatus === "full_confirmed") {
|
||||
return "full";
|
||||
}
|
||||
|
||||
const coverageReport = toRecordObject(input.coverageReport) ?? toRecordObject(debug.coverage_report);
|
||||
if (coverageReport) {
|
||||
@@ -176,10 +179,16 @@ function truthModeFrom(input: {
|
||||
if (input.truthGateStatus === "blocked_missing_anchor" || toStringList(input.debug.missing_required_filters).length > 0) {
|
||||
return "clarification_required";
|
||||
}
|
||||
if (input.truthGateStatus === "full_confirmed" || (input.coverageStatus === "full" && input.groundingStatus === "grounded")) {
|
||||
if (input.coverageStatus === "partial") {
|
||||
return "limited";
|
||||
}
|
||||
if (input.truthGateStatus === "full_confirmed" && input.coverageStatus === "full") {
|
||||
return "confirmed";
|
||||
}
|
||||
if (input.truthGateStatus === "partial_supported" || input.truthGateStatus === "limited_temporal_or_contextual" || input.coverageStatus === "partial") {
|
||||
if (input.coverageStatus === "full" && input.groundingStatus === "grounded") {
|
||||
return "confirmed";
|
||||
}
|
||||
if (input.truthGateStatus === "partial_supported" || input.truthGateStatus === "limited_temporal_or_contextual") {
|
||||
return "limited";
|
||||
}
|
||||
return "unsupported";
|
||||
@@ -199,6 +208,9 @@ function evidenceGradeFrom(
|
||||
if (isEvidenceGrade(explicit)) {
|
||||
return explicit;
|
||||
}
|
||||
if (debug.balance_confirmed === false || toNonEmptyString(debug.result_mode) === "heuristic_candidates") {
|
||||
return coverageStatus === "partial" ? "medium" : "weak";
|
||||
}
|
||||
if (coverageStatus === "blocked") {
|
||||
return "none";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user