Закрепить маржу и контекст организации в phase106
This commit is contained in:
parent
d0d7962b65
commit
e4eac8e465
|
|
@ -1694,7 +1694,8 @@ function hasNomenclatureMarginRankingSignal(text) {
|
||||||
const hasMarginCue = /(?:прибыл|марж|рентаб|наценк|себестоим|выручк|profit|margin|profitability|gross\s+spread|cogs)/iu.test(normalized);
|
const hasMarginCue = /(?:прибыл|марж|рентаб|наценк|себестоим|выручк|profit|margin|profitability|gross\s+spread|cogs)/iu.test(normalized);
|
||||||
const hasRankingCue = /(?:высок|низк|топ|сам(?:ая|ый|ое|ые|ой|ого|ому|ым|ых|ую)|больш|меньш|ранж|рейтинг|max|min|high|low|top|rank|best|worst)/iu.test(normalized);
|
const hasRankingCue = /(?:высок|низк|топ|сам(?:ая|ый|ое|ые|ой|ого|ому|ым|ых|ую)|больш|меньш|ранж|рейтинг|max|min|high|low|top|rank|best|worst)/iu.test(normalized);
|
||||||
const hasCalculationCue = /(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(normalized);
|
const hasCalculationCue = /(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(normalized);
|
||||||
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue);
|
const hasInspectionCue = /(?:провер\p{L}*|посмотр\p{L}*|разобра\p{L}*|какую|какие|что\s+можно|можно\s+(?:провер|посчит|рассчит|понять)|check|inspect|verify|estimate)/iu.test(normalized);
|
||||||
|
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue || hasInspectionCue);
|
||||||
}
|
}
|
||||||
function hasVatPeriodInspectionBridgeSignal(text) {
|
function hasVatPeriodInspectionBridgeSignal(text) {
|
||||||
const normalized = String(text ?? "").trim().toLowerCase();
|
const normalized = String(text ?? "").trim().toLowerCase();
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,8 @@ function hasInventoryMarginRankingSignal(text) {
|
||||||
const hasMarginCue = /(?:прибыл|марж|рентаб|наценк|себестоим|выручк|profit|margin|profitability|gross\s+spread|cogs)/iu.test(normalized);
|
const hasMarginCue = /(?:прибыл|марж|рентаб|наценк|себестоим|выручк|profit|margin|profitability|gross\s+spread|cogs)/iu.test(normalized);
|
||||||
const hasRankingCue = /(?:высок|низк|топ|сам(?:ая|ый|ое|ые|ой|ого|ому|ым|ых|ую)|больш|меньш|ранж|рейтинг|max|min|high|low|top|rank|best|worst)/iu.test(normalized);
|
const hasRankingCue = /(?:высок|низк|топ|сам(?:ая|ый|ое|ые|ой|ого|ому|ым|ых|ую)|больш|меньш|ранж|рейтинг|max|min|high|low|top|rank|best|worst)/iu.test(normalized);
|
||||||
const hasCalculationCue = /(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(normalized);
|
const hasCalculationCue = /(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(normalized);
|
||||||
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue);
|
const hasInspectionCue = /(?:провер\p{L}*|посмотр\p{L}*|разобра\p{L}*|какую|какие|что\s+можно|можно\s+(?:провер|посчит|рассчит|понять)|check|inspect|verify|estimate)/iu.test(normalized);
|
||||||
|
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue || hasInspectionCue);
|
||||||
}
|
}
|
||||||
function hasInventoryOnHandSignal(text) {
|
function hasInventoryOnHandSignal(text) {
|
||||||
const hasColloquialStockSnapshotCue = /(?:что|ч[еёо])\s+(?:у\s+нас\s+)?на\s+склад(?:е|у|ом|ах)(?=$|[\s,.;:!?])/iu.test(text);
|
const hasColloquialStockSnapshotCue = /(?:что|ч[еёо])\s+(?:у\s+нас\s+)?на\s+склад(?:е|у|ом|ах)(?=$|[\s,.;:!?])/iu.test(text);
|
||||||
|
|
|
||||||
|
|
@ -703,6 +703,7 @@ function businessOverviewOrganizationScopeLabel(value) {
|
||||||
}
|
}
|
||||||
const comparable = compactComparable(text);
|
const comparable = compactComparable(text);
|
||||||
if (/^(?:с|без)\s+разбивк/.test(comparable) ||
|
if (/^(?:с|без)\s+разбивк/.test(comparable) ||
|
||||||
|
/(?:без\s+(?:предположен\p{L}*|допущен\p{L}*|гадан\p{L}*|фантази\p{L}*|догад\p{L}*)|without\s+(?:guesses|assumptions|guessing))/iu.test(comparable) ||
|
||||||
/\b(?:входящ|исходящ|нетто|топ|контрагент|платеж|поступлен)\b/.test(comparable)) {
|
/\b(?:входящ|исходящ|нетто|топ|контрагент|платеж|поступлен)\b/.test(comparable)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,10 @@ function isReferentialOrganizationPlaceholder(value) {
|
||||||
if (isNegativeCounterpartyScopePlaceholder(value)) {
|
if (isNegativeCounterpartyScopePlaceholder(value)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
const text = compactLower((0, addressTextRepair_1.repairAddressMojibakeText)(value));
|
||||||
|
if (/(?:без\s+(?:предположен\p{L}*|допущен\p{L}*|гадан\p{L}*|фантази\p{L}*|догад\p{L}*)|without\s+(?:guesses|assumptions|guessing))/iu.test(text)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return new Set([
|
return new Set([
|
||||||
"эта организация",
|
"эта организация",
|
||||||
"этой организации",
|
"этой организации",
|
||||||
|
|
@ -925,7 +929,7 @@ function normalizeLooseOrganizationAlias(value) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (isNegativeCounterpartyScopePlaceholder(text)) {
|
if (isReferentialOrganizationPlaceholder(text)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const normalized = text
|
const normalized = text
|
||||||
|
|
|
||||||
|
|
@ -2200,7 +2200,11 @@ function hasNomenclatureMarginRankingSignal(text: string): boolean {
|
||||||
/(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(
|
/(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(
|
||||||
normalized
|
normalized
|
||||||
);
|
);
|
||||||
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue);
|
const hasInspectionCue =
|
||||||
|
/(?:провер\p{L}*|посмотр\p{L}*|разобра\p{L}*|какую|какие|что\s+можно|можно\s+(?:провер|посчит|рассчит|понять)|check|inspect|verify|estimate)/iu.test(
|
||||||
|
normalized
|
||||||
|
);
|
||||||
|
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue || hasInspectionCue);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasVatPeriodInspectionBridgeSignal(text: string): boolean {
|
function hasVatPeriodInspectionBridgeSignal(text: string): boolean {
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,11 @@ function hasInventoryMarginRankingSignal(text: string): boolean {
|
||||||
/(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(
|
/(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(
|
||||||
normalized
|
normalized
|
||||||
);
|
);
|
||||||
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue);
|
const hasInspectionCue =
|
||||||
|
/(?:провер\p{L}*|посмотр\p{L}*|разобра\p{L}*|какую|какие|что\s+можно|можно\s+(?:провер|посчит|рассчит|понять)|check|inspect|verify|estimate)/iu.test(
|
||||||
|
normalized
|
||||||
|
);
|
||||||
|
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue || hasInspectionCue);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasInventoryOnHandSignal(text: string): boolean {
|
function hasInventoryOnHandSignal(text: string): boolean {
|
||||||
|
|
|
||||||
|
|
@ -838,6 +838,9 @@ function businessOverviewOrganizationScopeLabel(value: unknown): string | null {
|
||||||
const comparable = compactComparable(text);
|
const comparable = compactComparable(text);
|
||||||
if (
|
if (
|
||||||
/^(?:с|без)\s+разбивк/.test(comparable) ||
|
/^(?:с|без)\s+разбивк/.test(comparable) ||
|
||||||
|
/(?:без\s+(?:предположен\p{L}*|допущен\p{L}*|гадан\p{L}*|фантази\p{L}*|догад\p{L}*)|without\s+(?:guesses|assumptions|guessing))/iu.test(
|
||||||
|
comparable
|
||||||
|
) ||
|
||||||
/\b(?:входящ|исходящ|нетто|топ|контрагент|платеж|поступлен)\b/.test(comparable)
|
/\b(?:входящ|исходящ|нетто|топ|контрагент|платеж|поступлен)\b/.test(comparable)
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,14 @@ function isReferentialOrganizationPlaceholder(value: string | null): boolean {
|
||||||
if (isNegativeCounterpartyScopePlaceholder(value)) {
|
if (isNegativeCounterpartyScopePlaceholder(value)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
const text = compactLower(repairAddressMojibakeText(value));
|
||||||
|
if (
|
||||||
|
/(?:без\s+(?:предположен\p{L}*|допущен\p{L}*|гадан\p{L}*|фантази\p{L}*|догад\p{L}*)|without\s+(?:guesses|assumptions|guessing))/iu.test(
|
||||||
|
text
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return new Set([
|
return new Set([
|
||||||
"эта организация",
|
"эта организация",
|
||||||
"этой организации",
|
"этой организации",
|
||||||
|
|
@ -1326,7 +1334,7 @@ function normalizeLooseOrganizationAlias(value: string | null): string | null {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (isNegativeCounterpartyScopePlaceholder(text)) {
|
if (isReferentialOrganizationPlaceholder(text)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const normalized = text
|
const normalized = text
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,16 @@ describe("addressIntentResolver regression bridges", () => {
|
||||||
expect(result.reasons).toContain("unicode_nomenclature_margin_ranking_bridge_signal_detected");
|
expect(result.reasons).toContain("unicode_nomenclature_margin_ranking_bridge_signal_detected");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("routes agent margin-inspection follow-up to inventory margin instead of unknown", () => {
|
||||||
|
const result = resolveAddressIntent(
|
||||||
|
"\u0442\u043e\u0433\u0434\u0430 \u043a\u0430\u043a\u0443\u044e \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0438\u043b\u0438 \u043c\u0430\u0440\u0436\u0443 \u043c\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043f\u043e \u0442\u043e\u0432\u0430\u0440\u0430\u043c \u0437\u0430 2020?"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||||
|
expect(result.intent).not.toBe("unknown");
|
||||||
|
expect(result.reasons).toContain("unicode_nomenclature_margin_ranking_bridge_signal_detected");
|
||||||
|
});
|
||||||
|
|
||||||
it("detects bare historical inventory root with explicit month-year", () => {
|
it("detects bare historical inventory root with explicit month-year", () => {
|
||||||
const result = resolveAddressIntent("остатки на март 2016");
|
const result = resolveAddressIntent("остатки на март 2016");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,15 @@ describe("addressInventoryIntentSignals", () => {
|
||||||
expect(result.intent).toBe("inventory_margin_ranking_for_nomenclature");
|
expect(result.intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("classifies margin inspection wording from the phase106 replay as inventory margin", () => {
|
||||||
|
const result = resolveInventoryAddressIntent(
|
||||||
|
"\u0442\u043e\u0433\u0434\u0430 \u043a\u0430\u043a\u0443\u044e \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0438\u043b\u0438 \u043c\u0430\u0440\u0436\u0443 \u043c\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043f\u043e \u0442\u043e\u0432\u0430\u0440\u0430\u043c \u0437\u0430 2020?"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result?.intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||||
|
expect(result?.reasons).toContain("inventory_margin_ranking_signal_detected");
|
||||||
|
});
|
||||||
|
|
||||||
it("classifies selected-object purchase provenance wording through the extracted inventory owner", () => {
|
it("classifies selected-object purchase provenance wording through the extracted inventory owner", () => {
|
||||||
const result = resolveInventoryAddressIntent("selected object supplier provenance");
|
const result = resolveInventoryAddressIntent("selected object supplier provenance");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1132,6 +1132,55 @@ describe("assistant MCP discovery response candidate", () => {
|
||||||
expect(candidate.reply_text).not.toContain("generic advice");
|
expect(candidate.reply_text).not.toContain("generic advice");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not render negative next-check instruction as organization scope", () => {
|
||||||
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
|
entryPoint({
|
||||||
|
turn_input: {
|
||||||
|
adapter_status: "ready",
|
||||||
|
turn_meaning_ref: {
|
||||||
|
raw_message:
|
||||||
|
"\u0447\u0442\u043e \u0434\u0430\u043b\u044c\u0448\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u043d\u044f\u0442\u044c \u0437\u0434\u043e\u0440\u043e\u0432\u044c\u0435 \u0431\u0438\u0437\u043d\u0435\u0441\u0430, \u0431\u0435\u0437 \u0433\u0430\u0434\u0430\u043d\u0438\u0439?",
|
||||||
|
asked_action_family: "broad_evaluation",
|
||||||
|
unsupported_but_understood_family: "broad_business_evaluation",
|
||||||
|
explicit_organization_scope: "\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0439"
|
||||||
|
},
|
||||||
|
data_need_graph: {
|
||||||
|
business_fact_family: "business_overview",
|
||||||
|
ranking_need: null,
|
||||||
|
reason_codes: ["data_need_graph_family_business_overview"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
user_facing_response_allowed: true,
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
requires_user_clarification: false,
|
||||||
|
pilot: {
|
||||||
|
pilot_scope: "business_overview_route_template_v1",
|
||||||
|
derived_business_overview: {
|
||||||
|
incoming_customer_revenue: { total_amount_human_ru: "47 628 853,03 руб." },
|
||||||
|
outgoing_supplier_payout: { total_amount_human_ru: "43 763 351,53 руб." },
|
||||||
|
net_amount_human_ru: "3 865 501,50 руб.",
|
||||||
|
net_direction: "net_incoming"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference",
|
||||||
|
headline: "generic advice should not leak",
|
||||||
|
confirmed_lines: [],
|
||||||
|
inference_lines: [],
|
||||||
|
unknown_lines: [],
|
||||||
|
limitation_lines: [],
|
||||||
|
next_step_line: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(candidate.reply_text).toContain("\u0412 \u043f\u0435\u0440\u0432\u0443\u044e \u043e\u0447\u0435\u0440\u0435\u0434\u044c \u043f\u043e \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438");
|
||||||
|
expect(candidate.reply_text).not.toContain("\u043f\u043e \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438 \u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0439");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps counterparty leaders for money followups instead of compact cashflow", () => {
|
it("keeps counterparty leaders for money followups instead of compact cashflow", () => {
|
||||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
entryPoint({
|
entryPoint({
|
||||||
|
|
|
||||||
|
|
@ -4112,4 +4112,39 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||||
);
|
);
|
||||||
expect(result.reason_codes).not.toContain("mcp_discovery_business_overview_suppressed_stale_counterparty");
|
expect(result.reason_codes).not.toContain("mcp_discovery_business_overview_suppressed_stale_counterparty");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses retained organization scope instead of negative next-check instruction text", () => {
|
||||||
|
const orgName = "\u041e\u041e\u041e \"\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441\"";
|
||||||
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
|
userMessage:
|
||||||
|
"\u0447\u0442\u043e \u0434\u0430\u043b\u044c\u0448\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u043d\u044f\u0442\u044c \u0437\u0434\u043e\u0440\u043e\u0432\u044c\u0435 \u0431\u0438\u0437\u043d\u0435\u0441\u0430, \u0431\u0435\u0437 \u0433\u0430\u0434\u0430\u043d\u0438\u0439?",
|
||||||
|
assistantTurnMeaning: {
|
||||||
|
asked_domain_family: "business_overview",
|
||||||
|
asked_action_family: "broad_evaluation",
|
||||||
|
explicit_organization_scope: "\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0439",
|
||||||
|
unsupported_but_understood_family: "broad_business_evaluation",
|
||||||
|
stale_replay_forbidden: true
|
||||||
|
},
|
||||||
|
predecomposeContract: {
|
||||||
|
entities: {
|
||||||
|
counterparty: "\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0439"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
followupContext: {
|
||||||
|
previous_discovery_pilot_scope: "business_overview_route_template_v1",
|
||||||
|
previous_filters: { organization: orgName },
|
||||||
|
root_filters: { organization: orgName },
|
||||||
|
previous_anchor_type: "organization",
|
||||||
|
previous_anchor_value: orgName,
|
||||||
|
previous_intent: "business_overview",
|
||||||
|
root_intent: "business_overview"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.turn_meaning_ref?.explicit_organization_scope).toBe(orgName);
|
||||||
|
expect(result.turn_meaning_ref?.explicit_organization_scope).not.toBe(
|
||||||
|
"\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0439"
|
||||||
|
);
|
||||||
|
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue