АРЧ АП11 - Включить response guard для capability binding ассистента
This commit is contained in:
parent
1c928a5d68
commit
918feaf06e
|
|
@ -2,6 +2,7 @@
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.runAssistantAddressLaneResponseRuntime = runAssistantAddressLaneResponseRuntime;
|
exports.runAssistantAddressLaneResponseRuntime = runAssistantAddressLaneResponseRuntime;
|
||||||
const assistantAddressTurnFinalizeRuntimeAdapter_1 = require("./assistantAddressTurnFinalizeRuntimeAdapter");
|
const assistantAddressTurnFinalizeRuntimeAdapter_1 = require("./assistantAddressTurnFinalizeRuntimeAdapter");
|
||||||
|
const assistantCapabilityBindingResponseGuard_1 = require("./assistantCapabilityBindingResponseGuard");
|
||||||
const assistantCapabilityRuntimeBindingAdapter_1 = require("./assistantCapabilityRuntimeBindingAdapter");
|
const assistantCapabilityRuntimeBindingAdapter_1 = require("./assistantCapabilityRuntimeBindingAdapter");
|
||||||
const assistantRuntimeContractResolver_1 = require("./assistantRuntimeContractResolver");
|
const assistantRuntimeContractResolver_1 = require("./assistantRuntimeContractResolver");
|
||||||
const assistantStateTransitionRuntimeAdapter_1 = require("./assistantStateTransitionRuntimeAdapter");
|
const assistantStateTransitionRuntimeAdapter_1 = require("./assistantStateTransitionRuntimeAdapter");
|
||||||
|
|
@ -207,14 +208,23 @@ function runAssistantAddressLaneResponseRuntime(input) {
|
||||||
addressRuntimeMeta: input.llmPreDecomposeMeta,
|
addressRuntimeMeta: input.llmPreDecomposeMeta,
|
||||||
replyType: normalizeAddressReplyType(input.addressLane.reply_type)
|
replyType: normalizeAddressReplyType(input.addressLane.reply_type)
|
||||||
});
|
});
|
||||||
|
const guardedResponse = (0, assistantCapabilityBindingResponseGuard_1.applyAssistantCapabilityBindingResponseGuard)({
|
||||||
|
assistantReply: safeAddressReply,
|
||||||
|
replyType: normalizeAddressReplyType(input.addressLane.reply_type),
|
||||||
|
capabilityBinding: debugWithCapabilityBinding.assistant_capability_binding_v1
|
||||||
|
});
|
||||||
|
const debugWithResponseGuard = {
|
||||||
|
...debugWithCapabilityBinding,
|
||||||
|
capability_binding_response_guard: guardedResponse.audit
|
||||||
|
};
|
||||||
const finalization = finalizeAddressTurnSafe({
|
const finalization = finalizeAddressTurnSafe({
|
||||||
sessionId: input.sessionId,
|
sessionId: input.sessionId,
|
||||||
userMessage: input.userMessage,
|
userMessage: input.userMessage,
|
||||||
effectiveAddressUserMessage: input.effectiveAddressUserMessage,
|
effectiveAddressUserMessage: input.effectiveAddressUserMessage,
|
||||||
assistantReply: safeAddressReply,
|
assistantReply: guardedResponse.assistantReply,
|
||||||
replyType: normalizeAddressReplyType(input.addressLane.reply_type),
|
replyType: guardedResponse.replyType,
|
||||||
addressLaneDebug: normalizeAddressLaneDebug(input.addressLane.debug),
|
addressLaneDebug: normalizeAddressLaneDebug(input.addressLane.debug),
|
||||||
debug: debugWithCapabilityBinding,
|
debug: debugWithResponseGuard,
|
||||||
carryoverMeta: normalizeCarryoverMeta(input.carryoverMeta),
|
carryoverMeta: normalizeCarryoverMeta(input.carryoverMeta),
|
||||||
llmPreDecomposeMeta: normalizeLlmPreDecomposeMeta(input.llmPreDecomposeMeta),
|
llmPreDecomposeMeta: normalizeLlmPreDecomposeMeta(input.llmPreDecomposeMeta),
|
||||||
appendItem: input.appendItem,
|
appendItem: input.appendItem,
|
||||||
|
|
@ -226,6 +236,6 @@ function runAssistantAddressLaneResponseRuntime(input) {
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
response: finalization.response,
|
response: finalization.response,
|
||||||
debug: debugWithCapabilityBinding
|
debug: debugWithResponseGuard
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
77
llm_normalizer/backend/dist/services/assistantCapabilityBindingResponseGuard.js
vendored
Normal file
77
llm_normalizer/backend/dist/services/assistantCapabilityBindingResponseGuard.js
vendored
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.applyAssistantCapabilityBindingResponseGuard = applyAssistantCapabilityBindingResponseGuard;
|
||||||
|
function formatMissingAnchors(anchors) {
|
||||||
|
if (anchors.length === 0) {
|
||||||
|
return "нужный объект, период или организацию";
|
||||||
|
}
|
||||||
|
return anchors.join(", ");
|
||||||
|
}
|
||||||
|
function buildClarificationReply(binding) {
|
||||||
|
return [
|
||||||
|
"Нужно уточнение, чтобы не подставить неподтвержденный объект в расчет.",
|
||||||
|
`Не хватает: ${formatMissingAnchors(binding.missing_anchors)}.`,
|
||||||
|
"Уточните это, и я продолжу тот же сценарий."
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
function buildBlockedReply(binding) {
|
||||||
|
const reasons = binding.violations.length > 0 ? binding.violations.join(", ") : "runtime_binding_blocked";
|
||||||
|
return [
|
||||||
|
"Не могу надежно подтвердить ответ в текущем контексте.",
|
||||||
|
`Проверка сценария остановила ответ: ${reasons}.`,
|
||||||
|
"Лучше уточнить объект или перезапустить вопрос от корневого запроса, чем выдавать это как подтвержденный факт."
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
function applyAssistantCapabilityBindingResponseGuard(input) {
|
||||||
|
const binding = input.capabilityBinding;
|
||||||
|
const baseAudit = {
|
||||||
|
schema_version: "assistant_capability_binding_response_guard_v1",
|
||||||
|
guard_owner: "assistantCapabilityBindingResponseGuard",
|
||||||
|
applied: false,
|
||||||
|
action: binding?.binding_action ?? "none",
|
||||||
|
original_reply_type: input.replyType,
|
||||||
|
guarded_reply_type: input.replyType,
|
||||||
|
reason_codes: binding?.reason_codes ?? []
|
||||||
|
};
|
||||||
|
if (!binding || binding.binding_action === "allow" || binding.binding_action === "observe_only") {
|
||||||
|
return {
|
||||||
|
assistantReply: input.assistantReply,
|
||||||
|
replyType: input.replyType,
|
||||||
|
audit: baseAudit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (binding.binding_action === "clarify") {
|
||||||
|
return {
|
||||||
|
assistantReply: buildClarificationReply(binding),
|
||||||
|
replyType: "partial_coverage",
|
||||||
|
audit: {
|
||||||
|
...baseAudit,
|
||||||
|
applied: true,
|
||||||
|
guarded_reply_type: "partial_coverage",
|
||||||
|
reason_codes: [...baseAudit.reason_codes, "capability_binding_guard_clarification_reply"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (binding.binding_action === "block") {
|
||||||
|
return {
|
||||||
|
assistantReply: buildBlockedReply(binding),
|
||||||
|
replyType: "partial_coverage",
|
||||||
|
audit: {
|
||||||
|
...baseAudit,
|
||||||
|
applied: true,
|
||||||
|
guarded_reply_type: "partial_coverage",
|
||||||
|
reason_codes: [...baseAudit.reason_codes, "capability_binding_guard_blocked_reply"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
assistantReply: input.assistantReply,
|
||||||
|
replyType: input.replyType === "factual" ? "partial_coverage" : input.replyType,
|
||||||
|
audit: {
|
||||||
|
...baseAudit,
|
||||||
|
applied: input.replyType === "factual",
|
||||||
|
guarded_reply_type: input.replyType === "factual" ? "partial_coverage" : input.replyType,
|
||||||
|
reason_codes: [...baseAudit.reason_codes, "capability_binding_guard_limited_reply_type"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
type AddressLlmPreDecomposeMetaLogInput,
|
type AddressLlmPreDecomposeMetaLogInput,
|
||||||
type FinalizeAssistantAddressTurnInput
|
type FinalizeAssistantAddressTurnInput
|
||||||
} from "./assistantAddressTurnFinalizeRuntimeAdapter";
|
} from "./assistantAddressTurnFinalizeRuntimeAdapter";
|
||||||
|
import { applyAssistantCapabilityBindingResponseGuard } from "./assistantCapabilityBindingResponseGuard";
|
||||||
import { attachAssistantCapabilityRuntimeBinding } from "./assistantCapabilityRuntimeBindingAdapter";
|
import { attachAssistantCapabilityRuntimeBinding } from "./assistantCapabilityRuntimeBindingAdapter";
|
||||||
import { attachAssistantRuntimeContractShadow } from "./assistantRuntimeContractResolver";
|
import { attachAssistantRuntimeContractShadow } from "./assistantRuntimeContractResolver";
|
||||||
import { attachAssistantStateTransition } from "./assistantStateTransitionRuntimeAdapter";
|
import { attachAssistantStateTransition } from "./assistantStateTransitionRuntimeAdapter";
|
||||||
|
|
@ -266,14 +267,23 @@ export function runAssistantAddressLaneResponseRuntime<ResponseType = AssistantM
|
||||||
addressRuntimeMeta: input.llmPreDecomposeMeta,
|
addressRuntimeMeta: input.llmPreDecomposeMeta,
|
||||||
replyType: normalizeAddressReplyType(input.addressLane.reply_type)
|
replyType: normalizeAddressReplyType(input.addressLane.reply_type)
|
||||||
});
|
});
|
||||||
|
const guardedResponse = applyAssistantCapabilityBindingResponseGuard({
|
||||||
|
assistantReply: safeAddressReply,
|
||||||
|
replyType: normalizeAddressReplyType(input.addressLane.reply_type),
|
||||||
|
capabilityBinding: debugWithCapabilityBinding.assistant_capability_binding_v1
|
||||||
|
});
|
||||||
|
const debugWithResponseGuard = {
|
||||||
|
...debugWithCapabilityBinding,
|
||||||
|
capability_binding_response_guard: guardedResponse.audit
|
||||||
|
};
|
||||||
const finalization = finalizeAddressTurnSafe({
|
const finalization = finalizeAddressTurnSafe({
|
||||||
sessionId: input.sessionId,
|
sessionId: input.sessionId,
|
||||||
userMessage: input.userMessage,
|
userMessage: input.userMessage,
|
||||||
effectiveAddressUserMessage: input.effectiveAddressUserMessage,
|
effectiveAddressUserMessage: input.effectiveAddressUserMessage,
|
||||||
assistantReply: safeAddressReply,
|
assistantReply: guardedResponse.assistantReply,
|
||||||
replyType: normalizeAddressReplyType(input.addressLane.reply_type),
|
replyType: guardedResponse.replyType,
|
||||||
addressLaneDebug: normalizeAddressLaneDebug(input.addressLane.debug),
|
addressLaneDebug: normalizeAddressLaneDebug(input.addressLane.debug),
|
||||||
debug: debugWithCapabilityBinding,
|
debug: debugWithResponseGuard,
|
||||||
carryoverMeta: normalizeCarryoverMeta(input.carryoverMeta),
|
carryoverMeta: normalizeCarryoverMeta(input.carryoverMeta),
|
||||||
llmPreDecomposeMeta: normalizeLlmPreDecomposeMeta(input.llmPreDecomposeMeta),
|
llmPreDecomposeMeta: normalizeLlmPreDecomposeMeta(input.llmPreDecomposeMeta),
|
||||||
appendItem: input.appendItem,
|
appendItem: input.appendItem,
|
||||||
|
|
@ -286,6 +296,6 @@ export function runAssistantAddressLaneResponseRuntime<ResponseType = AssistantM
|
||||||
|
|
||||||
return {
|
return {
|
||||||
response: finalization.response as ResponseType,
|
response: finalization.response as ResponseType,
|
||||||
debug: debugWithCapabilityBinding
|
debug: debugWithResponseGuard
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
import type { AssistantReplyType } from "../types/assistant";
|
||||||
|
import type {
|
||||||
|
AssistantCapabilityBindingAction,
|
||||||
|
AssistantCapabilityRuntimeBindingContract
|
||||||
|
} from "../types/assistantRuntimeContracts";
|
||||||
|
|
||||||
|
export interface ApplyAssistantCapabilityBindingResponseGuardInput {
|
||||||
|
assistantReply: string;
|
||||||
|
replyType: AssistantReplyType;
|
||||||
|
capabilityBinding: AssistantCapabilityRuntimeBindingContract | null | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantCapabilityBindingResponseGuardAudit {
|
||||||
|
schema_version: "assistant_capability_binding_response_guard_v1";
|
||||||
|
guard_owner: "assistantCapabilityBindingResponseGuard";
|
||||||
|
applied: boolean;
|
||||||
|
action: AssistantCapabilityBindingAction | "none";
|
||||||
|
original_reply_type: AssistantReplyType;
|
||||||
|
guarded_reply_type: AssistantReplyType;
|
||||||
|
reason_codes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApplyAssistantCapabilityBindingResponseGuardOutput {
|
||||||
|
assistantReply: string;
|
||||||
|
replyType: AssistantReplyType;
|
||||||
|
audit: AssistantCapabilityBindingResponseGuardAudit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMissingAnchors(anchors: string[]): string {
|
||||||
|
if (anchors.length === 0) {
|
||||||
|
return "нужный объект, период или организацию";
|
||||||
|
}
|
||||||
|
return anchors.join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildClarificationReply(binding: AssistantCapabilityRuntimeBindingContract): string {
|
||||||
|
return [
|
||||||
|
"Нужно уточнение, чтобы не подставить неподтвержденный объект в расчет.",
|
||||||
|
`Не хватает: ${formatMissingAnchors(binding.missing_anchors)}.`,
|
||||||
|
"Уточните это, и я продолжу тот же сценарий."
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBlockedReply(binding: AssistantCapabilityRuntimeBindingContract): string {
|
||||||
|
const reasons = binding.violations.length > 0 ? binding.violations.join(", ") : "runtime_binding_blocked";
|
||||||
|
return [
|
||||||
|
"Не могу надежно подтвердить ответ в текущем контексте.",
|
||||||
|
`Проверка сценария остановила ответ: ${reasons}.`,
|
||||||
|
"Лучше уточнить объект или перезапустить вопрос от корневого запроса, чем выдавать это как подтвержденный факт."
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyAssistantCapabilityBindingResponseGuard(
|
||||||
|
input: ApplyAssistantCapabilityBindingResponseGuardInput
|
||||||
|
): ApplyAssistantCapabilityBindingResponseGuardOutput {
|
||||||
|
const binding = input.capabilityBinding;
|
||||||
|
const baseAudit: AssistantCapabilityBindingResponseGuardAudit = {
|
||||||
|
schema_version: "assistant_capability_binding_response_guard_v1",
|
||||||
|
guard_owner: "assistantCapabilityBindingResponseGuard",
|
||||||
|
applied: false,
|
||||||
|
action: binding?.binding_action ?? "none",
|
||||||
|
original_reply_type: input.replyType,
|
||||||
|
guarded_reply_type: input.replyType,
|
||||||
|
reason_codes: binding?.reason_codes ?? []
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!binding || binding.binding_action === "allow" || binding.binding_action === "observe_only") {
|
||||||
|
return {
|
||||||
|
assistantReply: input.assistantReply,
|
||||||
|
replyType: input.replyType,
|
||||||
|
audit: baseAudit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.binding_action === "clarify") {
|
||||||
|
return {
|
||||||
|
assistantReply: buildClarificationReply(binding),
|
||||||
|
replyType: "partial_coverage",
|
||||||
|
audit: {
|
||||||
|
...baseAudit,
|
||||||
|
applied: true,
|
||||||
|
guarded_reply_type: "partial_coverage",
|
||||||
|
reason_codes: [...baseAudit.reason_codes, "capability_binding_guard_clarification_reply"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.binding_action === "block") {
|
||||||
|
return {
|
||||||
|
assistantReply: buildBlockedReply(binding),
|
||||||
|
replyType: "partial_coverage",
|
||||||
|
audit: {
|
||||||
|
...baseAudit,
|
||||||
|
applied: true,
|
||||||
|
guarded_reply_type: "partial_coverage",
|
||||||
|
reason_codes: [...baseAudit.reason_codes, "capability_binding_guard_blocked_reply"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
assistantReply: input.assistantReply,
|
||||||
|
replyType: input.replyType === "factual" ? "partial_coverage" : input.replyType,
|
||||||
|
audit: {
|
||||||
|
...baseAudit,
|
||||||
|
applied: input.replyType === "factual",
|
||||||
|
guarded_reply_type: input.replyType === "factual" ? "partial_coverage" : input.replyType,
|
||||||
|
reason_codes: [...baseAudit.reason_codes, "capability_binding_guard_limited_reply_type"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -479,6 +479,7 @@ export interface AssistantDebugPayload {
|
||||||
capability_binding_status?: AssistantCapabilityBindingStatus;
|
capability_binding_status?: AssistantCapabilityBindingStatus;
|
||||||
capability_binding_action?: AssistantCapabilityBindingAction;
|
capability_binding_action?: AssistantCapabilityBindingAction;
|
||||||
capability_binding_violations?: AssistantCapabilityBindingViolation[];
|
capability_binding_violations?: AssistantCapabilityBindingViolation[];
|
||||||
|
capability_binding_response_guard?: Record<string, unknown>;
|
||||||
execution_lane?: "address_query" | "deep_analysis";
|
execution_lane?: "address_query" | "deep_analysis";
|
||||||
llm_decomposition_applied?: boolean;
|
llm_decomposition_applied?: boolean;
|
||||||
llm_decomposition_attempted?: boolean;
|
llm_decomposition_attempted?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,10 @@ describe("assistant address lane response runtime adapter", () => {
|
||||||
}),
|
}),
|
||||||
capability_binding_contract: expect.objectContaining({
|
capability_binding_contract: expect.objectContaining({
|
||||||
schema_version: "assistant_capability_runtime_binding_v1"
|
schema_version: "assistant_capability_runtime_binding_v1"
|
||||||
|
}),
|
||||||
|
capability_binding_response_guard: expect.objectContaining({
|
||||||
|
schema_version: "assistant_capability_binding_response_guard_v1",
|
||||||
|
applied: false
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -155,9 +159,67 @@ describe("assistant address lane response runtime adapter", () => {
|
||||||
effective_carryover_depth: "none",
|
effective_carryover_depth: "none",
|
||||||
capability_binding_status: "not_applicable",
|
capability_binding_status: "not_applicable",
|
||||||
capability_binding_action: "observe_only",
|
capability_binding_action: "observe_only",
|
||||||
capability_binding_violations: []
|
capability_binding_violations: [],
|
||||||
|
capability_binding_response_guard: expect.objectContaining({
|
||||||
|
applied: false,
|
||||||
|
action: "observe_only"
|
||||||
|
})
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(runtime.response).toEqual({ ok: true });
|
expect(runtime.response).toEqual({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("guards blocked capability binding responses before finalizing the turn", () => {
|
||||||
|
const finalizeAddressTurn = vi.fn(() => ({
|
||||||
|
response: {
|
||||||
|
ok: true
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
runAssistantAddressLaneResponseRuntime({
|
||||||
|
sessionId: "asst-3",
|
||||||
|
userMessage: "кто поставщик?",
|
||||||
|
effectiveAddressUserMessage: "кто поставщик?",
|
||||||
|
addressLane: {
|
||||||
|
handled: true,
|
||||||
|
reply_text: "unsafe factual answer",
|
||||||
|
reply_type: "factual",
|
||||||
|
debug: {
|
||||||
|
capability_id: "inventory_inventory_purchase_provenance_for_item",
|
||||||
|
limited_reason_category: "missing_anchor",
|
||||||
|
missing_required_filters: ["item"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
knownOrganizations: [],
|
||||||
|
activeOrganization: null,
|
||||||
|
sanitizeOutgoingAssistantText: (text) => String(text ?? ""),
|
||||||
|
buildAddressDebugPayload: (addressDebug) => ({ ...(addressDebug as Record<string, unknown>) }),
|
||||||
|
buildAddressFollowupOffer: () => null,
|
||||||
|
mergeKnownOrganizations: (items) => items,
|
||||||
|
toNonEmptyString: (value) => (typeof value === "string" && value.trim() ? value.trim() : null),
|
||||||
|
appendItem: () => {},
|
||||||
|
getSession: () => ({ session_id: "asst-3", updated_at: "", items: [], investigation_state: null } as any),
|
||||||
|
persistSession: () => {},
|
||||||
|
cloneConversation: (items) => items,
|
||||||
|
logEvent: () => {},
|
||||||
|
messageIdFactory: () => "msg-3",
|
||||||
|
finalizeAddressTurn
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(finalizeAddressTurn).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
assistantReply: expect.stringContaining("Нужно уточнение"),
|
||||||
|
replyType: "partial_coverage",
|
||||||
|
debug: expect.objectContaining({
|
||||||
|
capability_binding_status: "blocked",
|
||||||
|
capability_binding_action: "clarify",
|
||||||
|
capability_binding_response_guard: expect.objectContaining({
|
||||||
|
applied: true,
|
||||||
|
action: "clarify",
|
||||||
|
guarded_reply_type: "partial_coverage"
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { applyAssistantCapabilityBindingResponseGuard } from "../src/services/assistantCapabilityBindingResponseGuard";
|
||||||
|
import type { AssistantCapabilityRuntimeBindingContract } from "../src/types/assistantRuntimeContracts";
|
||||||
|
|
||||||
|
function binding(overrides: Partial<AssistantCapabilityRuntimeBindingContract>): AssistantCapabilityRuntimeBindingContract {
|
||||||
|
return {
|
||||||
|
schema_version: "assistant_capability_runtime_binding_v1",
|
||||||
|
binding_owner: "assistantCapabilityRuntimeBindingAdapter",
|
||||||
|
capability_id: "inventory_inventory_purchase_provenance_for_item",
|
||||||
|
capability_contract_id: "inventory_inventory_purchase_provenance_for_item",
|
||||||
|
binding_status: "bound",
|
||||||
|
binding_action: "allow",
|
||||||
|
runtime_lane_expected: "address_exact",
|
||||||
|
runtime_lane_observed: "address_exact",
|
||||||
|
execution_adapter: "AddressQueryService",
|
||||||
|
transition_id: "T4",
|
||||||
|
transition_allowed: true,
|
||||||
|
required_anchors: ["item"],
|
||||||
|
provided_anchors: ["item"],
|
||||||
|
missing_anchors: [],
|
||||||
|
requires_focus_object: true,
|
||||||
|
focus_object_binding_status: "inferred_from_anchor",
|
||||||
|
result_shape: "supplier_purchase_provenance_trace",
|
||||||
|
answer_object_shape: "inventory_provenance_bundle",
|
||||||
|
truth_gate_behavior: "partial_or_blocked_if_evidence_insufficient",
|
||||||
|
truth_fallback_allowed: true,
|
||||||
|
answer_shape_compatible: true,
|
||||||
|
violations: [],
|
||||||
|
reason_codes: ["binding_status_bound"],
|
||||||
|
...overrides
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("assistant capability binding response guard", () => {
|
||||||
|
it("leaves allowed answers unchanged", () => {
|
||||||
|
const output = applyAssistantCapabilityBindingResponseGuard({
|
||||||
|
assistantReply: "answer",
|
||||||
|
replyType: "factual",
|
||||||
|
capabilityBinding: binding({})
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.assistantReply).toBe("answer");
|
||||||
|
expect(output.replyType).toBe("factual");
|
||||||
|
expect(output.audit.applied).toBe(false);
|
||||||
|
expect(output.audit.guarded_reply_type).toBe("factual");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("turns missing required anchors into clarification replies", () => {
|
||||||
|
const output = applyAssistantCapabilityBindingResponseGuard({
|
||||||
|
assistantReply: "unsafe answer",
|
||||||
|
replyType: "factual",
|
||||||
|
capabilityBinding: binding({
|
||||||
|
binding_status: "blocked",
|
||||||
|
binding_action: "clarify",
|
||||||
|
missing_anchors: ["item"],
|
||||||
|
focus_object_binding_status: "missing",
|
||||||
|
violations: ["required_anchor_missing", "focus_object_required_but_unbound"],
|
||||||
|
reason_codes: ["required_anchor_missing"]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.replyType).toBe("partial_coverage");
|
||||||
|
expect(output.assistantReply).toContain("Нужно уточнение");
|
||||||
|
expect(output.assistantReply).toContain("item");
|
||||||
|
expect(output.audit.applied).toBe(true);
|
||||||
|
expect(output.audit.reason_codes).toContain("capability_binding_guard_clarification_reply");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("turns blocked incompatible transitions into bounded replies", () => {
|
||||||
|
const output = applyAssistantCapabilityBindingResponseGuard({
|
||||||
|
assistantReply: "unsafe answer",
|
||||||
|
replyType: "factual",
|
||||||
|
capabilityBinding: binding({
|
||||||
|
binding_status: "blocked",
|
||||||
|
binding_action: "block",
|
||||||
|
violations: ["transition_not_supported_by_capability"],
|
||||||
|
reason_codes: ["transition_not_supported_by_capability"]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.replyType).toBe("partial_coverage");
|
||||||
|
expect(output.assistantReply).toContain("Не могу надежно подтвердить");
|
||||||
|
expect(output.assistantReply).toContain("transition_not_supported_by_capability");
|
||||||
|
expect(output.audit.applied).toBe(true);
|
||||||
|
expect(output.audit.reason_codes).toContain("capability_binding_guard_blocked_reply");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("downgrades factual reply type for limited bound answers without rewriting text", () => {
|
||||||
|
const output = applyAssistantCapabilityBindingResponseGuard({
|
||||||
|
assistantReply: "limited answer",
|
||||||
|
replyType: "factual",
|
||||||
|
capabilityBinding: binding({
|
||||||
|
binding_status: "bound_with_limits",
|
||||||
|
binding_action: "limit",
|
||||||
|
reason_codes: ["truth_mode_limited"]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.assistantReply).toBe("limited answer");
|
||||||
|
expect(output.replyType).toBe("partial_coverage");
|
||||||
|
expect(output.audit.applied).toBe(true);
|
||||||
|
expect(output.audit.reason_codes).toContain("capability_binding_guard_limited_reply_type");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue