Post-F: закрепить open-scope и bidirectional value-flow integrity
This commit is contained in:
parent
9ed5435866
commit
bba4717dbe
|
|
@ -1554,6 +1554,17 @@ function repairLikelyUtf8Mojibake(text) {
|
|||
function unicodeBridgeResolution(intent, confidence, reason) {
|
||||
return { intent, confidence, reasons: [reason] };
|
||||
}
|
||||
function hasBidirectionalValueFlowComparisonSignal(text) {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const hasIncomingCue = /(?:\u0432\u0445\u043e\u0434\u044f\u0449|\u043f\u043e\u0441\u0442\u0443\u043f|\u043f\u043e\u043b\u0443\u0447|inflow|incoming)/iu.test(normalized);
|
||||
const hasOutgoingCue = /(?:\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0441\u043f\u0438\u0441\u0430\u043d|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442\u0438\u043b|\u043e\u043f\u043b\u0430\u0442|outflow|outgoing|payout)/iu.test(normalized);
|
||||
const hasComparisonCue = /(?:\u0431\u043e\u043b\u044c\u0448|\u043c\u0435\u043d\u044c\u0448|\u0441\u0440\u0430\u0432|\u0438\u043b\u0438|\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|vs|versus)/iu.test(normalized);
|
||||
const hasValueFlowCue = /(?:\u0434\u0435\u043d\u044c\u0433|\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u0435\u0436|\u043f\u043e\u0442\u043e\u043a|\u043e\u0431\u043e\u0440\u043e\u0442|money|cash|flow)/iu.test(normalized);
|
||||
return hasIncomingCue && hasOutgoingCue && hasComparisonCue && hasValueFlowCue;
|
||||
}
|
||||
function resolveUnicodeAddressIntentBridge(text) {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
|
|
@ -1592,6 +1603,9 @@ function resolveUnicodeAddressIntentBridge(text) {
|
|||
]).has(byAnchorToken);
|
||||
const hasMoneyCue = /(?:деньг|денег|выручк|доход|оборот|заработ|прин[её]с|чек|ликвидн|revenue|turnover|money)/iu.test(normalized);
|
||||
const hasRankingCue = /(?:топ|ранк|сам(?:ый|ая|ое|ые)|больше\s+всего|наибольш|крупн|жирн|max|top|rank)/iu.test(normalized);
|
||||
if (hasBidirectionalValueFlowComparisonSignal(normalized)) {
|
||||
return unicodeBridgeResolution("unknown", "high", "unicode_bidirectional_value_flow_deferred_to_discovery");
|
||||
}
|
||||
if (/(?:за\s+какие\s+годы|диапазон\s+лет|покрыт(?:ие|ый)\s+период|какой\s+год.*актив|какой\s+месяц.*актив|год.*пассив|месяц.*пассив|минимальн.*док|минимальн.*операц|месяц[\s-]*пик|profile\s+period|top\s*year|top\s*month)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution("period_coverage_profile", "high", "unicode_period_coverage_bridge_signal_detected");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ const resolveStage_1 = require("./address_runtime/resolveStage");
|
|||
const composeStage_1 = require("./address_runtime/composeStage");
|
||||
const addressCapabilityPolicy_1 = require("./addressCapabilityPolicy");
|
||||
const addressRouteExpectations_1 = require("./addressRouteExpectations");
|
||||
const addressTextRepair_1 = require("./addressTextRepair");
|
||||
const assistantOrganizationMatcher_1 = require("./assistantOrganizationMatcher");
|
||||
const addressCoverageEvidencePolicy_1 = require("./addressCoverageEvidencePolicy");
|
||||
const addressTruthGatePolicy_1 = require("./addressTruthGatePolicy");
|
||||
|
|
@ -1617,6 +1618,21 @@ function isOrganizationScopedInventoryIntent(intent) {
|
|||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date");
|
||||
}
|
||||
function isOrganizationScopedValueFlowIntent(intent) {
|
||||
return intent === "customer_revenue_and_payments" || intent === "supplier_payouts_profile";
|
||||
}
|
||||
function hasExplicitSingleOrganizationValueFlowScopeRequest(userMessage) {
|
||||
const raw = String(userMessage ?? "").toLowerCase();
|
||||
const repaired = (0, addressTextRepair_1.repairAddressMojibakeText)(raw).toLowerCase();
|
||||
const normalized = `${raw} ${repaired}`;
|
||||
const hasOrganizationCue = /(?:организац|компани|фирм|контур|organization|company)/iu.test(normalized);
|
||||
if (!hasOrganizationCue) {
|
||||
return false;
|
||||
}
|
||||
const hasSingleOrganizationCue = /(?:по\s+одн(?:ой|у)\s+(?:организац|компани|фирм)|одн(?:ой|у)\s+(?:организац|компани|фирм)|one\s+(?:organization|company))/iu.test(normalized);
|
||||
const hasClarificationCue = /(?:если[^.?!]*(?:нужн|надо|потреб)[^.?!]*(?:организац|компани|фирм)|(?:уточн|спрос)[^.?!]*(?:организац|компани|фирм|ее|её))/iu.test(normalized);
|
||||
return hasSingleOrganizationCue || hasClarificationCue;
|
||||
}
|
||||
function shouldDeferInventoryOrganizationClarification(intent, filters, semanticFrame) {
|
||||
if (!isOrganizationScopedInventoryIntent(intent)) {
|
||||
return false;
|
||||
|
|
@ -2804,6 +2820,27 @@ class AddressQueryService {
|
|||
});
|
||||
const knownOrganizations = (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)(options.knownOrganizations ?? []);
|
||||
const activeOrganization = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.activeOrganization ?? null);
|
||||
if (isOrganizationScopedValueFlowIntent(intent.intent) &&
|
||||
hasExplicitSingleOrganizationValueFlowScopeRequest(userMessage) &&
|
||||
!resolvedOrganizationFromMessage) {
|
||||
const clarificationFilters = { ...filters.extracted_filters };
|
||||
delete clarificationFilters.organization;
|
||||
return buildOrganizationClarificationExecutionResult({
|
||||
mode,
|
||||
shape,
|
||||
intent,
|
||||
filters: clarificationFilters,
|
||||
organizations: knownOrganizations,
|
||||
reasons: [...baseReasons, "organization_clarification_required_from_explicit_value_flow_scope"],
|
||||
semanticFrame,
|
||||
capabilityAudit: buildCapabilityAudit(intent.intent),
|
||||
shadowRouteAudit: buildShadowRouteAudit({
|
||||
intent: intent.intent,
|
||||
requestedResultMode: (0, addressCoverageEvidencePolicy_1.resolveAddressRequestedResultMode)(intent.intent, filters.extracted_filters, semanticFrame) ?? undefined,
|
||||
filters: filters.extracted_filters
|
||||
})
|
||||
});
|
||||
}
|
||||
if (isOrganizationScopedInventoryIntent(intent.intent) &&
|
||||
!toNonEmptyFilterValue(filters.extracted_filters.organization) &&
|
||||
!activeOrganization &&
|
||||
|
|
|
|||
|
|
@ -1954,6 +1954,31 @@ function unicodeBridgeResolution(
|
|||
return { intent, confidence, reasons: [reason] };
|
||||
}
|
||||
|
||||
function hasBidirectionalValueFlowComparisonSignal(text: string): boolean {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasIncomingCue = /(?:\u0432\u0445\u043e\u0434\u044f\u0449|\u043f\u043e\u0441\u0442\u0443\u043f|\u043f\u043e\u043b\u0443\u0447|inflow|incoming)/iu.test(
|
||||
normalized
|
||||
);
|
||||
const hasOutgoingCue =
|
||||
/(?:\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0441\u043f\u0438\u0441\u0430\u043d|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442\u0438\u043b|\u043e\u043f\u043b\u0430\u0442|outflow|outgoing|payout)/iu.test(
|
||||
normalized
|
||||
);
|
||||
const hasComparisonCue =
|
||||
/(?:\u0431\u043e\u043b\u044c\u0448|\u043c\u0435\u043d\u044c\u0448|\u0441\u0440\u0430\u0432|\u0438\u043b\u0438|\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|vs|versus)/iu.test(
|
||||
normalized
|
||||
);
|
||||
const hasValueFlowCue =
|
||||
/(?:\u0434\u0435\u043d\u044c\u0433|\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u0435\u0436|\u043f\u043e\u0442\u043e\u043a|\u043e\u0431\u043e\u0440\u043e\u0442|money|cash|flow)/iu.test(
|
||||
normalized
|
||||
);
|
||||
|
||||
return hasIncomingCue && hasOutgoingCue && hasComparisonCue && hasValueFlowCue;
|
||||
}
|
||||
|
||||
function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolution | null {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
|
|
@ -2006,6 +2031,14 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
|
|||
normalized
|
||||
);
|
||||
|
||||
if (hasBidirectionalValueFlowComparisonSignal(normalized)) {
|
||||
return unicodeBridgeResolution(
|
||||
"unknown",
|
||||
"high",
|
||||
"unicode_bidirectional_value_flow_deferred_to_discovery"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
/(?:за\s+какие\s+годы|диапазон\s+лет|покрыт(?:ие|ый)\s+период|какой\s+год.*актив|какой\s+месяц.*актив|год.*пассив|месяц.*пассив|минимальн.*док|минимальн.*операц|месяц[\s-]*пик|profile\s+period|top\s*year|top\s*month)/iu.test(
|
||||
normalized
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ import {
|
|||
resolveShadowRouteIntent
|
||||
} from "./addressCapabilityPolicy";
|
||||
import { evaluateAddressRouteExpectation, type AddressRouteExpectationAudit } from "./addressRouteExpectations";
|
||||
import { repairAddressMojibakeText } from "./addressTextRepair";
|
||||
import {
|
||||
mergeKnownOrganizations,
|
||||
normalizeOrganizationScopeSearchText,
|
||||
|
|
@ -1994,6 +1995,31 @@ function isOrganizationScopedInventoryIntent(intent: AddressIntent): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
function isOrganizationScopedValueFlowIntent(intent: AddressIntent): boolean {
|
||||
return intent === "customer_revenue_and_payments" || intent === "supplier_payouts_profile";
|
||||
}
|
||||
|
||||
function hasExplicitSingleOrganizationValueFlowScopeRequest(userMessage: string): boolean {
|
||||
const raw = String(userMessage ?? "").toLowerCase();
|
||||
const repaired = repairAddressMojibakeText(raw).toLowerCase();
|
||||
const normalized = `${raw} ${repaired}`;
|
||||
const hasOrganizationCue = /(?:организац|компани|фирм|контур|organization|company)/iu.test(normalized);
|
||||
if (!hasOrganizationCue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasSingleOrganizationCue =
|
||||
/(?:по\s+одн(?:ой|у)\s+(?:организац|компани|фирм)|одн(?:ой|у)\s+(?:организац|компани|фирм)|one\s+(?:organization|company))/iu.test(
|
||||
normalized
|
||||
);
|
||||
const hasClarificationCue =
|
||||
/(?:если[^.?!]*(?:нужн|надо|потреб)[^.?!]*(?:организац|компани|фирм)|(?:уточн|спрос)[^.?!]*(?:организац|компани|фирм|ее|её))/iu.test(
|
||||
normalized
|
||||
);
|
||||
|
||||
return hasSingleOrganizationCue || hasClarificationCue;
|
||||
}
|
||||
|
||||
function shouldDeferInventoryOrganizationClarification(
|
||||
intent: AddressIntent,
|
||||
filters: AddressFilterSet,
|
||||
|
|
@ -3480,6 +3506,29 @@ export class AddressQueryService {
|
|||
});
|
||||
const knownOrganizations = mergeKnownOrganizations(options.knownOrganizations ?? []);
|
||||
const activeOrganization = normalizeOrganizationScopeValue(options.activeOrganization ?? null);
|
||||
if (
|
||||
isOrganizationScopedValueFlowIntent(intent.intent) &&
|
||||
hasExplicitSingleOrganizationValueFlowScopeRequest(userMessage) &&
|
||||
!resolvedOrganizationFromMessage
|
||||
) {
|
||||
const clarificationFilters: AddressFilterSet = { ...filters.extracted_filters };
|
||||
delete clarificationFilters.organization;
|
||||
return buildOrganizationClarificationExecutionResult({
|
||||
mode,
|
||||
shape,
|
||||
intent,
|
||||
filters: clarificationFilters,
|
||||
organizations: knownOrganizations,
|
||||
reasons: [...baseReasons, "organization_clarification_required_from_explicit_value_flow_scope"],
|
||||
semanticFrame,
|
||||
capabilityAudit: buildCapabilityAudit(intent.intent),
|
||||
shadowRouteAudit: buildShadowRouteAudit({
|
||||
intent: intent.intent,
|
||||
requestedResultMode: resolveAddressRequestedResultMode(intent.intent, filters.extracted_filters, semanticFrame) ?? undefined,
|
||||
filters: filters.extracted_filters
|
||||
})
|
||||
});
|
||||
}
|
||||
if (
|
||||
isOrganizationScopedInventoryIntent(intent.intent) &&
|
||||
!toNonEmptyFilterValue(filters.extracted_filters.organization) &&
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { resolveAddressIntent } from "../src/services/addressIntentResolver";
|
||||
|
||||
describe("address intent resolver bidirectional value-flow arbitration", () => {
|
||||
it("keeps incoming-vs-outgoing comparison out of one-sided payout routes", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"\u0447\u0442\u043e \u043f\u043e \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441 \u0431\u043e\u043b\u044c\u0448\u0435 \u0432 2020 \u0433\u043e\u0434\u0443: \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0438\u043b\u0438 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0434\u0435\u043d\u044c\u0433\u0438?"
|
||||
);
|
||||
|
||||
expect(result.intent).toBe("unknown");
|
||||
expect(result.reasons).toContain("unicode_bidirectional_value_flow_deferred_to_discovery");
|
||||
});
|
||||
});
|
||||
|
|
@ -4734,6 +4734,25 @@ describe("address decompose stage follow-up carryover", () => {
|
|||
expect(result?.baseReasons).not.toContain("open_items_from_followup_context");
|
||||
});
|
||||
|
||||
it("asks for organization when an open value-flow total explicitly requests one-organization scope", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(
|
||||
"\u0421 \u0416\u0443\u043a\u043e\u0432\u043a\u043e\u0439 \u0437\u0430\u043a\u043e\u043d\u0447\u0438\u043b\u0438. \u0422\u0435\u043f\u0435\u0440\u044c \u043d\u0443\u0436\u043d\u0430 \u0434\u0440\u0443\u0433\u0430\u044f \u0437\u0430\u0434\u0430\u0447\u0430: \u0431\u044b\u0441\u0442\u0440\u044b\u0439 \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u0441\u0440\u0435\u0437 \u043f\u043e \u043e\u0434\u043d\u043e\u0439 \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u0438. \u0415\u0441\u043b\u0438 \u0434\u043b\u044f \u043e\u0442\u0432\u0435\u0442\u0430 \u043d\u0443\u0436\u043d\u0430 \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044f, \u043f\u0440\u043e\u0441\u0442\u043e \u0443\u0442\u043e\u0447\u043d\u0438 \u0435\u0435. \u0421\u043a\u043e\u043b\u044c\u043a\u043e \u0432\u043e\u043e\u0431\u0449\u0435 \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0434\u0435\u043d\u0435\u0433 \u0431\u044b\u043b\u043e \u0437\u0430 2020 \u0433\u043e\u0434?",
|
||||
{
|
||||
knownOrganizations: [
|
||||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
|
||||
"\u041e\u041e\u041e \u0420\u043e\u043c\u0430\u0448\u043a\u0430"
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
expect(result?.reply_type).toBe("partial_coverage");
|
||||
expect(result?.debug.missing_required_filters).toContain("organization");
|
||||
expect(result?.debug.mcp_call_status).toBe("skipped");
|
||||
expect(result?.debug.reasons).toContain("organization_clarification_required_from_explicit_value_flow_scope");
|
||||
expect(result?.reply_text).toMatch(/организац/iu);
|
||||
});
|
||||
|
||||
it("keeps balance family in follow-up when user gives compact account token", () => {
|
||||
const result = runAddressDecomposeStage("вернись на 2020-12-31 по 60", {
|
||||
previous_intent: "documents_forming_balance",
|
||||
|
|
|
|||
Loading…
Reference in New Issue