Four local models and a human approval at every step, on one 8 GB GPU.

My MSc dissertation. A five-stage agentic workflow turns a hospitality client's intake form into a consultant-grade menu brief. Each stage reads only what the approved stage before it hands over. Open-weight models draft, another model family reviews, code checks what they miss, and a consultant approves each step.

Private industry code; a demonstration is available on request.

Sector
Hospitality
Method
Agentic AI and local LLMs
When
Jul – Sep 2026
Context
MSc dissertation with a UK hospitality consultancy (paper mark 85)
Role
Solo: requirements, workflow, evaluation, deployment and handover

Key figures

  • 4 open-weight models on one 8 GB GPU
  • 16.7 min for all five stages of an 8-dish brief on that GPU
  • 1,500+ automated tests at handover
The app's Stage 2 page for a fictional demo client: the approved strategy, a note that 8 of its 27 claims rest on live market research, a Word evidence register, and review notes beside the text.

How a brief is made, stage by stage

  • A model runs here
  • Deterministic code
  • An outside source
  • A stored artefact
  • A boundary the design refuses
  1. Five stages, a person at every gate

    A consultant fills in the client's intake form. Five stages then draft the brief, and each starts only when the stage before it is approved. A stage reads only what the approved stage before it hands over, and editing an earlier stage marks every later one stale. Four model roles load one at a time within the GPU's 8,188 MiB: Qwen3.6 35B-A3B drafts Stages 1, 2, 3 and 5, gemma-4-E4B composes the dishes, and Gemma 26B and gpt-oss-20b review work they did not write.

    Five boxes in a row, Stage 1 Brief to Stage 5 Copy, each naming the open-weight model that drafts it, joined by arrows that are human approval gates. Open full size: Five stages, a person at every gate
  2. Market research from free public sources

    Stage 2 starts from one point: the postcode, with a town search as the fallback. Map queries collect hospitality venues within 1.5 km, widened to 4 km when fewer than 30 are named, and demand generators within 800 m, alongside census, deprivation, hygiene-rating, company and food sources. Every response is cached with its URL, status and time, and one dead source never stops the bundle.

    A postcode resolves to a centroid, which feeds sixteen free sources grouped as place, official, culinary and operator data, from OpenStreetMap queries to census tables and Companies House. Open full size: Market research from free public sources
  3. Reading a rival's menu, cheapest method first

    A cross-encoder chooses ten nearby venues to profile, and each menu is read by the cheapest method that works: structured data, page text, menu pages, the PDF text layer, and a vision model only when a PDF is an image. On real menu PDFs, 83% had a usable text layer, so the vision model is the exception. Each rival yields a median price, the samples it rests on, up to twelve dishes and its own description.

    Six reading methods in cost order, from schema.org menu items to a guarded web search, with only the vision model filled as a model step, and the five fields one rival yields. Open full size: Reading a rival's menu, cheapest method first
  4. An evidence graph where every link has a source

    Venues from the map and the hygiene register are matched into one list and fused with cuisines, dishes and the location into a NetworkX graph. Every edge records its source and a confidence. A safe renderer turns raw totals into bounded phrases before any model reads them. Two projections leave the graph: the Stage 2 one never contains a dish name, and the Stage 4 one is stored but never given to the dish composer.

    Entity resolution feeds a graph of location, cuisine, competitor and dish nodes; a safe renderer sits before any model; two projections leave it, one of which never reaches the composer. Open full size: An evidence graph where every link has a source
  5. One reranker, four rankings

    A BGE cross-encoder, loaded once, ranks which ten competitors to profile, the Stage 4 dish pool, the description kept for each rival and every dish's brand fit. A cheaper embedding prefilter was tested and declined: its recall at 20 was 0.644 against 0.333 by chance, too weak to discard candidates, so the bounded pools are scored in full.

    The BGE reranker, a model step, feeds four ranking functions in one session; the Qwen3 embedding prefilter is shown as declined, with its recall at 20 of 0.644 against 0.333. Open full size: One reranker, four rankings
  6. A strategy that can only cite what was found

    Facts are grouped into seven fixed facets and the model writes one summary per facet. Its output schema accepts only that facet's fact identifiers, so an invented citation cannot decode, and a verifier drops any uncited sentence. The strategy is written in three ordered passes, then read by two models that did not write it: Gemma 26B as a defect gate and gpt-oss-20b as advice. This proves a citation belongs to the evidence, not that the evidence is true.

    Cited synthesis in five steps, three ordered strategy passes drawn as model steps, and two separate reader models: a Gemma 26B defect gate and a gpt-oss-20b quality read. Open full size: A strategy that can only cite what was found
  7. Stage 3 turns the brief into numbers a check can prove

    Stage 3 reads the approved brief and strategy, never the must-have lists. A model proposes the sections, counts, protein mix and price mix, and a classifier sorts the must-have dishes. Nine deterministic repairs then pin the client's counts, reserve dietary slots and derive the daypart matrix. The demo client's request for one vegan main becomes the typed cell "vegan": 1, which Stage 4 has to account for.

    Approved Stages 1 and 2 feed two model calls; nine numbered deterministic repairs follow; the output is typed state, including the cell vegan: 1. Open full size: Stage 3 turns the brief into numbers a check can prove
  8. Stage 4 drafts dishes as a set, then checks them

    The binding rules are rebuilt for every section: exclusions, sourcing commitments, dietary provisions and the must-haves, which are placed first. gemma-4-E4B drafts each section as a set, so it sees the neighbouring dishes, then labels each dish's protein, price tier and fit. Code rejects placeholders, duplicates and conflicts, and asks the model again only for the missing cell, within fixed budgets. The same model drafts and labels, so an independent read comes later.

    Binding rules above a loop of generate and classify, both model steps, then admit and top-up, both deterministic, with three properties of the loop listed below. Open full size: Stage 4 drafts dishes as a set, then checks them
  9. Selection is a search, and a shortfall comes with a proof

    No model picks the final menu. A search over up to 120,000 combinations per section chooses it under declared priorities: must-haves and exact counts never relax, and softer preferences give way in a fixed order. When a section cannot be filled, a maximum-flow certificate names the protein and price classes the dish pool cannot serve together. The shortfall is printed in the client's document, never silently cured.

    A deterministic search, a declared relaxation order from assert to suggest, a maximum-flow deficiency certificate, and an empty planned section as the one failure that fails. Open full size: Selection is a search, and a shortfall comes with a proof
  10. Every check runs again after the menu settles

    Enrichment, rewrites, deduplication and reconciliation change the menu after selection, so every audit runs again on the final text. Dietary claims are amended, weakened or withdrawn, for example vegan to vegetarian, rather than left wrong. Allergen claims are removed and left to supplier data and the operator's sign-off. gpt-oss-20b then reads the whole menu as advice only, and a person approves the stage.

    Six changes after selection, seven audits taken again, then delivery: a gpt-oss-20b advisory read, allergen claims stripped as a refused boundary, and human approval. Open full size: Every check runs again after the menu settles
  11. Review findings ordered by consequence

    The first review screen had 43 points that could raise a finding, shown in 13 different ways across four panels, all weighted the same. On one menu, 11 of 17 flags were price-tier arithmetic and none named the defect that decided its grade. Findings are now ordered by the consequence of not responding, after the medical-device alarm standard IEC 60601-1-8, so the serious one comes first.

    Before: 43 emitting points, 13 presentations and 4 panels with every finding weighted the same. After: five levels from look first to inspect what changed. Open full size: Review findings ordered by consequence
  12. Six kinds of guardrail, none of which compares

    Every guarantee in the route is one of six kinds: approval and staleness gates, grammar-locked output schemas, selectors with declared relaxation, repairs with fixed budgets, audits recomputed after rewriting, and disclosures printed in the document. None compares the output with a right answer, because there is none. Each proves a property of the state it can read, never of the world.

    Six guardrail families in a grid: dependency gates, schemas, selectors, bounded repairs, settled audits and disclosures, under the heading it compares nothing. Open full size: Six kinds of guardrail, none of which compares
  13. One laptop, behind an outbound tunnel

    The pilot ran on one Windows laptop with an 8 GB GPU. The consultant signed in through a Cloudflare tunnel that dials out, so no router port was opened. FastAPI serves the five approval gates and keeps every generated, edited and approved version, and the model loader keeps one model in memory at a time.

    Outside the host, a browser reaches the Cloudflare edge; inside the Windows host, a tunnel client, FastAPI, the project store and a model loader that holds one model at a time. Open full size: One laptop, behind an outbound tunnel
  14. What the consultant receives

    The finished project page for the demo client. The brief downloads as a Word document with tracked changes on, so the consultant's edits stay visible. It states what it leaves out: allergen data, prices, and menu copy that stays indicative until checked against the recipe. Points the client never answered are printed as questions, and tensions such as a gross-profit target outside the usual band are raised, not decided.

    The app's project page for a fictional client: all five stages ticked, a Word download, the brief's three stated exclusions, a usability feedback form and two consultant challenges. Open full size: What the consultant receives

