228 lines
11 KiB
TypeScript
228 lines
11 KiB
TypeScript
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("pilot_") ||
|
||
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 isValueFlowPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||
return (
|
||
pilot.pilot_scope === "counterparty_value_flow_query_movements_v1" ||
|
||
pilot.pilot_scope === "counterparty_supplier_payout_query_movements_v1"
|
||
);
|
||
}
|
||
|
||
function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
|
||
if (pilot.derived_value_flow && mode === "confirmed_with_bounded_inference") {
|
||
if (pilot.derived_value_flow.value_flow_direction === "outgoing_supplier_payout") {
|
||
return "По данным 1С найдены строки исходящих платежей/списаний; сумму можно называть только в рамках проверенного периода и найденных строк.";
|
||
}
|
||
return "По данным 1С найдены строки денежных движений; сумму можно называть только в рамках проверенного периода и найденных строк.";
|
||
}
|
||
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 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.pilot_scope === "counterparty_lifecycle_query_documents_v1") {
|
||
claims.push("Do not claim legal registration age unless a legal registration source is confirmed.");
|
||
claims.push("Do not present inferred activity duration as a formally confirmed legal fact.");
|
||
}
|
||
if (isValueFlowPilot(pilot)) {
|
||
claims.push("Do not claim full all-time turnover unless the checked period and coverage prove it.");
|
||
claims.push("Do not present a derived sum as a legal/accounting final total outside the checked 1C rows.");
|
||
}
|
||
if (pilot.evidence.confirmed_facts.length === 0) {
|
||
claims.push("Do not claim a confirmed business fact when confirmed_facts is empty.");
|
||
}
|
||
return claims;
|
||
}
|
||
|
||
function derivedActivityInferenceLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||
const period = pilot.derived_activity_period;
|
||
if (!period) {
|
||
return null;
|
||
}
|
||
return [
|
||
`По подтвержденным строкам активности в 1С период взаимодействия можно оценить примерно как ${period.duration_human_ru}.`,
|
||
`Первая найденная активность: ${period.first_activity_date}; последняя найденная активность: ${period.latest_activity_date}.`,
|
||
"Это вывод по данным 1С, а не юридически подтвержденный возраст регистрации."
|
||
].join(" ");
|
||
}
|
||
|
||
function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||
const flow = pilot.derived_value_flow;
|
||
if (!flow) {
|
||
return null;
|
||
}
|
||
const counterparty = flow.counterparty ? ` по контрагенту ${flow.counterparty}` : "";
|
||
const period = flow.period_scope ? ` за период ${flow.period_scope}` : " в проверенном окне";
|
||
const movementLabel =
|
||
flow.value_flow_direction === "outgoing_supplier_payout" ? "исходящих платежей/списаний" : "денежных движений";
|
||
const totalLabel =
|
||
flow.value_flow_direction === "outgoing_supplier_payout"
|
||
? "сумма исходящих платежей/списаний составляет"
|
||
: "сумма составляет";
|
||
const caveat =
|
||
flow.value_flow_direction === "outgoing_supplier_payout"
|
||
? "Это расчет по найденным строкам 1С, а не подтверждение полного объема платежей вне проверенного окна."
|
||
: "Это расчет по найденным строкам 1С, а не подтверждение полного оборота вне проверенного окна.";
|
||
const dates =
|
||
flow.first_movement_date && flow.latest_movement_date
|
||
? ` Первая найденная дата движения: ${flow.first_movement_date}; последняя: ${flow.latest_movement_date}.`
|
||
: "";
|
||
const limitCaveat = flow.coverage_limited_by_probe_limit
|
||
? " Лимит строк проверки достигнут; полный запрошенный период может быть покрыт не полностью."
|
||
: "";
|
||
return `По найденным строкам ${movementLabel} в 1С${counterparty}${period} ${totalLabel} ${flow.total_amount_human_ru} Учтено строк с суммой: ${flow.rows_with_amount} из ${flow.rows_matched}.${dates}${limitCaveat} ${caveat}`;
|
||
}
|
||
|
||
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");
|
||
}
|
||
const derivedInferenceLine = derivedActivityInferenceLine(pilot);
|
||
const inferenceLines = derivedInferenceLine
|
||
? [derivedInferenceLine]
|
||
: pilot.evidence.inferred_facts;
|
||
const derivedValueLine = derivedValueFlowConfirmedLine(pilot);
|
||
const confirmedLines = derivedValueLine
|
||
? [...pilot.evidence.confirmed_facts, derivedValueLine]
|
||
: pilot.evidence.confirmed_facts;
|
||
|
||
return {
|
||
schema_version: ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
|
||
policy_owner: "assistantMcpDiscoveryAnswerAdapter",
|
||
answer_mode: mode,
|
||
headline: headlineFor(mode, pilot),
|
||
confirmed_lines: uniqueStrings(confirmedLines),
|
||
inference_lines: uniqueStrings(inferenceLines),
|
||
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)
|
||
};
|
||
}
|