GameTheory commited on
Commit
6e32509
Β·
verified Β·
1 Parent(s): a6cb1df

Sync from GitHub

Browse files
Files changed (4) hide show
  1. JOURNEYS.md +55 -0
  2. README.md +3 -0
  3. app.py +18 -7
  4. library_sources.py +16 -8
JOURNEYS.md ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🧭 Customer Journeys
2
+
3
+ Every answer must hand the person their **next action** β€” a link, an
4
+ eligibility line, or a follow-up chip. If an answer names a service without a
5
+ route in, that's a bug (see the route-in audit, June 2026).
6
+
7
+ The design loop for each journey:
8
+
9
+ > **question β†’ intent β†’ live tool β†’ answer β†’ next action β†’ follow-up chips**
10
+
11
+ ## The journeys
12
+
13
+ | # | Who & what they ask | Intent | Tool | The answer must hand them |
14
+ |---|---|---|---|---|
15
+ | 1 | "Do you have Harry Potter audiobooks?" | Borrow a specific title | `search_catalogue` | Matches with format/year + detail links Β· **To borrow:** join-online / digital-membership links Β· full-results link |
16
+ | 2 | "Any good new crime novels?" | Discover something to read | `whats_new` | Newest titles Β· the same **To borrow** join link Β· see-more link |
17
+ | 3 | "Is Malvern library open now?" | Visit a branch today | `find_library` | 🟒/πŸ”΄ open-now + today's hours Β· address Β· facilities Β· **Libraries Unlocked hours + how to get access** Β· branch page |
18
+ | 4 | "A late-opening library with a cafΓ©" | Pick the *right* branch | `graph_search` | Matching branches as **links with addresses** Β· "ask me 'is it open now?'" hint |
19
+ | 5 | "When does the van visit Abberley?" | Catch the mobile library | `mobile_library` | Stop times Β· "join on the van" Β· enquiry email Β· timetable link |
20
+ | 6 | "What's on this week?" | Take the kids somewhere | `library_events` | Events as links with dates/places Β· "most are free, just turn up" |
21
+ | 7 | "Can I read newspapers for free?" | Read/research from home | `online_hub` | Resource name **linked to its council page** Β· what you need Β· **app-store + web links** (PressReader, BorrowBox) Β· access steps |
22
+ | 8 | "How do I print from my phone?" | Print without a printer | `printing_help` | Steps with **join link** + **device-guide page link** Β· prices Β· the PaperCut kiosk caveat |
23
+ | 9 | "What do I need to sign up?" | Join (right tier, least effort) | `membership_help` | Each tier **linked to its own page** β€” digital membership links straight to instant sign-up |
24
+ | 10 | "Can I get in after work?" | Use the library out of hours | `libraries_unlocked` | Hours Β· the 11 branches Β· induction requirement Β· **join-online link for non-members** |
25
+ | 11 | Greeting / off-topic | Find out what this thing does | none (`HELP`) | Capability list + four starter chips |
26
+
27
+ ## Journey-chaining (the chips)
28
+
29
+ Answers end with up to three **suggestion chips** (`NUDGES` in `app.py`) that
30
+ chain journeys together, EAST-style β€” e.g. a branch-hours answer (3) offers
31
+ "Tell me about Libraries Unlocked" (10); a catalogue answer (1) offers "Is it
32
+ on BorrowBox?" (7). The chip's answer must contain the links the nudge text
33
+ alludes to β€” the nudge itself stays short.
34
+
35
+ ## Cross-journey guarantees
36
+
37
+ - **Provenance:** every answer footer shows when it was checked, which tool
38
+ ran, and a source link.
39
+ - **Value receipt:** borrowing/online/printing answers show the Β£ saved
40
+ (`value_receipt`).
41
+ - **Tone:** lead with what the person *can* do β€” never open with a bare "no"
42
+ (`SYNTH_SYSTEM`). The print-from-home question is the canonical example:
43
+ "send it from your phone, collect at any branch" beats "no, but…".
44
+ - **Degradation:** if the live source is down, say so plainly; with no
45
+ `HF_TOKEN` the rendered data stands alone, so every link above lives in the
46
+ *data/renderers* (`library_sources.py`, the `render_*` functions) β€” never
47
+ only in LLM phrasing.
48
+
49
+ ## Maintaining this
50
+
51
+ When adding or changing an answer, walk the loop: *what will the person do
52
+ next?* If the answer mentions a service, membership step, app or page β€” link
53
+ it, in the renderer or curated data, and check the chips chain somewhere
54
+ useful. Then re-run the battery in the PR #5 audit (a question per row above)
55
+ against `respond()`.
README.md CHANGED
@@ -36,6 +36,9 @@ back from the council's own systems, and explains it:
36
  Every reply carries a **β€œchecked live just now” footer** with the tool used and a
