feat(extract): capture seat, class, platform, price + event venue contact

Request and map root-level seat/class/platform and a total price/currency into reservation metadata (shown on the card; price reuses the existing label). Read both the root and reservationFor and tolerate common field-name aliases (priceAmount, priceCurrencyISO4217Code, fareClass, ...) since models name these inconsistently. Also capture event/attraction venue telephone + url onto the auto-created place, matching lodging/restaurant.
This commit is contained in:
Maurice
2026-06-24 23:04:24 +02:00
parent 23d5a5bd9c
commit 5fa79bba52
6 changed files with 51 additions and 5 deletions
@@ -321,7 +321,7 @@ function mapEvent(r: KiReservation, source: ParsedBookingItem['source']): Parsed
const loc = e.location;
const c = coords(loc?.geo);
const venue: ParsedVenue | undefined = loc?.name ? { name: loc.name, ...(c ?? {}), address: formatAddress(loc.address) ?? undefined } : undefined;
const venue: ParsedVenue | undefined = loc?.name ? { name: loc.name, ...(c ?? {}), address: formatAddress(loc.address) ?? undefined, website: loc.url ?? undefined, phone: loc.telephone ?? undefined } : undefined;
return { type: 'event', title: e.name, reservation_time: toIsoString(e.startDate ?? r.startTime), reservation_end_time: toIsoString(e.endDate ?? r.endTime), confirmation_number: r.reservationNumber ?? null, location: loc ? (formatAddress(loc.address) ?? loc.name ?? null) : null, ...(venue ? { _venue: venue } : {}), source };
}
@@ -330,6 +330,33 @@ function mapEvent(r: KiReservation, source: ParsedBookingItem['source']): Parsed
// Public
// ---------------------------------------------------------------------------
/** Merge seat/class/platform/price into an item's metadata (type-agnostic).
* Models name these inconsistently and sometimes nest them under reservationFor,
* so check both levels and common aliases. The item's own metadata wins. */
function applyCommonMeta(item: ParsedBookingItem, r: KiReservation): ParsedBookingItem {
const rf = (r.reservationFor && typeof r.reservationFor === 'object' ? r.reservationFor : {}) as Record<string, unknown>;
const pick = (...keys: string[]): unknown => {
for (const k of keys) {
const v = (r as Record<string, unknown>)[k] ?? rf[k];
if (v != null && v !== '') return v;
}
return undefined;
};
const m: Record<string, unknown> = {};
const seat = pick('seat', 'seatNumber');
if (seat != null) m.seat = String(seat);
const cls = pick('class', 'bookingClass', 'fareClass', 'serviceClass', 'seatingType');
if (cls != null) m.class = String(cls);
const platform = pick('platform', 'departurePlatform');
if (platform != null) m.platform = String(platform);
const price = pick('price', 'priceAmount', 'totalPrice', 'total');
if (price != null) m.price = price;
const cur = pick('priceCurrency', 'priceCurrencyISO4217Code', 'currency');
if (cur != null) m.priceCurrency = String(cur);
if (Object.keys(m).length) item.metadata = { ...m, ...(item.metadata ?? {}) };
return item;
}
export function mapReservations(kiItems: KiReservation[], fileName: string): { items: ParsedBookingItem[]; warnings: string[] } {
const items: ParsedBookingItem[] = [];
const warnings: string[] = [];
@@ -353,7 +380,7 @@ export function mapReservations(kiItems: KiReservation[], fileName: string): { i
group.push(kiItems[++i]);
}
item = group.length > 1 ? mapFlightGroup(group, source) : mapFlight(r, source);
if (item) items.push(item);
if (item) items.push(applyCommonMeta(item, r));
continue;
}
@@ -370,7 +397,7 @@ export function mapReservations(kiItems: KiReservation[], fileName: string): { i
warnings.push(`Unknown type "${r['@type']}" in ${fileName}[${i}] — skipped`);
}
if (item) items.push(item);
if (item) items.push(applyCommonMeta(item, r));
}
return { items, warnings };
@@ -112,6 +112,8 @@ export interface KiEventVenue {
name?: string;
address?: string | KiAddress;
geo?: KiGeo;
telephone?: string;
url?: string;
}
export interface KiEvent {
@@ -135,6 +137,11 @@ export interface KiReservation {
reservationFor?: Record<string, unknown>;
pickupLocation?: KiEventVenue;
dropoffLocation?: KiEventVenue;
seat?: string;
class?: string;
platform?: string;
price?: number | string;
priceCurrency?: string;
[key: string]: unknown;
}
@@ -109,6 +109,11 @@ const ROOT_KEYS = new Set([
'endTime',
'pickupLocation',
'dropoffLocation',
'seat',
'class',
'platform',
'price',
'priceCurrency',
'reservationFor',
]);
+2 -1
View File
@@ -24,7 +24,8 @@ export function buildSystemPrompt(): string {
' LodgingReservation: { name, address, geo:{latitude,longitude}, telephone, url } — put check-in/out in root "checkinTime"/"checkoutTime"',
' FoodEstablishmentReservation: { name, address, geo, telephone, url } — put booking time in root "startTime"/"endTime"',
' RentalCarReservation: { name, model, make, rentalCompany:{name} } — put pickup/dropoff times in root "pickupTime"/"dropoffTime", and the pickup AND return stations in root "pickupLocation" and "dropoffLocation", each {name,address,geo:{latitude,longitude}}',
' EventReservation / TouristAttractionVisit: { name, startDate, endDate, location:{name,address,geo} }',
' EventReservation / TouristAttractionVisit: { name, startDate, endDate, location:{name,address,geo,telephone,url} }',
'When present, also include at the reservation ROOT: "seat", "class" (fare/cabin class), "platform" (trains/buses), and the total "price" (a number) with "priceCurrency" (ISO 4217 code, e.g. EUR).',
'Extract EVERY flight/segment in the document, including return legs — a round trip has TWO OR MORE flights, and each row of a flight table is a separate reservation. Do NOT stop after the first.',
"Each flight shares the booking's reservationNumber. Use the date shown for that specific flight as its departureTime; if a flight lists only one date (no separate arrival time), leave arrivalTime null — never reuse another flight's date.",
'If the document contains no recognizable reservation, return { "reservations": [] }.',