跳到正文
DEV Community· adi-ray·· 3 小時前AI 評分52

Fare Calendar 助旅行代理商找出最低票價日期,Gemma 4 解析供應商 API、Chronos 補足未搜尋日期

A cheapest-day fare calendar for travel agents: Gemma 4 reads the supplier APIs, Chronos fills the gaps

AI 導讀

adi-ray 為旅行代理商開發 Fare Calendar,從平台伺服器日誌列出各出發日期近期實際取得的最低票價,並為近期沒有搜尋紀錄的日期提供估算。Gemma 4 在本地生成供應商資料映射,由程式用真實回應驗證;Chronos-Bolt base 與同星期基準值的平均結果用於估算未搜尋日期。

正文

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle

What I Built

I build software for a travel agency that runs a B2B flight booking portal: sub-agents log in, search, and book across 13 suppliers (a GDS, low-cost carriers, NDC airlines and fixed-departure consolidators).

Watch an agent with a customer on the phone and you'll see the same thing every time:

"Is Tuesday cheaper than Wednesday?" search. "What about Thursday?" search.

Every search fans out to every supplier. It's slow for the customer, and it burns supplier quota: GDS look-to-book ratios are a real cost.

Fare Calendar answers "which day is cheapest?" before the agent searches. It's a date picker that shows the lowest fare under every date:

  • ● seen: a real fare an agent got for that date in the last 3 days
  • est.: a model estimate, with a likely range, for dates nobody searched recently

It's built entirely from the portal's own server logs, so it reflects the fares our suppliers actually return to our agents, not a public metasearch.

Who pays: B2B travel agencies and booking platforms. It plugs into an existing portal as a <fare-calendar> web component, reads logs the platform already writes, and runs on the agency's own hardware.

Demo

  • Public demo (synthetic data): https://fare-calendar-demo.onrender.com
  • Production deploy: behind a sign-in page for our agents. It serves real forecasts for 557 routes and 23,626 priced dates from 114k observed fares.

Fare calendar: a fare profile strip above the month, the lowest fare under every date, and a panel explaining the picked day

  • The strip above the month is a fare profile, one bar per day. The cheap troughs are visible before you read a single number.
  • Each day shows its lowest fare. Seen fares get a dot, estimates a dotted underline, and the cheapest day is marked in magenta.
  • Picking a day explains it: "₹6,210, likely ₹5,340 to ₹7,270, estimated from nearby dates because nobody searched this day recently."
  • It works with the keyboard, in dark mode and on a phone, and everything is labelled indicative. Agents reprice live before booking.

Code

https://github.com/adi-ray/fare-calendar

Python (FastAPI, pandas), a dependency-free web component, and 60 tests. Real logs, fares and private supplier mappings are git-ignored. The repo ships only synthetic data and the public Amadeus mapping.

How I Built It

Two open-weight models do two very different jobs.

1. Gemma 4 reads the suppliers' APIs

The fares are buried in 13 different response formats: SOAP XML from a GDS, Navitaire XML, IATA NDC XML, and assorted JSON. Hand-writing 13 parsers before a deadline wasn't going to happen.

So I wrote a tiny mapping language that works the same over JSON and XML, and had Gemma 4 write the mappings:

{
  "rows": "faresAvailable{*}",
  "fields": {
    "fare": "fares[*].passengerFares[*].fareAmount",
    "origin": "meta.origin",
    "destination": "meta.destination",
    "departure_date": "meta.departure_date"
  },
  "generated_by": "gemma4 (validated on 6/6 responses)"
}

Gemma sees a shrunken sample of one response. Code, not Gemma, decides whether a mapping is good. It applies the mapping to 6 real responses and rejects it unless:

  • most responses produce rows
  • fares look like per-adult totals, not IDs or base fares
  • the route the agent actually searched shows up

Failures go back to Gemma as specific feedback:

'fareComponentList' does not exist under …originDestinationOptionList[*]; keys there: ['fareComponentGroupList']. did you mean 'fareComponentGroupList'? 'fareComponentList' exists further down at 'fareComponentGroupList.fareComponentList'

Gemma never touches a fare at run time. Mappings are small JSON files a human can review.

The results: Gemma (local gemma4, 7.5B Q4, via Ollama on a 4 GB laptop GPU) mapped 7 suppliers in 1–6 attempts each. Three, IndiGo, SpiceJet and an NDC carrier, didn't converge in 11 attempts, so I hand-wrote those and labelled them as hand-written. IndiGo and SpiceJet store each fare as separate charges, with discounts as positive amounts, which needed sum:charges[ChargeType!=Discount] − sum:charges[ChargeType=Discount].

What I learned getting a small model to do this reliably:

  • Turn thinking off. Gemma 4's thinking mode is on by default in Ollama. On my GPU it turned a 5-second answer into a 58-second one, and the answer was worse.
  • Watch the context window. Ollama's default context silently dropped the start of long prompts, which is where my instructions were. Every early failure looked like "Gemma ignored the spec".
  • Make feedback name the fix. "Check your path" got repeated mistakes. "Use ^^.segment.from" got fixed mappings.
  • Bend the language toward the model. Gemma kept writing field paths from the document root, so the extractor now resolves those to the row's own ancestor.

2. Chronos-Bolt fills the empty dates