37
  link back to the official page.
38
 
 
 
 
39
  ## The honest small-model fit
40
 
41
  The model never invents library facts. The intelligence lives in the **live
 
36
  Every reply carries a **β€œchecked live just now” footer** with the tool used and a
37
  link back to the official page.
38
 
39
+ Each answer is designed around a customer journey β€” what the person is trying
40
+ to do and the next action the reply must hand them. See **[JOURNEYS.md](JOURNEYS.md)**.
41
+
42
  ## The honest small-model fit
43
 
44
  The model never invents library facts. The intelligence lives in the **live
app.py CHANGED
@@ -195,7 +195,7 @@ def render_catalogue(r):
195
  link = f" β€” [details]({it['detail_url']})" if it["detail_url"] else ""
196
  out.append(f"- {it['icon']} **{it['title']}** β€” {meta}{tag}{link}")
197
  out.append(f"\nβœ… **To borrow:** {ls.ELIGIBILITY['borrow_physical']} "
198
- f"eBooks/audio need free digital membership.")
199
  out.append(f"πŸ”Ž [Full results]({r['search_url']})")
200
  return "\n".join(out)
201
 
@@ -207,7 +207,8 @@ def render_whats_new(r):
207
  for it in r["items"]:
208
  meta = " Β· ".join(b for b in (it["author"], it["year"]) if b)
209
  out.append(f"- {it['icon']} **{it['title']}** β€” {meta}")
210
- out.append(f"\nπŸ”Ž [See more]({r['search_url']})")
 
211
  return "\n".join(out)
212
 
213
 
@@ -225,7 +226,9 @@ def render_find_library(r):
225
  f"{r['address']}\n",
226
  f"**Today ({r['today']}):** {r['today_staffed'] or 'see below'}"]
227
  if r.get("unlocked_today"):
228
- out.append(f"**Libraries Unlocked self-service:** {r['unlocked_today']}")
 
 
229
  if r.get("facilities"):
230
  out.append(f"\n**Facilities:** {', '.join(r['facilities'])}")
231
  out.append(f"\nβœ… {ls.ELIGIBILITY['visit']}")
@@ -261,7 +264,8 @@ def render_events(r):
261
  def render_online_hub(r):
262
  out = ["πŸ’» **Free online β€” with your library card:**\n"]
263
  for it in r["items"][:5]:
264
- out.append(f"**{it['name']}** β€” {it['summary']}")
 
265
  if it.get("what_you_need"):
266
  out.append(f" - βœ… **What you need:** {it['what_you_need']}")
267
  if it.get("access"):
@@ -278,7 +282,8 @@ def render_online_hub(r):
278
  def render_unlocked(r):
279
  out = ["πŸ”“ **Libraries Unlocked** β€” use the library 8am–8pm, Mon–Sat, even "
280
  "when it's unstaffed.",
281
- f"\nβœ… **What you need:** {r['what_you_need']}"]
 
282
  if r.get("unlocks"):
283
  out.append(f"\n{r['unlocks']}")
284
  else:
@@ -294,7 +299,9 @@ def render_unlocked(r):
294
  def render_membership(r):
295
  out = ["πŸͺͺ **What you need to sign up:**\n"]
296
  for tier in r["tiers"]:
297
- out.append(f"**{tier['tier']}** β€” {tier['what_you_need']}")
 
 
298
  out.append(f" - _Unlocks:_ {tier['unlocks']}\n")
