ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 3.1 - Добавлен строгий контракт семантического извлечения (валидность, качество, reason-codes, рекомендация применять canonical). Подключен semantic guard в predecompose/runtime: мягкое отклонение плохого canonical: отдельный safe-кейс для сырого полезного raw-фрагмента; fallback теперь выигрывает только когда это оправдано. Подключty semantic arbitration в tool-gate
This commit is contained in:
@@ -13,6 +13,8 @@ export type AddressPredecomposeAggregationProfile =
|
||||
| "open_items"
|
||||
| "unknown";
|
||||
|
||||
export type AddressSemanticExtractionQuality = "high" | "medium" | "low";
|
||||
|
||||
export interface AddressLlmPredecomposeContractV1 {
|
||||
schema_version: "address_llm_predecompose_contract_v1";
|
||||
source_message: string;
|
||||
@@ -41,6 +43,102 @@ export interface AddressLlmPredecomposeContractV1 {
|
||||
aggregation_profile: AddressPredecomposeAggregationProfile;
|
||||
}
|
||||
|
||||
export interface AddressSemanticExtractionContractV1 {
|
||||
schema_version: "address_semantic_extraction_contract_v1";
|
||||
source_message: string;
|
||||
canonical_message: string;
|
||||
canonical_rewrite_applied: boolean;
|
||||
extraction: {
|
||||
mode: AddressQuestionMode;
|
||||
mode_confidence: "high" | "medium" | "low";
|
||||
query_shape: AddressQueryShape;
|
||||
query_shape_confidence: "high" | "medium" | "low";
|
||||
intent: AddressIntent;
|
||||
intent_confidence: "high" | "medium" | "low";
|
||||
aggregation_profile: AddressPredecomposeAggregationProfile;
|
||||
};
|
||||
entities: AddressLlmPredecomposeContractV1["entities"];
|
||||
period: AddressLlmPredecomposeContractV1["period"];
|
||||
guard_hints: {
|
||||
source_data_signal_detected: boolean;
|
||||
canonical_data_signal_detected: boolean;
|
||||
data_scope_meta_query_detected: boolean;
|
||||
deep_investigation_signal_detected: boolean;
|
||||
required_anchor_missing: boolean;
|
||||
unsupported_low_confidence: boolean;
|
||||
semantic_drift_suspected: boolean;
|
||||
};
|
||||
quality: AddressSemanticExtractionQuality;
|
||||
valid: boolean;
|
||||
apply_canonical_recommended: boolean;
|
||||
reason_codes: string[];
|
||||
}
|
||||
|
||||
const ADDRESS_SEMANTIC_DATA_SIGNAL_PATTERN =
|
||||
/(?:\u0434\u043e\u043a|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u0441\u0447(?:\u0435|\u0451)\u0442|\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u0431\u043e\u0440\u043e\u0442|\u043f\u043b\u0430\u0442(?:\u0435|\u0451)\u0436|\u043e\u043f\u0435\u0440\u0430\u0446|\u043f\u0435\u0440\u0438\u043e\u0434|\u0433\u043e\u0434|counterparty|contract|document|account|balance|turnover|operations?|doki|doky|dokument|dogovor|kontragent|schet|saldo|platezh|oplata)/iu;
|
||||
|
||||
const ADDRESS_SEMANTIC_ENTITY_SIGNAL_PATTERN =
|
||||
/(?:\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u043a\u043e\u043d\u0442\u043e\u0440|customer|supplier|counterparty|company|vendor|client)/iu;
|
||||
|
||||
const ADDRESS_SEMANTIC_SCOPE_META_PATTERN =
|
||||
/(?:\u043a\u0430\u043a\u0430\u044f\s+\u0431\u0430\u0437\u0430|\u0431\u0430\u0437\u0430\s+\u043a\u0430\u043a\u043e\u0439\s+\u043a\u043e\u043d\u0442\u043e\u0440|\u043f\u043e\s+\u043a\u0430\u043a\u0438\u043c\s+\u043a\u043e\u043d\u0442\u043e\u0440|which\s+company\s+base|which\s+tenant|data\s+scope)/iu;
|
||||
|
||||
const ADDRESS_SEMANTIC_DEEP_INVESTIGATION_PATTERN =
|
||||
/(?:\u043f\u0440\u043e\u0432\u0435\u0440(?:\u044c|\u0438\u0442\u044c)|\u0440\u0430\u0437\u0431\u0435\u0440(?:\u0438|\u0430\u0442\u044c)|\u043f\u043e\u0447\u0435\u043c\u0443|\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c|\u0440\u0430\u0437\u0440\u044b\u0432|\u0445\u0432\u043e\u0441\u0442|root\s*cause|trace\s*chain|state\s+transition)/iu;
|
||||
|
||||
function normalizeCompact(value: unknown): string {
|
||||
return String(value ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function hasSemanticDataSignal(value: unknown): boolean {
|
||||
return ADDRESS_SEMANTIC_DATA_SIGNAL_PATTERN.test(normalizeCompact(value));
|
||||
}
|
||||
|
||||
function hasSemanticEntitySignal(value: unknown): boolean {
|
||||
return ADDRESS_SEMANTIC_ENTITY_SIGNAL_PATTERN.test(normalizeCompact(value));
|
||||
}
|
||||
|
||||
function hasSemanticDataScopeMetaSignal(value: unknown): boolean {
|
||||
return ADDRESS_SEMANTIC_SCOPE_META_PATTERN.test(normalizeCompact(value));
|
||||
}
|
||||
|
||||
function hasSemanticDeepInvestigationSignal(value: unknown): boolean {
|
||||
return ADDRESS_SEMANTIC_DEEP_INVESTIGATION_PATTERN.test(normalizeCompact(value));
|
||||
}
|
||||
|
||||
function requiredAnchorMissing(contract: AddressLlmPredecomposeContractV1): boolean {
|
||||
const intent = contract.intent;
|
||||
if (
|
||||
intent === "list_documents_by_counterparty" ||
|
||||
intent === "bank_operations_by_counterparty" ||
|
||||
intent === "list_contracts_by_counterparty"
|
||||
) {
|
||||
return !toNonEmptyString(contract.entities.counterparty);
|
||||
}
|
||||
if (intent === "list_documents_by_contract" || intent === "bank_operations_by_contract") {
|
||||
return !toNonEmptyString(contract.entities.contract);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function deriveSemanticQuality(input: {
|
||||
valid: boolean;
|
||||
unsupportedLowConfidence: boolean;
|
||||
requiredAnchorMissing: boolean;
|
||||
unknownIntentAndShape: boolean;
|
||||
}): AddressSemanticExtractionQuality {
|
||||
if (!input.valid) {
|
||||
return "low";
|
||||
}
|
||||
if (input.unsupportedLowConfidence || input.requiredAnchorMissing || input.unknownIntentAndShape) {
|
||||
return "medium";
|
||||
}
|
||||
return "high";
|
||||
}
|
||||
|
||||
function toNonEmptyString(value: unknown): string | null {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
@@ -167,3 +265,115 @@ export function buildAddressLlmPredecomposeContractV1(input: {
|
||||
aggregation_profile: inferAggregationProfile(intent.intent, shape.shape)
|
||||
};
|
||||
}
|
||||
|
||||
export function buildAddressSemanticExtractionContractV1(input: {
|
||||
sourceMessage: string;
|
||||
canonicalMessage: string;
|
||||
predecomposeContract?: AddressLlmPredecomposeContractV1 | null;
|
||||
}): AddressSemanticExtractionContractV1 {
|
||||
const sourceMessage = String(input.sourceMessage ?? "").trim();
|
||||
const canonicalMessage = String(input.canonicalMessage ?? "").trim() || sourceMessage;
|
||||
const predecomposeContract =
|
||||
input.predecomposeContract ??
|
||||
buildAddressLlmPredecomposeContractV1({
|
||||
sourceMessage,
|
||||
canonicalMessage
|
||||
});
|
||||
|
||||
const sourceDataSignal = hasSemanticDataSignal(sourceMessage);
|
||||
const canonicalDataSignal = hasSemanticDataSignal(canonicalMessage);
|
||||
const canonicalEntitySignal = hasSemanticEntitySignal(canonicalMessage);
|
||||
const dataScopeMetaSignal = hasSemanticDataScopeMetaSignal(sourceMessage);
|
||||
const deepInvestigationSignal =
|
||||
hasSemanticDeepInvestigationSignal(sourceMessage) || hasSemanticDeepInvestigationSignal(canonicalMessage);
|
||||
const unsupportedLowConfidence =
|
||||
predecomposeContract.mode === "unsupported" &&
|
||||
(predecomposeContract.mode_confidence === "low" || predecomposeContract.mode_confidence === "medium");
|
||||
const missingRequiredAnchor = requiredAnchorMissing(predecomposeContract);
|
||||
const unknownIntentAndShape =
|
||||
predecomposeContract.intent === "unknown" && predecomposeContract.query_shape === "UNKNOWN";
|
||||
const rewriteApplied = normalizeCompact(sourceMessage) !== normalizeCompact(canonicalMessage);
|
||||
const semanticDriftSuspected = rewriteApplied && sourceDataSignal && !canonicalDataSignal;
|
||||
|
||||
const reasonCodes: string[] = [];
|
||||
if (dataScopeMetaSignal) {
|
||||
reasonCodes.push("data_scope_meta_query_detected");
|
||||
}
|
||||
if (unsupportedLowConfidence) {
|
||||
reasonCodes.push("unsupported_low_confidence_contract");
|
||||
}
|
||||
if (missingRequiredAnchor) {
|
||||
reasonCodes.push("required_anchor_missing_for_intent");
|
||||
}
|
||||
if (semanticDriftSuspected) {
|
||||
reasonCodes.push("semantic_drift_source_vs_canonical");
|
||||
}
|
||||
if (unknownIntentAndShape && rewriteApplied) {
|
||||
reasonCodes.push("rewrite_without_structured_gain");
|
||||
}
|
||||
if (deepInvestigationSignal) {
|
||||
reasonCodes.push("deep_investigation_signal_detected");
|
||||
}
|
||||
|
||||
const valid =
|
||||
!dataScopeMetaSignal &&
|
||||
!semanticDriftSuspected &&
|
||||
!(unsupportedLowConfidence && !canonicalDataSignal && !sourceDataSignal && !canonicalEntitySignal) &&
|
||||
!(unknownIntentAndShape && rewriteApplied && !canonicalDataSignal && !canonicalEntitySignal);
|
||||
|
||||
const applyCanonicalRecommended =
|
||||
valid &&
|
||||
!(unsupportedLowConfidence && sourceDataSignal && rewriteApplied) &&
|
||||
!(missingRequiredAnchor && rewriteApplied);
|
||||
|
||||
const quality = deriveSemanticQuality({
|
||||
valid,
|
||||
unsupportedLowConfidence,
|
||||
requiredAnchorMissing: missingRequiredAnchor,
|
||||
unknownIntentAndShape
|
||||
});
|
||||
|
||||
return {
|
||||
schema_version: "address_semantic_extraction_contract_v1",
|
||||
source_message: sourceMessage,
|
||||
canonical_message: canonicalMessage,
|
||||
canonical_rewrite_applied: rewriteApplied,
|
||||
extraction: {
|
||||
mode: predecomposeContract.mode,
|
||||
mode_confidence: predecomposeContract.mode_confidence,
|
||||
query_shape: predecomposeContract.query_shape,
|
||||
query_shape_confidence: predecomposeContract.query_shape_confidence,
|
||||
intent: predecomposeContract.intent,
|
||||
intent_confidence: predecomposeContract.intent_confidence,
|
||||
aggregation_profile: predecomposeContract.aggregation_profile
|
||||
},
|
||||
entities: {
|
||||
account: predecomposeContract.entities.account,
|
||||
counterparty: predecomposeContract.entities.counterparty,
|
||||
contract: predecomposeContract.entities.contract,
|
||||
document_type: predecomposeContract.entities.document_type,
|
||||
document_ref: predecomposeContract.entities.document_ref,
|
||||
organization: predecomposeContract.entities.organization
|
||||
},
|
||||
period: {
|
||||
scope: predecomposeContract.period.scope,
|
||||
period_from: predecomposeContract.period.period_from,
|
||||
period_to: predecomposeContract.period.period_to,
|
||||
as_of_date: predecomposeContract.period.as_of_date,
|
||||
has_explicit_period: predecomposeContract.period.has_explicit_period
|
||||
},
|
||||
guard_hints: {
|
||||
source_data_signal_detected: sourceDataSignal,
|
||||
canonical_data_signal_detected: canonicalDataSignal,
|
||||
data_scope_meta_query_detected: dataScopeMetaSignal,
|
||||
deep_investigation_signal_detected: deepInvestigationSignal,
|
||||
required_anchor_missing: missingRequiredAnchor,
|
||||
unsupported_low_confidence: unsupportedLowConfidence,
|
||||
semantic_drift_suspected: semanticDriftSuspected
|
||||
},
|
||||
quality,
|
||||
valid,
|
||||
apply_canonical_recommended: applyCanonicalRecommended,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user