АДРЕСНЫЙ РЕЖИМ - локальная подель на декомпозе
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
import type { AddressIntent, AddressResponseType } from "../../types/addressQuery";
|
||||
|
||||
export interface ComposeStageRow {
|
||||
period: string | null;
|
||||
registrator: string;
|
||||
account_dt: string | null;
|
||||
account_kt: string | null;
|
||||
amount: number | null;
|
||||
analytics: string[];
|
||||
}
|
||||
|
||||
function uniqueStrings(values: string[]): string[] {
|
||||
return Array.from(
|
||||
new Set(
|
||||
values
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item.length > 0)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function formatTopRows(rows: ComposeStageRow[], limit = 6): string[] {
|
||||
return rows.slice(0, limit).map((row, index) => {
|
||||
const period = row.period ?? "дата не указана";
|
||||
const amount = row.amount !== null ? `${row.amount}` : "сумма не указана";
|
||||
const accounts = [row.account_dt ?? "-", row.account_kt ?? "-"].join(" / ");
|
||||
const analytics = row.analytics.length > 0 ? ` | аналитика: ${row.analytics.slice(0, 2).join("; ")}` : "";
|
||||
return `${index + 1}. ${period} | ${row.registrator} | ${accounts} | ${amount}${analytics}`;
|
||||
});
|
||||
}
|
||||
|
||||
export function contractCandidatesFromRows(rows: ComposeStageRow[]): string[] {
|
||||
const candidates: string[] = [];
|
||||
for (const row of rows) {
|
||||
for (const token of [row.registrator, ...row.analytics]) {
|
||||
const normalized = token.trim();
|
||||
if (!normalized) {
|
||||
continue;
|
||||
}
|
||||
if (/договор|contract|дог\./i.test(normalized)) {
|
||||
candidates.push(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
return uniqueStrings(candidates);
|
||||
}
|
||||
|
||||
export function composeFactualReply(
|
||||
intent: AddressIntent,
|
||||
rows: ComposeStageRow[]
|
||||
): { responseType: AddressResponseType; text: string } {
|
||||
if (intent === "account_balance_snapshot") {
|
||||
const movementSum = rows.reduce((sum, row) => sum + (row.amount ?? 0), 0);
|
||||
const lines = [
|
||||
"Адресный срез по счету собран (по движениям live MCP).",
|
||||
`Строк отобрано: ${rows.length}.`,
|
||||
`Сумма по отобранным движениям: ${movementSum}.`,
|
||||
...formatTopRows(rows, 4)
|
||||
];
|
||||
return {
|
||||
responseType: "FACTUAL_SUMMARY",
|
||||
text: lines.join("\n")
|
||||
};
|
||||
}
|
||||
|
||||
if (intent === "documents_forming_balance") {
|
||||
const movementSum = rows.reduce((sum, row) => sum + (row.amount ?? 0), 0);
|
||||
const lines = [
|
||||
"Собран drilldown документов, формирующих остаток по счету на указанную дату.",
|
||||
`Документных строк отобрано: ${rows.length}.`,
|
||||
`Сумма по отобранным движениям: ${movementSum}.`,
|
||||
...formatTopRows(rows, 8),
|
||||
"Можно уточнить выборку по контрагенту, договору или периоду."
|
||||
];
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: lines.join("\n")
|
||||
};
|
||||
}
|
||||
|
||||
if (intent === "list_open_contracts") {
|
||||
const contracts = contractCandidatesFromRows(rows);
|
||||
const lines = [
|
||||
"Собраны кандидаты по незакрытым договорным позициям (по live движениям 60/62/76).",
|
||||
`Строк движения: ${rows.length}.`,
|
||||
`Договорных кандидатов: ${contracts.length}.`
|
||||
];
|
||||
lines.push(...contracts.slice(0, 8).map((item, index) => `${index + 1}. ${item}`));
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: lines.join("\n")
|
||||
};
|
||||
}
|
||||
|
||||
if (intent === "open_items_by_counterparty_or_contract") {
|
||||
const lines = [
|
||||
"Собраны открытые позиции по указанному фильтру (контрагент/договор).",
|
||||
`Строк отобрано: ${rows.length}.`,
|
||||
...formatTopRows(rows, 6)
|
||||
];
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: lines.join("\n")
|
||||
};
|
||||
}
|
||||
|
||||
if (intent === "list_documents_by_counterparty") {
|
||||
const lines = [
|
||||
"Собран список документов по контрагенту (live address lane).",
|
||||
`Строк отобрано: ${rows.length}.`,
|
||||
...formatTopRows(rows, rows.length)
|
||||
];
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: lines.join("\n")
|
||||
};
|
||||
}
|
||||
|
||||
if (intent === "bank_operations_by_counterparty") {
|
||||
const lines = [
|
||||
"Собран список банковских операций по контрагенту (live address lane).",
|
||||
`Строк отобрано: ${rows.length}.`,
|
||||
...formatTopRows(rows, rows.length)
|
||||
];
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: lines.join("\n")
|
||||
};
|
||||
}
|
||||
|
||||
const title =
|
||||
intent === "list_payables_counterparties"
|
||||
? "Срез обязательств (payables) собран по движениям с account scope 60/76."
|
||||
: intent === "list_receivables_counterparties"
|
||||
? "Срез требований (receivables) собран по движениям с account scope 62/76."
|
||||
: "Срез адресного запроса собран.";
|
||||
|
||||
const lines = [title, `Строк отобрано: ${rows.length}.`, ...formatTopRows(rows, 6)];
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: lines.join("\n")
|
||||
};
|
||||
}
|
||||
|
||||
export function inferReplyType(responseType: AddressResponseType): "factual" | "partial_coverage" {
|
||||
if (responseType === "FACTUAL_LIST" || responseType === "FACTUAL_SUMMARY") {
|
||||
return "factual";
|
||||
}
|
||||
return "partial_coverage";
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
import type {
|
||||
AddressFilterSet,
|
||||
AddressIntent,
|
||||
AddressIntentResolution,
|
||||
AddressModeDetection,
|
||||
AddressQueryShapeDetection
|
||||
} from "../../types/addressQuery";
|
||||
import { detectAddressQuestionMode } from "../addressQueryClassifier";
|
||||
import { classifyAddressQueryShape } from "../addressQueryShapeClassifier";
|
||||
import { resolveAddressIntent } from "../addressIntentResolver";
|
||||
import { extractAddressFilters } from "../addressFilterExtractor";
|
||||
|
||||
export interface AddressFollowupContext {
|
||||
previous_intent?: AddressIntent;
|
||||
previous_filters?: AddressFilterSet;
|
||||
previous_anchor_type?: "account" | "counterparty" | "contract" | "document_ref" | "unknown" | null;
|
||||
previous_anchor_value?: string | null;
|
||||
}
|
||||
|
||||
export interface AddressDecomposeStageResult {
|
||||
mode: AddressModeDetection;
|
||||
shape: AddressQueryShapeDetection;
|
||||
intent: AddressIntentResolution;
|
||||
filters: {
|
||||
extracted_filters: AddressFilterSet;
|
||||
missing_required_filters: string[];
|
||||
warnings: string[];
|
||||
};
|
||||
baseReasons: string[];
|
||||
}
|
||||
|
||||
function hasExplicitPeriodWindow(filters: AddressFilterSet): boolean {
|
||||
return (
|
||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0) ||
|
||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
function toNonEmptyString(value: unknown): string | null {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
const normalized = String(value).trim();
|
||||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
|
||||
function hasAllTimeHint(text: string): boolean {
|
||||
return /(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+всю\s+истори(?:ю|и)|за\s+любой\s+период|for\s+all\s+time|all\s+time|for\s+entire\s+period|entire\s+period|for\s+any\s+period|any\s+period|for\s+full\s+history|full\s+history)/iu.test(
|
||||
String(text ?? "")
|
||||
);
|
||||
}
|
||||
|
||||
export function hasAddressFollowupContextSignal(text: string): boolean {
|
||||
const normalized = String(text ?? "").trim();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
if (hasAllTimeHint(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (/(?:^|\s)(?:и|а\s+еще|а\s+ещё|еще|ещё|также|по\s+этому|по\s+тому|это\s+же|в\s+этом|тот\s+же|also|same|that)/iu.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
return normalized.split(/\s+/).filter(Boolean).length <= 8;
|
||||
}
|
||||
|
||||
function mergeFollowupFilters(
|
||||
current: AddressFilterSet,
|
||||
intent: AddressIntent,
|
||||
userMessage: string,
|
||||
followupContext: AddressFollowupContext | null
|
||||
): { filters: AddressFilterSet; reasons: string[] } {
|
||||
const merged: AddressFilterSet = { ...current };
|
||||
const reasons: string[] = [];
|
||||
if (!followupContext) {
|
||||
return { filters: merged, reasons };
|
||||
}
|
||||
|
||||
const previous = followupContext.previous_filters ?? {};
|
||||
const previousAnchorValue = toNonEmptyString(followupContext.previous_anchor_value);
|
||||
const previousCounterparty = toNonEmptyString(previous.counterparty);
|
||||
const previousContract = toNonEmptyString(previous.contract);
|
||||
const previousAccount = toNonEmptyString(previous.account);
|
||||
const allTimeRequested = hasAllTimeHint(userMessage);
|
||||
|
||||
if (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty") {
|
||||
if (!toNonEmptyString(merged.counterparty)) {
|
||||
const inheritedCounterparty =
|
||||
previousCounterparty ??
|
||||
(followupContext.previous_anchor_type === "counterparty" ? previousAnchorValue : null);
|
||||
if (inheritedCounterparty) {
|
||||
merged.counterparty = inheritedCounterparty;
|
||||
reasons.push("counterparty_from_followup_context");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (intent === "account_balance_snapshot" || intent === "documents_forming_balance") {
|
||||
if (!toNonEmptyString(merged.account)) {
|
||||
const inheritedAccount =
|
||||
previousAccount ??
|
||||
(followupContext.previous_anchor_type === "account" ? previousAnchorValue : null);
|
||||
if (inheritedAccount) {
|
||||
merged.account = inheritedAccount;
|
||||
reasons.push("account_from_followup_context");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (intent === "open_items_by_counterparty_or_contract" || intent === "list_open_contracts") {
|
||||
if (!toNonEmptyString(merged.contract)) {
|
||||
const inheritedContract =
|
||||
previousContract ??
|
||||
(followupContext.previous_anchor_type === "contract" ? previousAnchorValue : null);
|
||||
if (inheritedContract) {
|
||||
merged.contract = inheritedContract;
|
||||
reasons.push("contract_from_followup_context");
|
||||
}
|
||||
}
|
||||
if (!toNonEmptyString(merged.counterparty)) {
|
||||
const inheritedCounterparty =
|
||||
previousCounterparty ??
|
||||
(followupContext.previous_anchor_type === "counterparty" ? previousAnchorValue : null);
|
||||
if (inheritedCounterparty) {
|
||||
merged.counterparty = inheritedCounterparty;
|
||||
reasons.push("counterparty_from_followup_context");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allTimeRequested) {
|
||||
if (toNonEmptyString(merged.period_from) || toNonEmptyString(merged.period_to)) {
|
||||
delete merged.period_from;
|
||||
delete merged.period_to;
|
||||
reasons.push("period_cleared_by_all_time_followup");
|
||||
}
|
||||
return { filters: merged, reasons };
|
||||
}
|
||||
|
||||
const currentHasPeriod = hasExplicitPeriodWindow(merged);
|
||||
const previousHasPeriod = hasExplicitPeriodWindow(previous);
|
||||
if (!currentHasPeriod && previousHasPeriod && hasAddressFollowupContextSignal(userMessage)) {
|
||||
if (toNonEmptyString(previous.period_from)) {
|
||||
merged.period_from = previous.period_from;
|
||||
}
|
||||
if (toNonEmptyString(previous.period_to)) {
|
||||
merged.period_to = previous.period_to;
|
||||
}
|
||||
reasons.push("period_from_followup_context");
|
||||
}
|
||||
|
||||
return { filters: merged, reasons };
|
||||
}
|
||||
|
||||
function resolveMissingRequiredFilters(intent: AddressIntent, filters: AddressFilterSet): string[] {
|
||||
const requiredByIntent: Record<string, Array<keyof AddressFilterSet>> = {
|
||||
account_balance_snapshot: ["account", "as_of_date"],
|
||||
documents_forming_balance: ["account", "as_of_date"],
|
||||
list_documents_by_counterparty: ["counterparty"],
|
||||
bank_operations_by_counterparty: ["counterparty"]
|
||||
};
|
||||
const required = requiredByIntent[intent] ?? [];
|
||||
return required.filter((key) => {
|
||||
const value = filters[key];
|
||||
return value === undefined || value === null || String(value).trim() === "";
|
||||
});
|
||||
}
|
||||
|
||||
function deriveIntentWithFollowupContext(
|
||||
detectedIntent: AddressIntentResolution,
|
||||
userMessage: string,
|
||||
followupContext: AddressFollowupContext | null
|
||||
): AddressIntentResolution {
|
||||
if (!followupContext || !followupContext.previous_intent) {
|
||||
return detectedIntent;
|
||||
}
|
||||
if (detectedIntent.intent !== "unknown") {
|
||||
return detectedIntent;
|
||||
}
|
||||
if (!hasAddressFollowupContextSignal(userMessage)) {
|
||||
return detectedIntent;
|
||||
}
|
||||
return {
|
||||
intent: followupContext.previous_intent,
|
||||
confidence: "low",
|
||||
reasons: [...detectedIntent.reasons, "intent_from_followup_context"]
|
||||
};
|
||||
}
|
||||
|
||||
export function runAddressDecomposeStage(
|
||||
userMessage: string,
|
||||
followupContext: AddressFollowupContext | null
|
||||
): AddressDecomposeStageResult | null {
|
||||
const detectedMode = detectAddressQuestionMode(userMessage);
|
||||
const mode =
|
||||
detectedMode.mode === "address_query"
|
||||
? detectedMode
|
||||
: followupContext && hasAddressFollowupContextSignal(userMessage)
|
||||
? {
|
||||
mode: "address_query" as const,
|
||||
confidence: "medium" as const,
|
||||
reasons: [...detectedMode.reasons, "address_mode_from_followup_context"]
|
||||
}
|
||||
: detectedMode;
|
||||
if (mode.mode !== "address_query") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const shape = classifyAddressQueryShape(userMessage);
|
||||
if (shape.shape === "EXPLAIN_OR_REASON") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const detectedIntent = resolveAddressIntent(userMessage);
|
||||
const intent = deriveIntentWithFollowupContext(detectedIntent, userMessage, followupContext);
|
||||
const extractedFilters = extractAddressFilters(userMessage, intent.intent);
|
||||
const followupMerged = mergeFollowupFilters(extractedFilters.extracted_filters, intent.intent, userMessage, followupContext);
|
||||
const filters = {
|
||||
extracted_filters: followupMerged.filters,
|
||||
missing_required_filters: resolveMissingRequiredFilters(intent.intent, followupMerged.filters),
|
||||
warnings: [...new Set([...extractedFilters.warnings, ...followupMerged.reasons])]
|
||||
};
|
||||
const followupContextApplied =
|
||||
Boolean(followupContext) &&
|
||||
(mode.reasons.includes("address_mode_from_followup_context") ||
|
||||
intent.reasons.includes("intent_from_followup_context") ||
|
||||
followupMerged.reasons.length > 0);
|
||||
const baseReasons = [
|
||||
...mode.reasons,
|
||||
...shape.reasons,
|
||||
...intent.reasons,
|
||||
...followupMerged.reasons,
|
||||
...(followupContextApplied ? ["address_followup_context_applied"] : [])
|
||||
];
|
||||
|
||||
return {
|
||||
mode,
|
||||
shape,
|
||||
intent,
|
||||
filters,
|
||||
baseReasons
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
import type { AddressFilterSet, AddressIntent } from "../../types/addressQuery";
|
||||
|
||||
const PARTY_ANCHOR_STOPWORDS = new Set([
|
||||
"ооо",
|
||||
"ао",
|
||||
"зао",
|
||||
"ип",
|
||||
"llc",
|
||||
"ltd",
|
||||
"company",
|
||||
"компания",
|
||||
"контрагент",
|
||||
"counterparty",
|
||||
"по",
|
||||
"by"
|
||||
]);
|
||||
|
||||
export interface AnchorResolutionDebug {
|
||||
anchor_type: "account" | "counterparty" | "contract" | "document_ref" | "unknown" | null;
|
||||
anchor_value_raw: string | null;
|
||||
anchor_value_resolved: string | null;
|
||||
resolver_confidence: "high" | "medium" | "low" | null;
|
||||
ambiguity_count: number;
|
||||
}
|
||||
|
||||
export interface ResolveStageRow {
|
||||
registrator: string;
|
||||
account_dt: string | null;
|
||||
account_kt: string | null;
|
||||
analytics: string[];
|
||||
}
|
||||
|
||||
function transliterateCyrillicToLatin(value: string): string {
|
||||
const map: Record<string, string> = {
|
||||
а: "a",
|
||||
б: "b",
|
||||
в: "v",
|
||||
г: "g",
|
||||
д: "d",
|
||||
е: "e",
|
||||
ё: "e",
|
||||
ж: "zh",
|
||||
з: "z",
|
||||
и: "i",
|
||||
й: "y",
|
||||
к: "k",
|
||||
л: "l",
|
||||
м: "m",
|
||||
н: "n",
|
||||
о: "o",
|
||||
п: "p",
|
||||
р: "r",
|
||||
с: "s",
|
||||
т: "t",
|
||||
у: "u",
|
||||
ф: "f",
|
||||
х: "h",
|
||||
ц: "ts",
|
||||
ч: "ch",
|
||||
ш: "sh",
|
||||
щ: "sch",
|
||||
ъ: "",
|
||||
ы: "y",
|
||||
ь: "",
|
||||
э: "e",
|
||||
ю: "yu",
|
||||
я: "ya"
|
||||
};
|
||||
let out = "";
|
||||
for (const char of String(value ?? "").toLowerCase()) {
|
||||
out += map[char] ?? char;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function normalizeSearchText(value: string): string {
|
||||
return String(value ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/ё/g, "е")
|
||||
.replace(/[^a-zа-я0-9]+/gi, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function tokenizeAnchor(value: string): string[] {
|
||||
return normalizeSearchText(value)
|
||||
.split(" ")
|
||||
.map((token) => token.trim())
|
||||
.filter((token) => token.length >= 2 && !PARTY_ANCHOR_STOPWORDS.has(token));
|
||||
}
|
||||
|
||||
function matchesAnchorText(searchable: string, anchor: string): boolean {
|
||||
const searchableNormalized = normalizeSearchText(searchable);
|
||||
const searchableLatin = transliterateCyrillicToLatin(searchableNormalized);
|
||||
const tokens = tokenizeAnchor(anchor);
|
||||
if (tokens.length === 0) {
|
||||
const direct = normalizeSearchText(anchor);
|
||||
if (!direct) {
|
||||
return false;
|
||||
}
|
||||
return searchableNormalized.includes(direct) || searchableLatin.includes(transliterateCyrillicToLatin(direct));
|
||||
}
|
||||
return tokens.every((token) => {
|
||||
const tokenLatin = transliterateCyrillicToLatin(token);
|
||||
return searchableNormalized.includes(token) || searchableLatin.includes(tokenLatin);
|
||||
});
|
||||
}
|
||||
|
||||
function uniqueStrings(values: string[]): string[] {
|
||||
return Array.from(
|
||||
new Set(
|
||||
values
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item.length > 0)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function resolvePrimaryAnchor(intent: AddressIntent, filters: AddressFilterSet): AnchorResolutionDebug {
|
||||
const account = typeof filters.account === "string" ? filters.account.trim() : "";
|
||||
const counterparty = typeof filters.counterparty === "string" ? filters.counterparty.trim() : "";
|
||||
const contract = typeof filters.contract === "string" ? filters.contract.trim() : "";
|
||||
const documentRef = typeof filters.document_ref === "string" ? filters.document_ref.trim() : "";
|
||||
|
||||
if (intent === "account_balance_snapshot" || intent === "documents_forming_balance") {
|
||||
if (account) {
|
||||
return {
|
||||
anchor_type: "account",
|
||||
anchor_value_raw: account,
|
||||
anchor_value_resolved: account,
|
||||
resolver_confidence: "high",
|
||||
ambiguity_count: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (counterparty) {
|
||||
return {
|
||||
anchor_type: "counterparty",
|
||||
anchor_value_raw: counterparty,
|
||||
anchor_value_resolved: counterparty,
|
||||
resolver_confidence: "medium",
|
||||
ambiguity_count: 0
|
||||
};
|
||||
}
|
||||
|
||||
if (contract) {
|
||||
return {
|
||||
anchor_type: "contract",
|
||||
anchor_value_raw: contract,
|
||||
anchor_value_resolved: contract,
|
||||
resolver_confidence: "medium",
|
||||
ambiguity_count: 0
|
||||
};
|
||||
}
|
||||
|
||||
if (documentRef) {
|
||||
return {
|
||||
anchor_type: "document_ref",
|
||||
anchor_value_raw: documentRef,
|
||||
anchor_value_resolved: documentRef,
|
||||
resolver_confidence: "medium",
|
||||
ambiguity_count: 0
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
anchor_type: "unknown",
|
||||
anchor_value_raw: null,
|
||||
anchor_value_resolved: null,
|
||||
resolver_confidence: "low",
|
||||
ambiguity_count: 0
|
||||
};
|
||||
}
|
||||
|
||||
export function refineAnchorFromRows(anchor: AnchorResolutionDebug, rows: ResolveStageRow[]): AnchorResolutionDebug {
|
||||
if (rows.length === 0) {
|
||||
return anchor;
|
||||
}
|
||||
if (anchor.anchor_type !== "counterparty" && anchor.anchor_type !== "contract") {
|
||||
return anchor;
|
||||
}
|
||||
const needleRaw = String(anchor.anchor_value_raw ?? "").trim();
|
||||
if (!needleRaw) {
|
||||
return anchor;
|
||||
}
|
||||
const candidates = uniqueStrings(
|
||||
rows
|
||||
.flatMap((row) => row.analytics)
|
||||
.map((value) => value.trim())
|
||||
.filter((value) => value.length >= 2 && matchesAnchorText(value, needleRaw))
|
||||
);
|
||||
if (candidates.length === 0) {
|
||||
return anchor;
|
||||
}
|
||||
if (candidates.length === 1) {
|
||||
return {
|
||||
...anchor,
|
||||
anchor_value_resolved: candidates[0],
|
||||
resolver_confidence: anchor.resolver_confidence === "high" ? "high" : "medium",
|
||||
ambiguity_count: 0
|
||||
};
|
||||
}
|
||||
return {
|
||||
...anchor,
|
||||
anchor_value_resolved: candidates[0],
|
||||
resolver_confidence: "low",
|
||||
ambiguity_count: candidates.length - 1
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user