299
  if r.get("need"):
300
  out.append(f"➑️ For your question: **{r['need']}**")
@@ -329,7 +336,11 @@ def render_graph(r):
329
  out = [f"🧭 Libraries matching **{crit}**:\n"]
330
  for b in r["branches"]:
331
  lu = " Β· open to 8pm" if b["libraries_unlocked"] else ""
332
- out.append(f"- **{b['name']}**{lu} β€” {', '.join(b['facilities'])}")
 
 
 
 
333
  return "\n".join(out)
334
  if r["kind"] == "entity":
335
  if not r["entities"]:
 
195
  link = f" β€” [details]({it['detail_url']})" if it["detail_url"] else ""
196
  out.append(f"- {it['icon']} **{it['title']}** β€” {meta}{tag}{link}")
197
  out.append(f"\nβœ… **To borrow:** {ls.ELIGIBILITY['borrow_physical']} "
198
+ f"eBooks/audio need free [digital membership]({ls.JOIN_URL}).")
199
  out.append(f"πŸ”Ž [Full results]({r['search_url']})")
200
  return "\n".join(out)
201
 
 
207
  for it in r["items"]:
208
  meta = " Β· ".join(b for b in (it["author"], it["year"]) if b)
209
  out.append(f"- {it['icon']} **{it['title']}** β€” {meta}")
210
+ out.append(f"\nβœ… **To borrow:** {ls.ELIGIBILITY['borrow_physical']}")
211
+ out.append(f"πŸ”Ž [See more]({r['search_url']})")
212
  return "\n".join(out)
213
 
214
 
 
226
  f"{r['address']}\n",
227
  f"**Today ({r['today']}):** {r['today_staffed'] or 'see below'}"]
228
  if r.get("unlocked_today"):
229
+ out.append(f"**Libraries Unlocked self-service:** {r['unlocked_today']} β€” "
230
+ f"{ls.ELIGIBILITY['unlocked']} "
231
+ f"[How to get access]({ls.UNLOCKED_URL})")
232
  if r.get("facilities"):
233
  out.append(f"\n**Facilities:** {', '.join(r['facilities'])}")
234
  out.append(f"\nβœ… {ls.ELIGIBILITY['visit']}")
 
264
  def render_online_hub(r):
265
  out = ["πŸ’» **Free online β€” with your library card:**\n"]
266
  for it in r["items"][:5]:
267
+ name = f"[{it['name']}]({it['url']})" if it.get("url") else it["name"]
268
+ out.append(f"**{name}** β€” {it['summary']}")
269
  if it.get("what_you_need"):
270
  out.append(f" - βœ… **What you need:** {it['what_you_need']}")
271
  if it.get("access"):
 
282
  def render_unlocked(r):
283
  out = ["πŸ”“ **Libraries Unlocked** β€” use the library 8am–8pm, Mon–Sat, even "
284
  "when it's unstaffed.",
285
+ f"\nβœ… **What you need:** {r['what_you_need']} "
286
+ f"Not a member yet? [Join online]({ls.JOIN_URL}) first."]
287
  if r.get("unlocks"):
288
  out.append(f"\n{r['unlocks']}")
289
  else:
 
299
  def render_membership(r):
300
  out = ["πŸͺͺ **What you need to sign up:**\n"]
301
  for tier in r["tiers"]:
302
+ name = (f"[{tier['tier']}]({tier['url']})" if tier.get("url")
303
+ else tier["tier"])
304
+ out.append(f"**{name}** β€” {tier['what_you_need']}")
305
  out.append(f" - _Unlocks:_ {tier['unlocks']}\n")
306
  if r.get("need"):
307
  out.append(f"➑️ For your question: **{r['need']}**")
 
336
  out = [f"🧭 Libraries matching **{crit}**:\n"]
337
  for b in r["branches"]:
338
  lu = " Β· open to 8pm" if b["libraries_unlocked"] else ""
