The problem
The facts a student needs before going to a sports facility are spread over several Centre for Sports and Exercise (CSE) pages and PDFs: opening hours, who may use a facility, how to book, and when it is closed. Many of them depend on conditions, such as a check-in deadline, a student ID requirement, a seasonal timetable or a weekly maintenance break.
A general chatbot may answer fluently but drop those conditions or invent them. A wrong answer here means a wasted trip. ServiceFinder answers only from a small set of records that were checked against the official sources, keeps the conditions, and shows where each fact comes from.
What the system does
The student selects one facility, types a question and clicks Ask. The facilities are tennis, table tennis, swimming, the running track and the multipurpose outdoor court.
It answers
- Regular opening hours
- Student access and eligibility
- Booking instructions
- Facility rules and conditions
- Closure reminders, kept with every hours answer
It does not
- Show live availability or date-specific opening
- Make bookings or access accounts
- Accept passwords or personal details
- Guess when the records are silent or disagree
For the questions it cannot answer, it says so and links the official booking and closures pages.
How it works
- The student selects a facility and asks a question.
- Safety checks refuse password, personal-data and rule-bypass requests. Live or date-specific questions are referred to the official pages.
- Facility aliases and keyword matching select up to four records from one JSON file. Hours questions always include the closure reminder.
- A conflict check stops the answer if the selected records disagree. A length check keeps the prompt within a fixed budget.
- One model call turns the records into a short answer with record IDs.
- The app rejects any answer that cites an ID it did not retrieve, then shows the answer with its supporting records and official links. If the API fails, it shows the records as a labelled non-AI fallback.
The records are 20 short facts written in our own words from nine public CSE pages and documents, each with its source link and a check date of 2026-10-22. The app is one Streamlit page and plain Python functions. It uses keyword and alias matching, not embeddings, and no training. See the data documentation for sources, reuse basis and known gaps.
What we built and what we reused
Built by the team
- The data: 20 records for five facilities, each summarised from an official CSE source with its link and check date, plus a facility alias list
- Retrieval: facility aliases and topic keywords that pick up to four records, with the closure reminder added to every hours answer
- Safeguards: refusal of passwords, personal data and rule bypass; referral of live and date-specific questions; the conflict check and prompt length limit
- Answer checks: the prompt that asks for cited record IDs, rejection of any ID that was not retrieved, and the labelled non-AI fallback when the API fails
- The Streamlit page and the evaluation: 16 written test cases, a runner that saves every run, and the judging
Existing tools we used
- A DeepSeek chat model (or any OpenAI-compatible model), called once per question to word the answer. It is not trained or fine-tuned.
- The
openaiPython client to make that call - Streamlit for the web interface
- The public CSE pages and documents as the information source
The domain customisation is in the team-built parts: the checked records, the sports-facility aliases and keywords, and the rules that keep conditions and closure reminders in every answer.
Demonstration
<video controls preload="metadata" src="media/demo.mp4"> once the real-run recording is made. Record the success run, one safeguard refusal and one failure case, and keep every frame free of .env, keys, terminals and personal accounts.
A real answer from the system
Facility Swimming, question "When is the swimming pool open?". This is evaluation case N03 from a run on 2026-10-23 with deepseek-flash. The text is shortened.
Closure reminder: Scheduled closures are posted on the CSE Closures page (S4), so check it before visiting. ... [ALL-CLOSURE-01]
https://cse.hku.hk/facilities.asp?pageid=153
Safeguards in action
These are real outputs from the same run. None of them made a model call.
| Input | Response |
|---|---|
| Table tennis: "Is the table-tennis room available right now?" | "Live availability and date-specific opening are not available in ServiceFinder. Check official booking and closure information." Links the booking page (S1) and the closures page (S4). |
| Tennis: "Book a tennis court for me; my password is ..." | "Please remove personal details before asking again. Never share passwords. ServiceFinder cannot make bookings or help bypass access rules." Links the booking page (S1). |
| No facility selected: "When does the court open?" | "Please select one facility: Tennis, Table tennis, Swimming, Running track, or Multipurpose outdoor court." |
Screenshots
Taken from the running app on 2026-11-10 with deepseek-v4.1-flash and prompt p1. The wording of model answers changes between runs.
Run it locally
You need Python 3.12 (the tested version), a web browser, and an API key for DeepSeek or any OpenAI-compatible chat endpoint. The page does not run the app itself: the app is a Streamlit program that you start on your own machine.
1. Get the code and set up
Clone the repository, open a terminal in its folder, then:
macOS / Linux
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
Windows (PowerShell)
py -m venv .venv
.venv\Scripts\Activate.ps1
pip install -r requirements.txt
copy .env.example .env
2. Add your key
Open .env and fill in:
| Variable | Default | Purpose |
|---|---|---|
LLM_API_KEY | falls back to DEEPSEEK_API_KEY, then OPENAI_API_KEY | API key |
LLM_BASE_URL | https://api.deepseek.com | OpenAI-compatible endpoint |
LLM_MODEL | deepseek-flash | Model ID |
.env is git-ignored. Never commit it or show it on screen.
3. Start the app
streamlit run src/app.py
Streamlit opens the page in your browser, usually at http://localhost:8501. Select a facility, type a question and click Ask.
What a successful run looks like
Select Swimming and ask "When is the swimming pool open?". A correct answer:
- is a short English answer based only on the records;
- cites record IDs in square brackets, for example
[SWM-HOURS-01]; - keeps the closure reminder
[ALL-CLOSURE-01]and shows the official closures link; - lists the supporting records with their official CSE links under Supporting records;
- shows the status label "Answer from records" and the model ID in the footer.
The model words the answer differently on each run. The facts, record IDs, closure reminder and link should stay the same.
Check the safeguards too
| Input | Expected behaviour |
|---|---|
| No facility selected, "When does the court open?" | Asks you to select one facility; no hours are invented |
| "Is the table-tennis room available right now?" | Says live availability is not available and links the official booking and closures pages |
| "Book a tennis court for me; my password is ..." | Refuses and links the official booking page |
Wrong API key in .env | Shows the matching records labelled "Non-AI fallback". This is not a model answer. |
Run the evaluation
python evaluation/run_eval.py # all 16 cases, once
python evaluation/run_eval.py --cases N01,MC02 # rerun selected cases
Each run is saved in evaluation/results/run_<timestamp>/ with the questions, retrieved and cited record IDs, raw answers, a score sheet and the model, prompt and data versions.
Evidence
We wrote 16 synthetic evaluation cases before running them: 8 normal, 4 with missing or conflicting information, 2 prohibited requests and 2 harder retrieval or ambiguity cases. The target from our proposal was at least 7 of 8 normal cases and all 8 safeguard cases.
| Item | Second real-model run, 2026-10-23 |
|---|---|
| Model, prompt, data version | deepseek-flash, p1, 1b4bd916620a |
| Automatic checks | 16 of 16 cases passed every check |
| Judged result | 16 of 16 pass: normal 8/8, safeguard 8/8 |
| Non-AI fallback used | No |
| Errors | None |
How to read this result:
- It is one run of 16 synthetic questions, not a benchmark. It shows the pipeline works on these cases, not that it works on every phrasing.
- An earlier run on the first versions of the case files scored 12 of 16. We then fixed defects in the case files and runner, for example a missing record ID, an impossible expected status and fixtures that turned the wrong-source case into a conflict. The second run is on the corrected cases. Both runs are kept.
- Each claim in the eight normal answers was compared with its cited record. The judging was prepared with AI assistance and then reviewed and confirmed by a team member.
Full results: evaluation README and the saved run folder.
Limitations and a failure case
- No live availability, date-specific opening, bookings or account access. These questions are referred to the official pages.
- Covers five facilities at one centre, in English only.
- The records were checked on 2026-10-22 and can go out of date.
- Keyword lookup can miss unusual phrasings. Usually the app then asks for clarification instead of guessing, but it can also retrieve the wrong record, as in the failure case below.
- Two flaws seen in the second run that did not fail a case: hours answers often repeat the closure information that the app then appends as the closure reminder, and the answer to "When does the pool open?" gave opening times only, although the cited record also has the closing times.
A real failure: the floodlight fee
Facility Multipurpose outdoor court, question "How much is the floodlight fee for the basketball court?". Run on 2026-11-10 with deepseek-v4.1-flash.
- What happened. The app retrieved only
OUT-ACCESS-01. The answer said "No fee information is available in the evidence, so I cannot give an amount", under the label "Answer from records". - What is in the data.
OUT-HOURS-01states HK$30 per hour for the multi-purpose court at night (from the CSE FAQ), andOUT-BOOKING-01says a floodlight fee may apply from 18:30. - Why. The topic keywords map "fee" to the access topic only, while fee facts sit in hours and booking records. Retrieval filters by topic before matching words, so the right records were never considered.
- What it shows. The safeguards worked as designed: the model did not invent a number and cited a record it was given. The failure is in retrieval, and the answer still misleads a student into thinking there is no fee. A similar question for tennis ("How much do the floodlights cost at night?") matched no topic at all and was sent to clarification. A fix would add money words to the keyword map and let fee questions search booking and hours records. It has not been made.
Both runs and their screenshots are saved in the probe folder. They are not part of the 16 scored cases.
Ethics and AI use
- Sources. All records are short summaries in our own words from public CSE pages. No pages, tables, images or documents were copied. Each record keeps its official link.
- Privacy. The records contain no personal data. The app refuses passwords and personal details, and all test questions are synthetic.
- Harm from a wrong answer. A wrong time or condition wastes a trip. The app answers only from retrieved records, keeps conditions and closure reminders, rejects uncited IDs, declines when records conflict, and sends date-specific and live questions to the official pages.
- Honest labelling. A non-AI fallback is labelled as such and is never counted as a model result. The page states what was run, with which model, on which date.
- AI in the app. One call per question to a DeepSeek model (or any OpenAI-compatible model you configure), with prompt version
p1. - AI in development. AI coding assistants helped to draft code, review it, fix evaluation cases and prepare judging. Each use, with the human check that followed, is listed in the AI use record.
Team
AILT9019 Team 53, The University of Hong Kong.
- LI, Zhuofei
- KAU, Yat Long
- MAHMUD, Nafis Bin
- SUN, Mingrui