ARCH: добавить answer adapter MCP discovery

This commit is contained in:
2026-04-20 10:05:43 +03:00
parent 4181bb9c9b
commit b4865f36b1
4 changed files with 423 additions and 0 deletions
@@ -0,0 +1,160 @@
import type { AssistantMcpDiscoveryPilotExecutionContract } from "./assistantMcpDiscoveryPilotExecutor";
export const ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION =
"assistant_mcp_discovery_answer_draft_v1" as const;
export type AssistantMcpDiscoveryAnswerMode =
| "confirmed_with_bounded_inference"
| "bounded_inference_only"
| "checked_sources_only"
| "needs_clarification"
| "blocked";
export interface AssistantMcpDiscoveryAnswerDraftContract {
schema_version: typeof ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION;
policy_owner: "assistantMcpDiscoveryAnswerAdapter";
answer_mode: AssistantMcpDiscoveryAnswerMode;
headline: string;
confirmed_lines: string[];
inference_lines: string[];
unknown_lines: string[];
limitation_lines: string[];
next_step_line: string | null;
internal_mechanics_allowed: false;
must_not_claim: string[];
reason_codes: string[];
}
function normalizeReasonCode(value: string): string | null {
const normalized = value
.trim()
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
.replace(/^_+|_+$/g, "")
.toLowerCase();
return normalized.length > 0 ? normalized.slice(0, 120) : null;
}
function pushReason(target: string[], value: string): void {
const normalized = normalizeReasonCode(value);
if (normalized && !target.includes(normalized)) {
target.push(normalized);
}
}
function uniqueStrings(values: string[]): string[] {
const result: string[] = [];
for (const value of values) {
const text = String(value ?? "").trim();
if (text && !result.includes(text)) {
result.push(text);
}
}
return result;
}
function isInternalMechanicsLine(value: string): boolean {
const text = value.toLowerCase();
return (
text.includes("primitive") ||
text.includes("query_documents") ||
text.includes("query_movements") ||
text.includes("resolve_entity_reference") ||
text.includes("probe_coverage") ||
text.includes("explain_evidence_basis") ||
text.includes("pilot_only_executes") ||
text.includes("runtime_") ||
text.includes("planner_") ||
text.includes("catalog_")
);
}
function userFacingLimitations(values: string[]): string[] {
return uniqueStrings(values).filter((value) => !isInternalMechanicsLine(value));
}
function modeFor(pilot: AssistantMcpDiscoveryPilotExecutionContract): AssistantMcpDiscoveryAnswerMode {
if (pilot.pilot_status === "blocked") {
return "blocked";
}
if (pilot.pilot_status === "skipped_needs_clarification") {
return "needs_clarification";
}
if (pilot.evidence.answer_permission === "confirmed_answer") {
return "confirmed_with_bounded_inference";
}
if (pilot.evidence.answer_permission === "bounded_inference") {
return "bounded_inference_only";
}
return "checked_sources_only";
}
function headlineFor(mode: AssistantMcpDiscoveryAnswerMode): string {
if (mode === "confirmed_with_bounded_inference") {
return "По данным 1С есть подтвержденная активность; длительность можно оценивать только как вывод из этих строк.";
}
if (mode === "bounded_inference_only") {
return "Точный факт не подтвержден, но есть ограниченная оценка по найденной активности в 1С.";
}
if (mode === "needs_clarification") {
return "Нужно уточнить контекст перед поиском в 1С.";
}
if (mode === "blocked") {
return "Поиск в 1С заблокирован runtime-политикой до выполнения.";
}
return "Я проверил доступный контур, но подтвержденного факта для ответа не получил.";
}
function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
if (mode === "needs_clarification") {
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
}
if (mode === "checked_sources_only" && pilot.query_limitations.length > 0) {
return "Можно повторить проверку после восстановления MCP-доступа или сузить вопрос до конкретного контрагента/периода.";
}
if (mode === "blocked") {
return "Нужно сначала снять policy/blocking причину, иначе данные 1С использовать нельзя.";
}
return null;
}
function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
const claims = [
"Do not claim legal registration age unless a legal registration source is confirmed.",
"Do not present inferred activity duration as a formally confirmed legal fact.",
"Do not expose MCP primitive names, query text, debug ids, or internal execution mechanics in the user answer.",
"Do not claim rows were checked when mcp_execution_performed=false."
];
if (pilot.evidence.confirmed_facts.length === 0) {
claims.push("Do not claim a confirmed business fact when confirmed_facts is empty.");
}
return claims;
}
export function buildAssistantMcpDiscoveryAnswerDraft(
pilot: AssistantMcpDiscoveryPilotExecutionContract
): AssistantMcpDiscoveryAnswerDraftContract {
const mode = modeFor(pilot);
const reasonCodes = [...pilot.reason_codes, ...pilot.evidence.reason_codes];
pushReason(reasonCodes, `answer_mode_${mode}`);
if (pilot.evidence.unknown_facts.length > 0) {
pushReason(reasonCodes, "answer_contains_unknown_fact_boundary");
}
if (pilot.evidence.inferred_facts.length > 0) {
pushReason(reasonCodes, "answer_contains_bounded_inference");
}
return {
schema_version: ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryAnswerAdapter",
answer_mode: mode,
headline: headlineFor(mode),
confirmed_lines: uniqueStrings(pilot.evidence.confirmed_facts),
inference_lines: uniqueStrings(pilot.evidence.inferred_facts),
unknown_lines: uniqueStrings(pilot.evidence.unknown_facts),
limitation_lines: userFacingLimitations([...pilot.query_limitations, ...pilot.evidence.query_limitations]),
next_step_line: nextStepFor(mode, pilot),
internal_mechanics_allowed: false,
must_not_claim: buildMustNotClaim(pilot),
reason_codes: uniqueStrings(reasonCodes)
};
}