Skip to content

Keep a trip under budget

Use this when a bot books several things that share one budget. GND sums the bookings in each state of the plan and refuses the step that goes over, and it refuses two activities booked for the same time.

Files on GitHub

Before you start

Set GND up first as the course planner's "Before you start" shows: GND from PyPI, or from a checkout for unreleased work on main, and python must be that environment's Python.

python -c "import gnd, pathlib, shutil; shutil.copytree(pathlib.Path(gnd.__file__).parent / 'examples/plan_apps', 'plan_apps', ignore=shutil.ignore_patterns('recordings', 'skill-sessions', '__pycache__'), dirs_exist_ok=True)"
gnd validate plan_apps/trip/model.orm.json
OK: plan_apps/trip/model.orm.json is a valid GND ORM model

Optionally, the rules in PyRel (What PyRel is, and when you need it), from python plan_apps/trip/app.py rules:

m.where(hotel_total(step, t, h), activity_total(step, t, a), budget(step, t, b)).require((h + a) <= b)
m.where(booked_slot(step, trip, activity_1, day, period), booked_slot(step, trip, activity_2, day, period)).require(activity_1 == activity_2)

hotel_total and activity_total are sums the model derives. Nobody stores them, so they can't drift from the bookings.

Ask for a trip

Plan three days in Lisbon under 900 EUR.

Read the refusal

Llama 3.1 8B plans three days in Lisbon (recorded run 2) A replay of a recorded session; the full text is in the transcript below the figure. Llama 3.1 8B plans three days in Lisbon (recorded run 2) attempt 1 the bot says: Fado night cannot be booked due to exceeding budget on any combination of its two possibilities plan book_hotel(trip='lisbon', hotel='alfama-inn', night='2025-05-12') plan book_activity(trip='lisbon', activity='belem-walk') plan book_hotel(trip='lisbon', hotel='palacio', night='2025-05-13') plan book_activity(trip='lisbon', activity='sintra-day') plan book_hotel(trip='lisbon', hotel='alfama-inn', night='2025-05-14') plan book_activity(trip='lisbon', activity='tram-28-tour') pyrel check: exit 1 states 4-6 no_overlap violated [{"activity": "belem-walk", "day": "2025-05-13", "period": "morning", "trip": "lisbon"}, {"activity": "sintra-day", "day": "2025-05-13", "period": "morning", "trip": "lisbon"}] the rule in PyRel: m.where(booked_slot(step, trip, activity_1, day, period), booked_slot(step, trip, activity_2, day, period)).require(activity_1 == activity_2) attempt 2 plan book_hotel(trip='lisbon', hotel='palacio', night='2025-05-12') plan book_activity(trip='lisbon', activity='fado-night') plan book_hotel(trip='lisbon', hotel='alfama-inn', night='2025-05-13') plan book_activity(trip='lisbon', activity='belem-walk') plan book_hotel(trip='lisbon', hotel='palacio', night='2025-05-14') plan book_activity(trip='lisbon', activity='oceanarium') pyrel check: exit 0 (accepted) executed: the new state is in <workspace>/next.pop.json
Transcript of the replay
attempt 1
  the bot says: Fado night cannot be booked due to exceeding budget on any combination of its two possibilities
  plan  book_hotel(trip='lisbon', hotel='alfama-inn', night='2025-05-12')
  plan  book_activity(trip='lisbon', activity='belem-walk')
  plan  book_hotel(trip='lisbon', hotel='palacio', night='2025-05-13')
  plan  book_activity(trip='lisbon', activity='sintra-day')
  plan  book_hotel(trip='lisbon', hotel='alfama-inn', night='2025-05-14')
  plan  book_activity(trip='lisbon', activity='tram-28-tour')
  pyrel check: exit 1
    states 4-6  no_overlap  violated  [{"activity": "belem-walk", "day": "2025-05-13", "period": "morning", "trip": "lisbon"}, {"activity": "sintra-day", "day": "2025-05-13", "period": "morning", "trip": "lisbon"}]
      the rule in PyRel: m.where(booked_slot(step, trip, activity_1, day, period), booked_slot(step, trip, activity_2, day, period)).require(activity_1 == activity_2)
