PreRelease v2
This commit is contained in:
222
lib/services/ai/analysis.normalizer.ts
Normal file
222
lib/services/ai/analysis.normalizer.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import {
|
||||
NormalizedAnalysis,
|
||||
SUPPORTED_CONTRACT_TYPES,
|
||||
SupportedContractType,
|
||||
ContactInfo,
|
||||
KeyPerson,
|
||||
ExplainabilityItem,
|
||||
} from "./analysis.types";
|
||||
|
||||
function mapContractType(rawType: unknown): SupportedContractType {
|
||||
const value = String(rawType ?? "")
|
||||
.trim()
|
||||
.toUpperCase()
|
||||
.replace(/\s+/g, "_");
|
||||
|
||||
if (SUPPORTED_CONTRACT_TYPES.includes(value as SupportedContractType)) {
|
||||
return value as SupportedContractType;
|
||||
}
|
||||
|
||||
const aliases: Record<string, SupportedContractType> = {
|
||||
AUTO_INSURANCE: "INSURANCE_AUTO",
|
||||
HOME_INSURANCE: "INSURANCE_HOME",
|
||||
HEALTH_INSURANCE: "INSURANCE_HEALTH",
|
||||
LIFE_INSURANCE: "INSURANCE_LIFE",
|
||||
MORTGAGE: "LOAN",
|
||||
CREDIT: "LOAN",
|
||||
CARD_CREDIT: "CREDIT_CARD",
|
||||
};
|
||||
|
||||
return aliases[value] ?? "OTHER";
|
||||
}
|
||||
|
||||
function toStringOrNull(value: unknown): string | null {
|
||||
const normalized = String(value ?? "").trim();
|
||||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
|
||||
function normalizeCurrency(value: unknown): string | null {
|
||||
const raw = String(value ?? "")
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
if (!raw) return null;
|
||||
|
||||
const symbolMap: Record<string, string> = {
|
||||
"€": "EUR",
|
||||
$: "USD",
|
||||
"£": "GBP",
|
||||
};
|
||||
|
||||
if (symbolMap[raw]) {
|
||||
return symbolMap[raw];
|
||||
}
|
||||
|
||||
// Accept ISO-like 3-letter currencies and common BFSI currencies.
|
||||
if (/^[A-Z]{3}$/.test(raw)) {
|
||||
return raw;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function toDateOrNull(value: unknown): string | null {
|
||||
const candidate = String(value ?? "").trim();
|
||||
if (!candidate) return null;
|
||||
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
const parsed = new Date(candidate);
|
||||
if (Number.isNaN(parsed.getTime())) return null;
|
||||
|
||||
return parsed.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function toStringList(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value
|
||||
.map((item) => String(item ?? "").trim())
|
||||
.filter((item) => item.length > 0)
|
||||
.slice(0, 25);
|
||||
}
|
||||
|
||||
function parseContactInfo(input: any): ContactInfo {
|
||||
return {
|
||||
name: toStringOrNull(input?.name),
|
||||
email: toStringOrNull(input?.email),
|
||||
phone: toStringOrNull(input?.phone),
|
||||
address: toStringOrNull(input?.address),
|
||||
role: toStringOrNull(input?.role),
|
||||
};
|
||||
}
|
||||
|
||||
function parseKeyPeople(input: any): KeyPerson[] {
|
||||
if (!Array.isArray(input)) return [];
|
||||
return input.slice(0, 10).map((person) => ({
|
||||
name: String(person?.name ?? "").trim() || "Unknown",
|
||||
role: toStringOrNull(person?.role),
|
||||
email: toStringOrNull(person?.email),
|
||||
phone: toStringOrNull(person?.phone),
|
||||
}));
|
||||
}
|
||||
|
||||
function parseRelevantDates(input: any): Array<{
|
||||
date: string;
|
||||
description: string;
|
||||
type: "EXPIRATION" | "RENEWAL" | "PAYMENT" | "REVIEW" | "OTHER";
|
||||
}> {
|
||||
if (!Array.isArray(input)) return [];
|
||||
|
||||
return input.slice(0, 15).map((dateObj) => {
|
||||
const dateStr = toDateOrNull(dateObj?.date);
|
||||
const type = String(dateObj?.type ?? "OTHER").toUpperCase();
|
||||
const isValidType = [
|
||||
"EXPIRATION",
|
||||
"RENEWAL",
|
||||
"PAYMENT",
|
||||
"REVIEW",
|
||||
"OTHER",
|
||||
].includes(type);
|
||||
|
||||
return {
|
||||
date: dateStr || "0000-01-01",
|
||||
description:
|
||||
String(dateObj?.description ?? "")
|
||||
.trim()
|
||||
.slice(0, 200) || "Important date",
|
||||
type: (isValidType ? type : "OTHER") as
|
||||
| "EXPIRATION"
|
||||
| "RENEWAL"
|
||||
| "PAYMENT"
|
||||
| "REVIEW"
|
||||
| "OTHER",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function parseExplainability(input: any): ExplainabilityItem[] {
|
||||
if (!Array.isArray(input)) return [];
|
||||
|
||||
return input
|
||||
.slice(0, 30)
|
||||
.map((item) => {
|
||||
const field = String(item?.field ?? "").trim();
|
||||
const why = String(item?.why ?? "").trim();
|
||||
const sourceSnippet = String(item?.sourceSnippet ?? "").trim();
|
||||
|
||||
if (!field || !why || !sourceSnippet) return null;
|
||||
|
||||
const confidenceRaw = Number(item?.sourceHints?.confidence);
|
||||
const confidence = Number.isFinite(confidenceRaw)
|
||||
? Math.max(0, Math.min(100, Math.round(confidenceRaw)))
|
||||
: null;
|
||||
|
||||
return {
|
||||
field: field.slice(0, 80),
|
||||
why: why.slice(0, 260),
|
||||
sourceSnippet: sourceSnippet.slice(0, 480),
|
||||
sourceHints: {
|
||||
page: toStringOrNull(item?.sourceHints?.page),
|
||||
section: toStringOrNull(item?.sourceHints?.section),
|
||||
confidence,
|
||||
},
|
||||
} as ExplainabilityItem;
|
||||
})
|
||||
.filter((value): value is ExplainabilityItem => value !== null);
|
||||
}
|
||||
|
||||
export function normalizeAnalysis(input: any): NormalizedAnalysis {
|
||||
const title = String(input?.title || "").trim() || "Untitled Contract";
|
||||
const summary = String(input?.summary || "").trim();
|
||||
const extractedText = String(input?.extractedText || "").trim();
|
||||
|
||||
if (summary.length < 10) {
|
||||
throw new Error("Summary is missing or too short.");
|
||||
}
|
||||
|
||||
if (extractedText.length < 30) {
|
||||
throw new Error("Extracted text is missing or too short.");
|
||||
}
|
||||
|
||||
const premiumValue =
|
||||
input?.premium === null || input?.premium === undefined
|
||||
? null
|
||||
: Number(input.premium);
|
||||
|
||||
const premium =
|
||||
premiumValue !== null && Number.isFinite(premiumValue) && premiumValue >= 0
|
||||
? Number(premiumValue.toFixed(2))
|
||||
: null;
|
||||
|
||||
const language = toStringOrNull(input?.language) || "en";
|
||||
|
||||
return {
|
||||
title,
|
||||
type: mapContractType(input?.type),
|
||||
provider: toStringOrNull(input?.provider),
|
||||
policyNumber: toStringOrNull(input?.policyNumber),
|
||||
startDate: toDateOrNull(input?.startDate),
|
||||
endDate: toDateOrNull(input?.endDate),
|
||||
premium,
|
||||
premiumCurrency: normalizeCurrency(input?.premiumCurrency),
|
||||
summary,
|
||||
keyPoints: {
|
||||
guarantees: toStringList(input?.keyPoints?.guarantees),
|
||||
exclusions: toStringList(input?.keyPoints?.exclusions),
|
||||
franchise: toStringOrNull(input?.keyPoints?.franchise),
|
||||
importantDates: toStringList(input?.keyPoints?.importantDates),
|
||||
explainability: parseExplainability(input?.keyPoints?.explainability),
|
||||
},
|
||||
extractedText: extractedText.slice(0, 12000),
|
||||
language,
|
||||
keyPeople: parseKeyPeople(input?.keyPeople),
|
||||
contactInfo: parseContactInfo(input?.contactInfo),
|
||||
importantContacts: Array.isArray(input?.importantContacts)
|
||||
? input.importantContacts
|
||||
.slice(0, 10)
|
||||
.map((c: any) => parseContactInfo(c))
|
||||
: [],
|
||||
relevantDates: parseRelevantDates(input?.relevantDates),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user