ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Спека exact-маршрута payables на дату: confirmed_balance без эвристического финала
This commit is contained in:
@@ -1,4 +1,9 @@
|
||||
import type { AddressIntent, AddressResponseType } from "../../types/addressQuery";
|
||||
import type {
|
||||
AddressEvidenceStrength,
|
||||
AddressIntent,
|
||||
AddressResponseType,
|
||||
AddressResultMode
|
||||
} from "../../types/addressQuery";
|
||||
|
||||
export interface ComposeStageRow {
|
||||
period: string | null;
|
||||
@@ -14,6 +19,13 @@ interface ComposeFactualReplyOptions {
|
||||
periodFrom?: string;
|
||||
periodTo?: string;
|
||||
asOfDate?: string;
|
||||
requestedResultMode?: AddressResultMode;
|
||||
}
|
||||
|
||||
export interface ComposeReplySemantics {
|
||||
result_mode?: AddressResultMode;
|
||||
evidence_strength?: AddressEvidenceStrength;
|
||||
balance_confirmed?: boolean;
|
||||
}
|
||||
|
||||
type PeriodProfileFocus =
|
||||
@@ -618,6 +630,307 @@ interface CounterpartyRiskAggregate {
|
||||
lastPeriod: string | null;
|
||||
}
|
||||
|
||||
type PayablesLiabilityCategory = "supplier_or_contractor" | "bank_or_credit" | "tax_or_state" | "other";
|
||||
|
||||
interface PayablesCounterpartyRiskAggregate extends CounterpartyRiskAggregate {
|
||||
category: PayablesLiabilityCategory;
|
||||
categoryReasons: string[];
|
||||
}
|
||||
|
||||
interface PayablesConfirmedBalanceAggregate {
|
||||
name: string;
|
||||
outstandingAmount: number;
|
||||
operations: number;
|
||||
firstPeriod: string | null;
|
||||
lastPeriod: string | null;
|
||||
category: PayablesLiabilityCategory;
|
||||
categoryReasons: string[];
|
||||
}
|
||||
|
||||
function liabilityCategoryLabel(category: PayablesLiabilityCategory): string {
|
||||
if (category === "supplier_or_contractor") {
|
||||
return "поставщики/подрядчики";
|
||||
}
|
||||
if (category === "bank_or_credit") {
|
||||
return "банки/кредиты";
|
||||
}
|
||||
if (category === "tax_or_state") {
|
||||
return "налоги/госорганы";
|
||||
}
|
||||
return "прочие";
|
||||
}
|
||||
|
||||
function classifyPayablesLiabilityCategory(row: ComposeStageRow, counterparty: string): {
|
||||
scores: Record<PayablesLiabilityCategory, number>;
|
||||
reasons: string[];
|
||||
} {
|
||||
const scores: Record<PayablesLiabilityCategory, number> = {
|
||||
supplier_or_contractor: 0,
|
||||
bank_or_credit: 0,
|
||||
tax_or_state: 0,
|
||||
other: 0
|
||||
};
|
||||
const reasons = new Set<string>();
|
||||
const text = `${counterparty} ${row.registrator} ${row.analytics.join(" ")}`.toLowerCase();
|
||||
|
||||
const accountPrefixes = [extractAccountSectionCode(row.account_dt), extractAccountSectionCode(row.account_kt)].filter(
|
||||
(item): item is string => Boolean(item)
|
||||
);
|
||||
if (accountPrefixes.includes("60")) {
|
||||
scores.supplier_or_contractor += 3;
|
||||
reasons.add("участие счета 60");
|
||||
}
|
||||
if (accountPrefixes.includes("66") || accountPrefixes.includes("67")) {
|
||||
scores.bank_or_credit += 4;
|
||||
reasons.add("участие счета 66/67");
|
||||
}
|
||||
if (accountPrefixes.includes("68") || accountPrefixes.includes("69")) {
|
||||
scores.tax_or_state += 4;
|
||||
reasons.add("участие счета 68/69");
|
||||
}
|
||||
if (accountPrefixes.includes("76")) {
|
||||
scores.supplier_or_contractor += 1;
|
||||
reasons.add("участие счета 76");
|
||||
}
|
||||
|
||||
if (/(?:банк|сбер|втб|альфа|газпромбанк|кредит|депозит|loan|overdraft|deposit)/iu.test(text)) {
|
||||
scores.bank_or_credit += 3;
|
||||
reasons.add("банк/кредит в аналитике");
|
||||
}
|
||||
if (/(?:уфк|ифнс|фнс|налог|пфр|фсс|сфр|казнач|бюджет|гос|департамент|министер|муницип|город москвы|федерал)/iu.test(text)) {
|
||||
scores.tax_or_state += 3;
|
||||
reasons.add("налог/госорган в аналитике");
|
||||
}
|
||||
if (/(?:\bип\b|ооо|ао|зао|пао|подряд|поставщик|supplier|vendor|contractor)/iu.test(text)) {
|
||||
scores.supplier_or_contractor += 2;
|
||||
reasons.add("коммерческий контрагент в аналитике");
|
||||
}
|
||||
|
||||
return {
|
||||
scores,
|
||||
reasons: Array.from(reasons)
|
||||
};
|
||||
}
|
||||
|
||||
const PAYABLES_CATEGORY_KEYS: PayablesLiabilityCategory[] = ["supplier_or_contractor", "bank_or_credit", "tax_or_state", "other"];
|
||||
|
||||
function resolvePayablesLiabilityCategory(
|
||||
scores: Record<PayablesLiabilityCategory, number>
|
||||
): PayablesLiabilityCategory {
|
||||
let winner: PayablesLiabilityCategory = "other";
|
||||
let best = Number.NEGATIVE_INFINITY;
|
||||
for (const key of PAYABLES_CATEGORY_KEYS) {
|
||||
const score = scores[key];
|
||||
if (score > best) {
|
||||
best = score;
|
||||
winner = key;
|
||||
}
|
||||
}
|
||||
if (best <= 0) {
|
||||
return "other";
|
||||
}
|
||||
return winner;
|
||||
}
|
||||
|
||||
function hasPayablesSectionPrefix(account: string | null): boolean {
|
||||
const section = extractAccountSectionCode(account);
|
||||
return section === "60" || section === "76";
|
||||
}
|
||||
|
||||
function resolvePayablesAsOfDate(options: ComposeFactualReplyOptions): string {
|
||||
const explicit = normalizeIsoDateOnly(options.asOfDate);
|
||||
if (explicit) {
|
||||
return explicit;
|
||||
}
|
||||
const periodTo = normalizeIsoDateOnly(options.periodTo);
|
||||
if (periodTo) {
|
||||
return periodTo;
|
||||
}
|
||||
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
||||
if (periodFrom) {
|
||||
return periodFrom;
|
||||
}
|
||||
const now = new Date();
|
||||
return toIsoDate(now.getUTCFullYear(), now.getUTCMonth() + 1, now.getUTCDate());
|
||||
}
|
||||
|
||||
function buildPayablesCounterpartyRiskAggregate(rows: ComposeStageRow[]): PayablesCounterpartyRiskAggregate[] {
|
||||
const byCounterparty = new Map<
|
||||
string,
|
||||
{
|
||||
base: CounterpartyRiskAggregate;
|
||||
categoryScores: Record<PayablesLiabilityCategory, number>;
|
||||
reasons: Set<string>;
|
||||
}
|
||||
>();
|
||||
|
||||
for (const row of rows) {
|
||||
const name = extractCounterpartyName(row);
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
const amountRaw = row.amount ?? 0;
|
||||
if (!Number.isFinite(amountRaw)) {
|
||||
continue;
|
||||
}
|
||||
const amount = Math.abs(amountRaw);
|
||||
const classified = classifyPayablesLiabilityCategory(row, name);
|
||||
|
||||
const current = byCounterparty.get(name);
|
||||
if (!current) {
|
||||
byCounterparty.set(name, {
|
||||
base: {
|
||||
name,
|
||||
totalAmount: amount,
|
||||
operations: 1,
|
||||
firstPeriod: row.period,
|
||||
lastPeriod: row.period
|
||||
},
|
||||
categoryScores: {
|
||||
supplier_or_contractor: classified.scores.supplier_or_contractor,
|
||||
bank_or_credit: classified.scores.bank_or_credit,
|
||||
tax_or_state: classified.scores.tax_or_state,
|
||||
other: classified.scores.other
|
||||
},
|
||||
reasons: new Set(classified.reasons)
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
current.base.totalAmount += amount;
|
||||
current.base.operations += 1;
|
||||
if ((row.period ?? "") < (current.base.firstPeriod ?? "")) {
|
||||
current.base.firstPeriod = row.period;
|
||||
}
|
||||
if ((row.period ?? "") > (current.base.lastPeriod ?? "")) {
|
||||
current.base.lastPeriod = row.period;
|
||||
}
|
||||
current.categoryScores.supplier_or_contractor += classified.scores.supplier_or_contractor;
|
||||
current.categoryScores.bank_or_credit += classified.scores.bank_or_credit;
|
||||
current.categoryScores.tax_or_state += classified.scores.tax_or_state;
|
||||
current.categoryScores.other += classified.scores.other;
|
||||
for (const reason of classified.reasons) {
|
||||
current.reasons.add(reason);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(byCounterparty.values())
|
||||
.map((item) => ({
|
||||
...item.base,
|
||||
category: resolvePayablesLiabilityCategory(item.categoryScores),
|
||||
categoryReasons: Array.from(item.reasons).slice(0, 2)
|
||||
}))
|
||||
.sort((left, right) => {
|
||||
if (right.totalAmount !== left.totalAmount) {
|
||||
return right.totalAmount - left.totalAmount;
|
||||
}
|
||||
if (right.operations !== left.operations) {
|
||||
return right.operations - left.operations;
|
||||
}
|
||||
return left.name.localeCompare(right.name);
|
||||
});
|
||||
}
|
||||
|
||||
function buildPayablesConfirmedBalanceAggregate(
|
||||
rows: ComposeStageRow[],
|
||||
asOfDate: string
|
||||
): PayablesConfirmedBalanceAggregate[] {
|
||||
const byCounterparty = new Map<
|
||||
string,
|
||||
{
|
||||
outstandingAmount: number;
|
||||
operations: number;
|
||||
firstPeriod: string | null;
|
||||
lastPeriod: string | null;
|
||||
categoryScores: Record<PayablesLiabilityCategory, number>;
|
||||
reasons: Set<string>;
|
||||
}
|
||||
>();
|
||||
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
|
||||
|
||||
for (const row of rows) {
|
||||
const name = extractCounterpartyName(row);
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
const rowTimestamp = toUtcDayTimestamp(row.period);
|
||||
if (asOfTimestamp !== null && rowTimestamp !== null && rowTimestamp > asOfTimestamp) {
|
||||
continue;
|
||||
}
|
||||
const amount = row.amount;
|
||||
if (!Number.isFinite(amount)) {
|
||||
continue;
|
||||
}
|
||||
const absAmount = Math.abs(amount);
|
||||
let delta = 0;
|
||||
if (hasPayablesSectionPrefix(row.account_kt)) {
|
||||
delta += absAmount;
|
||||
}
|
||||
if (hasPayablesSectionPrefix(row.account_dt)) {
|
||||
delta -= absAmount;
|
||||
}
|
||||
if (Math.abs(delta) <= 0.0000001) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const classified = classifyPayablesLiabilityCategory(row, name);
|
||||
const current = byCounterparty.get(name);
|
||||
if (!current) {
|
||||
byCounterparty.set(name, {
|
||||
outstandingAmount: delta,
|
||||
operations: 1,
|
||||
firstPeriod: row.period,
|
||||
lastPeriod: row.period,
|
||||
categoryScores: {
|
||||
supplier_or_contractor: classified.scores.supplier_or_contractor,
|
||||
bank_or_credit: classified.scores.bank_or_credit,
|
||||
tax_or_state: classified.scores.tax_or_state,
|
||||
other: classified.scores.other
|
||||
},
|
||||
reasons: new Set(classified.reasons)
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
current.outstandingAmount += delta;
|
||||
current.operations += 1;
|
||||
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
|
||||
current.firstPeriod = row.period;
|
||||
}
|
||||
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
|
||||
current.lastPeriod = row.period;
|
||||
}
|
||||
current.categoryScores.supplier_or_contractor += classified.scores.supplier_or_contractor;
|
||||
current.categoryScores.bank_or_credit += classified.scores.bank_or_credit;
|
||||
current.categoryScores.tax_or_state += classified.scores.tax_or_state;
|
||||
current.categoryScores.other += classified.scores.other;
|
||||
for (const reason of classified.reasons) {
|
||||
current.reasons.add(reason);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(byCounterparty.entries())
|
||||
.map(([name, item]) => ({
|
||||
name,
|
||||
outstandingAmount: item.outstandingAmount,
|
||||
operations: item.operations,
|
||||
firstPeriod: item.firstPeriod,
|
||||
lastPeriod: item.lastPeriod,
|
||||
category: resolvePayablesLiabilityCategory(item.categoryScores),
|
||||
categoryReasons: Array.from(item.reasons).slice(0, 2)
|
||||
}))
|
||||
.filter((item) => item.outstandingAmount > 0.005)
|
||||
.sort((left, right) => {
|
||||
if (right.outstandingAmount !== left.outstandingAmount) {
|
||||
return right.outstandingAmount - left.outstandingAmount;
|
||||
}
|
||||
if (right.operations !== left.operations) {
|
||||
return right.operations - left.operations;
|
||||
}
|
||||
return left.name.localeCompare(right.name);
|
||||
});
|
||||
}
|
||||
|
||||
function buildCounterpartyRiskAggregate(rows: ComposeStageRow[]): CounterpartyRiskAggregate[] {
|
||||
const byCounterparty = new Map<string, CounterpartyRiskAggregate>();
|
||||
|
||||
@@ -885,7 +1198,7 @@ export function composeFactualReply(
|
||||
intent: AddressIntent,
|
||||
rows: ComposeStageRow[],
|
||||
options: ComposeFactualReplyOptions = {}
|
||||
): { responseType: AddressResponseType; text: string } {
|
||||
): { responseType: AddressResponseType; text: string; semantics?: ComposeReplySemantics } {
|
||||
if (intent === "document_type_and_account_section_profile") {
|
||||
const rowsByMarker = new Map<string, ComposeStageRow[]>();
|
||||
for (const row of rows) {
|
||||
@@ -1940,34 +2253,172 @@ export function composeFactualReply(
|
||||
}
|
||||
|
||||
if (intent === "list_payables_counterparties") {
|
||||
const counterparties = buildCounterpartyRiskAggregate(rows);
|
||||
const lines = [
|
||||
"Проверил поставщиков с признаками незакрытых хвостов по взаиморасчетам (контур 60/76).",
|
||||
`Строк в выборке: ${rows.length}.`,
|
||||
`Контрагентов с сигналом: ${counterparties.length}.`
|
||||
];
|
||||
if (counterparties.length > 0) {
|
||||
lines.push("Приоритет ручной проверки (по сумме/частоте хвостов):");
|
||||
lines.push(
|
||||
...counterparties
|
||||
.slice(0, 8)
|
||||
.map(
|
||||
const counterparties = buildPayablesCounterpartyRiskAggregate(rows);
|
||||
const payablesAsOfDate = resolvePayablesAsOfDate(options);
|
||||
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
||||
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
||||
const periodTo = normalizeIsoDateOnly(options.periodTo);
|
||||
const scopeLine = asOfDate
|
||||
? `- Дата среза: ${formatDateRu(asOfDate)}.`
|
||||
: periodFrom || periodTo
|
||||
? `- Период анализа: ${formatDateRu(periodFrom ?? "...")}..${formatDateRu(periodTo ?? "...")}.`
|
||||
: null;
|
||||
const carryoverLine =
|
||||
asOfDate || periodFrom || periodTo
|
||||
? "- В список могут попадать обязательства, возникшие раньше выбранного периода, если они потенциально оставались открытыми на дату среза."
|
||||
: null;
|
||||
|
||||
const formatHeuristicItem = (item: PayablesCounterpartyRiskAggregate, index: number): string =>
|
||||
`${index + 1}. ${item.name} | сумма сигнала: ${formatMoney(item.totalAmount)} | операций: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}`;
|
||||
|
||||
const pushCategorySlice = (
|
||||
lines: string[],
|
||||
title: string,
|
||||
items: PayablesCounterpartyRiskAggregate[],
|
||||
limit: number
|
||||
): void => {
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
lines.push("");
|
||||
lines.push(title);
|
||||
lines.push(...items.slice(0, limit).map(formatHeuristicItem));
|
||||
};
|
||||
|
||||
const buildHeuristicLines = (forcedFallbackFromConfirmed: boolean): string[] => {
|
||||
const lines = [
|
||||
"Блок 1. Статус результата",
|
||||
forcedFallbackFromConfirmed
|
||||
? "- Режим результата: эвристический скоринг в рамках fallback, потому что подтвержденный срез обязательств к оплате недоступен."
|
||||
: "- Режим результата: эвристический скоринг (shortlist кандидатов по признакам незакрытых обязательств в контуре 60/76).",
|
||||
"- Тип результата: кандидаты для ручной проверки, а не финальный платежный реестр.",
|
||||
"",
|
||||
"Блок 2. Как читать результат",
|
||||
"- Это shortlist кандидатов: нужна ручная проверка бухгалтером.",
|
||||
"- Это не подтвержденный остаток к оплате и не готовое платежное поручение.",
|
||||
...(scopeLine ? [scopeLine] : []),
|
||||
...(carryoverLine ? [carryoverLine] : []),
|
||||
"",
|
||||
"Блок 3. Сводка выборки",
|
||||
`- Строк в выборке: ${rows.length}.`,
|
||||
`- Контрагентов-кандидатов: ${counterparties.length}.`
|
||||
];
|
||||
|
||||
if (counterparties.length > 0) {
|
||||
const categoryCounts = counterparties.reduce<Record<PayablesLiabilityCategory, number>>(
|
||||
(acc, item) => {
|
||||
acc[item.category] += 1;
|
||||
return acc;
|
||||
},
|
||||
{ supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 }
|
||||
);
|
||||
const suppliers = counterparties.filter((item) => item.category === "supplier_or_contractor");
|
||||
const banks = counterparties.filter((item) => item.category === "bank_or_credit");
|
||||
const taxOrState = counterparties.filter((item) => item.category === "tax_or_state");
|
||||
const other = counterparties.filter((item) => item.category === "other");
|
||||
|
||||
lines.push("");
|
||||
lines.push("Блок 4. Категории обязательств");
|
||||
lines.push(`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${categoryCounts.supplier_or_contractor}`);
|
||||
lines.push(`- ${liabilityCategoryLabel("bank_or_credit")}: ${categoryCounts.bank_or_credit}`);
|
||||
lines.push(`- ${liabilityCategoryLabel("tax_or_state")}: ${categoryCounts.tax_or_state}`);
|
||||
lines.push(`- ${liabilityCategoryLabel("other")}: ${categoryCounts.other}`);
|
||||
|
||||
lines.push("");
|
||||
lines.push("Блок 5. Кандидаты на проверку в первую очередь");
|
||||
pushCategorySlice(lines, "5.1 Поставщики/подрядчики:", suppliers, 6);
|
||||
pushCategorySlice(lines, "5.2 Банки/кредиты:", banks, 4);
|
||||
pushCategorySlice(lines, "5.3 Налоги/госорганы:", taxOrState, 4);
|
||||
pushCategorySlice(lines, "5.4 Прочие:", other, 4);
|
||||
|
||||
lines.push("");
|
||||
lines.push("Блок 6. Примеры исходных строк");
|
||||
lines.push(...formatTopRows(rows, 4));
|
||||
} else {
|
||||
lines.push("");
|
||||
lines.push("Блок 4. Категории обязательств");
|
||||
lines.push("- Явных кандидатов на незакрытые обязательства по доступному срезу не найдено.");
|
||||
lines.push("");
|
||||
lines.push("Блок 5. Примеры исходных строк");
|
||||
lines.push(...formatTopRows(rows, 6));
|
||||
}
|
||||
|
||||
return lines;
|
||||
};
|
||||
|
||||
if (options.requestedResultMode === "confirmed_balance") {
|
||||
const confirmedBalances = buildPayablesConfirmedBalanceAggregate(rows, payablesAsOfDate);
|
||||
if (confirmedBalances.length > 0) {
|
||||
const categoryCounts = confirmedBalances.reduce<Record<PayablesLiabilityCategory, number>>(
|
||||
(acc, item) => {
|
||||
acc[item.category] += 1;
|
||||
return acc;
|
||||
},
|
||||
{ supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 }
|
||||
);
|
||||
const lines: string[] = [
|
||||
"Блок 1. Статус результата",
|
||||
"- Режим результата: подтвержденный срез обязательств к оплате по состоянию на дату среза в контуре 60/76.",
|
||||
"- Тип результата: подтвержденные остатки к оплате.",
|
||||
"",
|
||||
"Блок 2. Что учтено",
|
||||
`- Дата среза: ${formatDateRu(payablesAsOfDate)}.`,
|
||||
...(periodFrom || periodTo
|
||||
? [`- Период анализа: ${formatDateRu(periodFrom ?? "...")}..${formatDateRu(periodTo ?? "...")}.`]
|
||||
: []),
|
||||
"- Основание: движения обязательств и оплат в пределах доступного live-среза.",
|
||||
...(carryoverLine ? [carryoverLine] : []),
|
||||
"",
|
||||
"Блок 3. Сводка выборки",
|
||||
`- Строк в выборке: ${rows.length}.`,
|
||||
`- Контрагентов с подтвержденным остатком: ${confirmedBalances.length}.`,
|
||||
"",
|
||||
"Блок 4. Категории обязательств",
|
||||
`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${categoryCounts.supplier_or_contractor}`,
|
||||
`- ${liabilityCategoryLabel("bank_or_credit")}: ${categoryCounts.bank_or_credit}`,
|
||||
`- ${liabilityCategoryLabel("tax_or_state")}: ${categoryCounts.tax_or_state}`,
|
||||
`- ${liabilityCategoryLabel("other")}: ${categoryCounts.other}`,
|
||||
"",
|
||||
"Блок 5. Кому нужно заплатить в первую очередь (по сумме остатка):",
|
||||
...confirmedBalances.slice(0, 10).map(
|
||||
(item, index) =>
|
||||
`${index + 1}. ${item.name} | сумма сигнала: ${formatMoney(item.totalAmount)} | операций: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}`
|
||||
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток к оплате: ${formatMoney(item.outstandingAmount)} | операций в срезе: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}`
|
||||
)
|
||||
);
|
||||
lines.push("Примеры исходных строк:");
|
||||
lines.push(...formatTopRows(rows, 4));
|
||||
} else {
|
||||
lines.push("Явных признаков системной задолженности по доступному срезу не найдено.");
|
||||
lines.push(...formatTopRows(rows, 6));
|
||||
];
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: lines.join("\n"),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: "strong",
|
||||
balance_confirmed: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const fallbackLines = buildHeuristicLines(true);
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: fallbackLines.join("\n"),
|
||||
semantics: {
|
||||
result_mode: "heuristic_candidates",
|
||||
evidence_strength: counterparties.length > 0 ? "medium" : "weak",
|
||||
balance_confirmed: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const lines = buildHeuristicLines(false);
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: lines.join("\n")
|
||||
text: lines.join("\n"),
|
||||
semantics: {
|
||||
result_mode: "heuristic_candidates",
|
||||
evidence_strength: counterparties.length > 0 ? "medium" : "weak",
|
||||
balance_confirmed: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (intent === "list_receivables_counterparties") {
|
||||
const counterparties = buildCounterpartyRiskAggregate(rows);
|
||||
const debtAgingFocus = hasReceivablesDebtAgingFocus(options.userMessage);
|
||||
|
||||
Reference in New Issue
Block a user