Numbers on this page come from the MSc dissertation (2026) and the system's own run records. The code and the client material are private; the screens show a fictional demo client.

How it was evaluated

  1. A harness that never runs in production

    A menu brief has no answer key, so the evaluation compares runs instead. The offline harness clones frozen, approved upstream state so that only the part under test changes, records the model and decoder settings, reruns the same scorers and keeps every document and verdict. Each instrument is labelled by how far it can be trusted: the dietary-claim check reached 91% precision and about 92% estimated recall on 80 hand-labelled dishes, and the whole-menu judge is advisory only.

    The harness never runs in production. Four things it does on the left; four instruments on the right, from exact deterministic checks to action-anchored A to D bands. Open full size: A harness that never runs in production
  2. Fifteen configurations screened on the 8 GB GPU

    Speed came first, as a feasibility screen: the predicted decode speed of every configuration that fitted within 8,188 MiB, from 1.6 to 108.5 tokens per second. The darker bar is the model deployed at Stage 4. Speed only decided which models could be tried; whole documents decided which one was good enough.

    Predicted decode speed, tokens per second (higher is faster)

    Fifteen configurations screened on the 8 GB GPU
    Item Tokens per second
    Mellum2 12B-A2.5B MXFP4 108.5
    Ministral3 3B Q4 88.2
    Qwen3-4B Instruct Q4 76.0
    Qwen3.5 4B Q4 64.9
    Gemma E4B Q4 60.5
    Qwen3 8B Q4 46.1
    gpt-oss-20B MXFP4 40.3
    Qwen3-30B-A3B Q4 35.9
    Qwen3.6-35B IQ2_M 35.4
    Qwen3.6-35B Q4 34.1
    Qwen3.6-35B MTP Q4 34.0
    Qwen3.6-35B Q3 29.1
    Gemma4 26B-A4B Q4 28.2
    Gemma4 12B Q4 16.9
    Magistral Small 24B Q4 1.6
  3. The fastest model was not the best one

    On one full route, Qwen3.6 35B took 37 minutes and its brief graded C; gemma-4-E4B took 22 and Qwen3-4B 16, and both graded D. Then the upstream stages were frozen and only Stage 4 was swapped, for six clients: gemma-4-E4B graded B, C, C, C, D and D, and Qwen3-4B graded D all six times while running faster. gemma-4-E4B was deployed: it beat Qwen3-4B in the paired runs at about half the 35B's Stage 4 time.

    Wall time in minutes

    The fastest model was not the best one
    Item Full routeStage 4 alone
    Qwen3.6 35B 37 min 23.5 min
    gemma-4-E4B 22 min 12.7 min
    Qwen3-4B 16 min 6.2 min
  4. Where 551 candidate dishes went

    In one larger profile, the Stage 4 loop drafted 551 candidates to deliver 74 dishes, and 64% of model time went to checks and repairs rather than writing. Most candidates were removed by deterministic checks, not by a model: 320 failed the distinctness fence and 35 the name check, while the model judge rejected 21. The recorded source does not attribute 101 to a single gate, and the chart says so rather than guessing.

    Candidate dishes, one Stage 4 profile

    Where 551 candidate dishes went
    Item Dishes
    Failed the distinctness fence 320
    Not attributed to one gate 101
    Failed the name check 35
    Rejected by the model judge 21
    Kept and delivered 74
  5. The shortfall the labels missed

    Every structural check first trusted the dietary label the model wrote beside each dish. A hand recount of the dishes' components found up to 47 dishes short of the plan, up to 37 more than the labels reported, in all five composition architectures. A resolver on the settled menu cut the worst case from 46 to 26. That is safe degradation, not proof of dietary compliance, so allergen indication was taken out of the generated brief.

    Dishes short of the dietary plan

    The shortfall the labels missed
    Item Reported by the menu's own labelsFound by hand recount
    Gemma, class assigned 3 33
    Qwen, class assigned 2 31
    Gemma, class chosen 1 31
    Qwen, class chosen 18 47
    Pipeline, slot by slot 9 46
  6. Four results, each with what it rests on

    Narrowing what the model must cite beat giving it more to read: a wider projection cut the citations in the delivered strategy from three to one, while identifier-locked synthesis raised them to seven. Over 25 identical upstream states, neither composition architecture won outright. Every planned section count was delivered in 24 of 24 documents. And whole documents, not speed, chose the Stage 4 model.

    Four findings with their populations: constraint beat retrieval, neither architecture won, the plan was reproduced, documents chose the model; plus the hidden dietary shortfall. Open full size: Four results, each with what it rests on

