АРЧ АП11 - Commit title: Добавить контрактный слой переходов и capability-деклараций ассистента
This commit is contained in:
@@ -47,4 +47,21 @@ describe("inventory warehouse anchor extraction", () => {
|
||||
expect(result.semantic_frame?.date_scope_kind).toBe("implicit_current");
|
||||
expect(result.semantic_frame?.date_basis_hint).toBe("implicit_current_snapshot");
|
||||
});
|
||||
it("does not materialize profanity tail as warehouse anchor in slang stock query", () => {
|
||||
const filters = extractAddressFilters(
|
||||
"ассистент, рассказывай что нам на складе ебаном лежит",
|
||||
"inventory_on_hand_as_of_date"
|
||||
).extracted_filters;
|
||||
|
||||
expect(filters.warehouse).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not materialize repair phrasing as warehouse anchor in stock follow-up", () => {
|
||||
const filters = extractAddressFilters(
|
||||
"остатки на складе какие имелось в виду",
|
||||
"inventory_on_hand_as_of_date"
|
||||
).extracted_filters;
|
||||
|
||||
expect(filters.warehouse).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5038,6 +5038,11 @@ it("routes old purchase residue questions to aging-by-purchase-date", () => {
|
||||
expect(result.intent).toBe("inventory_sale_trace_for_item");
|
||||
});
|
||||
|
||||
it("routes colloquial buyer wording with 'впарили' to inventory sale trace intent", () => {
|
||||
const result = resolveAddressIntent("Кому мы впарили этот товар Шкаф картотечный?");
|
||||
expect(result.intent).toBe("inventory_sale_trace_for_item");
|
||||
});
|
||||
|
||||
it("keeps inventory provenance wording out of inventory-on-hand routing", () => {
|
||||
const result = resolveAddressIntent("От кого куплен товар Шкаф картоотечный и когда был куплен?");
|
||||
expect(result.intent).toBe("inventory_purchase_provenance_for_item");
|
||||
|
||||
@@ -1064,4 +1064,65 @@ describe("assistant living chat mode", () => {
|
||||
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(1);
|
||||
expect(chatClient.chat).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
it("does not treat short emotional reaction as organization-selection confirmation", async () => {
|
||||
const normalizer = {
|
||||
normalize: vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
trace_id: "norm-chat-affective-reaction",
|
||||
prompt_version: "normalizer_v2_0_2",
|
||||
schema_version: "v2_0_2",
|
||||
normalized: null,
|
||||
validation: { passed: false, errors: ["mock"] },
|
||||
route_hint_summary: null,
|
||||
raw_model_output: {},
|
||||
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
|
||||
latency_ms: 1,
|
||||
request_count_for_case: 1
|
||||
})
|
||||
} as any;
|
||||
|
||||
const sessions = new AssistantSessionStore();
|
||||
const sessionId = "asst-living-chat-affective-reaction";
|
||||
sessions.ensureSession(sessionId);
|
||||
sessions.appendItem(sessionId, {
|
||||
message_id: "msg-seed-selected-org",
|
||||
session_id: sessionId,
|
||||
role: "assistant",
|
||||
text: "Отлично, фиксирую рабочую организацию: ООО Альтернатива Плюс.",
|
||||
reply_type: "factual_with_explanation",
|
||||
created_at: new Date().toISOString(),
|
||||
trace_id: "chat-seed-selected-org",
|
||||
debug: {
|
||||
assistant_known_organizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд"],
|
||||
assistant_selected_organization: "ООО Альтернатива Плюс",
|
||||
assistant_active_organization: "ООО Альтернатива Плюс"
|
||||
}
|
||||
} as any);
|
||||
|
||||
const addressQueryService = {
|
||||
tryHandle: vi.fn().mockResolvedValue({ handled: false })
|
||||
} as any;
|
||||
const chatClient = {
|
||||
chat: vi.fn().mockResolvedValue({
|
||||
raw: { id: "chat-affective-reaction" },
|
||||
outputText: "Понимаю, это выглядит неприятно. Давай разберем следующий шаг.",
|
||||
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
|
||||
})
|
||||
} as any;
|
||||
|
||||
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
|
||||
const response = await service.handleMessage({
|
||||
session_id: sessionId,
|
||||
user_message: "ну ахуеть",
|
||||
llmProvider: "local",
|
||||
model: "qwen2.5",
|
||||
useMock: false
|
||||
} as any);
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
expect(response.reply_type).toBe("factual_with_explanation");
|
||||
expect(response.debug?.living_chat_response_source).not.toBe("deterministic_data_scope_selection_contract");
|
||||
expect(String(response.assistant_reply)).not.toContain("фиксирую рабочую организацию");
|
||||
expect(chatClient.chat).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveAddressCapabilityRouteDecision } from "../src/services/addressCapabilityPolicy";
|
||||
import {
|
||||
getAssistantCapabilityContract,
|
||||
getAssistantCapabilityContractByIntent,
|
||||
getAssistantTransitionContract,
|
||||
listAssistantTransitionContracts,
|
||||
listInventoryCapabilityContracts
|
||||
} from "../src/services/assistantRuntimeContractRegistry";
|
||||
|
||||
describe("assistant runtime contract registry", () => {
|
||||
it("declares the architecture turnaround transition set T1-T10", () => {
|
||||
const transitions = listAssistantTransitionContracts();
|
||||
expect(transitions.map((item) => item.transition_id)).toEqual(["T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8", "T9", "T10"]);
|
||||
|
||||
const ids = new Set(transitions.map((item) => item.transition_id));
|
||||
expect(ids.size).toBe(10);
|
||||
expect(transitions.every((item) => item.schema_version === "assistant_runtime_contracts_v1")).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps selected-object action follow-ups object-scoped instead of generic-chat scoped", () => {
|
||||
const transition = getAssistantTransitionContract("T4");
|
||||
expect(transition).not.toBeNull();
|
||||
expect(transition?.required_prior_state).toContain("selected_object_frame_state");
|
||||
expect(transition?.allowed_carryover_depth).toBe("object_only");
|
||||
expect(transition?.forbidden_carryover).toContain("generic_chat_fallback");
|
||||
expect(transition?.forbidden_carryover).toContain("object_focus_reset");
|
||||
});
|
||||
|
||||
it("declares meta follow-up as answer-object reuse, not blind exact-route replay", () => {
|
||||
const transition = getAssistantTransitionContract("T8");
|
||||
expect(transition).not.toBeNull();
|
||||
expect(transition?.required_prior_state).toEqual(["answer_context_state", "coverage_gate_state"]);
|
||||
expect(transition?.allowed_carryover_depth).toBe("meta_only");
|
||||
expect(transition?.state_mutations).toContain("reuse_answer_object_without_blind_replay");
|
||||
expect(transition?.forbidden_carryover).toContain("blind_exact_route_replay");
|
||||
expect(transition?.expected_answer_mode).toBe("meta");
|
||||
});
|
||||
|
||||
it("keeps pilot inventory capability ids aligned with the current address capability policy", () => {
|
||||
for (const contract of listInventoryCapabilityContracts()) {
|
||||
for (const intent of contract.intent_ids) {
|
||||
const decision = resolveAddressCapabilityRouteDecision(intent);
|
||||
expect(decision.capability_id).toBe(contract.capability_id);
|
||||
expect(decision.capability_route_mode).toBe("exact");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("declares root inventory snapshot as root-capable and focus-object-free", () => {
|
||||
const contract = getAssistantCapabilityContract("confirmed_inventory_on_hand_as_of_date");
|
||||
expect(contract).not.toBeNull();
|
||||
expect(contract?.entry_modes).toEqual(["root_entry", "root_followup", "clarification_resume"]);
|
||||
expect(contract?.supported_transition_classes).toEqual(["T1", "T2", "T7"]);
|
||||
expect(contract?.requires_focus_object).toBe(false);
|
||||
expect(contract?.result_shape).toBe("item_list_with_quantity_cost_warehouse_organization");
|
||||
expect(contract?.required_scenario_families).toContain("colloquial");
|
||||
});
|
||||
|
||||
it("declares selected-item provenance as focus-object and bundle-aware", () => {
|
||||
const contract = getAssistantCapabilityContractByIntent("inventory_purchase_provenance_for_item");
|
||||
expect(contract?.capability_id).toBe("inventory_inventory_purchase_provenance_for_item");
|
||||
expect(contract?.requires_focus_object).toBe(true);
|
||||
expect(contract?.accepted_focus_object_kinds).toEqual(["inventory_item", "item"]);
|
||||
expect(contract?.supported_transition_classes).toEqual(["T3", "T4", "T5", "T7"]);
|
||||
expect(contract?.required_anchors).toEqual(["item"]);
|
||||
expect(contract?.bundle_reuse_policy).toBe("provenance_bundle_preferred");
|
||||
expect(contract?.anchor_admissibility_rules).toContain("confirmed_focus_object_beats_semantic_hint");
|
||||
expect(contract?.required_scenario_families).toContain("ui_selected_object_colloquial");
|
||||
expect(contract?.required_scenario_families).toContain("pronoun_followup");
|
||||
});
|
||||
|
||||
it("keeps truth semantics outside answer wording for every pilot inventory capability", () => {
|
||||
for (const contract of listInventoryCapabilityContracts()) {
|
||||
expect(contract.coverage_gate_behavior).toBe("partial_or_blocked_if_evidence_insufficient");
|
||||
expect(contract.truth_mode_fallbacks).toEqual(["limited", "clarification_required", "unsupported"]);
|
||||
expect(contract.blocked_reason_codes).toEqual(
|
||||
expect.arrayContaining(["missing_anchor", "route_expectation_failure", "execution_error", "insufficient_evidence"])
|
||||
);
|
||||
expect(contract.route_expectation_failure_behavior).toBe("blocked_route_expectation_failure");
|
||||
expect(contract.execution_error_behavior).toBe("blocked_execution_error");
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user