279 lines
9.5 KiB
TypeScript
279 lines
9.5 KiB
TypeScript
import type { AssistantMcpDiscoveryDataNeedGraphContract } from "./assistantMcpDiscoveryDataNeedGraph";
|
|
import type {
|
|
AssistantMcpDiscoveryPlanContract,
|
|
AssistantMcpDiscoveryPrimitive,
|
|
AssistantMcpDiscoveryTurnMeaningRef
|
|
} from "./assistantMcpDiscoveryPolicy";
|
|
|
|
export const ASSISTANT_EVIDENCE_PLANNER_SCHEMA_VERSION = "assistant_evidence_planner_v1" as const;
|
|
|
|
export type AssistantEvidencePlannerStatus = "ready_for_execution" | "needs_clarification" | "blocked";
|
|
export type AssistantEvidenceCoverageExpectation = "confirmed_coverage" | "bounded_inference" | "clarification";
|
|
export type AssistantEvidenceAnswerMode =
|
|
| "confirmed_business_answer"
|
|
| "bounded_business_inference"
|
|
| "clarification_required"
|
|
| "checked_sources_only";
|
|
|
|
export interface AssistantEvidenceDataNeedContract {
|
|
business_fact_family: string | null;
|
|
action_family: string | null;
|
|
aggregation_need: string | null;
|
|
comparison_need: string | null;
|
|
ranking_need: string | null;
|
|
time_scope_need: string | null;
|
|
proof_expectation: string | null;
|
|
subject_candidates: string[];
|
|
}
|
|
|
|
export interface AssistantEvidenceAxesContract {
|
|
required_axes: string[];
|
|
provided_axes: string[];
|
|
missing_axes: string[];
|
|
user_actionable_missing_axes: string[];
|
|
clarification_gaps: string[];
|
|
}
|
|
|
|
export interface AssistantEvidencePrimitivePlanContract {
|
|
selected_chain_id: string;
|
|
allowed_primitives: AssistantMcpDiscoveryPrimitive[];
|
|
rejected_primitives: string[];
|
|
execution_budget: AssistantMcpDiscoveryPlanContract["execution_budget"];
|
|
}
|
|
|
|
export interface AssistantEvidenceCoverageGateContract {
|
|
requires_evidence_gate: true;
|
|
expected_coverage: AssistantEvidenceCoverageExpectation;
|
|
answer_permission_if_satisfied: AssistantEvidenceAnswerMode;
|
|
answer_may_use_raw_model_claims: false;
|
|
}
|
|
|
|
export interface AssistantEvidenceAnswerContract {
|
|
answer_mode: AssistantEvidenceAnswerMode;
|
|
required_user_layers: string[];
|
|
forbidden_overclaim_flags: string[];
|
|
must_keep_internal_mechanics_hidden: true;
|
|
}
|
|
|
|
export interface AssistantEvidencePlannerContract {
|
|
schema_version: typeof ASSISTANT_EVIDENCE_PLANNER_SCHEMA_VERSION;
|
|
policy_owner: "assistantEvidencePlanner";
|
|
planner_status: AssistantEvidencePlannerStatus;
|
|
semantic_data_need: string | null;
|
|
selected_chain_id: string;
|
|
data_need: AssistantEvidenceDataNeedContract;
|
|
evidence_axes: AssistantEvidenceAxesContract;
|
|
primitive_plan: AssistantEvidencePrimitivePlanContract;
|
|
coverage_gate: AssistantEvidenceCoverageGateContract;
|
|
answer_contract: AssistantEvidenceAnswerContract;
|
|
reason_codes: string[];
|
|
}
|
|
|
|
export interface BuildAssistantEvidencePlannerInput {
|
|
selectedChainId: string;
|
|
plannerStatus: AssistantEvidencePlannerStatus;
|
|
semanticDataNeed?: string | null;
|
|
dataNeedGraph?: AssistantMcpDiscoveryDataNeedGraphContract | null;
|
|
discoveryPlan: AssistantMcpDiscoveryPlanContract;
|
|
additionalMissingAxes?: string[] | null;
|
|
}
|
|
|
|
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 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: unknown[]): string[] {
|
|
const result: string[] = [];
|
|
for (const value of values) {
|
|
const text = toNonEmptyString(value);
|
|
if (text && !result.includes(text)) {
|
|
result.push(text);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function providedAxesFromMeaning(meaning: AssistantMcpDiscoveryTurnMeaningRef | null): string[] {
|
|
const result: string[] = [];
|
|
if ((meaning?.explicit_entity_candidates?.length ?? 0) > 0) {
|
|
result.push("counterparty");
|
|
result.push("business_entity");
|
|
}
|
|
if (toNonEmptyString(meaning?.explicit_organization_scope)) {
|
|
result.push("organization");
|
|
}
|
|
if (toNonEmptyString(meaning?.explicit_date_scope)) {
|
|
result.push("period");
|
|
}
|
|
if (toNonEmptyString(meaning?.asked_aggregation_axis)) {
|
|
result.push("aggregate_axis");
|
|
}
|
|
if (toNonEmptyString(meaning?.metadata_scope_hint)) {
|
|
result.push("metadata_scope");
|
|
}
|
|
return uniqueStrings(result);
|
|
}
|
|
|
|
function missingAxes(requiredAxes: string[], providedAxes: string[]): string[] {
|
|
return requiredAxes.filter((axis) => !providedAxes.includes(axis));
|
|
}
|
|
|
|
const USER_ACTIONABLE_AXIS_SET = new Set([
|
|
"counterparty",
|
|
"business_entity",
|
|
"organization",
|
|
"period",
|
|
"as_of_date",
|
|
"item",
|
|
"supplier",
|
|
"buyer",
|
|
"warehouse",
|
|
"document",
|
|
"contract",
|
|
"metadata_scope",
|
|
"lane_family_choice"
|
|
]);
|
|
|
|
function userActionableMissingAxes(axes: string[]): string[] {
|
|
return axes.filter((axis) => USER_ACTIONABLE_AXIS_SET.has(axis));
|
|
}
|
|
|
|
function coverageExpectationFor(
|
|
graph: AssistantMcpDiscoveryDataNeedGraphContract | null
|
|
): AssistantEvidenceCoverageExpectation {
|
|
if (graph?.proof_expectation === "bounded_inference") {
|
|
return "bounded_inference";
|
|
}
|
|
if (graph?.proof_expectation === "clarification_required") {
|
|
return "clarification";
|
|
}
|
|
return "confirmed_coverage";
|
|
}
|
|
|
|
function answerModeFor(input: {
|
|
plannerStatus: AssistantEvidencePlannerStatus;
|
|
coverageExpectation: AssistantEvidenceCoverageExpectation;
|
|
}): AssistantEvidenceAnswerMode {
|
|
if (input.plannerStatus === "needs_clarification") {
|
|
return "clarification_required";
|
|
}
|
|
if (input.plannerStatus === "blocked") {
|
|
return "checked_sources_only";
|
|
}
|
|
if (input.coverageExpectation === "bounded_inference") {
|
|
return "bounded_business_inference";
|
|
}
|
|
if (input.coverageExpectation === "clarification") {
|
|
return "clarification_required";
|
|
}
|
|
return "confirmed_business_answer";
|
|
}
|
|
|
|
function requiredUserLayersFor(answerMode: AssistantEvidenceAnswerMode): string[] {
|
|
if (answerMode === "clarification_required") {
|
|
return ["clarifying_question", "why_needed", "available_calculation"];
|
|
}
|
|
if (answerMode === "bounded_business_inference") {
|
|
return ["business_conclusion", "key_figures", "evidence_boundary", "next_step"];
|
|
}
|
|
if (answerMode === "checked_sources_only") {
|
|
return ["checked_sources_boundary", "unknowns", "next_probe"];
|
|
}
|
|
return ["direct_business_answer", "key_figures", "evidence_boundary", "next_step"];
|
|
}
|
|
|
|
export function buildAssistantEvidencePlanner(
|
|
input: BuildAssistantEvidencePlannerInput
|
|
): AssistantEvidencePlannerContract {
|
|
const graph = input.dataNeedGraph ?? null;
|
|
const plan = input.discoveryPlan;
|
|
const turnMeaning = plan.turn_meaning_ref;
|
|
const requiredAxes = uniqueStrings(plan.required_axes);
|
|
const providedAxes = providedAxesFromMeaning(turnMeaning);
|
|
const graphClarificationGaps = uniqueStrings(graph?.clarification_gaps ?? []);
|
|
const additionalAxisGaps = uniqueStrings(input.additionalMissingAxes ?? []).filter(
|
|
(axis) => !providedAxes.includes(axis) && (requiredAxes.includes(axis) || USER_ACTIONABLE_AXIS_SET.has(axis)),
|
|
);
|
|
const axisGaps = uniqueStrings([...additionalAxisGaps, ...missingAxes(requiredAxes, providedAxes)]);
|
|
const actionableAxisGaps = userActionableMissingAxes(axisGaps);
|
|
const clarificationGaps = uniqueStrings([...graphClarificationGaps, ...axisGaps]);
|
|
const coverageExpectation = coverageExpectationFor(graph);
|
|
const answerMode = answerModeFor({
|
|
plannerStatus: input.plannerStatus,
|
|
coverageExpectation
|
|
});
|
|
const reasonCodes = uniqueStrings(plan.reason_codes);
|
|
|
|
pushReason(reasonCodes, "evidence_planner_contract_built");
|
|
if (graph) {
|
|
pushReason(reasonCodes, "evidence_planner_consumed_data_need_graph");
|
|
}
|
|
if (clarificationGaps.length > 0) {
|
|
pushReason(reasonCodes, "evidence_planner_has_missing_axes_or_gaps");
|
|
}
|
|
|
|
return {
|
|
schema_version: ASSISTANT_EVIDENCE_PLANNER_SCHEMA_VERSION,
|
|
policy_owner: "assistantEvidencePlanner",
|
|
planner_status: input.plannerStatus,
|
|
semantic_data_need: toNonEmptyString(input.semanticDataNeed),
|
|
selected_chain_id: input.selectedChainId,
|
|
data_need: {
|
|
business_fact_family: graph?.business_fact_family ?? null,
|
|
action_family: graph?.action_family ?? null,
|
|
aggregation_need: graph?.aggregation_need ?? null,
|
|
comparison_need: graph?.comparison_need ?? null,
|
|
ranking_need: graph?.ranking_need ?? null,
|
|
time_scope_need: graph?.time_scope_need ?? null,
|
|
proof_expectation: graph?.proof_expectation ?? null,
|
|
subject_candidates: uniqueStrings(graph?.subject_candidates ?? [])
|
|
},
|
|
evidence_axes: {
|
|
required_axes: requiredAxes,
|
|
provided_axes: providedAxes,
|
|
missing_axes: axisGaps,
|
|
user_actionable_missing_axes: actionableAxisGaps,
|
|
clarification_gaps: clarificationGaps
|
|
},
|
|
primitive_plan: {
|
|
selected_chain_id: input.selectedChainId,
|
|
allowed_primitives: plan.allowed_primitives,
|
|
rejected_primitives: plan.rejected_primitives,
|
|
execution_budget: plan.execution_budget
|
|
},
|
|
coverage_gate: {
|
|
requires_evidence_gate: true,
|
|
expected_coverage: coverageExpectation,
|
|
answer_permission_if_satisfied: answerMode,
|
|
answer_may_use_raw_model_claims: false
|
|
},
|
|
answer_contract: {
|
|
answer_mode: answerMode,
|
|
required_user_layers: requiredUserLayersFor(answerMode),
|
|
forbidden_overclaim_flags: uniqueStrings(graph?.forbidden_overclaim_flags ?? []),
|
|
must_keep_internal_mechanics_hidden: true
|
|
},
|
|
reason_codes: reasonCodes
|
|
};
|
|
}
|