attempt 2
  plan  book_hotel(trip='lisbon', hotel='palacio', night='2025-05-12')
  plan  book_activity(trip='lisbon', activity='fado-night')
  plan  book_hotel(trip='lisbon', hotel='alfama-inn', night='2025-05-13')
  plan  book_activity(trip='lisbon', activity='belem-walk')
  plan  book_hotel(trip='lisbon', hotel='palacio', night='2025-05-14')
  plan  book_activity(trip='lisbon', activity='oceanarium')
  pyrel check: exit 0 (accepted)
  executed: the new state is in <workspace>/next.pop.json

Llama 3.1 8B stays inside the budget but books the Sintra day trip and the Belém walk for the same morning. The checker, here the app's optional PyRel backend (the DuckDB program GND generates), refuses the plan from state 4, where the second booking lands, and names both activities, the day and the period.

The repaired plan

The model swaps in different activities and the checker accepts the plan: 880 euros in all. It was the first trip run, in the order of the results table (llama-3.1-8b/trip/run-2), refused on a rule and then accepted.

GND checks the rules, not the wish behind them. In another recorded run, Llama 3.2 3B repaired an overlap by booking one night and one activity for a three-day trip, and GND accepted it, because no rule asks for three nights. If it matters, write it as a rule.

To try the loop without a model provider:

python plan_apps/trip/app.py ask "Plan three days in Lisbon under 900 EUR." --out next-trip.pop.json
attempt 1
  plan  book_hotel(trip='lisbon', hotel='palacio', night='2025-05-12')
  plan  book_hotel(trip='lisbon', hotel='palacio', night='2025-05-13')
  plan  book_hotel(trip='lisbon', hotel='palacio', night='2025-05-14')
  plan  book_activity(trip='lisbon', activity='tram-28-tour')
  plan  book_activity(trip='lisbon', activity='fado-night')
  plan  book_activity(trip='lisbon', activity='sintra-day')
  gnd check: exit 1
    state 4  within_budget  violated  [{"$t": "lisbon", "$h": 900, "$a": 40, "$b": 900}]
      the rule in PyRel: m.where(hotel_total(step, t, h), activity_total(step, t, a), budget(step, t, b)).require((h + a) <= b)
    state 5  within_budget  violated  [{"$t": "lisbon", "$h": 900, "$a": 100, "$b": 900}]
    state 6  within_budget  violated  [{"$t": "lisbon", "$h": 900, "$a": 190, "$b": 900}]
attempt 2
  plan  book_hotel(trip='lisbon', hotel='alfama-inn', night='2025-05-12')
  plan  book_hotel(trip='lisbon', hotel='alfama-inn', night='2025-05-13')
  plan  book_hotel(trip='lisbon', hotel='alfama-inn', night='2025-05-14')
  plan  book_activity(trip='lisbon', activity='tram-28-tour')
  plan  book_activity(trip='lisbon', activity='fado-night')
  plan  book_activity(trip='lisbon', activity='sintra-day')
  plan  book_activity(trip='lisbon', activity='belem-walk')
  gnd check: exit 1
    state 7  no_overlap  violated  [{"trip": "lisbon", "activity": "belem-walk", "day": "2025-05-13", "period": "morning"}, {"trip": "lisbon", "activity": "sintra-day", "day": "2025-05-13", "period": "morning"}]
      the rule in PyRel: m.where(booked_slot(step, trip, activity_1, day, period), booked_slot(step, trip, activity_2, day, period)).require(activity_1 == activity_2)
attempt 3
  plan  book_hotel(trip='lisbon', hotel='alfama-inn', night='2025-05-12')
  plan  book_hotel(trip='lisbon', hotel='alfama-inn', night='2025-05-13')
  plan  book_hotel(trip='lisbon', hotel='alfama-inn', night='2025-05-14')
  plan  book_activity(trip='lisbon', activity='tram-28-tour')
  plan  book_activity(trip='lisbon', activity='fado-night')
  plan  book_activity(trip='lisbon', activity='sintra-day')
  plan  book_activity(trip='lisbon', activity='oceanarium')
  gnd check: exit 0 (accepted)
  executed: the new state is in next-trip.pop.json