Agents search a handful of dates. To fill the rest:

  1. A lead-time model separates "which day you fly" from "how far ahead you searched", since fares climb as departure nears.
  2. Chronos-Bolt base (open weights, zero-shot, CPU) forecasts each unsearched stretch of the calendar from the departure dates before it. Missing days are passed as gaps, not invented. Its forecast is averaged with a same-weekday baseline: the two make different mistakes, and the average beats both.
  3. The estimate is re-priced for today's lead time and blended with older searches for that exact date.

3. Data hygiene was half the work

  • Placeholders. One consolidator lists some dates at ₹100 (seats on request). In a lowest-fare calendar that wins every day, and it took my backtest error to 308%. Fares under ₹500, and anything outside 0.25×–4× the route median, are now dropped and counted.
  • Per-leg pricing. Some carriers price connecting itineraries per leg, so a DEL→HYD→BOM search could pose as a cheap "DEL-HYD fare". Only the searched route is kept.
  • Group totals. Some suppliers price "2 adults" as one total, so only single-adult searches are used (85% of traffic).

4. Serving it on Render

Chronos runs where the logs live and writes a 565 KB forecasts.json. The API on Render is FastAPI plus the standard library, with no PyTorch. Two services come from one blueprint:

  • Public demo: synthetic data, free plan.
  • Private: Starter with a disk (paid with the Render credit). Agents sign in on a sign-in page with a signed session cookie, and the portal can embed it with an API key. It takes nightly uploads, and a Secret File fallback serves whichever forecast is newer.

Results (honest version)

The backtest uses 14 cutoff days (Oct 1–14). The calendar is built only from earlier logs and compared with the lowest fare agents actually got over the next two days, across 11,544 searched dates:

seen fares only weekday baseline Chronos-Bolt base + baseline
searched dates with a fare shown 36% 41% 41%
error when showing a real recent fare 1.0% 1.0% 1.0%
error on estimated dates (591) n/a 18.7% 16.2%

How I picked the forecaster (5 cutoffs, 498 estimated dates):

forecaster error on estimated dates
weekday baseline 18.4%
Chronos-Bolt small, alone 20.1%
Chronos-Bolt base, alone 17.6%
Chronos-Bolt small + baseline 17.9%
Chronos-Bolt base + baseline 15.1%

The calendar's value is mostly coverage. It puts a number on 41% of the dates agents go on to search, up from 36%, and the "seen" fares are accurate.

Estimates are rough but measurably better than the obvious baseline. My first deploy used Chronos-Bolt small, which did worse than "same weekday, last few weeks". The bigger base model, averaged with that baseline, cuts estimate error by about 13% over 14 cutoffs. One honest caveat: on a separate 5-day subset with only 63 estimated dates, the ensemble came out slightly worse (20.3% vs 19.0%). It's one month of noisy, mixed GDS and fixed-departure history, and the backtest re-runs as logs accumulate.

Why Does Open Innovation Matter?

  • The data can't leave. Supplier fares are negotiated rates, and supplier responses are commercially confidential. Pasting them into a hosted LLM to "help write a parser" is a non-starter. Gemma 4 runs locally, so every supplier response it read stayed on the laptop.
  • It costs nothing per use. Mapping a supplier is a one-off job on a 4 GB laptop GPU. Forecasts for 557 routes take a few minutes on a CPU. The deployed API needs no GPU and no AI vendor.
  • I could change how the model behaves. Turning thinking off, setting the context size and forcing JSON output are all switches I control. A closed API would have left me guessing why answers degraded.
  • Swapping models was a one-line change. When Chronos-Bolt small lost to the baseline, I tried a 4× bigger open model the same afternoon, backtested five variants on real data, and shipped the winner at no cost. No vendor pricing page, no waiting list.
  • No lock-in. The mappings are plain JSON, the forecaster is configurable (FARECAL_FORECASTER, FARECAL_MODEL_ID), and Gemma can be replaced by any OpenAI-compatible local server.
  • Open models plus deterministic checks is the pattern that worked. The model proposes, code verifies against real data. That's what made a 7.5B model trustworthy enough to touch production data.

My Agent Session

This is a curated version of the build session with Claude Code: the turning points, including the wrong turns. Local paths, the private backend's internals and real per-route fares are removed.

My First User

During beta testing, our ops team switched Fare Calendar on for a few of our agents and some of our own staff.

They liked it, and they told our tech team it’s one of the features most in demand from our clients and agents. They’d been asking for something like this.

Prize Categories

  • Best Use of Gemma: Gemma 4, run locally, writes the supplier mappings that the whole pipeline depends on (7 of 11), verified by code against real responses.
  • Best Use of Render: two services from one blueprint. A public demo on the free plan, and a private Starter service with a persistent disk for nightly forecast uploads, behind a sign-in page.

What shaped the approach: "The Fare Observation Schema I'd Build Before a Flight Scraper" by arritola92 is why "seen" and "estimate" are kept apart and nothing is presented as bookable. "Chronos vs Toto: Zero-Shot Forecasting Benchmark Results" by Team Parseable is why I tried a zero-shot forecaster before training anything. "I priced 150 flights across November…" by ishemah matched the weekday effects I built the synthetic demo around.

Built with an AI coding agent (Claude Code) and reviewed by me. The product itself runs on open-weight models: Gemma 4 and Chronos-Bolt.

來源:DEV Community · dev.to