ARCH: добавить candidate ответа MCP discovery

This commit is contained in:
2026-04-20 11:58:12 +03:00
parent f0d7e81ec0
commit 58c1358960
4 changed files with 497 additions and 0 deletions
@@ -0,0 +1,204 @@
import type { AssistantMcpDiscoveryRuntimeEntryPointContract } from "./assistantMcpDiscoveryRuntimeEntryPoint";
export const ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION =
"assistant_mcp_discovery_response_candidate_v1" as const;
export type AssistantMcpDiscoveryResponseCandidateStatus =
| "ready_for_guarded_use"
| "clarification_candidate"
| "checked_sources_only_candidate"
| "not_applicable"
| "blocked"
| "unsupported";
export interface AssistantMcpDiscoveryResponseCandidateContract {
schema_version: typeof ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION;
policy_owner: "assistantMcpDiscoveryResponseCandidate";
candidate_status: AssistantMcpDiscoveryResponseCandidateStatus;
hot_runtime_wired: false;
reply_type: "partial_coverage" | "clarification_required" | "no_grounded_answer";
reply_text: string | null;
eligible_for_future_hot_runtime: boolean;
must_keep_internal_mechanics_hidden: true;
reason_codes: string[];
}
function toRecordObject(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value as Record<string, unknown>;
}
function toNonEmptyString(value: unknown): string | null {
if (value === null || value === undefined) {
return null;
}
const text = String(value).trim();
return text.length > 0 ? text : null;
}
function toStringList(value: unknown): string[] {
if (!Array.isArray(value)) {
return [];
}
return value.map((item) => toNonEmptyString(item)).filter((item): item is string => Boolean(item));
}
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 hasInternalMechanics(value: string): boolean {
const text = value.toLowerCase();
return (
text.includes("query_documents") ||
text.includes("query_movements") ||
text.includes("primitive") ||
text.includes("runtime_") ||
text.includes("planner_") ||
text.includes("catalog_") ||
text.includes("select ")
);
}
function userFacingLines(values: string[]): string[] {
return uniqueStrings(values).filter((line) => !hasInternalMechanics(line));
}
function localizeLine(value: string): string {
const counterpartyMatch = value.match(/^1C activity rows were found for counterparty\s+(.+)$/i);
if (counterpartyMatch) {
return `В 1С найдены строки активности по контрагенту ${counterpartyMatch[1]}.`;
}
if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки активности по запрошенному контрагентскому контуру.";
}
if (/^Business activity duration may be inferred from first and latest confirmed 1C activity rows$/i.test(value)) {
return "Длительность деловой активности можно оценивать только как вывод по первой и последней подтвержденной строке активности в 1С.";
}
if (/^Legal registration date is not proven by this MCP discovery pilot$/i.test(value)) {
return "Юридическая дата регистрации этим поиском не подтверждена.";
}
return value;
}
function section(title: string, lines: string[]): string | null {
const clean = userFacingLines(lines.map(localizeLine));
if (clean.length === 0) {
return null;
}
return `${title}\n${clean.map((line) => `- ${line}`).join("\n")}`;
}
function statusFrom(entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null): AssistantMcpDiscoveryResponseCandidateStatus {
if (!entryPoint || entryPoint.entry_status === "skipped_not_applicable") {
return "not_applicable";
}
if (entryPoint.entry_status === "skipped_needs_more_context") {
return "clarification_candidate";
}
const bridgeStatus = toNonEmptyString(toRecordObject(entryPoint.bridge)?.bridge_status);
if (bridgeStatus === "answer_draft_ready") {
return "ready_for_guarded_use";
}
if (bridgeStatus === "needs_clarification") {
return "clarification_candidate";
}
if (bridgeStatus === "checked_sources_only") {
return "checked_sources_only_candidate";
}
if (bridgeStatus === "blocked") {
return "blocked";
}
if (bridgeStatus === "unsupported") {
return "unsupported";
}
return "not_applicable";
}
function replyTypeFor(
status: AssistantMcpDiscoveryResponseCandidateStatus
): AssistantMcpDiscoveryResponseCandidateContract["reply_type"] {
if (status === "clarification_candidate") {
return "clarification_required";
}
if (status === "blocked" || status === "unsupported" || status === "not_applicable") {
return "no_grounded_answer";
}
return "partial_coverage";
}
function buildReplyText(entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract, status: AssistantMcpDiscoveryResponseCandidateStatus): string | null {
const bridge = toRecordObject(entryPoint.bridge);
const draft = toRecordObject(bridge?.answer_draft);
if (!draft) {
if (status === "clarification_candidate") {
return "Нужно уточнить контекст перед поиском в 1С: контрагента, период или организацию.";
}
return null;
}
const blocks = [
toNonEmptyString(draft.headline) ? `Коротко: ${localizeLine(String(draft.headline))}` : null,
section("Что подтверждено:", toStringList(draft.confirmed_lines)),
section("Что можно сказать только как вывод:", toStringList(draft.inference_lines)),
section("Что не подтверждено:", toStringList(draft.unknown_lines)),
section("Ограничения проверки:", toStringList(draft.limitation_lines)),
toNonEmptyString(draft.next_step_line) ? `Следующий шаг: ${localizeLine(String(draft.next_step_line))}` : null
].filter((item): item is string => Boolean(item));
const reply = blocks.join("\n\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
}
export function buildAssistantMcpDiscoveryResponseCandidate(
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null | undefined
): AssistantMcpDiscoveryResponseCandidateContract {
const entry = entryPoint ?? null;
const status = statusFrom(entry);
const reasonCodes = uniqueStrings(entry?.reason_codes ?? []);
pushReason(reasonCodes, `mcp_discovery_response_candidate_${status}`);
pushReason(reasonCodes, "mcp_discovery_response_candidate_not_hot_wired");
const replyText =
entry && (status === "ready_for_guarded_use" || status === "checked_sources_only_candidate" || status === "clarification_candidate")
? buildReplyText(entry, status)
: null;
return {
schema_version: ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryResponseCandidate",
candidate_status: replyText ? status : status === "clarification_candidate" ? status : status,
hot_runtime_wired: false,
reply_type: replyTypeFor(status),
reply_text: replyText,
eligible_for_future_hot_runtime:
Boolean(replyText) && (status === "ready_for_guarded_use" || status === "checked_sources_only_candidate" || status === "clarification_candidate"),
must_keep_internal_mechanics_hidden: true,
reason_codes: reasonCodes
};
}