Архитектура: ввести answer-inspection follow-up contract и закрыть phase15 replay
This commit is contained in:
parent
c605fae3a3
commit
1d48f01841
|
|
@ -376,6 +376,21 @@ Still open after the accepted phase12 replay:
|
||||||
- this matters because `root_context_only` VAT pivots from inventory drilldown should preserve restored organization/date filters without pretending that restored scope is itself a user-selected follow-up anchor;
|
- this matters because `root_context_only` VAT pivots from inventory drilldown should preserve restored organization/date filters without pretending that restored scope is itself a user-selected follow-up anchor;
|
||||||
- targeted `assistantAddressFollowupContext` and `assistantTransitionPolicy` suites are now green after the fix, explicitly protecting the `inventory drilldown -> VAT pivot` regression where selected-item carryover must be removed while the inventory root company/date window remains intact;
|
- targeted `assistantAddressFollowupContext` and `assistantTransitionPolicy` suites are now green after the fix, explicitly protecting the `inventory drilldown -> VAT pivot` regression where selected-item carryover must be removed while the inventory root company/date window remains intact;
|
||||||
- live replay `address_truth_harness_phase12_wider_saved_session_pool_live_20260418_rerun10` remains accepted `20/20`, which is the critical proof that this anchor-sanitization convergence did not reopen the flagship saved-session continuity path.
|
- live replay `address_truth_harness_phase12_wider_saved_session_pool_live_20260418_rerun10` remains accepted `20/20`, which is the critical proof that this anchor-sanitization convergence did not reopen the flagship saved-session continuity path.
|
||||||
|
- the next replay-breadth pass now proves a different late-session contour around answer inspection and self-correction:
|
||||||
|
- a new live pack `address_truth_harness_phase15_answer_inspection_followup` validates `smalltalk -> company fixation -> historical inventory -> selected-item purchase provenance -> selected-item sale trace -> answer inspection -> VAT-on-purchase-date bridge` inside one shared session;
|
||||||
|
- the first strict replay exposed a real architecture seam rather than a wording issue:
|
||||||
|
- after a grounded selected-item sale trace, the user could ask `у тебя написано кто контрагент: рабочая станция - это ошибка?`;
|
||||||
|
- the runtime was still trying to treat that as a fresh address retrieval request, which collapsed into `unknown / unsupported` instead of inspecting the already grounded previous answer;
|
||||||
|
- the fix is now explicit in the orchestration layer:
|
||||||
|
- living-mode policy exposes a dedicated answer-inspection signal for self-correction wording;
|
||||||
|
- meta follow-up policy can now recognize `answer inspection over grounded answer` as its own follow-up class instead of leaving it to the generic address lane;
|
||||||
|
- route policy now keeps that class out of the address lane and deliberately routes it back into living-chat inspection logic;
|
||||||
|
- living-chat runtime now serves a deterministic inspection reply contract for selected-item provenance / sale-trace answers, explicitly distinguishing `selected item` from `counterparty` and preserving the next business move;
|
||||||
|
- this matters architecturally because another ambient monolith behavior is now an explicit runtime contract:
|
||||||
|
- grounded answer inspection is no longer left to accidental prompt luck;
|
||||||
|
- self-correction over a previous exact answer can now coexist with selected-object continuity instead of breaking the session into unsupported chat;
|
||||||
|
- the neighboring bridge `selected-item trace -> VAT on purchase date` remains alive after the inspection turn, which proves that answer inspection no longer tears down the active business frame;
|
||||||
|
- live replay `address_truth_harness_phase15_answer_inspection_followup_live_20260418_rerun5` is accepted `9/9`, which is the critical proof that this inspection-follow-up contour now survives as a real saved-session path instead of a one-off manual rescue.
|
||||||
|
|
||||||
## Next Execution Slice (2026-04-18)
|
## Next Execution Slice (2026-04-18)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,221 @@
|
||||||
|
{
|
||||||
|
"schema_version": "domain_truth_harness_spec_v1",
|
||||||
|
"scenario_id": "address_truth_harness_phase15_answer_inspection_followup",
|
||||||
|
"domain": "address_phase15_answer_inspection_followup",
|
||||||
|
"title": "Phase 15 answer-inspection replay for selected-item trace continuity",
|
||||||
|
"description": "Focused AGENT replay for a different saved-session seam: after a historical inventory slice and selected-item purchase/sale trace, the user inspects the previous answer shape and then asks for VAT on the purchase date using colloquial wording. The scenario validates that result-inspection follow-ups stay grounded instead of collapsing into unsupported address mode, and that the purchase-date VAT bridge still survives after the inspection turn.",
|
||||||
|
"bindings": {},
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": "step_01_smalltalk_scope_offer",
|
||||||
|
"title": "Smalltalk entry offers organization proactively",
|
||||||
|
"question": "приветик - че как там дела",
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"(?i)привет",
|
||||||
|
"(?i)альтернатива плюс|лайсвуд|райм"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"(?i)mcp",
|
||||||
|
"(?i)tool_gate_reason",
|
||||||
|
"(?i)living_reason"
|
||||||
|
],
|
||||||
|
"criticality": "important",
|
||||||
|
"semantic_tags": [
|
||||||
|
"smalltalk_entry",
|
||||||
|
"scope_offer"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_02_choose_organization",
|
||||||
|
"title": "Explicit company selection fixes the session contour",
|
||||||
|
"question": "Альтернатива Плюс",
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"(?i)зафиксир|рабочую организац|работаем по",
|
||||||
|
"(?i)Альтернатива Плюс"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"organization_authority"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_03_inventory_march_2016",
|
||||||
|
"title": "Current inventory root enters the warehouse contour first",
|
||||||
|
"question": "что там на складе по остаткам?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"inventory_on_hand_as_of_date"
|
||||||
|
],
|
||||||
|
"expected_recipe": "address_inventory_on_hand_as_of_date_v1",
|
||||||
|
"required_filters": {
|
||||||
|
"as_of_date": "2026-04-18",
|
||||||
|
"organization": "ООО Альтернатива Плюс"
|
||||||
|
},
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"(?i)на складе|остат",
|
||||||
|
"18\\.04\\.2026"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"inventory_root",
|
||||||
|
"current_snapshot"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_04_inventory_history_capability",
|
||||||
|
"title": "Inventory history capability handshake before month-only follow-up",
|
||||||
|
"question": "а исторические остатки на другие даты умеешь?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual_with_explanation"
|
||||||
|
],
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"(?i)^да, могу",
|
||||||
|
"(?i)историческ",
|
||||||
|
"(?i)дат|месяц|год"
|
||||||
|
],
|
||||||
|
"criticality": "important",
|
||||||
|
"semantic_tags": [
|
||||||
|
"inventory_history_capability",
|
||||||
|
"context_setup"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_05_inventory_march_2016",
|
||||||
|
"title": "Historical inventory anchor on March 2016",
|
||||||
|
"question": "март 2016",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"inventory_on_hand_as_of_date"
|
||||||
|
],
|
||||||
|
"expected_recipe": "address_inventory_on_hand_as_of_date_v1",
|
||||||
|
"required_filters": {
|
||||||
|
"as_of_date": "2016-03-31",
|
||||||
|
"period_from": "2016-03-01",
|
||||||
|
"period_to": "2016-03-31",
|
||||||
|
"organization": "ООО Альтернатива Плюс"
|
||||||
|
},
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"31\\.03\\.2016",
|
||||||
|
"(?i)на складе"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"inventory_root",
|
||||||
|
"historical_anchor"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_06_selected_item_purchase_provenance",
|
||||||
|
"title": "Selected workstation purchase provenance",
|
||||||
|
"question": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"inventory_purchase_provenance_for_item"
|
||||||
|
],
|
||||||
|
"expected_recipe": "address_inventory_purchase_provenance_for_item_v1",
|
||||||
|
"required_filters": {
|
||||||
|
"item": "Рабочая станция универсального специалиста (индивидуальное изготовление)",
|
||||||
|
"as_of_date": "{{step_05_inventory_march_2016.filters.as_of_date}}"
|
||||||
|
},
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"(?i)рабочая станция",
|
||||||
|
"(?i)поставщик|закуп"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"selected_object",
|
||||||
|
"purchase_provenance"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_07_selected_item_sale_trace",
|
||||||
|
"title": "Buyer / sale trace follow-up on the same selected item",
|
||||||
|
"question": "а кому продали?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"inventory_sale_trace_for_item"
|
||||||
|
],
|
||||||
|
"expected_recipe": "address_inventory_sale_trace_for_item_v1",
|
||||||
|
"required_filters": {
|
||||||
|
"item": "Рабочая станция универсального специалиста (индивидуальное изготовление)",
|
||||||
|
"as_of_date": "{{step_05_inventory_march_2016.filters.as_of_date}}"
|
||||||
|
},
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"(?i)покупател|выбыт|прод",
|
||||||
|
"(?i)рабочая станция"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"selected_object",
|
||||||
|
"sale_trace"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_08_answer_inspection_counterparty_label",
|
||||||
|
"title": "Result-inspection follow-up stays grounded instead of unsupported",
|
||||||
|
"question": "у тебя написано кто контрагент: рабочая станция - это ошибка?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"(?i)рабочая станция",
|
||||||
|
"(?i)контрагент|номенклатур|позици|товар|поле"
|
||||||
|
],
|
||||||
|
"forbidden_direct_answer_patterns": [
|
||||||
|
"(?i)пока не поддерживается",
|
||||||
|
"(?i)ограничени[яй] адресного режима",
|
||||||
|
"(?i)не представляется возможным",
|
||||||
|
"(?i)система не поддерживает",
|
||||||
|
"(?i)tool_gate_reason",
|
||||||
|
"(?i)mcp"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"answer_inspection",
|
||||||
|
"selected_object_context",
|
||||||
|
"grounded_self_correction"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_09_vat_on_purchase_date_after_inspection",
|
||||||
|
"title": "VAT bridge survives after the inspection turn",
|
||||||
|
"question": "ндс можешь прикинуть на дату покупки рабочей станции?",
|
||||||
|
"allowed_reply_types": [
|
||||||
|
"factual",
|
||||||
|
"factual_with_explanation",
|
||||||
|
"partial_coverage"
|
||||||
|
],
|
||||||
|
"expected_intents": [
|
||||||
|
"vat_liability_confirmed_for_tax_period"
|
||||||
|
],
|
||||||
|
"required_direct_answer_patterns_any": [
|
||||||
|
"(?i)ндс",
|
||||||
|
"(?i)дата покупки|налогов|период|2015|феврал|июл"
|
||||||
|
],
|
||||||
|
"forbidden_direct_answer_patterns": [
|
||||||
|
"(?i)пока не поддерживается",
|
||||||
|
"(?i)ограничени[яй] адресного режима",
|
||||||
|
"(?i)tool_gate_reason",
|
||||||
|
"(?i)mcp"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": [
|
||||||
|
"purchase_date_vat_bridge",
|
||||||
|
"selected_object",
|
||||||
|
"post_inspection_continuity"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -75,6 +75,36 @@ function buildAddressMemoryRecapReply(input) {
|
||||||
}
|
}
|
||||||
return "Да, помню предыдущий адресный контур. Могу кратко напомнить, что мы уже подтвердили, или сразу продолжить следующий шаг.";
|
return "Да, помню предыдущий адресный контур. Могу кратко напомнить, что мы уже подтвердили, или сразу продолжить следующий шаг.";
|
||||||
}
|
}
|
||||||
|
function buildSelectedObjectAnswerInspectionReply(input) {
|
||||||
|
const extractedFilters = input.addressDebug?.extracted_filters && typeof input.addressDebug.extracted_filters === "object"
|
||||||
|
? input.addressDebug.extracted_filters
|
||||||
|
: null;
|
||||||
|
const item = input.toNonEmptyString(extractedFilters?.item) ??
|
||||||
|
(String(input.addressDebug?.anchor_type ?? "") === "item"
|
||||||
|
? input.toNonEmptyString(input.addressDebug?.anchor_value_resolved) ??
|
||||||
|
input.toNonEmptyString(input.addressDebug?.anchor_value_raw)
|
||||||
|
: null);
|
||||||
|
const detectedIntent = String(input.addressDebug?.detected_intent ?? "");
|
||||||
|
const itemLabel = item ?? "эта позиция";
|
||||||
|
if (detectedIntent === "inventory_sale_trace_for_item") {
|
||||||
|
return [
|
||||||
|
`Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а сама позиция, по которой мы смотрели продажу.`,
|
||||||
|
"В предыдущем ответе я показывал документы выбытия по этой позиции. Покупатель в доступных данных отдельно не выделен, поэтому назвать контрагента-покупателя я там не мог.",
|
||||||
|
"Если хочешь, следующим шагом могу отдельно проверить, можно ли вытащить покупателя по связанным документам реализации."
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
if (detectedIntent === "inventory_purchase_provenance_for_item" ||
|
||||||
|
detectedIntent === "inventory_purchase_documents_for_item") {
|
||||||
|
return [
|
||||||
|
`Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а сама позиция / номенклатура.`,
|
||||||
|
"В предыдущем ответе речь шла о закупке этой позиции: я перечислял поставщиков или закупочные документы по ней, а не называл саму позицию контрагентом."
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
`Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а выбранный объект разбора.`,
|
||||||
|
"Я сейчас уточняю именно смысл предыдущего grounded-ответа по этой позиции, а не запускаю новый адресный поиск."
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
async function runAssistantLivingChatRuntime(input) {
|
async function runAssistantLivingChatRuntime(input) {
|
||||||
const userMessage = String(input.userMessage ?? "");
|
const userMessage = String(input.userMessage ?? "");
|
||||||
const organizationAuthority = (0, assistantContinuityPolicy_1.resolveAssistantOrganizationAuthority)({
|
const organizationAuthority = (0, assistantContinuityPolicy_1.resolveAssistantOrganizationAuthority)({
|
||||||
|
|
@ -114,6 +144,7 @@ async function runAssistantLivingChatRuntime(input) {
|
||||||
const contextualMemoryRecapFollowup = memoryRecapContext.contextualMemoryRecapFollowup;
|
const contextualMemoryRecapFollowup = memoryRecapContext.contextualMemoryRecapFollowup;
|
||||||
const lastGroundedInventoryAddressDebug = memoryRecapContext.lastGroundedInventoryAddressDebug;
|
const lastGroundedInventoryAddressDebug = memoryRecapContext.lastGroundedInventoryAddressDebug;
|
||||||
const lastMemoryAddressDebug = memoryRecapContext.lastMemoryAddressDebug;
|
const lastMemoryAddressDebug = memoryRecapContext.lastMemoryAddressDebug;
|
||||||
|
const contextualAnswerInspectionFollowup = String(input.modeDecision?.reason ?? "") === "answer_inspection_followup_detected";
|
||||||
if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) {
|
if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) {
|
||||||
chatText = input.buildAssistantSafetyRefusalReply();
|
chatText = input.buildAssistantSafetyRefusalReply();
|
||||||
livingChatSource = "deterministic_safety_refusal";
|
livingChatSource = "deterministic_safety_refusal";
|
||||||
|
|
@ -178,6 +209,13 @@ async function runAssistantLivingChatRuntime(input) {
|
||||||
activeOrganization = scopedOrganization ?? activeOrganization;
|
activeOrganization = scopedOrganization ?? activeOrganization;
|
||||||
livingChatSource = "deterministic_memory_recap_contract";
|
livingChatSource = "deterministic_memory_recap_contract";
|
||||||
}
|
}
|
||||||
|
else if (contextualAnswerInspectionFollowup) {
|
||||||
|
chatText = buildSelectedObjectAnswerInspectionReply({
|
||||||
|
addressDebug: continuitySnapshot.lastGroundedItemAddressDebug ?? continuitySnapshot.lastGroundedAddressDebug,
|
||||||
|
toNonEmptyString: input.toNonEmptyString
|
||||||
|
});
|
||||||
|
livingChatSource = "deterministic_answer_inspection_contract";
|
||||||
|
}
|
||||||
else if (capabilityMetaQuery) {
|
else if (capabilityMetaQuery) {
|
||||||
chatText = input.buildAssistantCapabilityContractReply(userMessage);
|
chatText = input.buildAssistantCapabilityContractReply(userMessage);
|
||||||
livingChatSource = "deterministic_capability_contract";
|
livingChatSource = "deterministic_capability_contract";
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,29 @@ function createAssistantLivingModePolicy(deps) {
|
||||||
hasDataRetrievalRequestSignal(sample) ||
|
hasDataRetrievalRequestSignal(sample) ||
|
||||||
hasStrongDataIntentSignal(sample));
|
hasStrongDataIntentSignal(sample));
|
||||||
}
|
}
|
||||||
|
function hasAnswerInspectionFollowupSignal(userMessage) {
|
||||||
|
const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||||||
|
const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
|
||||||
|
const samples = [rawText, repairedText]
|
||||||
|
.filter((item) => item.length > 0)
|
||||||
|
.map((item) => item.replace(/ё/g, "е"));
|
||||||
|
if (samples.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasInspectionCue = samples.some((sample) => /(?:ошибк|неверн|не так|перепутал|у тебя написано|ты написал|это ошибка|правильно ли|корректн)/iu.test(sample));
|
||||||
|
if (!hasInspectionCue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasAnswerObjectCue = samples.some((sample) => /(?:контрагент|покупател|поставщик|поле|ответ|позици|номенклат|документ)/iu.test(sample));
|
||||||
|
if (!hasAnswerObjectCue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasFreshRetrievalAction = samples.some((sample) => /(?:покажи|выведи|найди|список|проверь по базе|подними документы|достань|рассчитай|посчитай|сколько|кто\s+нам|кто\s+у\s+нас)/iu.test(sample));
|
||||||
|
if (hasFreshRetrievalAction) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
function hasConversationMemoryRecallFollowupSignal(userMessage) {
|
function hasConversationMemoryRecallFollowupSignal(userMessage) {
|
||||||
const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||||||
const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
|
const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
|
||||||
|
|
@ -348,6 +371,7 @@ function createAssistantLivingModePolicy(deps) {
|
||||||
hasDataRetrievalRequestSignal,
|
hasDataRetrievalRequestSignal,
|
||||||
hasOrganizationFactLookupSignal,
|
hasOrganizationFactLookupSignal,
|
||||||
hasMetaAnswerFollowupSignal,
|
hasMetaAnswerFollowupSignal,
|
||||||
|
hasAnswerInspectionFollowupSignal,
|
||||||
hasConversationMemoryRecallFollowupSignal,
|
hasConversationMemoryRecallFollowupSignal,
|
||||||
hasHistoricalCapabilityFollowupSignal,
|
hasHistoricalCapabilityFollowupSignal,
|
||||||
hasOrganizationFactFollowupSignal,
|
hasOrganizationFactFollowupSignal,
|
||||||
|
|
|
||||||
|
|
@ -27,14 +27,16 @@ function createAssistantMetaFollowupPolicy(deps) {
|
||||||
return {
|
return {
|
||||||
dataScopeMetaQuery: false,
|
dataScopeMetaQuery: false,
|
||||||
capabilityMetaQuery: false,
|
capabilityMetaQuery: false,
|
||||||
metaAnswerFollowupSignal: false
|
metaAnswerFollowupSignal: false,
|
||||||
|
answerInspectionFollowupSignal: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
dataScopeMetaQuery: hasSignalAcrossSamples(samples, deps.hasAssistantDataScopeMetaQuestionSignal),
|
dataScopeMetaQuery: hasSignalAcrossSamples(samples, deps.hasAssistantDataScopeMetaQuestionSignal),
|
||||||
capabilityMetaQuery: hasSignalAcrossSamples(samples, deps.shouldHandleAsAssistantCapabilityMetaQuery) ||
|
capabilityMetaQuery: hasSignalAcrossSamples(samples, deps.shouldHandleAsAssistantCapabilityMetaQuery) ||
|
||||||
hasImplicitHistoricalCapabilityMetaSignal(samples),
|
hasImplicitHistoricalCapabilityMetaSignal(samples),
|
||||||
metaAnswerFollowupSignal: hasSignalAcrossSamples(samples, deps.hasMetaAnswerFollowupSignal)
|
metaAnswerFollowupSignal: hasSignalAcrossSamples(samples, deps.hasMetaAnswerFollowupSignal),
|
||||||
|
answerInspectionFollowupSignal: hasSignalAcrossSamples(samples, deps.hasAnswerInspectionFollowupSignal)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function resolveHardMetaMode(input) {
|
function resolveHardMetaMode(input) {
|
||||||
|
|
@ -60,9 +62,18 @@ function createAssistantMetaFollowupPolicy(deps) {
|
||||||
(!input.llmContractIntent || String(input.llmContractIntent) === "unknown") &&
|
(!input.llmContractIntent || String(input.llmContractIntent) === "unknown") &&
|
||||||
String(input.llmContractMode ?? "") !== "address_query");
|
String(input.llmContractMode ?? "") !== "address_query");
|
||||||
}
|
}
|
||||||
|
function isAnswerInspectionFollowupOverGroundedAnswer(input) {
|
||||||
|
return Boolean(input.followupContext &&
|
||||||
|
input.hasPriorAddressAnswerContext &&
|
||||||
|
input.answerInspectionFollowupSignal &&
|
||||||
|
!input.dataScopeMetaQuery &&
|
||||||
|
!input.capabilityMetaQuery &&
|
||||||
|
!input.aggregateBusinessAnalyticsSignal);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
resolveMetaSignalSet,
|
resolveMetaSignalSet,
|
||||||
resolveHardMetaMode,
|
resolveHardMetaMode,
|
||||||
isMetaFollowupOverGroundedAnswer
|
isMetaFollowupOverGroundedAnswer,
|
||||||
|
isAnswerInspectionFollowupOverGroundedAnswer
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ function shouldBypassStrictDeepInvestigationCueForAddressIntent(intent) {
|
||||||
return Boolean(intent && ADDRESS_INTENTS_ALLOW_STRICT_DEEP_INVESTIGATION_BYPASS.has(intent));
|
return Boolean(intent && ADDRESS_INTENTS_ALLOW_STRICT_DEEP_INVESTIGATION_BYPASS.has(intent));
|
||||||
}
|
}
|
||||||
function createAssistantRoutePolicy(deps) {
|
function createAssistantRoutePolicy(deps) {
|
||||||
const { repairAddressMojibake, findLastGroundedAddressAnswerDebug, findLastOrganizationClarificationAddressDebug, mergeKnownOrganizations, normalizeOrganizationScopeValue, resolveOrganizationSelectionFromMessage, resolveMetaSignalSet, resolveHardMetaMode, isMetaFollowupOverGroundedAnswer, hasDataRetrievalRequestSignal, hasOrganizationFactLookupSignal, hasOrganizationFactFollowupSignal, hasAggregateBusinessAnalyticsSignal, hasStandaloneAddressTopicSignal, hasOpenContractsAddressSignal, detectAddressQuestionMode, resolveAddressIntent, toNonEmptyString, hasStrictDeepInvestigationCue, hasStrongDataIntentSignal, hasAccountingSignal, hasDangerOrCoercionSignal, hasAddressFollowupContextSignal, hasShortDebtMirrorFollowupSignal, isInventorySelectedObjectIntent, hasShortInventoryObjectFollowupSignal, isGroundedInventoryContextDebug, resolveRouteMemorySignals, findLastAddressAssistantItem, resolveAddressToolGateDecision, hasSameDateAccountFollowupSignalForPredecompose, hasLooseAllTimeAddressLookupSignal, hasDeepAnalysisPreferenceSignal, hasDirectDeepAnalysisSignal, compactWhitespace, hasDeepSessionContinuationSignal, resolveLivingAssistantModeDecision, resolveProviderExecutionState } = deps;
|
const { repairAddressMojibake, findLastGroundedAddressAnswerDebug, findLastOrganizationClarificationAddressDebug, mergeKnownOrganizations, normalizeOrganizationScopeValue, resolveOrganizationSelectionFromMessage, resolveMetaSignalSet, resolveHardMetaMode, isMetaFollowupOverGroundedAnswer, isAnswerInspectionFollowupOverGroundedAnswer, hasDataRetrievalRequestSignal, hasOrganizationFactLookupSignal, hasOrganizationFactFollowupSignal, hasAggregateBusinessAnalyticsSignal, hasStandaloneAddressTopicSignal, hasOpenContractsAddressSignal, detectAddressQuestionMode, resolveAddressIntent, toNonEmptyString, hasStrictDeepInvestigationCue, hasStrongDataIntentSignal, hasAccountingSignal, hasDangerOrCoercionSignal, hasAddressFollowupContextSignal, hasShortDebtMirrorFollowupSignal, isInventorySelectedObjectIntent, hasShortInventoryObjectFollowupSignal, isGroundedInventoryContextDebug, resolveRouteMemorySignals, findLastAddressAssistantItem, resolveAddressToolGateDecision, hasSameDateAccountFollowupSignalForPredecompose, hasLooseAllTimeAddressLookupSignal, hasDeepAnalysisPreferenceSignal, hasDirectDeepAnalysisSignal, compactWhitespace, hasDeepSessionContinuationSignal, resolveLivingAssistantModeDecision, resolveProviderExecutionState } = deps;
|
||||||
function hasInventoryRootRestatementFollowupSignal(text) {
|
function hasInventoryRootRestatementFollowupSignal(text) {
|
||||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()).replace(/ё/g, "е");
|
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()).replace(/ё/g, "е");
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
|
|
@ -580,6 +580,7 @@ function createAssistantRoutePolicy(deps) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const metaAnswerFollowupSignal = metaSignals.metaAnswerFollowupSignal;
|
const metaAnswerFollowupSignal = metaSignals.metaAnswerFollowupSignal;
|
||||||
|
const answerInspectionFollowupSignal = metaSignals.answerInspectionFollowupSignal;
|
||||||
const preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected &&
|
const preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected &&
|
||||||
llmPreDecomposeMeta?.applied &&
|
llmPreDecomposeMeta?.applied &&
|
||||||
llmContractMode === "address_query") ||
|
llmContractMode === "address_query") ||
|
||||||
|
|
@ -707,6 +708,23 @@ function createAssistantRoutePolicy(deps) {
|
||||||
followupContext,
|
followupContext,
|
||||||
hasPriorAddressAnswerContext,
|
hasPriorAddressAnswerContext,
|
||||||
metaAnswerFollowupSignal,
|
metaAnswerFollowupSignal,
|
||||||
|
answerInspectionFollowupSignal,
|
||||||
|
vatEvaluativeFollowupSignal,
|
||||||
|
dataScopeMetaQuery,
|
||||||
|
capabilityMetaQuery,
|
||||||
|
aggregateBusinessAnalyticsSignal,
|
||||||
|
dataRetrievalSignal,
|
||||||
|
strongDataSignal,
|
||||||
|
resolvedMode: resolvedModeDetection.mode,
|
||||||
|
resolvedIntent: resolvedIntentResolution.intent,
|
||||||
|
llmContractIntent,
|
||||||
|
llmContractMode
|
||||||
|
});
|
||||||
|
const answerInspectionFollowupOverGroundedAnswer = isAnswerInspectionFollowupOverGroundedAnswer({
|
||||||
|
followupContext,
|
||||||
|
hasPriorAddressAnswerContext,
|
||||||
|
metaAnswerFollowupSignal,
|
||||||
|
answerInspectionFollowupSignal,
|
||||||
vatEvaluativeFollowupSignal,
|
vatEvaluativeFollowupSignal,
|
||||||
dataScopeMetaQuery,
|
dataScopeMetaQuery,
|
||||||
capabilityMetaQuery,
|
capabilityMetaQuery,
|
||||||
|
|
@ -762,6 +780,11 @@ function createAssistantRoutePolicy(deps) {
|
||||||
toolGateDecision = "skip_address_lane";
|
toolGateDecision = "skip_address_lane";
|
||||||
toolGateReason = "meta_followup_over_grounded_answer";
|
toolGateReason = "meta_followup_over_grounded_answer";
|
||||||
}
|
}
|
||||||
|
if (answerInspectionFollowupOverGroundedAnswer) {
|
||||||
|
runAddressLane = false;
|
||||||
|
toolGateDecision = "skip_address_lane";
|
||||||
|
toolGateReason = "answer_inspection_followup_over_grounded_answer";
|
||||||
|
}
|
||||||
let livingDecision = resolveLivingAssistantModeDecision({
|
let livingDecision = resolveLivingAssistantModeDecision({
|
||||||
userMessage: rawUserMessage,
|
userMessage: rawUserMessage,
|
||||||
addressLaneTriggered: runAddressLane,
|
addressLaneTriggered: runAddressLane,
|
||||||
|
|
@ -802,6 +825,12 @@ function createAssistantRoutePolicy(deps) {
|
||||||
reason: "meta_followup_over_grounded_answer"
|
reason: "meta_followup_over_grounded_answer"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (answerInspectionFollowupOverGroundedAnswer) {
|
||||||
|
livingDecision = {
|
||||||
|
mode: "chat",
|
||||||
|
reason: "answer_inspection_followup_detected"
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
runAddressLane,
|
runAddressLane,
|
||||||
toolGateDecision,
|
toolGateDecision,
|
||||||
|
|
@ -834,6 +863,7 @@ function createAssistantRoutePolicy(deps) {
|
||||||
deep_analysis_signal_fallback_to_deep: deepAnalysisSignalFallbackToDeep,
|
deep_analysis_signal_fallback_to_deep: deepAnalysisSignalFallbackToDeep,
|
||||||
aggregate_analytics_signal_fallback_to_deep: aggregateAnalyticsFallbackToDeep,
|
aggregate_analytics_signal_fallback_to_deep: aggregateAnalyticsFallbackToDeep,
|
||||||
deep_session_continuation_fallback_to_deep: deepSessionContinuationFallbackToDeep,
|
deep_session_continuation_fallback_to_deep: deepSessionContinuationFallbackToDeep,
|
||||||
|
answer_inspection_followup_over_grounded_answer: answerInspectionFollowupOverGroundedAnswer,
|
||||||
final_decision: {
|
final_decision: {
|
||||||
run_address_lane: runAddressLane,
|
run_address_lane: runAddressLane,
|
||||||
tool_gate_decision: toolGateDecision,
|
tool_gate_decision: toolGateDecision,
|
||||||
|
|
|
||||||
|
|
@ -4223,7 +4223,8 @@ const assistantLivingModePolicy = (0, assistantLivingModePolicy_1.createAssistan
|
||||||
const assistantMetaFollowupPolicy = (0, assistantMetaFollowupPolicy_1.createAssistantMetaFollowupPolicy)({
|
const assistantMetaFollowupPolicy = (0, assistantMetaFollowupPolicy_1.createAssistantMetaFollowupPolicy)({
|
||||||
hasAssistantDataScopeMetaQuestionSignal: assistantLivingModePolicy.hasAssistantDataScopeMetaQuestionSignal,
|
hasAssistantDataScopeMetaQuestionSignal: assistantLivingModePolicy.hasAssistantDataScopeMetaQuestionSignal,
|
||||||
shouldHandleAsAssistantCapabilityMetaQuery: assistantLivingModePolicy.shouldHandleAsAssistantCapabilityMetaQuery,
|
shouldHandleAsAssistantCapabilityMetaQuery: assistantLivingModePolicy.shouldHandleAsAssistantCapabilityMetaQuery,
|
||||||
hasMetaAnswerFollowupSignal: assistantLivingModePolicy.hasMetaAnswerFollowupSignal
|
hasMetaAnswerFollowupSignal: assistantLivingModePolicy.hasMetaAnswerFollowupSignal,
|
||||||
|
hasAnswerInspectionFollowupSignal: assistantLivingModePolicy.hasAnswerInspectionFollowupSignal
|
||||||
});
|
});
|
||||||
const assistantMemoryRecapPolicy = (0, assistantMemoryRecapPolicy_1.createAssistantMemoryRecapPolicy)({
|
const assistantMemoryRecapPolicy = (0, assistantMemoryRecapPolicy_1.createAssistantMemoryRecapPolicy)({
|
||||||
hasHistoricalCapabilityFollowupSignal: assistantLivingModePolicy.hasHistoricalCapabilityFollowupSignal,
|
hasHistoricalCapabilityFollowupSignal: assistantLivingModePolicy.hasHistoricalCapabilityFollowupSignal,
|
||||||
|
|
@ -4240,6 +4241,7 @@ const assistantRoutePolicy = (0, assistantRoutePolicy_1.createAssistantRoutePoli
|
||||||
resolveMetaSignalSet: assistantMetaFollowupPolicy.resolveMetaSignalSet,
|
resolveMetaSignalSet: assistantMetaFollowupPolicy.resolveMetaSignalSet,
|
||||||
resolveHardMetaMode: assistantMetaFollowupPolicy.resolveHardMetaMode,
|
resolveHardMetaMode: assistantMetaFollowupPolicy.resolveHardMetaMode,
|
||||||
isMetaFollowupOverGroundedAnswer: assistantMetaFollowupPolicy.isMetaFollowupOverGroundedAnswer,
|
isMetaFollowupOverGroundedAnswer: assistantMetaFollowupPolicy.isMetaFollowupOverGroundedAnswer,
|
||||||
|
isAnswerInspectionFollowupOverGroundedAnswer: assistantMetaFollowupPolicy.isAnswerInspectionFollowupOverGroundedAnswer,
|
||||||
hasDataRetrievalRequestSignal: assistantLivingModePolicy.hasDataRetrievalRequestSignal,
|
hasDataRetrievalRequestSignal: assistantLivingModePolicy.hasDataRetrievalRequestSignal,
|
||||||
hasOrganizationFactLookupSignal,
|
hasOrganizationFactLookupSignal,
|
||||||
hasOrganizationFactFollowupSignal: assistantLivingModePolicy.hasOrganizationFactFollowupSignal,
|
hasOrganizationFactFollowupSignal: assistantLivingModePolicy.hasOrganizationFactFollowupSignal,
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,45 @@ function buildAddressMemoryRecapReply(input: {
|
||||||
return "Да, помню предыдущий адресный контур. Могу кратко напомнить, что мы уже подтвердили, или сразу продолжить следующий шаг.";
|
return "Да, помню предыдущий адресный контур. Могу кратко напомнить, что мы уже подтвердили, или сразу продолжить следующий шаг.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildSelectedObjectAnswerInspectionReply(input: {
|
||||||
|
addressDebug: Record<string, unknown> | null;
|
||||||
|
toNonEmptyString: (value: unknown) => string | null;
|
||||||
|
}): string {
|
||||||
|
const extractedFilters =
|
||||||
|
input.addressDebug?.extracted_filters && typeof input.addressDebug.extracted_filters === "object"
|
||||||
|
? (input.addressDebug.extracted_filters as Record<string, unknown>)
|
||||||
|
: null;
|
||||||
|
const item =
|
||||||
|
input.toNonEmptyString(extractedFilters?.item) ??
|
||||||
|
(String(input.addressDebug?.anchor_type ?? "") === "item"
|
||||||
|
? input.toNonEmptyString(input.addressDebug?.anchor_value_resolved) ??
|
||||||
|
input.toNonEmptyString(input.addressDebug?.anchor_value_raw)
|
||||||
|
: null);
|
||||||
|
const detectedIntent = String(input.addressDebug?.detected_intent ?? "");
|
||||||
|
const itemLabel = item ?? "эта позиция";
|
||||||
|
|
||||||
|
if (detectedIntent === "inventory_sale_trace_for_item") {
|
||||||
|
return [
|
||||||
|
`Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а сама позиция, по которой мы смотрели продажу.`,
|
||||||
|
"В предыдущем ответе я показывал документы выбытия по этой позиции. Покупатель в доступных данных отдельно не выделен, поэтому назвать контрагента-покупателя я там не мог.",
|
||||||
|
"Если хочешь, следующим шагом могу отдельно проверить, можно ли вытащить покупателя по связанным документам реализации."
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detectedIntent === "inventory_purchase_provenance_for_item" ||
|
||||||
|
detectedIntent === "inventory_purchase_documents_for_item") {
|
||||||
|
return [
|
||||||
|
`Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а сама позиция / номенклатура.`,
|
||||||
|
"В предыдущем ответе речь шла о закупке этой позиции: я перечислял поставщиков или закупочные документы по ней, а не называл саму позицию контрагентом."
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
`Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а выбранный объект разбора.`,
|
||||||
|
"Я сейчас уточняю именно смысл предыдущего grounded-ответа по этой позиции, а не запускаю новый адресный поиск."
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
export async function runAssistantLivingChatRuntime(
|
export async function runAssistantLivingChatRuntime(
|
||||||
input: AssistantLivingChatRuntimeInput
|
input: AssistantLivingChatRuntimeInput
|
||||||
): Promise<AssistantLivingChatRuntimeOutput> {
|
): Promise<AssistantLivingChatRuntimeOutput> {
|
||||||
|
|
@ -205,6 +244,8 @@ export async function runAssistantLivingChatRuntime(
|
||||||
const contextualMemoryRecapFollowup = memoryRecapContext.contextualMemoryRecapFollowup;
|
const contextualMemoryRecapFollowup = memoryRecapContext.contextualMemoryRecapFollowup;
|
||||||
const lastGroundedInventoryAddressDebug = memoryRecapContext.lastGroundedInventoryAddressDebug;
|
const lastGroundedInventoryAddressDebug = memoryRecapContext.lastGroundedInventoryAddressDebug;
|
||||||
const lastMemoryAddressDebug = memoryRecapContext.lastMemoryAddressDebug;
|
const lastMemoryAddressDebug = memoryRecapContext.lastMemoryAddressDebug;
|
||||||
|
const contextualAnswerInspectionFollowup =
|
||||||
|
String(input.modeDecision?.reason ?? "") === "answer_inspection_followup_detected";
|
||||||
|
|
||||||
if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) {
|
if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) {
|
||||||
chatText = input.buildAssistantSafetyRefusalReply();
|
chatText = input.buildAssistantSafetyRefusalReply();
|
||||||
|
|
@ -266,6 +307,12 @@ export async function runAssistantLivingChatRuntime(
|
||||||
});
|
});
|
||||||
activeOrganization = scopedOrganization ?? activeOrganization;
|
activeOrganization = scopedOrganization ?? activeOrganization;
|
||||||
livingChatSource = "deterministic_memory_recap_contract";
|
livingChatSource = "deterministic_memory_recap_contract";
|
||||||
|
} else if (contextualAnswerInspectionFollowup) {
|
||||||
|
chatText = buildSelectedObjectAnswerInspectionReply({
|
||||||
|
addressDebug: continuitySnapshot.lastGroundedItemAddressDebug ?? continuitySnapshot.lastGroundedAddressDebug,
|
||||||
|
toNonEmptyString: input.toNonEmptyString
|
||||||
|
});
|
||||||
|
livingChatSource = "deterministic_answer_inspection_contract";
|
||||||
} else if (capabilityMetaQuery) {
|
} else if (capabilityMetaQuery) {
|
||||||
chatText = input.buildAssistantCapabilityContractReply(userMessage);
|
chatText = input.buildAssistantCapabilityContractReply(userMessage);
|
||||||
livingChatSource = "deterministic_capability_contract";
|
livingChatSource = "deterministic_capability_contract";
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ export interface AssistantLivingModePolicy {
|
||||||
hasDataRetrievalRequestSignal: (text: unknown) => boolean;
|
hasDataRetrievalRequestSignal: (text: unknown) => boolean;
|
||||||
hasOrganizationFactLookupSignal: (text: unknown) => boolean;
|
hasOrganizationFactLookupSignal: (text: unknown) => boolean;
|
||||||
hasMetaAnswerFollowupSignal: (text: unknown) => boolean;
|
hasMetaAnswerFollowupSignal: (text: unknown) => boolean;
|
||||||
|
hasAnswerInspectionFollowupSignal: (text: unknown) => boolean;
|
||||||
hasConversationMemoryRecallFollowupSignal: (text: unknown) => boolean;
|
hasConversationMemoryRecallFollowupSignal: (text: unknown) => boolean;
|
||||||
hasHistoricalCapabilityFollowupSignal: (text: unknown) => boolean;
|
hasHistoricalCapabilityFollowupSignal: (text: unknown) => boolean;
|
||||||
hasOrganizationFactFollowupSignal: (userMessage: unknown, items: unknown[]) => boolean;
|
hasOrganizationFactFollowupSignal: (userMessage: unknown, items: unknown[]) => boolean;
|
||||||
|
|
@ -160,6 +161,30 @@ export function createAssistantLivingModePolicy(deps: AssistantLivingModePolicyD
|
||||||
hasStrongDataIntentSignal(sample));
|
hasStrongDataIntentSignal(sample));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasAnswerInspectionFollowupSignal(userMessage) {
|
||||||
|
const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||||||
|
const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
|
||||||
|
const samples = [rawText, repairedText]
|
||||||
|
.filter((item) => item.length > 0)
|
||||||
|
.map((item) => item.replace(/ё/g, "е"));
|
||||||
|
if (samples.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasInspectionCue = samples.some((sample) => /(?:ошибк|неверн|не так|перепутал|у тебя написано|ты написал|это ошибка|правильно ли|корректн)/iu.test(sample));
|
||||||
|
if (!hasInspectionCue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasAnswerObjectCue = samples.some((sample) => /(?:контрагент|покупател|поставщик|поле|ответ|позици|номенклат|документ)/iu.test(sample));
|
||||||
|
if (!hasAnswerObjectCue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasFreshRetrievalAction = samples.some((sample) => /(?:покажи|выведи|найди|список|проверь по базе|подними документы|достань|рассчитай|посчитай|сколько|кто\s+нам|кто\s+у\s+нас)/iu.test(sample));
|
||||||
|
if (hasFreshRetrievalAction) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function hasConversationMemoryRecallFollowupSignal(userMessage) {
|
function hasConversationMemoryRecallFollowupSignal(userMessage) {
|
||||||
const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||||||
const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
|
const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
|
||||||
|
|
@ -421,6 +446,7 @@ export function createAssistantLivingModePolicy(deps: AssistantLivingModePolicyD
|
||||||
hasDataRetrievalRequestSignal,
|
hasDataRetrievalRequestSignal,
|
||||||
hasOrganizationFactLookupSignal,
|
hasOrganizationFactLookupSignal,
|
||||||
hasMetaAnswerFollowupSignal,
|
hasMetaAnswerFollowupSignal,
|
||||||
|
hasAnswerInspectionFollowupSignal,
|
||||||
hasConversationMemoryRecallFollowupSignal,
|
hasConversationMemoryRecallFollowupSignal,
|
||||||
hasHistoricalCapabilityFollowupSignal,
|
hasHistoricalCapabilityFollowupSignal,
|
||||||
hasOrganizationFactFollowupSignal,
|
hasOrganizationFactFollowupSignal,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ export interface ResolveAssistantMetaFollowupOverGroundedAnswerInput {
|
||||||
followupContext?: unknown;
|
followupContext?: unknown;
|
||||||
hasPriorAddressAnswerContext?: boolean;
|
hasPriorAddressAnswerContext?: boolean;
|
||||||
metaAnswerFollowupSignal?: boolean;
|
metaAnswerFollowupSignal?: boolean;
|
||||||
|
answerInspectionFollowupSignal?: boolean;
|
||||||
vatEvaluativeFollowupSignal?: boolean;
|
vatEvaluativeFollowupSignal?: boolean;
|
||||||
dataScopeMetaQuery?: boolean;
|
dataScopeMetaQuery?: boolean;
|
||||||
capabilityMetaQuery?: boolean;
|
capabilityMetaQuery?: boolean;
|
||||||
|
|
@ -33,12 +34,14 @@ export interface AssistantMetaSignalSet {
|
||||||
dataScopeMetaQuery: boolean;
|
dataScopeMetaQuery: boolean;
|
||||||
capabilityMetaQuery: boolean;
|
capabilityMetaQuery: boolean;
|
||||||
metaAnswerFollowupSignal: boolean;
|
metaAnswerFollowupSignal: boolean;
|
||||||
|
answerInspectionFollowupSignal: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AssistantMetaFollowupPolicyDeps {
|
export interface AssistantMetaFollowupPolicyDeps {
|
||||||
hasAssistantDataScopeMetaQuestionSignal: (text: unknown) => boolean;
|
hasAssistantDataScopeMetaQuestionSignal: (text: unknown) => boolean;
|
||||||
shouldHandleAsAssistantCapabilityMetaQuery: (text: unknown) => boolean;
|
shouldHandleAsAssistantCapabilityMetaQuery: (text: unknown) => boolean;
|
||||||
hasMetaAnswerFollowupSignal: (text: unknown) => boolean;
|
hasMetaAnswerFollowupSignal: (text: unknown) => boolean;
|
||||||
|
hasAnswerInspectionFollowupSignal: (text: unknown) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectMessageSamples(input: ResolveAssistantMetaSignalSetInput): string[] {
|
function collectMessageSamples(input: ResolveAssistantMetaSignalSetInput): string[] {
|
||||||
|
|
@ -83,7 +86,8 @@ export function createAssistantMetaFollowupPolicy(
|
||||||
return {
|
return {
|
||||||
dataScopeMetaQuery: false,
|
dataScopeMetaQuery: false,
|
||||||
capabilityMetaQuery: false,
|
capabilityMetaQuery: false,
|
||||||
metaAnswerFollowupSignal: false
|
metaAnswerFollowupSignal: false,
|
||||||
|
answerInspectionFollowupSignal: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
@ -97,6 +101,10 @@ export function createAssistantMetaFollowupPolicy(
|
||||||
metaAnswerFollowupSignal: hasSignalAcrossSamples(
|
metaAnswerFollowupSignal: hasSignalAcrossSamples(
|
||||||
samples,
|
samples,
|
||||||
deps.hasMetaAnswerFollowupSignal
|
deps.hasMetaAnswerFollowupSignal
|
||||||
|
),
|
||||||
|
answerInspectionFollowupSignal: hasSignalAcrossSamples(
|
||||||
|
samples,
|
||||||
|
deps.hasAnswerInspectionFollowupSignal
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -132,9 +140,23 @@ export function createAssistantMetaFollowupPolicy(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAnswerInspectionFollowupOverGroundedAnswer(
|
||||||
|
input: ResolveAssistantMetaFollowupOverGroundedAnswerInput
|
||||||
|
): boolean {
|
||||||
|
return Boolean(
|
||||||
|
input.followupContext &&
|
||||||
|
input.hasPriorAddressAnswerContext &&
|
||||||
|
input.answerInspectionFollowupSignal &&
|
||||||
|
!input.dataScopeMetaQuery &&
|
||||||
|
!input.capabilityMetaQuery &&
|
||||||
|
!input.aggregateBusinessAnalyticsSignal
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
resolveMetaSignalSet,
|
resolveMetaSignalSet,
|
||||||
resolveHardMetaMode,
|
resolveHardMetaMode,
|
||||||
isMetaFollowupOverGroundedAnswer
|
isMetaFollowupOverGroundedAnswer,
|
||||||
|
isAnswerInspectionFollowupOverGroundedAnswer
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ export function createAssistantRoutePolicy(deps) {
|
||||||
resolveMetaSignalSet,
|
resolveMetaSignalSet,
|
||||||
resolveHardMetaMode,
|
resolveHardMetaMode,
|
||||||
isMetaFollowupOverGroundedAnswer,
|
isMetaFollowupOverGroundedAnswer,
|
||||||
|
isAnswerInspectionFollowupOverGroundedAnswer,
|
||||||
hasDataRetrievalRequestSignal,
|
hasDataRetrievalRequestSignal,
|
||||||
hasOrganizationFactLookupSignal,
|
hasOrganizationFactLookupSignal,
|
||||||
hasOrganizationFactFollowupSignal,
|
hasOrganizationFactFollowupSignal,
|
||||||
|
|
@ -619,6 +620,7 @@ export function createAssistantRoutePolicy(deps) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const metaAnswerFollowupSignal = metaSignals.metaAnswerFollowupSignal;
|
const metaAnswerFollowupSignal = metaSignals.metaAnswerFollowupSignal;
|
||||||
|
const answerInspectionFollowupSignal = metaSignals.answerInspectionFollowupSignal;
|
||||||
const preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected &&
|
const preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected &&
|
||||||
llmPreDecomposeMeta?.applied &&
|
llmPreDecomposeMeta?.applied &&
|
||||||
llmContractMode === "address_query") ||
|
llmContractMode === "address_query") ||
|
||||||
|
|
@ -746,6 +748,23 @@ export function createAssistantRoutePolicy(deps) {
|
||||||
followupContext,
|
followupContext,
|
||||||
hasPriorAddressAnswerContext,
|
hasPriorAddressAnswerContext,
|
||||||
metaAnswerFollowupSignal,
|
metaAnswerFollowupSignal,
|
||||||
|
answerInspectionFollowupSignal,
|
||||||
|
vatEvaluativeFollowupSignal,
|
||||||
|
dataScopeMetaQuery,
|
||||||
|
capabilityMetaQuery,
|
||||||
|
aggregateBusinessAnalyticsSignal,
|
||||||
|
dataRetrievalSignal,
|
||||||
|
strongDataSignal,
|
||||||
|
resolvedMode: resolvedModeDetection.mode,
|
||||||
|
resolvedIntent: resolvedIntentResolution.intent,
|
||||||
|
llmContractIntent,
|
||||||
|
llmContractMode
|
||||||
|
});
|
||||||
|
const answerInspectionFollowupOverGroundedAnswer = isAnswerInspectionFollowupOverGroundedAnswer({
|
||||||
|
followupContext,
|
||||||
|
hasPriorAddressAnswerContext,
|
||||||
|
metaAnswerFollowupSignal,
|
||||||
|
answerInspectionFollowupSignal,
|
||||||
vatEvaluativeFollowupSignal,
|
vatEvaluativeFollowupSignal,
|
||||||
dataScopeMetaQuery,
|
dataScopeMetaQuery,
|
||||||
capabilityMetaQuery,
|
capabilityMetaQuery,
|
||||||
|
|
@ -801,6 +820,11 @@ export function createAssistantRoutePolicy(deps) {
|
||||||
toolGateDecision = "skip_address_lane";
|
toolGateDecision = "skip_address_lane";
|
||||||
toolGateReason = "meta_followup_over_grounded_answer";
|
toolGateReason = "meta_followup_over_grounded_answer";
|
||||||
}
|
}
|
||||||
|
if (answerInspectionFollowupOverGroundedAnswer) {
|
||||||
|
runAddressLane = false;
|
||||||
|
toolGateDecision = "skip_address_lane";
|
||||||
|
toolGateReason = "answer_inspection_followup_over_grounded_answer";
|
||||||
|
}
|
||||||
let livingDecision = resolveLivingAssistantModeDecision({
|
let livingDecision = resolveLivingAssistantModeDecision({
|
||||||
userMessage: rawUserMessage,
|
userMessage: rawUserMessage,
|
||||||
addressLaneTriggered: runAddressLane,
|
addressLaneTriggered: runAddressLane,
|
||||||
|
|
@ -841,6 +865,12 @@ export function createAssistantRoutePolicy(deps) {
|
||||||
reason: "meta_followup_over_grounded_answer"
|
reason: "meta_followup_over_grounded_answer"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (answerInspectionFollowupOverGroundedAnswer) {
|
||||||
|
livingDecision = {
|
||||||
|
mode: "chat",
|
||||||
|
reason: "answer_inspection_followup_detected"
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
runAddressLane,
|
runAddressLane,
|
||||||
toolGateDecision,
|
toolGateDecision,
|
||||||
|
|
@ -873,6 +903,7 @@ export function createAssistantRoutePolicy(deps) {
|
||||||
deep_analysis_signal_fallback_to_deep: deepAnalysisSignalFallbackToDeep,
|
deep_analysis_signal_fallback_to_deep: deepAnalysisSignalFallbackToDeep,
|
||||||
aggregate_analytics_signal_fallback_to_deep: aggregateAnalyticsFallbackToDeep,
|
aggregate_analytics_signal_fallback_to_deep: aggregateAnalyticsFallbackToDeep,
|
||||||
deep_session_continuation_fallback_to_deep: deepSessionContinuationFallbackToDeep,
|
deep_session_continuation_fallback_to_deep: deepSessionContinuationFallbackToDeep,
|
||||||
|
answer_inspection_followup_over_grounded_answer: answerInspectionFollowupOverGroundedAnswer,
|
||||||
final_decision: {
|
final_decision: {
|
||||||
run_address_lane: runAddressLane,
|
run_address_lane: runAddressLane,
|
||||||
tool_gate_decision: toolGateDecision,
|
tool_gate_decision: toolGateDecision,
|
||||||
|
|
|
||||||
|
|
@ -4180,7 +4180,8 @@ const assistantLivingModePolicy = (0, assistantLivingModePolicy_1.createAssistan
|
||||||
const assistantMetaFollowupPolicy = (0, assistantMetaFollowupPolicy_1.createAssistantMetaFollowupPolicy)({
|
const assistantMetaFollowupPolicy = (0, assistantMetaFollowupPolicy_1.createAssistantMetaFollowupPolicy)({
|
||||||
hasAssistantDataScopeMetaQuestionSignal: assistantLivingModePolicy.hasAssistantDataScopeMetaQuestionSignal,
|
hasAssistantDataScopeMetaQuestionSignal: assistantLivingModePolicy.hasAssistantDataScopeMetaQuestionSignal,
|
||||||
shouldHandleAsAssistantCapabilityMetaQuery: assistantLivingModePolicy.shouldHandleAsAssistantCapabilityMetaQuery,
|
shouldHandleAsAssistantCapabilityMetaQuery: assistantLivingModePolicy.shouldHandleAsAssistantCapabilityMetaQuery,
|
||||||
hasMetaAnswerFollowupSignal: assistantLivingModePolicy.hasMetaAnswerFollowupSignal
|
hasMetaAnswerFollowupSignal: assistantLivingModePolicy.hasMetaAnswerFollowupSignal,
|
||||||
|
hasAnswerInspectionFollowupSignal: assistantLivingModePolicy.hasAnswerInspectionFollowupSignal
|
||||||
});
|
});
|
||||||
const assistantMemoryRecapPolicy = (0, assistantMemoryRecapPolicy_1.createAssistantMemoryRecapPolicy)({
|
const assistantMemoryRecapPolicy = (0, assistantMemoryRecapPolicy_1.createAssistantMemoryRecapPolicy)({
|
||||||
hasHistoricalCapabilityFollowupSignal: assistantLivingModePolicy.hasHistoricalCapabilityFollowupSignal,
|
hasHistoricalCapabilityFollowupSignal: assistantLivingModePolicy.hasHistoricalCapabilityFollowupSignal,
|
||||||
|
|
@ -4197,6 +4198,7 @@ const assistantRoutePolicy = (0, assistantRoutePolicy_1.createAssistantRoutePoli
|
||||||
resolveMetaSignalSet: assistantMetaFollowupPolicy.resolveMetaSignalSet,
|
resolveMetaSignalSet: assistantMetaFollowupPolicy.resolveMetaSignalSet,
|
||||||
resolveHardMetaMode: assistantMetaFollowupPolicy.resolveHardMetaMode,
|
resolveHardMetaMode: assistantMetaFollowupPolicy.resolveHardMetaMode,
|
||||||
isMetaFollowupOverGroundedAnswer: assistantMetaFollowupPolicy.isMetaFollowupOverGroundedAnswer,
|
isMetaFollowupOverGroundedAnswer: assistantMetaFollowupPolicy.isMetaFollowupOverGroundedAnswer,
|
||||||
|
isAnswerInspectionFollowupOverGroundedAnswer: assistantMetaFollowupPolicy.isAnswerInspectionFollowupOverGroundedAnswer,
|
||||||
hasDataRetrievalRequestSignal: assistantLivingModePolicy.hasDataRetrievalRequestSignal,
|
hasDataRetrievalRequestSignal: assistantLivingModePolicy.hasDataRetrievalRequestSignal,
|
||||||
hasOrganizationFactLookupSignal,
|
hasOrganizationFactLookupSignal,
|
||||||
hasOrganizationFactFollowupSignal: assistantLivingModePolicy.hasOrganizationFactFollowupSignal,
|
hasOrganizationFactFollowupSignal: assistantLivingModePolicy.hasOrganizationFactFollowupSignal,
|
||||||
|
|
|
||||||
|
|
@ -255,4 +255,38 @@ describe("assistant living chat runtime adapter", () => {
|
||||||
expect(output.debug?.living_chat_continuity_active_organization).toBe("ООО Альтернатива Плюс");
|
expect(output.debug?.living_chat_continuity_active_organization).toBe("ООО Альтернатива Плюс");
|
||||||
expect(executeLlmChat).not.toHaveBeenCalled();
|
expect(executeLlmChat).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
it("builds deterministic answer inspection reply over grounded selected-object sale trace", async () => {
|
||||||
|
const executeLlmChat = vi.fn(async () => "raw-llm");
|
||||||
|
const input = buildRuntimeInput({
|
||||||
|
userMessage: "у тебя написано кто контрагент: рабочая станция - это ошибка?",
|
||||||
|
modeDecision: { mode: "chat", reason: "answer_inspection_followup_detected" },
|
||||||
|
sessionItems: [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
debug: {
|
||||||
|
execution_lane: "address_query",
|
||||||
|
answer_grounding_check: {
|
||||||
|
status: "grounded"
|
||||||
|
},
|
||||||
|
detected_intent: "inventory_sale_trace_for_item",
|
||||||
|
extracted_filters: {
|
||||||
|
item: "Рабочая станция универсального специалиста",
|
||||||
|
organization: "ООО Альтернатива Плюс",
|
||||||
|
as_of_date: "2016-03-31"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
executeLlmChat
|
||||||
|
});
|
||||||
|
|
||||||
|
const output = await runAssistantLivingChatRuntime(input);
|
||||||
|
|
||||||
|
expect(output.handled).toBe(true);
|
||||||
|
expect(output.chatText).toContain("не контрагент");
|
||||||
|
expect(output.chatText).toContain("Рабочая станция универсального специалиста");
|
||||||
|
expect(output.chatText).toContain("Покупатель");
|
||||||
|
expect(output.debug?.living_chat_response_source).toBe("deterministic_answer_inspection_contract");
|
||||||
|
expect(executeLlmChat).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,12 @@ describe("assistantLivingModePolicy", () => {
|
||||||
expect(policy.hasConversationMemoryRecallFollowupSignal("а что мы уже выяснили по этой позиции?")).toBe(true);
|
expect(policy.hasConversationMemoryRecallFollowupSignal("а что мы уже выяснили по этой позиции?")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("detects answer inspection wording for previous answer correction", () => {
|
||||||
|
const policy = buildPolicy();
|
||||||
|
|
||||||
|
expect(policy.hasAnswerInspectionFollowupSignal("у тебя написано кто контрагент: рабочая станция - это ошибка?")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("routes casual small-talk to chat mode", () => {
|
it("routes casual small-talk to chat mode", () => {
|
||||||
const policy = buildPolicy();
|
const policy = buildPolicy();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@ import { createAssistantMetaFollowupPolicy } from "../src/services/assistantMeta
|
||||||
|
|
||||||
const policy = createAssistantMetaFollowupPolicy({
|
const policy = createAssistantMetaFollowupPolicy({
|
||||||
hasAssistantDataScopeMetaQuestionSignal: (text: unknown) =>
|
hasAssistantDataScopeMetaQuestionSignal: (text: unknown) =>
|
||||||
/по какой компании|какая база/i.test(String(text ?? "")),
|
/по какой компании|какая база/i.test(String(text ?? "")),
|
||||||
shouldHandleAsAssistantCapabilityMetaQuery: (text: unknown) =>
|
shouldHandleAsAssistantCapabilityMetaQuery: (text: unknown) =>
|
||||||
/что ты можешь|что ты умеешь/i.test(String(text ?? "")),
|
/что ты можешь|что ты умеешь/i.test(String(text ?? "")),
|
||||||
hasMetaAnswerFollowupSignal: (text: unknown) =>
|
hasMetaAnswerFollowupSignal: (text: unknown) =>
|
||||||
/это норм|что думаешь/i.test(String(text ?? ""))
|
/это норм|что думаешь/i.test(String(text ?? "")),
|
||||||
|
hasAnswerInspectionFollowupSignal: (text: unknown) =>
|
||||||
|
/это ошибка|у тебя написано кто контрагент/i.test(String(text ?? ""))
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("assistantMetaFollowupPolicy", () => {
|
describe("assistantMetaFollowupPolicy", () => {
|
||||||
|
|
@ -15,13 +17,14 @@ describe("assistantMetaFollowupPolicy", () => {
|
||||||
const signals = policy.resolveMetaSignalSet({
|
const signals = policy.resolveMetaSignalSet({
|
||||||
rawUserMessage: "",
|
rawUserMessage: "",
|
||||||
repairedRawUserMessage: "",
|
repairedRawUserMessage: "",
|
||||||
effectiveAddressUserMessage: "по какой компании мы можем работать?",
|
effectiveAddressUserMessage: "по какой компании мы можем работать?",
|
||||||
repairedEffectiveAddressUserMessage: ""
|
repairedEffectiveAddressUserMessage: ""
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(signals.dataScopeMetaQuery).toBe(true);
|
expect(signals.dataScopeMetaQuery).toBe(true);
|
||||||
expect(signals.capabilityMetaQuery).toBe(false);
|
expect(signals.capabilityMetaQuery).toBe(false);
|
||||||
expect(signals.metaAnswerFollowupSignal).toBe(false);
|
expect(signals.metaAnswerFollowupSignal).toBe(false);
|
||||||
|
expect(signals.answerInspectionFollowupSignal).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("treats historical capability phrasing as capability meta follow-up", () => {
|
it("treats historical capability phrasing as capability meta follow-up", () => {
|
||||||
|
|
@ -72,4 +75,26 @@ describe("assistantMetaFollowupPolicy", () => {
|
||||||
|
|
||||||
expect(detected).toBe(true);
|
expect(detected).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("detects answer inspection follow-up over grounded answer", () => {
|
||||||
|
const signals = policy.resolveMetaSignalSet({
|
||||||
|
rawUserMessage: "у тебя написано кто контрагент: рабочая станция - это ошибка?",
|
||||||
|
repairedRawUserMessage: "",
|
||||||
|
effectiveAddressUserMessage: "",
|
||||||
|
repairedEffectiveAddressUserMessage: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(signals.answerInspectionFollowupSignal).toBe(true);
|
||||||
|
|
||||||
|
const detected = policy.isAnswerInspectionFollowupOverGroundedAnswer({
|
||||||
|
followupContext: { previous_intent: "inventory_sale_trace_for_item", previous_anchor_type: "item" },
|
||||||
|
hasPriorAddressAnswerContext: true,
|
||||||
|
answerInspectionFollowupSignal: true,
|
||||||
|
dataScopeMetaQuery: false,
|
||||||
|
capabilityMetaQuery: false,
|
||||||
|
aggregateBusinessAnalyticsSignal: false
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(detected).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,8 @@ function buildPolicy(overrides: Record<string, unknown> = {}) {
|
||||||
return {
|
return {
|
||||||
dataScopeMetaQuery: /по какой компании|какая база|по каким конторам/i.test(samples),
|
dataScopeMetaQuery: /по какой компании|какая база|по каким конторам/i.test(samples),
|
||||||
capabilityMetaQuery: /что ты можешь|что ты умеешь/i.test(samples),
|
capabilityMetaQuery: /что ты можешь|что ты умеешь/i.test(samples),
|
||||||
metaAnswerFollowupSignal: /это норм|что думаешь/i.test(samples)
|
metaAnswerFollowupSignal: /это норм|что думаешь/i.test(samples),
|
||||||
|
answerInspectionFollowupSignal: /это ошибка|у тебя написано кто контрагент/i.test(samples)
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
resolveHardMetaMode: (input: {
|
resolveHardMetaMode: (input: {
|
||||||
|
|
@ -61,6 +62,7 @@ function buildPolicy(overrides: Record<string, unknown> = {}) {
|
||||||
? "capability"
|
? "capability"
|
||||||
: null,
|
: null,
|
||||||
isMetaFollowupOverGroundedAnswer: () => false,
|
isMetaFollowupOverGroundedAnswer: () => false,
|
||||||
|
isAnswerInspectionFollowupOverGroundedAnswer: () => false,
|
||||||
hasDataRetrievalRequestSignal: () => false,
|
hasDataRetrievalRequestSignal: () => false,
|
||||||
hasOrganizationFactLookupSignal: () => false,
|
hasOrganizationFactLookupSignal: () => false,
|
||||||
hasOrganizationFactFollowupSignal: () => false,
|
hasOrganizationFactFollowupSignal: () => false,
|
||||||
|
|
@ -236,6 +238,32 @@ describe("assistantRoutePolicy", () => {
|
||||||
expect(decision.livingReason).toBe("memory_recap_followup_detected");
|
expect(decision.livingReason).toBe("memory_recap_followup_detected");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("routes answer inspection follow-up over grounded selected-object answer to chat", () => {
|
||||||
|
const policy = buildPolicy({
|
||||||
|
findLastGroundedAddressAnswerDebug: () => ({ execution_lane: "address_query" }),
|
||||||
|
resolveAddressToolGateDecision: () => ({
|
||||||
|
runAddressLane: true,
|
||||||
|
decision: "run_address_lane",
|
||||||
|
reason: "address_mode_classifier_detected"
|
||||||
|
}),
|
||||||
|
detectAddressQuestionMode: () => ({ mode: "address_query", confidence: "high" }),
|
||||||
|
isAnswerInspectionFollowupOverGroundedAnswer: () => true
|
||||||
|
});
|
||||||
|
|
||||||
|
const decision = policy.resolveAssistantOrchestrationDecision({
|
||||||
|
rawUserMessage: "у тебя написано кто контрагент: рабочая станция - это ошибка?",
|
||||||
|
effectiveAddressUserMessage: "у тебя написано кто контрагент: рабочая станция - это ошибка?",
|
||||||
|
followupContext: { previous_intent: "inventory_sale_trace_for_item", previous_anchor_type: "item" },
|
||||||
|
llmPreDecomposeMeta: null,
|
||||||
|
useMock: false
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(decision.runAddressLane).toBe(false);
|
||||||
|
expect(decision.toolGateReason).toBe("answer_inspection_followup_over_grounded_answer");
|
||||||
|
expect(decision.livingMode).toBe("chat");
|
||||||
|
expect(decision.livingReason).toBe("answer_inspection_followup_detected");
|
||||||
|
});
|
||||||
|
|
||||||
it("routes organization fact lookup away from address lane even with follow-up context", () => {
|
it("routes organization fact lookup away from address lane even with follow-up context", () => {
|
||||||
const policy = buildPolicy({
|
const policy = buildPolicy({
|
||||||
hasDataRetrievalRequestSignal: () => true,
|
hasDataRetrievalRequestSignal: () => true,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue