ARCH: ввести data-need graph и довести open-scope comparison до live replay
This commit is contained in:
@@ -192,6 +192,24 @@ function isMovementLaneClarification(pilot: AssistantMcpDiscoveryPilotExecutionC
|
||||
);
|
||||
}
|
||||
|
||||
function isRankedValueFlowClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||
return (
|
||||
pilot.reason_codes.includes("planner_selected_top_ranked_value_flow_from_data_need_graph") ||
|
||||
pilot.reason_codes.includes("planner_selected_bottom_ranked_value_flow_from_data_need_graph") ||
|
||||
pilot.dry_run.reason_codes.includes("planner_selected_top_ranked_value_flow_from_data_need_graph") ||
|
||||
pilot.dry_run.reason_codes.includes("planner_selected_bottom_ranked_value_flow_from_data_need_graph")
|
||||
);
|
||||
}
|
||||
|
||||
function isBidirectionalValueFlowComparisonClarification(
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract
|
||||
): boolean {
|
||||
return (
|
||||
pilot.reason_codes.includes("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph") ||
|
||||
pilot.dry_run.reason_codes.includes("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph")
|
||||
);
|
||||
}
|
||||
|
||||
function isDocumentLaneClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||
return (
|
||||
isDocumentPilot(pilot) ||
|
||||
@@ -207,7 +225,14 @@ function laneScopeSuffix(pilot: AssistantMcpDiscoveryPilotExecutionContract): st
|
||||
return entity ? ` по "${entity}"` : "";
|
||||
}
|
||||
|
||||
function dryRunHasAxis(pilot: AssistantMcpDiscoveryPilotExecutionContract, axis: string): boolean {
|
||||
return pilot.dry_run.execution_steps.some((step) => step.provided_axes.includes(axis));
|
||||
}
|
||||
|
||||
function dryRunMissingAxis(pilot: AssistantMcpDiscoveryPilotExecutionContract, axis: string): boolean {
|
||||
if (dryRunHasAxis(pilot, axis)) {
|
||||
return false;
|
||||
}
|
||||
return pilot.dry_run.execution_steps.some((step) =>
|
||||
step.missing_axis_options.some((option) => option.includes(axis))
|
||||
);
|
||||
@@ -216,8 +241,10 @@ function dryRunMissingAxis(pilot: AssistantMcpDiscoveryPilotExecutionContract, a
|
||||
function clarificationNeedRu(
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract
|
||||
): { subject: string; verb: string } {
|
||||
const hasCounterparty = dryRunHasAxis(pilot, "counterparty");
|
||||
const hasAccount = dryRunHasAxis(pilot, "account");
|
||||
const needsPeriod = dryRunMissingAxis(pilot, "period");
|
||||
const needsOrganization = dryRunMissingAxis(pilot, "organization");
|
||||
const needsOrganization = !hasCounterparty && !hasAccount && dryRunMissingAxis(pilot, "organization");
|
||||
if (needsPeriod && needsOrganization) {
|
||||
return { subject: "проверяемый период и организацию", verb: "нужно" };
|
||||
}
|
||||
@@ -281,6 +308,9 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
||||
) {
|
||||
return "По текущему каталожному поиску 1С точный контрагент пока не подтвержден.";
|
||||
}
|
||||
if (pilot.derived_ranked_value_flow && mode === "confirmed_with_bounded_inference") {
|
||||
return "По данным 1С можно построить ограниченный ranking по контрагентам на подтвержденных строках денежных движений.";
|
||||
}
|
||||
if (isMovementPilot(pilot) && mode === "confirmed_with_bounded_inference") {
|
||||
return `По движениям${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
|
||||
}
|
||||
@@ -340,6 +370,14 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
||||
const need = clarificationNeedRu(pilot);
|
||||
return `Могу идти дальше по документам${laneScopeSuffix(pilot)}, но для запуска поиска в 1С ${need.verb} ${need.subject}.`;
|
||||
}
|
||||
if (mode === "needs_clarification" && isBidirectionalValueFlowComparisonClarification(pilot)) {
|
||||
const need = clarificationNeedRu(pilot);
|
||||
return `Могу сравнить входящий и исходящий денежный поток, но для bounded поиска в 1С ${need.verb} ${need.subject}.`;
|
||||
}
|
||||
if (mode === "needs_clarification" && isRankedValueFlowClarification(pilot)) {
|
||||
const need = clarificationNeedRu(pilot);
|
||||
return `Могу посчитать ranking по денежному потоку между контрагентами, но для bounded поиска в 1С ${need.verb} ${need.subject}.`;
|
||||
}
|
||||
if (mode === "needs_clarification") {
|
||||
return "Нужно уточнить контекст перед поиском в 1С.";
|
||||
}
|
||||
@@ -378,6 +416,12 @@ function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
||||
if (mode === "needs_clarification" && isDocumentLaneClarification(pilot)) {
|
||||
return clarificationNextStepLine(pilot, "документам");
|
||||
}
|
||||
if (mode === "needs_clarification" && isBidirectionalValueFlowComparisonClarification(pilot)) {
|
||||
return clarificationNextStepLine(pilot, "сравнению входящих и исходящих денежных потоков");
|
||||
}
|
||||
if (mode === "needs_clarification" && isRankedValueFlowClarification(pilot)) {
|
||||
return clarificationNextStepLine(pilot, "ranking-поиску между контрагентами");
|
||||
}
|
||||
if (mode === "needs_clarification") {
|
||||
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
|
||||
}
|
||||
@@ -413,6 +457,10 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract):
|
||||
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.derived_ranked_value_flow) {
|
||||
claims.push("Do not present a bounded ranking as a complete all-time ranking outside the checked period and organization.");
|
||||
claims.push("Do not imply the top-ranked counterparty is globally final when probe-limit or scope boundaries still exist.");
|
||||
}
|
||||
if (isDocumentPilot(pilot)) {
|
||||
claims.push("Do not claim full document history outside the checked period.");
|
||||
claims.push("Do not present the confirmed document rows as a complete document universe.");
|
||||
@@ -556,6 +604,43 @@ function derivedEntityResolutionInferenceLine(pilot: AssistantMcpDiscoveryPilotE
|
||||
return null;
|
||||
}
|
||||
|
||||
function derivedRankedValueFlowInferenceLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||
const ranking = pilot.derived_ranked_value_flow;
|
||||
if (!ranking) {
|
||||
return null;
|
||||
}
|
||||
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
|
||||
const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне";
|
||||
return `Ranking по контрагентам${organization}${period} рассчитан только по подтвержденным строкам 1С и не доказывает полный исторический срез вне проверенного окна.`;
|
||||
}
|
||||
|
||||
function derivedRankedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||
const ranking = pilot.derived_ranked_value_flow;
|
||||
if (!ranking || ranking.ranked_values.length <= 0) {
|
||||
return null;
|
||||
}
|
||||
const leader = ranking.ranked_values[0];
|
||||
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
|
||||
const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне";
|
||||
const directionLead =
|
||||
ranking.ranking_need === "bottom_asc"
|
||||
? ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "Меньше всего заплатили контрагенту"
|
||||
: "Меньше всего денег принёс контрагент"
|
||||
: ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "Больше всего заплатили контрагенту"
|
||||
: "Больше всего денег принёс контрагент";
|
||||
const tail = ranking.ranked_values
|
||||
.slice(1, 3)
|
||||
.map((bucket) => `${bucket.axis_value} — ${bucket.total_amount_human_ru}`)
|
||||
.join("; ");
|
||||
const trail = tail ? ` Следом: ${tail}.` : "";
|
||||
const limitCaveat = ranking.coverage_limited_by_probe_limit
|
||||
? " Лимит строк проверки достигнут; ranking может быть неполным."
|
||||
: "";
|
||||
return `${directionLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${trail}${limitCaveat}`;
|
||||
}
|
||||
|
||||
function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||
const flow = pilot.derived_value_flow;
|
||||
if (!flow) {
|
||||
@@ -662,13 +747,17 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
|
||||
const derivedInferenceLine =
|
||||
derivedActivityInferenceLine(pilot) ??
|
||||
derivedMetadataInferenceLine(pilot) ??
|
||||
derivedRankedValueFlowInferenceLine(pilot) ??
|
||||
derivedEntityResolutionInferenceLine(pilot);
|
||||
const inferenceLines = derivedInferenceLine
|
||||
? [derivedInferenceLine]
|
||||
: pilot.evidence.inferred_facts;
|
||||
const derivedMetadataLine = derivedMetadataConfirmedLine(pilot);
|
||||
const derivedEntityResolutionLine = derivedEntityResolutionConfirmedLine(pilot);
|
||||
const derivedValueLine = derivedBidirectionalValueFlowConfirmedLine(pilot) ?? derivedValueFlowConfirmedLine(pilot);
|
||||
const derivedValueLine =
|
||||
derivedBidirectionalValueFlowConfirmedLine(pilot) ??
|
||||
derivedRankedValueFlowConfirmedLine(pilot) ??
|
||||
derivedValueFlowConfirmedLine(pilot);
|
||||
const monthlyConfirmedLines =
|
||||
derivedBidirectionalValueFlowMonthlyLines(pilot).length > 0
|
||||
? derivedBidirectionalValueFlowMonthlyLines(pilot)
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscoveryPolicy";
|
||||
|
||||
export const ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION =
|
||||
"assistant_data_need_graph_v1" as const;
|
||||
|
||||
export type AssistantMcpDiscoveryDataNeedProofExpectation =
|
||||
| "schema_surface"
|
||||
| "entity_grounding"
|
||||
| "coverage_checked_fact"
|
||||
| "bounded_inference"
|
||||
| "clarification_required";
|
||||
|
||||
export interface AssistantMcpDiscoveryDataNeedGraphContract {
|
||||
schema_version: typeof ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION;
|
||||
policy_owner: "assistantMcpDiscoveryDataNeedGraph";
|
||||
subject_candidates: string[];
|
||||
business_fact_family: string | null;
|
||||
action_family: string | null;
|
||||
aggregation_need: string | null;
|
||||
time_scope_need: string | null;
|
||||
comparison_need: string | null;
|
||||
ranking_need: string | null;
|
||||
proof_expectation: AssistantMcpDiscoveryDataNeedProofExpectation;
|
||||
clarification_gaps: string[];
|
||||
decomposition_candidates: string[];
|
||||
forbidden_overclaim_flags: string[];
|
||||
reason_codes: string[];
|
||||
}
|
||||
|
||||
export interface BuildAssistantMcpDiscoveryDataNeedGraphInput {
|
||||
semanticDataNeed?: string | null;
|
||||
rawUtterance?: string | null;
|
||||
turnMeaning?: AssistantMcpDiscoveryTurnMeaningRef | 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 lower(value: unknown): string {
|
||||
return String(value ?? "").trim().toLowerCase();
|
||||
}
|
||||
|
||||
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 pushUnique(target: string[], value: string | null | undefined): void {
|
||||
const text = toNonEmptyString(value);
|
||||
if (text && !target.includes(text)) {
|
||||
target.push(text);
|
||||
}
|
||||
}
|
||||
|
||||
function businessFactFamilyFor(input: {
|
||||
semanticDataNeed: string;
|
||||
domain: string;
|
||||
action: string;
|
||||
unsupported: string;
|
||||
}): string | null {
|
||||
const combined = `${input.semanticDataNeed} ${input.domain} ${input.action} ${input.unsupported}`.trim();
|
||||
if (combined.includes("metadata lane clarification")) {
|
||||
return "schema_surface";
|
||||
}
|
||||
if (combined.includes("metadata")) {
|
||||
return "schema_surface";
|
||||
}
|
||||
if (combined.includes("entity discovery") || combined.includes("entity_resolution")) {
|
||||
return "entity_grounding";
|
||||
}
|
||||
if (combined.includes("lifecycle") || combined.includes("activity")) {
|
||||
return "activity_lifecycle";
|
||||
}
|
||||
if (combined.includes("movement")) {
|
||||
return "movement_evidence";
|
||||
}
|
||||
if (combined.includes("document")) {
|
||||
return "document_evidence";
|
||||
}
|
||||
if (combined.includes("value-flow") || combined.includes("turnover") || combined.includes("payout") || combined.includes("net")) {
|
||||
return "value_flow";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function aggregationNeedFor(axis: string): string | null {
|
||||
if (!axis) {
|
||||
return null;
|
||||
}
|
||||
if (axis === "month") {
|
||||
return "by_month";
|
||||
}
|
||||
return `by_${axis}`;
|
||||
}
|
||||
|
||||
function timeScopeNeedFor(input: {
|
||||
family: string | null;
|
||||
explicitDateScope: string | null;
|
||||
}): string | null {
|
||||
if (input.explicitDateScope) {
|
||||
return "explicit_period";
|
||||
}
|
||||
if (input.family === "value_flow" || input.family === "movement_evidence" || input.family === "document_evidence") {
|
||||
return "period_required";
|
||||
}
|
||||
if (input.family === "activity_lifecycle") {
|
||||
return "open_activity_window";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function comparisonNeedFor(action: string): string | null {
|
||||
if (action === "net_value_flow") {
|
||||
return "incoming_vs_outgoing";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function allowsOpenScopeWithoutSubject(input: {
|
||||
family: string | null;
|
||||
comparisonNeed: string | null;
|
||||
rankingNeed: string | null;
|
||||
}): boolean {
|
||||
if (input.family !== "value_flow") {
|
||||
return false;
|
||||
}
|
||||
return Boolean(input.rankingNeed || input.comparisonNeed === "incoming_vs_outgoing");
|
||||
}
|
||||
|
||||
function rankingNeedFromRawUtterance(value: string): string | null {
|
||||
const text = lower(value);
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
/(?:\btop[-\s]?\d+\b|\btop\b|топ[-\s]?\d+|топ\b|сам(?:ый|ая|ое|ые)\b|больше\s+всего|наибол[её]е|highest|largest|most)/iu.test(
|
||||
text
|
||||
)
|
||||
) {
|
||||
return "top_desc";
|
||||
}
|
||||
if (/(?:меньше\s+всего|наимен[ьш]е|lowest|smallest|least)/iu.test(text)) {
|
||||
return "bottom_asc";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function proofExpectationFor(input: {
|
||||
family: string | null;
|
||||
clarificationGaps: string[];
|
||||
}): AssistantMcpDiscoveryDataNeedProofExpectation {
|
||||
if (input.clarificationGaps.length > 0) {
|
||||
return "clarification_required";
|
||||
}
|
||||
if (input.family === "schema_surface") {
|
||||
return "schema_surface";
|
||||
}
|
||||
if (input.family === "entity_grounding") {
|
||||
return "entity_grounding";
|
||||
}
|
||||
if (input.family === "activity_lifecycle") {
|
||||
return "bounded_inference";
|
||||
}
|
||||
return "coverage_checked_fact";
|
||||
}
|
||||
|
||||
function decompositionCandidatesFor(input: {
|
||||
family: string | null;
|
||||
action: string;
|
||||
aggregationNeed: string | null;
|
||||
comparisonNeed: string | null;
|
||||
rankingNeed: string | null;
|
||||
openScopeWithoutSubject: boolean;
|
||||
}): string[] {
|
||||
const result: string[] = [];
|
||||
if (input.family === "schema_surface") {
|
||||
pushUnique(result, "inspect_metadata_surface");
|
||||
return result;
|
||||
}
|
||||
if (input.family === "entity_grounding") {
|
||||
pushUnique(result, "search_business_entity");
|
||||
pushUnique(result, "resolve_entity_reference");
|
||||
pushUnique(result, "probe_coverage");
|
||||
return result;
|
||||
}
|
||||
if (input.family === "value_flow") {
|
||||
if (input.rankingNeed && input.openScopeWithoutSubject) {
|
||||
pushUnique(result, "collect_scoped_movements");
|
||||
pushUnique(result, "aggregate_ranked_axis_values");
|
||||
pushUnique(result, "probe_coverage");
|
||||
return result;
|
||||
}
|
||||
if (input.comparisonNeed === "incoming_vs_outgoing" && input.openScopeWithoutSubject) {
|
||||
pushUnique(result, "collect_incoming_movements");
|
||||
pushUnique(result, "collect_outgoing_movements");
|
||||
if (input.aggregationNeed === "by_month") {
|
||||
pushUnique(result, "aggregate_by_month");
|
||||
}
|
||||
pushUnique(result, "probe_coverage");
|
||||
return result;
|
||||
}
|
||||
pushUnique(result, "resolve_entity_reference");
|
||||
if (input.action === "net_value_flow") {
|
||||
pushUnique(result, "collect_incoming_movements");
|
||||
pushUnique(result, "collect_outgoing_movements");
|
||||
} else {
|
||||
pushUnique(result, "collect_scoped_movements");
|
||||
}
|
||||
pushUnique(result, input.aggregationNeed === "by_month" ? "aggregate_by_month" : "aggregate_checked_amounts");
|
||||
pushUnique(result, "probe_coverage");
|
||||
return result;
|
||||
}
|
||||
if (input.family === "movement_evidence") {
|
||||
pushUnique(result, "resolve_entity_reference");
|
||||
pushUnique(result, "fetch_scoped_movements");
|
||||
pushUnique(result, "probe_coverage");
|
||||
return result;
|
||||
}
|
||||
if (input.family === "document_evidence") {
|
||||
pushUnique(result, "resolve_entity_reference");
|
||||
pushUnique(result, "fetch_scoped_documents");
|
||||
pushUnique(result, "probe_coverage");
|
||||
return result;
|
||||
}
|
||||
if (input.family === "activity_lifecycle") {
|
||||
pushUnique(result, "resolve_entity_reference");
|
||||
pushUnique(result, "fetch_supporting_documents");
|
||||
pushUnique(result, "probe_coverage");
|
||||
pushUnique(result, "explain_evidence_basis");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function forbiddenOverclaimFlagsFor(family: string | null): string[] {
|
||||
const result: string[] = ["no_raw_model_claims"];
|
||||
if (family === "schema_surface") {
|
||||
pushUnique(result, "no_fake_schema_surface");
|
||||
}
|
||||
if (family === "entity_grounding") {
|
||||
pushUnique(result, "no_unresolved_entity_claim");
|
||||
}
|
||||
if (family === "activity_lifecycle") {
|
||||
pushUnique(result, "no_legal_age_claim_without_evidence");
|
||||
}
|
||||
if (family === "value_flow" || family === "movement_evidence" || family === "document_evidence") {
|
||||
pushUnique(result, "no_unchecked_fact_totals");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function buildAssistantMcpDiscoveryDataNeedGraph(
|
||||
input: BuildAssistantMcpDiscoveryDataNeedGraphInput
|
||||
): AssistantMcpDiscoveryDataNeedGraphContract {
|
||||
const semanticDataNeed = lower(input.semanticDataNeed);
|
||||
const turnMeaning = input.turnMeaning ?? null;
|
||||
const domain = lower(turnMeaning?.asked_domain_family);
|
||||
const action = lower(turnMeaning?.asked_action_family);
|
||||
const unsupported = lower(turnMeaning?.unsupported_but_understood_family);
|
||||
const rawUtterance = lower(input.rawUtterance);
|
||||
const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis);
|
||||
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
|
||||
const subjectCandidates = (turnMeaning?.explicit_entity_candidates ?? [])
|
||||
.map((item) => toNonEmptyString(item))
|
||||
.filter((item): item is string => Boolean(item));
|
||||
const businessFactFamily = businessFactFamilyFor({
|
||||
semanticDataNeed,
|
||||
domain,
|
||||
action,
|
||||
unsupported
|
||||
});
|
||||
const aggregationNeed = aggregationNeedFor(aggregationAxis);
|
||||
const comparisonNeed = comparisonNeedFor(action);
|
||||
const rankingNeed = rankingNeedFromRawUtterance(rawUtterance);
|
||||
const openScopeWithoutSubject =
|
||||
subjectCandidates.length === 0 &&
|
||||
allowsOpenScopeWithoutSubject({
|
||||
family: businessFactFamily,
|
||||
comparisonNeed,
|
||||
rankingNeed
|
||||
});
|
||||
const clarificationGaps: string[] = [];
|
||||
if (unsupported === "metadata_lane_choice_clarification" || action === "resolve_next_lane") {
|
||||
pushUnique(clarificationGaps, "lane_family_choice");
|
||||
}
|
||||
if (subjectCandidates.length === 0 && businessFactFamily !== "schema_surface" && !openScopeWithoutSubject) {
|
||||
pushUnique(clarificationGaps, "subject");
|
||||
}
|
||||
const timeScopeNeed = timeScopeNeedFor({
|
||||
family: businessFactFamily,
|
||||
explicitDateScope
|
||||
});
|
||||
if (timeScopeNeed === "period_required" && !explicitDateScope) {
|
||||
pushUnique(clarificationGaps, "period");
|
||||
}
|
||||
const decompositionCandidates = decompositionCandidatesFor({
|
||||
family: businessFactFamily,
|
||||
action,
|
||||
aggregationNeed,
|
||||
comparisonNeed,
|
||||
rankingNeed,
|
||||
openScopeWithoutSubject
|
||||
});
|
||||
const reasonCodes: string[] = [];
|
||||
pushReason(reasonCodes, "data_need_graph_built");
|
||||
if (businessFactFamily) {
|
||||
pushReason(reasonCodes, `data_need_graph_family_${businessFactFamily}`);
|
||||
} else {
|
||||
pushReason(reasonCodes, "data_need_graph_family_unknown");
|
||||
}
|
||||
if (aggregationNeed) {
|
||||
pushReason(reasonCodes, `data_need_graph_aggregation_${aggregationNeed}`);
|
||||
}
|
||||
if (rankingNeed) {
|
||||
pushReason(reasonCodes, `data_need_graph_ranking_${rankingNeed}`);
|
||||
}
|
||||
if (comparisonNeed) {
|
||||
pushReason(reasonCodes, `data_need_graph_comparison_${comparisonNeed}`);
|
||||
}
|
||||
if (clarificationGaps.length > 0) {
|
||||
pushReason(reasonCodes, "data_need_graph_has_clarification_gaps");
|
||||
}
|
||||
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||
subject_candidates: subjectCandidates,
|
||||
business_fact_family: businessFactFamily,
|
||||
action_family: toNonEmptyString(turnMeaning?.asked_action_family),
|
||||
aggregation_need: aggregationNeed,
|
||||
time_scope_need: timeScopeNeed,
|
||||
comparison_need: comparisonNeed,
|
||||
ranking_need: rankingNeed,
|
||||
proof_expectation: proofExpectationFor({
|
||||
family: businessFactFamily,
|
||||
clarificationGaps
|
||||
}),
|
||||
clarification_gaps: clarificationGaps,
|
||||
decomposition_candidates: decompositionCandidates,
|
||||
forbidden_overclaim_flags: forbiddenOverclaimFlagsFor(businessFactFamily),
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
@@ -78,6 +78,28 @@ export interface AssistantMcpDiscoveryDerivedValueFlow {
|
||||
inference_basis: "sum_of_confirmed_1c_value_flow_rows";
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryRankedValueFlowBucket {
|
||||
axis_value: string;
|
||||
rows_with_amount: number;
|
||||
total_amount: number;
|
||||
total_amount_human_ru: string;
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryDerivedRankedValueFlow {
|
||||
value_flow_direction: "incoming_customer_revenue" | "outgoing_supplier_payout";
|
||||
ranking_need: "top_desc" | "bottom_asc";
|
||||
ranking_axis: "counterparty";
|
||||
organization_scope: string | null;
|
||||
period_scope: string | null;
|
||||
rows_matched: number;
|
||||
rows_with_amount: number;
|
||||
ranked_values: AssistantMcpDiscoveryRankedValueFlowBucket[];
|
||||
coverage_limited_by_probe_limit: boolean;
|
||||
coverage_recovered_by_period_chunking: boolean;
|
||||
period_chunking_granularity: AssistantMcpDiscoveryAggregationAxis | null;
|
||||
inference_basis: "ranked_counterparty_totals_from_confirmed_1c_value_flow_rows";
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryValueFlowSideSummary {
|
||||
rows_matched: number;
|
||||
rows_with_amount: number;
|
||||
@@ -187,6 +209,7 @@ export interface AssistantMcpDiscoveryPilotExecutionContract {
|
||||
derived_metadata_surface: AssistantMcpDiscoveryDerivedMetadataSurface | null;
|
||||
derived_entity_resolution: AssistantMcpDiscoveryDerivedEntityResolution | null;
|
||||
derived_activity_period: AssistantMcpDiscoveryDerivedActivityPeriod | null;
|
||||
derived_ranked_value_flow?: AssistantMcpDiscoveryDerivedRankedValueFlow | null;
|
||||
derived_value_flow: AssistantMcpDiscoveryDerivedValueFlow | null;
|
||||
derived_bidirectional_value_flow: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null;
|
||||
query_limitations: string[];
|
||||
@@ -334,6 +357,20 @@ function buildValueFlowFilters(planner: AssistantMcpDiscoveryPlannerContract): A
|
||||
};
|
||||
}
|
||||
|
||||
function organizationScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract): string | null {
|
||||
return toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_organization_scope);
|
||||
}
|
||||
|
||||
function rankingNeedForPlanner(
|
||||
planner: AssistantMcpDiscoveryPlannerContract
|
||||
): AssistantMcpDiscoveryDerivedRankedValueFlow["ranking_need"] | null {
|
||||
const rankingNeed = toNonEmptyString(planner.data_need_graph?.ranking_need)?.toLowerCase();
|
||||
if (rankingNeed === "top_desc" || rankingNeed === "bottom_asc") {
|
||||
return rankingNeed;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeEntityResolutionText(value: string | null): string {
|
||||
return String(value ?? "")
|
||||
.toLowerCase()
|
||||
@@ -544,7 +581,11 @@ function isMovementEvidencePilotEligible(planner: AssistantMcpDiscoveryPlannerCo
|
||||
}
|
||||
|
||||
function isValueFlowPilotEligible(planner: AssistantMcpDiscoveryPlannerContract): boolean {
|
||||
if (planner.selected_chain_id === "value_flow") {
|
||||
if (
|
||||
planner.selected_chain_id === "value_flow" ||
|
||||
planner.selected_chain_id === "value_flow_ranking" ||
|
||||
planner.selected_chain_id === "value_flow_comparison"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||
@@ -1429,6 +1470,17 @@ function rowAmountValue(row: Record<string, unknown>): number | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function rowCounterpartyValue(row: Record<string, unknown>): string | null {
|
||||
const candidates = [row["Контрагент"], row["Counterparty"], row["counterparty"], row["Наименование"], row["name"]];
|
||||
for (const candidate of candidates) {
|
||||
const text = toNonEmptyString(candidate);
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function monthBucketFromIsoDate(isoDate: string | null): string | null {
|
||||
const match = isoDate?.match(/^(\d{4})-(\d{2})-\d{2}$/);
|
||||
return match ? `${match[1]}-${match[2]}` : null;
|
||||
@@ -1629,6 +1681,74 @@ function deriveValueFlow(
|
||||
};
|
||||
}
|
||||
|
||||
function deriveRankedValueFlow(
|
||||
result: AssistantMcpDiscoveryCoverageAwareQueryResult | null,
|
||||
input: {
|
||||
organizationScope: string | null;
|
||||
periodScope: string | null;
|
||||
direction: AssistantMcpDiscoveryDerivedRankedValueFlow["value_flow_direction"];
|
||||
rankingNeed: AssistantMcpDiscoveryDerivedRankedValueFlow["ranking_need"];
|
||||
}
|
||||
): AssistantMcpDiscoveryDerivedRankedValueFlow | null {
|
||||
if (!result || result.error || result.matched_rows <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const buckets = new Map<string, { rows_with_amount: number; total_amount: number }>();
|
||||
let rowsWithAmount = 0;
|
||||
for (const row of result.rows) {
|
||||
const axisValue = rowCounterpartyValue(row);
|
||||
const amount = rowAmountValue(row);
|
||||
if (!axisValue || amount === null) {
|
||||
continue;
|
||||
}
|
||||
rowsWithAmount += 1;
|
||||
const current = buckets.get(axisValue) ?? { rows_with_amount: 0, total_amount: 0 };
|
||||
current.rows_with_amount += 1;
|
||||
current.total_amount += amount;
|
||||
buckets.set(axisValue, current);
|
||||
}
|
||||
|
||||
if (rowsWithAmount <= 0 || buckets.size <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rankedValues = Array.from(buckets.entries())
|
||||
.map(([axisValue, bucket]) => ({
|
||||
axis_value: axisValue,
|
||||
rows_with_amount: bucket.rows_with_amount,
|
||||
total_amount: bucket.total_amount,
|
||||
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount)
|
||||
}))
|
||||
.sort((left, right) => {
|
||||
const amountDelta = right.total_amount - left.total_amount;
|
||||
if (input.rankingNeed === "bottom_asc") {
|
||||
if (amountDelta !== 0) {
|
||||
return -amountDelta;
|
||||
}
|
||||
} else if (amountDelta !== 0) {
|
||||
return amountDelta;
|
||||
}
|
||||
return left.axis_value.localeCompare(right.axis_value, "ru");
|
||||
})
|
||||
.slice(0, 5);
|
||||
|
||||
return {
|
||||
value_flow_direction: input.direction,
|
||||
ranking_need: input.rankingNeed,
|
||||
ranking_axis: "counterparty",
|
||||
organization_scope: input.organizationScope,
|
||||
period_scope: input.periodScope,
|
||||
rows_matched: result.matched_rows,
|
||||
rows_with_amount: rowsWithAmount,
|
||||
ranked_values: rankedValues,
|
||||
coverage_limited_by_probe_limit: result.coverage_limited_by_probe_limit,
|
||||
coverage_recovered_by_period_chunking: result.coverage_recovered_by_period_chunking,
|
||||
period_chunking_granularity: result.period_chunking_granularity,
|
||||
inference_basis: "ranked_counterparty_totals_from_confirmed_1c_value_flow_rows"
|
||||
};
|
||||
}
|
||||
|
||||
function deriveValueFlowSideSummary(
|
||||
result: AssistantMcpDiscoveryCoverageAwareQueryResult | null
|
||||
): AssistantMcpDiscoveryValueFlowSideSummary {
|
||||
@@ -1798,6 +1918,18 @@ function buildValueFlowConfirmedFacts(
|
||||
];
|
||||
}
|
||||
|
||||
function buildRankedValueFlowConfirmedFacts(derived: AssistantMcpDiscoveryDerivedRankedValueFlow | null): string[] {
|
||||
if (!derived || derived.ranked_values.length <= 0) {
|
||||
return [];
|
||||
}
|
||||
const leader = derived.ranked_values[0];
|
||||
const directionLabel =
|
||||
derived.value_flow_direction === "outgoing_supplier_payout" ? "supplier-payout" : "incoming value-flow";
|
||||
return [
|
||||
`1C ${directionLabel} rows were ranked by counterparty for the checked scope; leader=${leader.axis_value}, rows_with_amount=${leader.rows_with_amount}`
|
||||
];
|
||||
}
|
||||
|
||||
function buildBidirectionalValueFlowConfirmedFacts(
|
||||
derived: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null
|
||||
): string[] {
|
||||
@@ -1880,6 +2012,19 @@ function buildValueFlowInferredFacts(derived: AssistantMcpDiscoveryDerivedValueF
|
||||
return facts;
|
||||
}
|
||||
|
||||
function buildRankedValueFlowInferredFacts(derived: AssistantMcpDiscoveryDerivedRankedValueFlow | null): string[] {
|
||||
if (!derived) {
|
||||
return [];
|
||||
}
|
||||
const facts = ["Counterparty ranking was calculated from confirmed 1C movement rows grouped by counterparty"];
|
||||
if (derived.coverage_recovered_by_period_chunking && derived.period_chunking_granularity === "month") {
|
||||
facts.push(
|
||||
"Requested period coverage for counterparty ranking was recovered through monthly 1C probes after a broad probe hit the row limit"
|
||||
);
|
||||
}
|
||||
return facts;
|
||||
}
|
||||
|
||||
function buildBidirectionalValueFlowInferredFacts(
|
||||
derived: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null
|
||||
): string[] {
|
||||
@@ -1939,6 +2084,22 @@ function buildValueFlowUnknownFacts(
|
||||
return unknownFacts;
|
||||
}
|
||||
|
||||
function buildRankedValueFlowUnknownFacts(
|
||||
periodScope: string | null,
|
||||
derived: AssistantMcpDiscoveryDerivedRankedValueFlow | null
|
||||
): string[] {
|
||||
const unknownFacts: string[] = [];
|
||||
if (derived?.coverage_limited_by_probe_limit) {
|
||||
unknownFacts.push("Complete requested-period ranking coverage is not proven because the MCP discovery probe row limit was reached");
|
||||
}
|
||||
unknownFacts.push(
|
||||
periodScope
|
||||
? "Full ranking outside the checked period is not proven by this MCP discovery pilot"
|
||||
: "Full all-time counterparty ranking is not proven without an explicit checked period"
|
||||
);
|
||||
return unknownFacts;
|
||||
}
|
||||
|
||||
function buildBidirectionalValueFlowUnknownFacts(
|
||||
periodScope: string | null,
|
||||
derived: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null
|
||||
@@ -1979,6 +2140,8 @@ function pilotScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract): As
|
||||
return "metadata_inspection_v1";
|
||||
case "movement_evidence":
|
||||
return "counterparty_movement_evidence_query_movements_v1";
|
||||
case "value_flow_comparison":
|
||||
case "value_flow_ranking":
|
||||
case "value_flow":
|
||||
return valueFlowPilotProfile(planner).scope;
|
||||
case "document_evidence":
|
||||
@@ -2107,7 +2270,9 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||
|
||||
const counterparty = firstEntityCandidate(planner);
|
||||
const dateScope = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_date_scope);
|
||||
const organizationScope = organizationScopeForPlanner(planner);
|
||||
const aggregationAxis = aggregationAxisForPlanner(planner);
|
||||
const rankingNeed = rankingNeedForPlanner(planner);
|
||||
|
||||
if (metadataPilotEligible) {
|
||||
let metadataResult: AddressMcpMetadataRowsResult | null = null;
|
||||
@@ -2694,6 +2859,50 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||
}
|
||||
|
||||
const sourceRowsSummary = queryResult ? summarizeValueFlowRows(queryResult) : null;
|
||||
if (planner.selected_chain_id === "value_flow_ranking" && rankingNeed) {
|
||||
const derivedRankedValueFlow = deriveRankedValueFlow(queryResult, {
|
||||
organizationScope,
|
||||
periodScope: dateScope,
|
||||
direction: valueFlowProfile.direction,
|
||||
rankingNeed
|
||||
});
|
||||
if (derivedRankedValueFlow) {
|
||||
pushReason(reasonCodes, "pilot_derived_ranked_value_flow_from_confirmed_rows");
|
||||
}
|
||||
const evidence = resolveAssistantMcpDiscoveryEvidence({
|
||||
plan: planner.discovery_plan,
|
||||
probeResults,
|
||||
confirmedFacts: buildRankedValueFlowConfirmedFacts(derivedRankedValueFlow),
|
||||
inferredFacts: buildRankedValueFlowInferredFacts(derivedRankedValueFlow),
|
||||
unknownFacts: buildRankedValueFlowUnknownFacts(dateScope, derivedRankedValueFlow),
|
||||
sourceRowsSummary,
|
||||
queryLimitations,
|
||||
recommendedNextProbe: "explain_evidence_basis"
|
||||
});
|
||||
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryPilotExecutor",
|
||||
pilot_status: "executed",
|
||||
pilot_scope: valueFlowProfile.scope,
|
||||
dry_run: dryRun,
|
||||
mcp_execution_performed: executedPrimitives.length > 0,
|
||||
executed_primitives: executedPrimitives,
|
||||
skipped_primitives: skippedPrimitives,
|
||||
probe_results: probeResults,
|
||||
evidence,
|
||||
source_rows_summary: sourceRowsSummary,
|
||||
derived_metadata_surface: null,
|
||||
derived_entity_resolution: null,
|
||||
derived_activity_period: null,
|
||||
derived_ranked_value_flow: derivedRankedValueFlow,
|
||||
derived_value_flow: null,
|
||||
derived_bidirectional_value_flow: null,
|
||||
query_limitations: queryLimitations,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
|
||||
const derivedValueFlow = deriveValueFlow(
|
||||
queryResult,
|
||||
counterparty,
|
||||
@@ -2733,6 +2942,7 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||
derived_metadata_surface: null,
|
||||
derived_entity_resolution: null,
|
||||
derived_activity_period: null,
|
||||
derived_ranked_value_flow: null,
|
||||
derived_value_flow: derivedValueFlow,
|
||||
derived_bidirectional_value_flow: null,
|
||||
query_limitations: queryLimitations,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
reviewAssistantMcpDiscoveryPlanAgainstCatalog,
|
||||
type AssistantMcpCatalogPlanReview
|
||||
} from "./assistantMcpCatalogIndex";
|
||||
import type { AssistantMcpDiscoveryDataNeedGraphContract } from "./assistantMcpDiscoveryDataNeedGraph";
|
||||
|
||||
export const ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION = "assistant_mcp_discovery_planner_v1" as const;
|
||||
|
||||
@@ -17,6 +18,8 @@ export type AssistantMcpDiscoveryChainId =
|
||||
| "metadata_inspection"
|
||||
| "metadata_lane_clarification"
|
||||
| "value_flow"
|
||||
| "value_flow_comparison"
|
||||
| "value_flow_ranking"
|
||||
| "lifecycle"
|
||||
| "movement_evidence"
|
||||
| "document_evidence"
|
||||
@@ -24,6 +27,7 @@ export type AssistantMcpDiscoveryChainId =
|
||||
|
||||
export interface AssistantMcpDiscoveryPlannerInput {
|
||||
semanticDataNeed?: string | null;
|
||||
dataNeedGraph?: AssistantMcpDiscoveryDataNeedGraphContract | null;
|
||||
turnMeaning?: AssistantMcpDiscoveryTurnMeaningRef | null;
|
||||
}
|
||||
|
||||
@@ -32,6 +36,7 @@ export interface AssistantMcpDiscoveryPlannerContract {
|
||||
policy_owner: "assistantMcpDiscoveryPlanner";
|
||||
planner_status: AssistantMcpDiscoveryPlannerStatus;
|
||||
semantic_data_need: string | null;
|
||||
data_need_graph: AssistantMcpDiscoveryDataNeedGraphContract | null;
|
||||
selected_chain_id: AssistantMcpDiscoveryChainId;
|
||||
selected_chain_summary: string;
|
||||
proposed_primitives: AssistantMcpDiscoveryPrimitive[];
|
||||
@@ -93,6 +98,10 @@ function hasEntity(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefin
|
||||
return (meaning?.explicit_entity_candidates?.length ?? 0) > 0;
|
||||
}
|
||||
|
||||
function hasSubjectCandidates(graph: AssistantMcpDiscoveryDataNeedGraphContract | null | undefined): boolean {
|
||||
return (graph?.subject_candidates.length ?? 0) > 0;
|
||||
}
|
||||
|
||||
function aggregationAxis(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined): string | null {
|
||||
return toNonEmptyString(meaning?.asked_aggregation_axis)?.toLowerCase() ?? null;
|
||||
}
|
||||
@@ -136,14 +145,150 @@ function budgetOverrideFor(input: AssistantMcpDiscoveryPlannerInput, recipe: Pla
|
||||
|
||||
function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
|
||||
const meaning = input.turnMeaning ?? null;
|
||||
const dataNeedGraph = input.dataNeedGraph ?? null;
|
||||
const domain = lower(meaning?.asked_domain_family);
|
||||
const action = lower(meaning?.asked_action_family);
|
||||
const unsupported = lower(meaning?.unsupported_but_understood_family);
|
||||
const graphFactFamily = lower(dataNeedGraph?.business_fact_family);
|
||||
const graphAction = lower(dataNeedGraph?.action_family);
|
||||
const graphAggregation = lower(dataNeedGraph?.aggregation_need);
|
||||
const graphClarificationGaps = (dataNeedGraph?.clarification_gaps ?? []).map((item) => lower(item));
|
||||
const combined = `${domain} ${action} ${unsupported}`.trim();
|
||||
const axes: string[] = [];
|
||||
const requestedAggregationAxis = aggregationAxis(meaning);
|
||||
addScopeAxes(axes, meaning);
|
||||
|
||||
if (graphClarificationGaps.includes("lane_family_choice")) {
|
||||
pushUnique(axes, "lane_family_choice");
|
||||
return {
|
||||
semanticDataNeed: "metadata lane clarification",
|
||||
chainId: "metadata_lane_clarification",
|
||||
chainSummary: "Preserve the ambiguous metadata surface and ask the user to choose the next data lane before running MCP probes.",
|
||||
primitives: [],
|
||||
axes,
|
||||
reason: "planner_selected_metadata_lane_clarification_from_data_need_graph"
|
||||
};
|
||||
}
|
||||
|
||||
if (graphFactFamily === "value_flow") {
|
||||
if (dataNeedGraph?.comparison_need === "incoming_vs_outgoing" && !hasSubjectCandidates(dataNeedGraph)) {
|
||||
pushUnique(axes, "amount");
|
||||
pushUnique(axes, "coverage_target");
|
||||
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
|
||||
pushUnique(axes, "calendar_month");
|
||||
}
|
||||
return {
|
||||
semanticDataNeed: "bidirectional value-flow comparison evidence",
|
||||
chainId: "value_flow_comparison",
|
||||
chainSummary:
|
||||
"Query incoming and outgoing movements for the checked period and organization, compare the checked sides, and probe coverage before answering a bounded comparison.",
|
||||
primitives: ["query_movements", "probe_coverage"],
|
||||
axes,
|
||||
reason: "planner_selected_bidirectional_value_flow_comparison_from_data_need_graph"
|
||||
};
|
||||
}
|
||||
if (dataNeedGraph?.ranking_need && !hasSubjectCandidates(dataNeedGraph)) {
|
||||
pushUnique(axes, "aggregate_axis");
|
||||
pushUnique(axes, "amount");
|
||||
pushUnique(axes, "coverage_target");
|
||||
return {
|
||||
semanticDataNeed: "ranked value-flow evidence",
|
||||
chainId: "value_flow_ranking",
|
||||
chainSummary:
|
||||
"Query scoped movements for the checked period and organization, aggregate checked amounts by counterparty, then probe coverage before answering a bounded ranking.",
|
||||
primitives: ["query_movements", "aggregate_by_axis", "probe_coverage"],
|
||||
axes,
|
||||
reason:
|
||||
dataNeedGraph.ranking_need === "bottom_asc"
|
||||
? "planner_selected_bottom_ranked_value_flow_from_data_need_graph"
|
||||
: "planner_selected_top_ranked_value_flow_from_data_need_graph"
|
||||
};
|
||||
}
|
||||
pushUnique(axes, "aggregate_axis");
|
||||
pushUnique(axes, "amount");
|
||||
pushUnique(axes, "coverage_target");
|
||||
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
|
||||
pushUnique(axes, "calendar_month");
|
||||
}
|
||||
return {
|
||||
semanticDataNeed: "counterparty value-flow evidence",
|
||||
chainId: "value_flow",
|
||||
chainSummary: "Resolve the business entity, query scoped movements, aggregate checked amounts, then probe coverage before answering.",
|
||||
primitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
|
||||
axes,
|
||||
reason:
|
||||
requestedAggregationAxis === "month" || graphAggregation === "by_month"
|
||||
? "planner_selected_monthly_value_flow_from_data_need_graph"
|
||||
: "planner_selected_value_flow_from_data_need_graph"
|
||||
};
|
||||
}
|
||||
|
||||
if (graphFactFamily === "activity_lifecycle") {
|
||||
pushUnique(axes, "document_date");
|
||||
pushUnique(axes, "coverage_target");
|
||||
pushUnique(axes, "evidence_basis");
|
||||
return {
|
||||
semanticDataNeed: "counterparty lifecycle evidence",
|
||||
chainId: "lifecycle",
|
||||
chainSummary: "Resolve the business entity, query supporting documents, probe coverage, then explain the evidence basis for the inferred activity window.",
|
||||
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage", "explain_evidence_basis"],
|
||||
axes,
|
||||
reason: "planner_selected_lifecycle_from_data_need_graph"
|
||||
};
|
||||
}
|
||||
|
||||
if (graphFactFamily === "schema_surface") {
|
||||
pushUnique(axes, "metadata_scope");
|
||||
return {
|
||||
semanticDataNeed: "1C metadata evidence",
|
||||
chainId: "metadata_inspection",
|
||||
chainSummary: "Inspect the 1C metadata surface first, then ground the next safe lane from confirmed schema evidence.",
|
||||
primitives: ["inspect_1c_metadata"],
|
||||
axes,
|
||||
reason: "planner_selected_metadata_from_data_need_graph"
|
||||
};
|
||||
}
|
||||
|
||||
if (graphFactFamily === "movement_evidence") {
|
||||
pushUnique(axes, "coverage_target");
|
||||
return {
|
||||
semanticDataNeed: "movement evidence",
|
||||
chainId: "movement_evidence",
|
||||
chainSummary: "Resolve the business entity, fetch scoped movement rows, and probe coverage without pretending to have a full movement universe.",
|
||||
primitives: ["resolve_entity_reference", "query_movements", "probe_coverage"],
|
||||
axes,
|
||||
reason: "planner_selected_movement_from_data_need_graph"
|
||||
};
|
||||
}
|
||||
|
||||
if (graphFactFamily === "document_evidence") {
|
||||
pushUnique(axes, "coverage_target");
|
||||
return {
|
||||
semanticDataNeed: "document evidence",
|
||||
chainId: "document_evidence",
|
||||
chainSummary: "Resolve the business entity, fetch scoped document rows, and probe coverage before stating the checked document evidence.",
|
||||
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
|
||||
axes,
|
||||
reason: "planner_selected_document_from_data_need_graph"
|
||||
};
|
||||
}
|
||||
|
||||
if (graphFactFamily === "entity_grounding" || (!graphFactFamily && (dataNeedGraph?.subject_candidates.length ?? 0) > 0)) {
|
||||
pushUnique(axes, "business_entity");
|
||||
pushUnique(axes, "coverage_target");
|
||||
return {
|
||||
semanticDataNeed: "entity discovery evidence",
|
||||
chainId: "entity_resolution",
|
||||
chainSummary: "Search candidate business entities, resolve the most relevant 1C reference, and prove whether the entity grounding is stable enough for the next probe.",
|
||||
primitives: ["search_business_entity", "resolve_entity_reference", "probe_coverage"],
|
||||
axes,
|
||||
reason:
|
||||
graphAction === "search_business_entity"
|
||||
? "planner_selected_entity_resolution_from_data_need_graph"
|
||||
: "planner_selected_entity_resolution_recipe"
|
||||
};
|
||||
}
|
||||
|
||||
if (includesAny(combined, ["metadata_lane_choice_clarification", "resolve_next_lane"])) {
|
||||
pushUnique(axes, "lane_family_choice");
|
||||
return {
|
||||
@@ -267,8 +412,12 @@ export function planAssistantMcpDiscovery(
|
||||
const recipe = recipeFor(input);
|
||||
const budgetOverride = budgetOverrideFor(input, recipe);
|
||||
const semanticDataNeed = toNonEmptyString(input.semanticDataNeed) ?? recipe.semanticDataNeed;
|
||||
const dataNeedGraph = input.dataNeedGraph ?? null;
|
||||
const reasonCodes: string[] = [];
|
||||
pushReason(reasonCodes, recipe.reason);
|
||||
if (dataNeedGraph) {
|
||||
pushReason(reasonCodes, "planner_consumed_data_need_graph_v1");
|
||||
}
|
||||
if (budgetOverride.maxProbeCount) {
|
||||
pushReason(reasonCodes, "planner_enabled_chunked_coverage_probe_budget");
|
||||
}
|
||||
@@ -296,6 +445,7 @@ export function planAssistantMcpDiscovery(
|
||||
policy_owner: "assistantMcpDiscoveryPlanner",
|
||||
planner_status: plannerStatus,
|
||||
semantic_data_need: semanticDataNeed,
|
||||
data_need_graph: dataNeedGraph,
|
||||
selected_chain_id: recipe.chainId,
|
||||
selected_chain_summary: recipe.chainSummary,
|
||||
proposed_primitives: recipe.primitives,
|
||||
|
||||
@@ -91,6 +91,36 @@ function userFacingLines(values: string[]): string[] {
|
||||
}
|
||||
|
||||
function localizeLine(value: string): string {
|
||||
if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) {
|
||||
return "В 1С найдены строки активности в запрошенном срезе.";
|
||||
}
|
||||
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
|
||||
return "В 1С найдены строки входящих денежных поступлений в запрошенном срезе.";
|
||||
}
|
||||
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
|
||||
return "В 1С найдены строки исходящих платежей и списаний в запрошенном срезе.";
|
||||
}
|
||||
const openScopeBidirectionalMatch = value.match(
|
||||
/^1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=(found|not_found), outgoing=(found|not_found)$/i
|
||||
);
|
||||
if (openScopeBidirectionalMatch) {
|
||||
const incoming =
|
||||
openScopeBidirectionalMatch[1] === "found"
|
||||
? "входящие строки найдены"
|
||||
: "входящие строки не найдены";
|
||||
const outgoing =
|
||||
openScopeBidirectionalMatch[2] === "found"
|
||||
? "исходящие строки найдены"
|
||||
: "исходящие строки не найдены";
|
||||
return `В 1С проверены входящие и исходящие денежные строки в запрошенном срезе: ${incoming}, ${outgoing}.`;
|
||||
}
|
||||
if (
|
||||
/^Requested period hit the MCP row limit, but the approved monthly recovery probe budget is smaller than the required subperiod count$/i.test(
|
||||
value
|
||||
)
|
||||
) {
|
||||
return "Запрошенный период уперся в лимит строк MCP; доступного бюджета помесячных дозапросов не хватило, чтобы покрыть все подпериоды.";
|
||||
}
|
||||
const counterpartyMatch = value.match(/^1C activity rows were found for counterparty\s+(.+)$/i);
|
||||
if (counterpartyMatch) {
|
||||
return `В 1С найдены строки активности по контрагенту ${counterpartyMatch[1]}.`;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
planAssistantMcpDiscovery,
|
||||
type AssistantMcpDiscoveryPlannerContract
|
||||
} from "./assistantMcpDiscoveryPlanner";
|
||||
import type { AssistantMcpDiscoveryDataNeedGraphContract } from "./assistantMcpDiscoveryDataNeedGraph";
|
||||
import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscoveryPolicy";
|
||||
|
||||
export const ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION =
|
||||
@@ -25,6 +26,7 @@ export type AssistantMcpDiscoveryRuntimeBridgeStatus =
|
||||
|
||||
export interface AssistantMcpDiscoveryRuntimeBridgeInput {
|
||||
semanticDataNeed?: string | null;
|
||||
dataNeedGraph?: AssistantMcpDiscoveryDataNeedGraphContract | null;
|
||||
turnMeaning?: AssistantMcpDiscoveryTurnMeaningRef | null;
|
||||
deps?: AssistantMcpDiscoveryPilotExecutorDeps;
|
||||
}
|
||||
@@ -98,6 +100,7 @@ export async function runAssistantMcpDiscoveryRuntimeBridge(
|
||||
): Promise<AssistantMcpDiscoveryRuntimeBridgeContract> {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
semanticDataNeed: input.semanticDataNeed,
|
||||
dataNeedGraph: input.dataNeedGraph,
|
||||
turnMeaning: input.turnMeaning
|
||||
});
|
||||
const pilot = await executeAssistantMcpDiscoveryPilot(planner, input.deps);
|
||||
|
||||
@@ -101,6 +101,7 @@ export async function runAssistantMcpDiscoveryRuntimeEntryPoint(
|
||||
|
||||
const bridge = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||
semanticDataNeed: turnInput.semantic_data_need,
|
||||
dataNeedGraph: turnInput.data_need_graph,
|
||||
turnMeaning: turnInput.turn_meaning_ref,
|
||||
deps: input.deps
|
||||
});
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscoveryPolicy";
|
||||
import {
|
||||
buildAssistantMcpDiscoveryDataNeedGraph,
|
||||
type AssistantMcpDiscoveryDataNeedGraphContract
|
||||
} from "./assistantMcpDiscoveryDataNeedGraph";
|
||||
|
||||
export const ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION =
|
||||
"assistant_mcp_discovery_turn_input_v1" as const;
|
||||
@@ -25,6 +29,7 @@ export interface AssistantMcpDiscoveryTurnInputContract {
|
||||
adapter_status: AssistantMcpDiscoveryTurnInputStatus;
|
||||
should_run_discovery: boolean;
|
||||
semantic_data_need: string | null;
|
||||
data_need_graph: AssistantMcpDiscoveryDataNeedGraphContract | null;
|
||||
turn_meaning_ref: AssistantMcpDiscoveryTurnMeaningRef | null;
|
||||
source_signal: AssistantMcpDiscoveryTurnInputSource;
|
||||
reason_codes: string[];
|
||||
@@ -114,6 +119,10 @@ function compactLower(value: unknown): string {
|
||||
.trim();
|
||||
}
|
||||
|
||||
function sameScopedName(left: string | null, right: string | null): boolean {
|
||||
return Boolean(left && right && compactLower(left) === compactLower(right));
|
||||
}
|
||||
|
||||
function candidateValue(value: unknown): string | null {
|
||||
const direct = toNonEmptyString(value);
|
||||
if (direct && direct !== "[object Object]") {
|
||||
@@ -407,6 +416,12 @@ function hasBidirectionalValueFlowSignal(text: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function hasValueRankingSignal(text: string): boolean {
|
||||
return /(?:кто\s+больше\s+всего.*ден[её]г|больше\s+всего.*ден[её]г|прин[её]с.*ден[её]г|сам(?:ый|ая|ое|ые).*(?:доходн|прибыльн)|most.*money|highest\s+(?:revenue|payment))/iu.test(
|
||||
text
|
||||
);
|
||||
}
|
||||
|
||||
function hasMonthlyAggregationSignal(text: string): boolean {
|
||||
return /(?:\u043f\u043e\s+\u043c\u0435\u0441\u044f\u0446\u0430\u043c|\u043f\u043e\u043c\u0435\u0441\u044f\u0447\u043d\u043e|\u0435\u0436\u0435\u043c\u0435\u0441\u044f\u0447\u043d\u043e|month\s+by\s+month|by\s+month|monthly)/iu.test(
|
||||
text
|
||||
@@ -741,7 +756,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
const rawLifecycleSignal = hasLifecycleSignal(rawText);
|
||||
const rawBidirectionalValueFlowSignal = !rawLifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
|
||||
const rawValueFlowSignal =
|
||||
!rawLifecycleSignal && (hasValueFlowSignal(rawText) || rawBidirectionalValueFlowSignal);
|
||||
!rawLifecycleSignal &&
|
||||
(hasValueFlowSignal(rawText) || hasValueRankingSignal(rawText) || rawBidirectionalValueFlowSignal);
|
||||
const rawMetadataSignal = !rawLifecycleSignal && !rawValueFlowSignal && hasMetadataSignal(rawText);
|
||||
const rawEntityResolutionSignal =
|
||||
!rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
|
||||
@@ -773,6 +789,18 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
|
||||
const assistantTurnMeaningDateScope = toNonEmptyString(assistantTurnMeaning?.explicit_date_scope);
|
||||
const assistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
|
||||
const predecomposeOrganizationMirrorsCounterparty = sameScopedName(
|
||||
predecomposeEntities.counterparty,
|
||||
predecomposeEntities.organization
|
||||
);
|
||||
const organizationMirrorsPredecomposeCounterparty = Boolean(
|
||||
(rawBidirectionalValueFlowSignal || hasValueRankingSignal(rawText)) &&
|
||||
(sameScopedName(predecomposeEntities.counterparty, assistantTurnMeaningOrganizationScope) ||
|
||||
predecomposeOrganizationMirrorsCounterparty)
|
||||
);
|
||||
const normalizedPredecomposeCounterparty = organizationMirrorsPredecomposeCounterparty
|
||||
? null
|
||||
: predecomposeEntities.counterparty;
|
||||
const predecomposeDateScope = collectDateScope(predecomposeContract);
|
||||
const followupDiscoverySeedApplicable = Boolean(
|
||||
followupSeed.domain &&
|
||||
@@ -1038,7 +1066,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) {
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, candidate);
|
||||
}
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, predecomposeEntities.counterparty);
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, normalizedPredecomposeCounterparty);
|
||||
pushNormalizedEntityResolutionCandidate(entityCandidates, followupSeed.counterparty);
|
||||
} else {
|
||||
if (groundedFollowupEntity) {
|
||||
@@ -1047,7 +1075,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) {
|
||||
pushScopedEntityCandidate(entityCandidates, candidate, groundedFollowupEntity);
|
||||
}
|
||||
pushScopedEntityCandidate(entityCandidates, predecomposeEntities.counterparty, groundedFollowupEntity);
|
||||
pushScopedEntityCandidate(entityCandidates, normalizedPredecomposeCounterparty, groundedFollowupEntity);
|
||||
if (!groundedFollowupEntity) {
|
||||
pushScopedEntityCandidate(entityCandidates, followupSeed.counterparty, null);
|
||||
pushScopedEntityCandidate(entityCandidates, followupSeed.discoveryEntity, null);
|
||||
@@ -1058,14 +1086,26 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
pushUnique(entityCandidates, followupSeed.discoveryEntity);
|
||||
pushUnique(entityCandidates, rawMetadataScopeHint);
|
||||
}
|
||||
if (valueFlowSignal && !predecomposeEntities.counterparty && !followupSeed.counterparty) {
|
||||
const openScopeValueFlowWithoutCounterparty =
|
||||
valueFlowSignal && !normalizedPredecomposeCounterparty && !followupSeed.counterparty;
|
||||
const valueFlowOrganizationStaysScope =
|
||||
openScopeValueFlowWithoutCounterparty &&
|
||||
(bidirectionalValueFlowSignal || hasValueRankingSignal(rawText));
|
||||
if (openScopeValueFlowWithoutCounterparty && !valueFlowOrganizationStaysScope) {
|
||||
pushUnique(entityCandidates, predecomposeEntities.organization);
|
||||
pushUnique(entityCandidates, followupSeed.organization);
|
||||
}
|
||||
const explicitOrganizationScope =
|
||||
valueFlowSignal && !predecomposeEntities.counterparty && !followupSeed.counterparty
|
||||
? null
|
||||
: predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope ?? followupSeed.organization;
|
||||
valueFlowOrganizationStaysScope || !openScopeValueFlowWithoutCounterparty
|
||||
? predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope ?? followupSeed.organization
|
||||
: null;
|
||||
if (valueFlowOrganizationStaysScope && explicitOrganizationScope) {
|
||||
for (let index = entityCandidates.length - 1; index >= 0; index -= 1) {
|
||||
if (entityCandidates[index] === explicitOrganizationScope) {
|
||||
entityCandidates.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
const explicitDateScope = assistantTurnMeaningDateScope ?? predecomposeDateScope ?? rawDateScope ?? followupSeed.dateScope;
|
||||
|
||||
const turnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {
|
||||
@@ -1312,7 +1352,10 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
if (unsupported) {
|
||||
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
|
||||
}
|
||||
if (predecomposeEntities.counterparty) {
|
||||
if (
|
||||
!(valueFlowOrganizationStaysScope && normalizedPredecomposeCounterparty === explicitOrganizationScope) &&
|
||||
normalizedPredecomposeCounterparty
|
||||
) {
|
||||
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
|
||||
}
|
||||
if (followupSeed.counterparty) {
|
||||
@@ -1330,6 +1373,17 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
if (runDiscovery && !hasTurnMeaning) {
|
||||
pushReason(reasonCodes, "mcp_discovery_turn_meaning_missing");
|
||||
}
|
||||
const dataNeedGraph =
|
||||
runDiscovery && hasTurnMeaning
|
||||
? buildAssistantMcpDiscoveryDataNeedGraph({
|
||||
semanticDataNeed,
|
||||
rawUtterance: rawSignalSourceText,
|
||||
turnMeaning: cleanTurnMeaning
|
||||
})
|
||||
: null;
|
||||
if (dataNeedGraph) {
|
||||
pushReason(reasonCodes, "mcp_discovery_data_need_graph_built");
|
||||
}
|
||||
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION,
|
||||
@@ -1337,6 +1391,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
||||
adapter_status: !runDiscovery ? "not_applicable" : hasTurnMeaning ? "ready" : "needs_more_context",
|
||||
should_run_discovery: runDiscovery,
|
||||
semantic_data_need: runDiscovery ? semanticDataNeed : null,
|
||||
data_need_graph: dataNeedGraph,
|
||||
turn_meaning_ref: runDiscovery && hasTurnMeaning ? cleanTurnMeaning : null,
|
||||
source_signal: sourceSignal,
|
||||
reason_codes: reasonCodes
|
||||
|
||||
Reference in New Issue
Block a user