339
+ name = f"[{b['name']}]({b['url']})" if b.get("url") else b["name"]
340
+ addr = f" β€” {b['address']}" if b.get("address") else ""
341
+ out.append(f"- **{name}**{lu}{addr} β€” {', '.join(b['facilities'])}")
342
+ out.append("\nπŸ’¬ Ask me _\"is it open now?\"_ about any of these for "
343
+ "live hours.")
344
  return "\n".join(out)
345
  if r["kind"] == "entity":
346
  if not r["entities"]:
library_sources.py CHANGED
@@ -38,6 +38,8 @@ CATALOGUE_SEARCH = (
38
  MOBILE_INDEX = f"{GOV}/council-services/libraries/your-library-membership/mobile-library"
39
  EVENTS_URL = f"{GOV}/council-services/libraries/library-events-and-activities"
40
  PRINTING_URL = f"{GOV}/council-services/libraries/printing-and-photocopying-services"
 
 
41
  ONLINE_HUB = f"{GOV}/council-services/libraries/online-library-hub"
42
 
43
  UA = (
@@ -331,11 +333,12 @@ PRINT_YOUR_WAY = {
331
  ),
332
  "device_requirements": "Android 12+, iOS 16+, macOS 12 (Monterey)+, or Windows 11.",
333
  "steps": [
334
- "Be a full library member (free to join with a library card).",
335
  "Top up your PaperCut print account at a self-service kiosk in any "
336
  "Worcestershire library (some kiosks are cash-only, so check first).",
337
  "One-time setup: download and follow the Print Your Way guide for your "
338
- "device (Android / iOS / macOS / Windows).",
 
339
  "Open your document, choose the mono or colour print queue, set your "
340
  "options and send β€” authenticate with your library number and PIN.",
341
  "Release the job at any public printer in any Worcestershire library "
@@ -379,8 +382,10 @@ def kb() -> dict:
379
 
380
  # "What you need to sign up" β€” the eligibility that varies per service.
381
  ELIGIBILITY = {
382
- "borrow_physical": "Free **full membership** (join online + collect, or in any library).",
383
- "borrow_digital": "Free **digital membership** β€” instant, just a Worcestershire postcode.",
 
 
384
  "printing": "Full membership + top up a PaperCut account at a library kiosk.",
385
  "mobile": "Free full membership β€” you can join on the van.",
386
  "unlocked": "Full member, aged 15+, after a short one-off staff induction.",
@@ -471,7 +476,7 @@ def libraries_unlocked(branch: str | None = None) -> dict:
471
  "hours": "8:00am to 8:00pm, Monday to Saturday",
472
  "what_you_need": tier.get("what_you_need", ELIGIBILITY["unlocked"]),
473
  "unlocks": tier.get("unlocks", ""),
474
- "page_url": tier.get("url", f"{GOV}/council-services/libraries/libraries-unlocked"),
475
  "checked": _now()}
476
  if branch:
477
  b = branch.strip().lower()
@@ -490,7 +495,9 @@ CURATED_HUB = {
490
  "what_you_need": "Worcestershire library card number + PIN (free full membership, residents). No separate PressReader account needed.",
491
  "at_home": True,
492
  "access": [
493
- "Get the PressReader app (or use pressreader.com).",
 
 
494
  "Tap 'Libraries & Groups' and search/select 'Worcestershire'.",
495
  "Sign in with your library card number + PIN.",
496
  "At home you get a 30-day pass (re-confirm monthly); on library Wi-Fi it's 7 days.",
@@ -504,7 +511,8 @@ CURATED_HUB = {
504
  "what_you_need": "Library card number + PIN; choose 'Worcestershire' in the app.",
505
  "at_home": True,
506
  "access": [
507
- "Download the BorrowBox app.",
 
508
  "Select 'Worcestershire' as your library service.",
509
  "Sign in with your card number + PIN.",
510
  ],
@@ -648,7 +656,7 @@ def online_hub(topic: str | None = None) -> dict:
648
  def membership_help(service: str | None = None) -> dict:
649
  """What you need to sign up β€” the cross-service membership matrix."""
650
  out = {"tiers": kb().get("membership_tiers", []),
651
- "page_url": f"{GOV}/council-services/libraries/your-library-membership/join-library",
652
  "checked": _now()}
653
  if service:
654
  s = service.lower()
 
38
  MOBILE_INDEX = f"{GOV}/council-services/libraries/your-library-membership/mobile-library"
39
  EVENTS_URL = f"{GOV}/council-services/libraries/library-events-and-activities"
40
  PRINTING_URL = f"{GOV}/council-services/libraries/printing-and-photocopying-services"
41
+ UNLOCKED_URL = f"{GOV}/council-services/libraries/libraries-unlocked"
42
+ JOIN_URL = f"{GOV}/council-services/libraries/your-library-membership/join-library"
43
  ONLINE_HUB = f"{GOV}/council-services/libraries/online-library-hub"
44
 
45
  UA = (
 
333
  ),
334
  "device_requirements": "Android 12+, iOS 16+, macOS 12 (Monterey)+, or Windows 11.",
335
  "steps": [
336
+ f"Be a full library member β€” free, [join online]({JOIN_URL}) or in any library.",
337
  "Top up your PaperCut print account at a self-service kiosk in any "
338
  "Worcestershire library (some kiosks are cash-only, so check first).",
339
  "One-time setup: download and follow the Print Your Way guide for your "
340
+ f"device β€” Android, iOS, macOS and Windows guides are on the "
341
+ f"[printing page]({PRINTING_URL}).",
342
  "Open your document, choose the mono or colour print queue, set your "
343
  "options and send β€” authenticate with your library number and PIN.",
344
  "Release the job at any public printer in any Worcestershire library "
 
382
 
383
  # "What you need to sign up" β€” the eligibility that varies per service.
384
  ELIGIBILITY = {
385
+ "borrow_physical": f"Free **full membership** ([join online]({JOIN_URL}) + collect, "
386
+ "or in any library).",
387
+ "borrow_digital": f"Free **digital membership** β€” instant, just a Worcestershire "
388
+ f"postcode ([sign up]({JOIN_URL})).",
389
  "printing": "Full membership + top up a PaperCut account at a library kiosk.",
390
  "mobile": "Free full membership β€” you can join on the van.",
391
  "unlocked": "Full member, aged 15+, after a short one-off staff induction.",
 
476
  "hours": "8:00am to 8:00pm, Monday to Saturday",
477
  "what_you_need": tier.get("what_you_need", ELIGIBILITY["unlocked"]),
478
  "unlocks": tier.get("unlocks", ""),
479
+ "page_url": tier.get("url", UNLOCKED_URL),
480
  "checked": _now()}
481
  if branch:
482
  b = branch.strip().lower()
 
495
  "what_you_need": "Worcestershire library card number + PIN (free full membership, residents). No separate PressReader account needed.",
496
  "at_home": True,
497
  "access": [
498
+ "Get the PressReader app β€” [iPhone/iPad](https://apps.apple.com/app/pressreader/id313904711) "
499
+ "or [Android](https://play.google.com/store/apps/details?id=com.newspaperdirect.pressreader.android) β€” "
500
+ "or read in your browser at [pressreader.com](https://www.pressreader.com).",
501
  "Tap 'Libraries & Groups' and search/select 'Worcestershire'.",
502
  "Sign in with your library card number + PIN.",
503
  "At home you get a 30-day pass (re-confirm monthly); on library Wi-Fi it's 7 days.",
 
511
  "what_you_need": "Library card number + PIN; choose 'Worcestershire' in the app.",
512
  "at_home": True,
513
  "access": [
514
+ "Download the BorrowBox app β€” [iPhone/iPad](https://apps.apple.com/app/borrowbox-library/id562843562) "
515
+ "or [Android](https://play.google.com/store/apps/details?id=com.bolindadigital.BorrowBoxLibrary).",
516
  "Select 'Worcestershire' as your library service.",
517
  "Sign in with your card number + PIN.",
518
  ],
 
656
  def membership_help(service: str | None = None) -> dict:
657
  """What you need to sign up β€” the cross-service membership matrix."""
658
  out = {"tiers": kb().get("membership_tiers", []),
659
+ "page_url": JOIN_URL,
660
  "checked": _now()}
661
  if service:
662
  s = service.lower()