A to D grades are provisional predictions by a model from another family, read against a written rubric, not consultant grades. Most comparisons are single draws: they show mechanisms, not average effects.

The question

A hospitality consultancy turns each client’s intake form into a menu development brief. Can open-weight models on one consumer GPU draft that brief, with a consultant approving every step and nothing the client relies on left unchecked? This was my MSc dissertation, built with a UK consultancy; its code and the client material stay private.

Built from the consultancy’s own brief

The requirements came from the consultancy’s developer brief and its archive of example files. No example input came with its finished output, and a menu has no single right answer, so there was nothing to score against. The design answers a narrower question: which properties of the brief can be guaranteed, and at which stage. An approval records the hashes of everything upstream, so an earlier edit marks every later stage stale instead of silently keeping work that no longer matches.

Deployed and handed over

It ran as a private pilot behind an authenticated tunnel, on four open-weight models sharing one 8 GB GPU, and was handed over with over 1,500 automated tests and an operating manual. A practising consultant approved four of the five stages without edits and judged the output usable with minor edits.

Limits

  • The code and the client’s material are private: this page describes the work, and a demonstration is available on request.
  • The verdict comes from one practising consultant on a pilot, not from a measured effect across many clients.
  • The A to D grades used to compare runs are a model’s provisional predictions against a written rubric, not consultant grades.
  • No stage can guarantee that a source is true, that a dish meets a dietary claim, or that the menu will be profitable. Those stay with the consultant.

Built with

  • Python
  • FastAPI
  • Pydantic
  • llama.cpp
  • Qwen3.6-35B-A3B
  • Gemma 4
  • gpt-oss-20b
  • BGE reranker
  • NetworkX
  • pytest
  • Playwright
  • Cloudflare Tunnel
Next project SpineLens AI