Spaces:
Runtime error
Runtime error
Sync from GitHub
Browse files- FIELD_NOTES.md +32 -3
- JOURNEYS.md +4 -1
- PLAN.md +43 -16
- README.md +33 -11
- SUBMISSION.md +5 -4
- app.py +156 -15
- build_hive_kb.py +264 -0
- graph_build.py +82 -6
- graph_rag.py +5 -1
- hive_kb.json +0 -0
- index.html +8 -7
- library_graph.json +746 -283
- library_sources.py +362 -8
FIELD_NOTES.md
CHANGED
|
@@ -77,6 +77,33 @@ contextual "did you know?" nudge at the moment of contact). The nudges aren't de
|
|
| 77 |
β they're the mechanism that attacks the #1 barrier (awareness of breadth), and the
|
| 78 |
trace log tells Jack which ones actually convert.
|
| 79 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
## What surprised us
|
| 81 |
|
| 82 |
- A **single Atom endpoint** saved the hardest integration.
|
|
@@ -90,10 +117,12 @@ trace log tells Jack which ones actually convert.
|
|
| 90 |
|
| 91 |
```bash
|
| 92 |
pip install -r requirements.txt gradio
|
| 93 |
-
python build_kb.py && python
|
| 94 |
python app.py # run (no-LLM mode without HF_TOKEN)
|
|
|
|
| 95 |
```
|
| 96 |
|
| 97 |
-
Everything reads
|
| 98 |
-
|
|
|
|
| 99 |
a laptop.
|
|
|
|
| 77 |
β they're the mechanism that attacks the #1 barrier (awareness of breadth), and the
|
| 78 |
trace log tells Jack which ones actually convert.
|
| 79 |
|
| 80 |
+
## The Hive re-think: exclusion β provenance
|
| 81 |
+
|
| 82 |
+
Our first rule was "never touch thehiveworcester.org β it goes stale". Working
|
| 83 |
+
with Jack's team showed that rule threw away the only public description of
|
| 84 |
+
Worcester's biggest library asset: The Hive is Europe's first joint
|
| 85 |
+
university+public library, open 8:30amβ10pm every day, home to the
|
| 86 |
+
Worcestershire Archive & Archaeology Service, 800+ study spaces, a Business &
|
| 87 |
+
IP Centre and a Youth Hub β and almost none of that lives on the council site.
|
| 88 |
+
**Staleness isn't a reason to exclude a source; it's a reason to tag it.** So
|
| 89 |
+
we crawl all 57 pages (`build_hive_kb.py`), record the *exact* offering of each
|
| 90 |
+
page, stamp every fact with its source URL + crawl date, and apply one
|
| 91 |
+
precedence rule: where the Hive and council sites conflict (hours, prices,
|
| 92 |
+
membership), the council wins and the answer says so. The site even flags its
|
| 93 |
+
own events page as static β so live events still come only from the council.
|
| 94 |
+
|
| 95 |
+
## Going copy-level: "do you have it?" β "here's how you get it"
|
| 96 |
+
|
| 97 |
+
A catalogue hit isn't an outcome; a borrowed book is. `where_to_get` extends
|
| 98 |
+
item search down to the **individual copies**: it reads the item's detail page
|
| 99 |
+
for per-branch holdings (which branch, shelf mark, on-shelf or out), and offers
|
| 100 |
+
the fastest route per format β walk in at the named branch, reserve free, or
|
| 101 |
+
borrow the eBook on BorrowBox *tonight* (the Atom feed's `Electronic Access`
|
| 102 |
+
field often carries the direct link). The detail page is the most fragile
|
| 103 |
+
markup we touch, so there are two independent parsers (holdings table, then a
|
| 104 |
+
branch-name-anchored text scan) and a hard honesty rule: **if neither parses,
|
| 105 |
+
say so and link the item page β never guess availability.**
|
| 106 |
+
|
| 107 |
## What surprised us
|
| 108 |
|
| 109 |
- A **single Atom endpoint** saved the hardest integration.
|
|
|
|
| 117 |
|
| 118 |
```bash
|
| 119 |
pip install -r requirements.txt gradio
|
| 120 |
+
python build_kb.py && python build_hive_kb.py && python graph_build.py
|
| 121 |
python app.py # run (no-LLM mode without HF_TOKEN)
|
| 122 |
+
python library_sources.py # live self-test incl. copy-level Β§5
|
| 123 |
```
|
| 124 |
|
| 125 |
+
Everything reads official sources β `worcestershire.gov.uk` and the catalogue live,
|
| 126 |
+
plus every page of `thehiveworcester.org` with per-fact provenance and the council
|
| 127 |
+
taking precedence on conflicts. Public data, public good, on a model that fits on
|
| 128 |
a laptop.
|
JOURNEYS.md
CHANGED
|
@@ -12,6 +12,7 @@ The design loop for each journey:
|
|
| 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 |
|
|
@@ -23,6 +24,7 @@ The design loop for each journey:
|
|
| 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 |
|
|
@@ -35,7 +37,8 @@ alludes to β the nudge itself stays short.
|
|
| 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"
|
|
|
|
| 12 |
|
| 13 |
| # | Who & what they ask | Intent | Tool | The answer must hand them |
|
| 14 |
|---|---|---|---|---|
|
| 15 |
+
| 0 | "How do I **get** Wolf Hall?" | Walk out with (or download) THIS title today | `where_to_get` | Every format held, then the fastest route per format: **on the shelf at a named branch with shelf mark** (copy-level, live) β free reservation steps β **BorrowBox tonight** (direct link when the catalogue carries one) Β· eligibility per route |
|
| 16 |
| 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 |
|
| 17 |
| 2 | "Any good new crime novels?" | Discover something to read | `whats_new` | Newest titles Β· the same **To borrow** join link Β· see-more link |
|
| 18 |
| 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 |
|
|
|
|
| 24 |
| 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 |
|
| 25 |
| 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** |
|
| 26 |
| 11 | Greeting / off-topic | Find out what this thing does | none (`HELP`) | Capability list + four starter chips |
|
| 27 |
+
| 12 | "What can I do at The Hive?" / "the archives" / "hire a room" | Use Worcester's flagship β incl. its uni-grade extras | `hive_info` | The exact offering of the relevant Hive **page(s)** (offerings, what-you-need, prices, level in building) Β· extended-capability gems Β· 8:30amβ10pm hours Β· **provenance footer: Hive pages, crawl date, council-wins-on-conflict** |
|
| 28 |
|
| 29 |
## Journey-chaining (the chips)
|
| 30 |
|
|
|
|
| 37 |
## Cross-journey guarantees
|
| 38 |
|
| 39 |
- **Provenance:** every answer footer shows when it was checked, which tool
|
| 40 |
+
ran, and a source link. Hive answers additionally name the crawl date and
|
| 41 |
+
defer to the council on conflicts (hours, prices, membership).
|
| 42 |
- **Value receipt:** borrowing/online/printing answers show the Β£ saved
|
| 43 |
(`value_receipt`).
|
| 44 |
- **Tone:** lead with what the person *can* do β never open with a bare "no"
|
PLAN.md
CHANGED
|
@@ -6,10 +6,21 @@ Companion docs: [README](README.md) Β· [SUBMISSION](SUBMISSION.md) Β· [IDEAS](ID
|
|
| 6 |
## Goal
|
| 7 |
|
| 8 |
Give Worcestershire Libraries one conversational voice that answers any resident
|
| 9 |
-
question from **
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
## Architecture
|
| 15 |
|
|
@@ -19,14 +30,19 @@ about what it offers."
|
|
| 19 |
β fallback (no-token safe) β
|
| 20 |
βββββββββββββββββ¬βββββββββββββββββββββββββββββ
|
| 21 |
βΌ
|
| 22 |
-
βββββββββββββββββ
|
| 23 |
-
β
|
| 24 |
-
β
|
| 25 |
-
β
|
| 26 |
-
β
|
| 27 |
-
β
|
| 28 |
-
β
|
| 29 |
-
β
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
βββββββββββββββββββββ¬βββββββββββββββββββββββββ
|
| 31 |
βΌ
|
| 32 |
synthesise (β€32B) + eligibility + Β£-receipt + EAST nudge
|
|
@@ -40,8 +56,9 @@ about what it offers."
|
|
| 40 |
| Layer | File | Role |
|
| 41 |
|---|---|---|
|
| 42 |
| Ingestion | `build_kb.py` β `library_kb.json` | Crawl all 218 library URLs β 87 services / 23 branches / 17 resources, with eligibility & facilities |
|
| 43 |
-
|
|
| 44 |
-
|
|
|
|
|
| 45 |
| Retrieval | `graph_rag.py` | local/global graph search (multi-hop) |
|
| 46 |
| Traces | `trace.py` β `traces.jsonl` | per-turn structured agent trace (Open Trace badge) |
|
| 47 |
| App | `app.py` | router, renders, behaviour-change layer, Gradio UI |
|
|
@@ -56,8 +73,16 @@ about what it offers."
|
|
| 56 |
- [x] **P5 β Behaviour-change layer.** EAST nudges, Β£-saved value receipt, quick-reply chips (DCMS/COM-B grounded).
|
| 57 |
- [x] **P6 β Traces.** JSONL logging + in-chat "how I answered" panel.
|
| 58 |
- [x] **P7 β KB refinement.** Curate all 17 hub resources, filter time-bound junk, tag seasonal pages.
|
| 59 |
-
- [
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
## The behaviour-change layer (why, not just what)
|
| 63 |
|
|
@@ -90,6 +115,8 @@ source correctness.
|
|
| 90 |
| HF Inference flaky / no token | **No-LLM fallback** renders raw live data β demo never breaks |
|
| 91 |
| Council site HTML changes | Re-run `build_kb.py` (re-crawl) any time; tools fail soft |
|
| 92 |
| SirsiDynix slow/timeouts | per-call try/except; app degrades gracefully |
|
|
|
|
|
|
|
| 93 |
| Gradio 6 needs Py3.10+ | tested logic locally; boot-test on the Space first |
|
| 94 |
| Facility data sparse (e.g. "study space") | multi-hop honest about coverage; lead demo on cafΓ©+meeting |
|
| 95 |
|
|
|
|
| 6 |
## Goal
|
| 7 |
|
| 8 |
Give Worcestershire Libraries one conversational voice that answers any resident
|
| 9 |
+
question from **official, source-cited data at every granularity** β down to
|
| 10 |
+
*which branch has the copy on its shelf right now* and *exactly what every page
|
| 11 |
+
of the Hive offers* β tells them **exactly what they need to sign up**, and
|
| 12 |
+
**surfaces the services they never knew existed** β on a β€32B model,
|
| 13 |
+
locally-capable. The DCMS-evidenced answer to "the library doesn't shout about
|
| 14 |
+
what it offers." Built with/for Jack's library-resources team.
|
| 15 |
+
|
| 16 |
+
The granularity ladder:
|
| 17 |
+
|
| 18 |
+
| Level | Data | Tool |
|
| 19 |
+
|---|---|---|
|
| 20 |
+
| Service | 87 council service pages, eligibility-first | KB tools |
|
| 21 |
+
| Page | all 57 pages of thehiveworcester.org, exact offerings + provenance | `hive_info` |
|
| 22 |
+
| Item | live catalogue (Atom feed), incl. BorrowBox deep links | `search_catalogue` |
|
| 23 |
+
| Copy | per-branch holdings + status from the item's detail page | `where_to_get` |
|
| 24 |
|
| 25 |
## Architecture
|
| 26 |
|
|
|
|
| 30 |
β fallback (no-token safe) β
|
| 31 |
βββββββββββββββββ¬βββββββββββββββββββββββββββββ
|
| 32 |
βΌ
|
| 33 |
+
βββββββββββββββββ 12 tools ββββββββββββββββββ βββ GraphRAG βββ
|
| 34 |
+
β where_to_get (copy-level: shelf @ β β local_search β
|
| 35 |
+
β branch β reserve β β β global_searchβ
|
| 36 |
+
β BorrowBox tonight) β β multi-hop β
|
| 37 |
+
β hive_info (57 Hive pages, exact βββββ over graph β
|
| 38 |
+
β offer + provenance) β ββββββββββββββββ
|
| 39 |
+
β search_catalogue (SirsiDynix Atom feed) β library_graph.json
|
| 40 |
+
β whats_new (newest titles) β 349 nodes/502 edges
|
| 41 |
+
β find_library (hours/open-now/facils) β incl. 26 HiveService
|
| 42 |
+
β mobile_library (154 villages) β
|
| 43 |
+
β library_events (live events) β
|
| 44 |
+
β online_hub (PressReader/BorrowBoxβ¦)β
|
| 45 |
+
β libraries_unlocked Β· printing Β· membership β
|
| 46 |
βββββββββββββββββββββ¬βββββββββββββββββββββββββ
|
| 47 |
βΌ
|
| 48 |
synthesise (β€32B) + eligibility + Β£-receipt + EAST nudge
|
|
|
|
| 56 |
| Layer | File | Role |
|
| 57 |
|---|---|---|
|
| 58 |
| Ingestion | `build_kb.py` β `library_kb.json` | Crawl all 218 library URLs β 87 services / 23 branches / 17 resources, with eligibility & facilities |
|
| 59 |
+
| Hive ingestion | `build_hive_kb.py` β `hive_kb.json` | Crawl all 57 Hive pages β exact per-page offerings, what-you-need, prices, levels; curated `hive_profile` of 26 extended capabilities, every fact source-tagged |
|
| 60 |
+
| Graph | `graph_build.py` β `library_graph.json` | Deterministic GraphRAG over BOTH KBs: entities β relationships β communities β reports (incl. 26 HiveService nodes; offline rebuild keeps villages) |
|
| 61 |
+
| Sources | `library_sources.py` | 12 live/KB tools + curated hub access + membership tiers + copy-level availability parser |
|
| 62 |
| Retrieval | `graph_rag.py` | local/global graph search (multi-hop) |
|
| 63 |
| Traces | `trace.py` β `traces.jsonl` | per-turn structured agent trace (Open Trace badge) |
|
| 64 |
| App | `app.py` | router, renders, behaviour-change layer, Gradio UI |
|
|
|
|
| 73 |
- [x] **P5 β Behaviour-change layer.** EAST nudges, Β£-saved value receipt, quick-reply chips (DCMS/COM-B grounded).
|
| 74 |
- [x] **P6 β Traces.** JSONL logging + in-chat "how I answered" panel.
|
| 75 |
- [x] **P7 β KB refinement.** Curate all 17 hub resources, filter time-bound junk, tag seasonal pages.
|
| 76 |
+
- [x] **P8 β The Hive, page-level.** Reverse the exclusion: crawl all 57 pages
|
| 77 |
+
with provenance (`build_hive_kb.py`), `hive_info` tool, Hive in the graph
|
| 78 |
+
(open-late, archives, 26 capabilities), council-wins-on-conflict rule.
|
| 79 |
+
- [x] **P9 β Copy-level "where to get it".** `where_to_get`: per-branch
|
| 80 |
+
holdings/status from the item detail page (two parse strategies, fail-soft),
|
| 81 |
+
free-reservation steps, BorrowBox deep links from the Atom feed's
|
| 82 |
+
Electronic Access field. *Live-verify with `python library_sources.py`.*
|
| 83 |
+
- [ ] **P10 β Deploy** to the Space + `HF_TOKEN` + smoke-test (user action),
|
| 84 |
+
incl. the new self-test Β§5 (copy-level) against the live catalogue.
|
| 85 |
+
- [ ] **P11 β Stretch:** custom `gr.Server` frontend; a small fine-tune (π― badge); llama.cpp local run (ππ¦).
|
| 86 |
|
| 87 |
## The behaviour-change layer (why, not just what)
|
| 88 |
|
|
|
|
| 115 |
| HF Inference flaky / no token | **No-LLM fallback** renders raw live data β demo never breaks |
|
| 116 |
| Council site HTML changes | Re-run `build_kb.py` (re-crawl) any time; tools fail soft |
|
| 117 |
| SirsiDynix slow/timeouts | per-call try/except; app degrades gracefully |
|
| 118 |
+
| Hive site goes stale again | every Hive fact shows its crawl date; council wins on conflict; `build_hive_kb.py` re-crawls in minutes |
|
| 119 |
+
| Detail-page holdings markup shifts | two independent parse strategies (table + branch-name text scan); if both miss, the answer says so and links the item page β never guesses |
|
| 120 |
| Gradio 6 needs Py3.10+ | tested logic locally; boot-test on the Space first |
|
| 121 |
| Facility data sparse (e.g. "study space") | multi-hop honest about coverage; lead demo on cafΓ©+meeting |
|
| 122 |
|
README.md
CHANGED
|
@@ -14,7 +14,9 @@ short_description: Live, source-cited answers about your local library
|
|
| 14 |
# π Worcestershire Libraries β Live Assistant
|
| 15 |
|
| 16 |
A small-model (**β€ 32B**) agent that answers real questions about Worcestershire
|
| 17 |
-
Libraries
|
|
|
|
|
|
|
| 18 |
|
| 19 |
> Built for the [Build Small Hackathon](https://huggingface.co/build-small-hackathon)
|
| 20 |
> Β· **Backyard AI** track. The "someone I know" is **Jack, a library resources
|
|
@@ -28,7 +30,9 @@ back from the council's own systems, and explains it:
|
|
| 28 |
|
| 29 |
| You ask⦠| It mines⦠| Source |
|
| 30 |
|---|---|---|
|
|
|
|
| 31 |
| π βDo you have Harry Potter audiobooks?β | the **SirsiDynix catalogue** (books, eBooks, audio, DVDs) | `wcc.ent.sirsidynix.net.uk` |
|
|
|
|
| 32 |
| π βWhen does the mobile library visit Abberley?β | the **mobile-library timetable** (154 villages) | `worcestershire.gov.uk` |
|
| 33 |
| π
βWhat's on this week?β | **events & activities** | `worcestershire.gov.uk` |
|
| 34 |
| π¨οΈ βHow do I print from my phone?β | **Print Your Way** steps & prices | `worcestershire.gov.uk` |
|
|
@@ -52,11 +56,21 @@ know a catalogue it was never trained on.
|
|
| 52 |
mode* β deterministic keyword routing + the raw live data. The demo never
|
| 53 |
breaks.
|
| 54 |
|
| 55 |
-
##
|
| 56 |
|
| 57 |
-
We
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
## Architecture
|
| 62 |
|
|
@@ -64,13 +78,16 @@ catalogue are used, so answers are trustworthy and current.
|
|
| 64 |
question βββΆ route() (LLM JSON router, keyword fallback)
|
| 65 |
β
|
| 66 |
βΌ
|
| 67 |
-
library_sources.py βββΆ
|
| 68 |
-
β
|
| 69 |
-
β
|
| 70 |
-
β
|
| 71 |
-
β
|
|
|
|
|
|
|
|
|
|
| 72 |
βΌ
|
| 73 |
-
synthesize() βββΆ warm, grounded answer +
|
| 74 |
```
|
| 75 |
|
| 76 |
`library_sources.py` has **zero** Gradio/LLM dependencies and is independently
|
|
@@ -82,6 +99,11 @@ testable against the live sites: `python library_sources.py`.
|
|
| 82 |
pip install -r requirements.txt gradio
|
| 83 |
export HF_TOKEN=hf_xxx # optional β omit for no-LLM mode
|
| 84 |
python app.py # http://localhost:7860
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
```
|
| 86 |
|
| 87 |
## Bonus quests in reach
|
|
|
|
| 14 |
# π Worcestershire Libraries β Live Assistant
|
| 15 |
|
| 16 |
A small-model (**β€ 32B**) agent that answers real questions about Worcestershire
|
| 17 |
+
Libraries from official data **at every granularity** β service pages β every
|
| 18 |
+
page of the Hive's site β catalogue items β the individual copies on a branch's
|
| 19 |
+
shelf β mined live at question time wherever the source allows it.
|
| 20 |
|
| 21 |
> Built for the [Build Small Hackathon](https://huggingface.co/build-small-hackathon)
|
| 22 |
> Β· **Backyard AI** track. The "someone I know" is **Jack, a library resources
|
|
|
|
| 30 |
|
| 31 |
| You ask⦠| It mines⦠| Source |
|
| 32 |
|---|---|---|
|
| 33 |
+
| π βHow do I **get** Wolf Hall?β | the item's **copy-level holdings** β which branch has it on the shelf *now*, free reservation, or the eBook on BorrowBox tonight | `wcc.ent.sirsidynix.net.uk` |
|
| 34 |
| π βDo you have Harry Potter audiobooks?β | the **SirsiDynix catalogue** (books, eBooks, audio, DVDs) | `wcc.ent.sirsidynix.net.uk` |
|
| 35 |
+
| π βWhat can I do at The Hive?β | **every page of the Hive's site** β archives & archaeology, 800+ study spaces, room hire, open 8:30amβ10pm daily | `thehiveworcester.org` |
|
| 36 |
| π βWhen does the mobile library visit Abberley?β | the **mobile-library timetable** (154 villages) | `worcestershire.gov.uk` |
|
| 37 |
| π
βWhat's on this week?β | **events & activities** | `worcestershire.gov.uk` |
|
| 38 |
| π¨οΈ βHow do I print from my phone?β | **Print Your Way** steps & prices | `worcestershire.gov.uk` |
|
|
|
|
| 56 |
mode* β deterministic keyword routing + the raw live data. The demo never
|
| 57 |
breaks.
|
| 58 |
|
| 59 |
+
## The Hive, with provenance
|
| 60 |
|
| 61 |
+
We originally excluded thehiveworcester.org because parts of it run stale. The
|
| 62 |
+
re-think: **include it page-by-page, but tag every fact with its source** β
|
| 63 |
+
because the Hive's site is the *only* place much of Worcester's extended offer
|
| 64 |
+
is described (Explore the Past archives & archaeology, Europe's first joint
|
| 65 |
+
university+public library, 800+ study spaces, room hire, the BIPC, the Youth
|
| 66 |
+
Hub). The rules that keep answers trustworthy:
|
| 67 |
+
|
| 68 |
+
- every Hive fact carries its **source page + crawl date** (`hive_kb.json`,
|
| 69 |
+
rebuilt by `build_hive_kb.py`)
|
| 70 |
+
- where Hive and council pages conflict (hours, prices, membership), the
|
| 71 |
+
**council page wins** β and the answer says which source it used
|
| 72 |
+
- the Hive's own events page is static, so **live events always come from the
|
| 73 |
+
council site**
|
| 74 |
|
| 75 |
## Architecture
|
| 76 |
|
|
|
|
| 78 |
question βββΆ route() (LLM JSON router, keyword fallback)
|
| 79 |
β
|
| 80 |
βΌ
|
| 81 |
+
library_sources.py βββΆ the granularity ladder
|
| 82 |
+
β service β’ 87 council service pages (library_kb.json)
|
| 83 |
+
β page β’ 57 Hive pages, exact offer (hive_kb.json)
|
| 84 |
+
β item β’ search_catalogue() (live Atom feed)
|
| 85 |
+
β copy β’ where_to_get() (live holdings:
|
| 86 |
+
β shelf @ branch β reserve β BorrowBox tonight)
|
| 87 |
+
β plus β’ mobile_library() Β· library_events()
|
| 88 |
+
β β’ find_library() Β· hive_info() Β· printing_help()
|
| 89 |
βΌ
|
| 90 |
+
synthesize() βββΆ warm, grounded answer + named-source footer
|
| 91 |
```
|
| 92 |
|
| 93 |
`library_sources.py` has **zero** Gradio/LLM dependencies and is independently
|
|
|
|
| 99 |
pip install -r requirements.txt gradio
|
| 100 |
export HF_TOKEN=hf_xxx # optional β omit for no-LLM mode
|
| 101 |
python app.py # http://localhost:7860
|
| 102 |
+
|
| 103 |
+
# refresh the knowledge bases + graph any time:
|
| 104 |
+
python build_kb.py # council pages -> library_kb.json
|
| 105 |
+
python build_hive_kb.py # every Hive page -> hive_kb.json
|
| 106 |
+
python graph_build.py # both KBs -> library_graph.json
|
| 107 |
```
|
| 108 |
|
| 109 |
## Bonus quests in reach
|
SUBMISSION.md
CHANGED
|
@@ -95,22 +95,23 @@ Backyard entries **lead with a face + a quote** β the demo opens on Jack.
|
|
| 95 |
|
| 96 |
```
|
| 97 |
question β route (LLM JSON + keyword fallback)
|
| 98 |
-
β
|
|
|
|
| 99 |
β GraphRAG search ββ council pages (87 services / 23 branches / 17 resources)
|
| 100 |
-
β synthesise (β€32B) library_graph.json:
|
| 101 |
β answer + eligibility + Β£βreceipt + EAST nudge + source + open trace
|
| 102 |
```
|
| 103 |
|
| 104 |
- **Inspiration:** microsoft/graphrag, microsoft/markitdown, IBM Docling.
|
| 105 |
- **Evidence spine:** DCMS/Ipsos *What works to engage nonβusers* (COMβB) + BIT **EAST**.
|
| 106 |
- **Honest constraint fit:** small model + big graph; **noβLLM fallback** so the demo never breaks.
|
| 107 |
-
- **Sources:** worcestershire.gov.uk
|
| 108 |
|
| 109 |
## 8. Goβlive checklist (for Jack, Friday AM)
|
| 110 |
|
| 111 |
- [ ] Create Space `build-small-hackathon/wpl-discovery` (Gradio, public).
|
| 112 |
- [ ] Push: `app.py`, `library_sources.py`, `graph_rag.py`, `trace.py`,
|
| 113 |
-
`library_kb.json`, `library_graph.json`, `requirements.txt`, `README.md`, `LICENSE`.
|
| 114 |
*(build_kb.py / graph_build.py are buildβtime only β optional to include.)*
|
| 115 |
- [ ] Add Space secret **`HF_TOKEN`** (read scope) β enables the model. *Without it the
|
| 116 |
app still runs in noβLLM mode, so a missing token won't block Jack's test.*
|
|
|
|
| 95 |
|
| 96 |
```
|
| 97 |
question β route (LLM JSON + keyword fallback)
|
| 98 |
+
β 12 live/KB tools ββ SirsiDynix catalogue (Atom) down to COPY level
|
| 99 |
+
β ββ all 57 Hive pages, exact offer (provenanceβtagged)
|
| 100 |
β GraphRAG search ββ council pages (87 services / 23 branches / 17 resources)
|
| 101 |
+
β synthesise (β€32B) library_graph.json: 349 nodes Β· 502 edges Β· 26 Hive services
|
| 102 |
β answer + eligibility + Β£βreceipt + EAST nudge + source + open trace
|
| 103 |
```
|
| 104 |
|
| 105 |
- **Inspiration:** microsoft/graphrag, microsoft/markitdown, IBM Docling.
|
| 106 |
- **Evidence spine:** DCMS/Ipsos *What works to engage nonβusers* (COMβB) + BIT **EAST**.
|
| 107 |
- **Honest constraint fit:** small model + big graph; **noβLLM fallback** so the demo never breaks.
|
| 108 |
+
- **Sources:** worcestershire.gov.uk + the live catalogue (checked at question time), plus every page of thehiveworcester.org crawled with perβfact provenance β council wins on conflicts.
|
| 109 |
|
| 110 |
## 8. Goβlive checklist (for Jack, Friday AM)
|
| 111 |
|
| 112 |
- [ ] Create Space `build-small-hackathon/wpl-discovery` (Gradio, public).
|
| 113 |
- [ ] Push: `app.py`, `library_sources.py`, `graph_rag.py`, `trace.py`,
|
| 114 |
+
`library_kb.json`, `hive_kb.json`, `library_graph.json`, `requirements.txt`, `README.md`, `LICENSE`.
|
| 115 |
*(build_kb.py / graph_build.py are buildβtime only β optional to include.)*
|
| 116 |
- [ ] Add Space secret **`HF_TOKEN`** (read scope) β enables the model. *Without it the
|
| 117 |
app still runs in noβLLM mode, so a missing token won't block Jack's test.*
|
app.py
CHANGED
|
@@ -2,9 +2,15 @@
|
|
| 2 |
Worcestershire Libraries β Live Assistant (Build Small Hackathon)
|
| 3 |
|
| 4 |
A small-model (<=32B) civic agent that answers real questions about
|
| 5 |
-
Worcestershire Libraries from
|
|
|
|
|
|
|
| 6 |
|
| 7 |
β’ Live tools β catalogue, mobile library, events, branch hours/facilities
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
β’ Knowledge graph (GraphRAG-style) β multi-hop "which late library has a cafΓ©?"
|
| 9 |
β’ Curated detail β exactly what you need to sign up for each service
|
| 10 |
β’ Behaviour-change layer β EAST nudges + Β£-saved "value receipt" (DCMS-evidenced)
|
|
@@ -54,9 +60,21 @@ def llm(messages, *, max_tokens=512, temperature=0.3, stream=False):
|
|
| 54 |
|
| 55 |
TOOLS = {
|
| 56 |
"search_catalogue": {
|
| 57 |
-
"desc": "
|
| 58 |
"args: {\"query\": \"<title / author / subject>\"}",
|
| 59 |
"fn": lambda a: ls.search_catalogue(a.get("query", ""), limit=6)},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
"whats_new": {
|
| 61 |
"desc": "Newest titles in a genre, for fun recommendations. "
|
| 62 |
"args: {\"genre\": \"<genre/topic>\"}",
|
|
@@ -114,6 +132,25 @@ def keyword_route(q: str) -> tuple[str, dict]:
|
|
| 114 |
if w in t)
|
| 115 |
if re.search(r"\b(print|printing|photocopy|photocopies|scan|copier)\b", t):
|
| 116 |
return "printing_help", {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
if (re.search(r"\b(which|what) librar|librar(y|ies) (with|that has)\b", t)
|
| 118 |
or feat_hits >= 2 or "overall" in t):
|
| 119 |
return "graph_search", {"query": q}
|
|
@@ -124,8 +161,8 @@ def keyword_route(q: str) -> tuple[str, dict]:
|
|
| 124 |
r"facilit|where is|near me|study space)\b", t):
|
| 125 |
m = re.findall(r"\b([A-Z][a-z]+(?:[ -][A-Z][a-z]+)*)\b", q)
|
| 126 |
return "find_library", {"name": (m[-1] if m else "")}
|
| 127 |
-
if re.search(r"\b(unlocked|8pm|after hours|out of hours|
|
| 128 |
-
r"open late)\b", t):
|
| 129 |
return "libraries_unlocked", {}
|
| 130 |
platform = re.search(r"\b(borrowbox|pressreader|ancestry|espacenet|ebsco|oxford|"
|
| 131 |
r"theory test|bfi|cobra|digital library|online (library )?hub)\b", t)
|
|
@@ -364,8 +401,96 @@ def render_graph(r):
|
|
| 364 |
return "\n".join(out)
|
| 365 |
|
| 366 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 367 |
RENDER = {
|
| 368 |
"search_catalogue": render_catalogue, "whats_new": render_whats_new,
|
|
|
|
| 369 |
"find_library": render_find_library, "mobile_library": render_mobile,
|
| 370 |
"library_events": render_events, "online_hub": render_online_hub,
|
| 371 |
"libraries_unlocked": render_unlocked, "membership_help": render_membership,
|
|
@@ -380,6 +505,8 @@ RENDER = {
|
|
| 380 |
def value_receipt(tool, raw):
|
| 381 |
if tool in ("search_catalogue", "whats_new") and raw.get("items"):
|
| 382 |
return "π· _Borrowing instead of buying β **Β£9βοΏ½οΏ½20 saved** per title._"
|
|
|
|
|
|
|
| 383 |
if tool == "online_hub":
|
| 384 |
return ("π· _Free with your card β a newspaper or eBook subscription is "
|
| 385 |
"**~Β£8βΒ£12/month** you don't pay._")
|
|
@@ -410,8 +537,12 @@ NUDGES = {
|
|
| 410 |
["Set up digital membership", "What's the difference?", "What can I borrow online?"]),
|
| 411 |
"graph_search": ("π‘ Tell me what matters (late, cafΓ©, study space) and I'll match a branch.",
|
| 412 |
["Late-opening + cafΓ©", "Find a book", "What's on this week?"]),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
}
|
| 414 |
-
HELP_CHIPS = ["
|
| 415 |
"What's on this week?", "How do I print from my phone?"]
|
| 416 |
|
| 417 |
|
|
@@ -426,6 +557,7 @@ SYNTH_SYSTEM = (
|
|
| 426 |
"data. Lead with what the person CAN do: if a service meets their need in a "
|
| 427 |
"different way (e.g. they can't print AT home, but can send a job from home "
|
| 428 |
"and collect it at any branch), open with that β never with a bare 'no'. "
|
|
|
|
| 429 |
"If the data doesn't answer it, say so and point to the source link.")
|
| 430 |
|
| 431 |
|
|
@@ -453,14 +585,18 @@ def synthesize_stream(question, rendered):
|
|
| 453 |
HELP = (
|
| 454 |
"π I'm the **Worcestershire Libraries assistant**. I check the council site "
|
| 455 |
"and catalogue *live* and can help you:\n\n"
|
| 456 |
-
"- π **
|
|
|
|
| 457 |
"- π **Branch hours, 'open now?', toilets, parking**\n"
|
|
|
|
|
|
|
| 458 |
"- π **Mobile library** times for your village\n"
|
| 459 |
"- π
**What's on** this week\n"
|
| 460 |
"- π» **Free online** β newspapers, magazines, family history\n"
|
| 461 |
"- π¨οΈ **Printing** from your phone\n\n"
|
| 462 |
-
"
|
| 463 |
-
"
|
|
|
|
| 464 |
|
| 465 |
|
| 466 |
# --------------------------------------------------------------------------- #
|
|
@@ -505,7 +641,9 @@ def respond(message, history):
|
|
| 505 |
value = value_receipt(tool, raw)
|
| 506 |
nudge, chips = NUDGES.get(tool, ("", []))
|
| 507 |
checked = raw.get("checked", "")
|
| 508 |
-
|
|
|
|
|
|
|
| 509 |
+ (f" Β· {checked}" if checked else "")
|
| 510 |
+ f" Β· tool `{tool}` ({how})"
|
| 511 |
+ (f" Β· [source]({source})" if source else "") + "</sub>")
|
|
@@ -545,9 +683,10 @@ def build_demo():
|
|
| 545 |
with gr.Blocks(title="Worcestershire Libraries β Live Assistant") as demo:
|
| 546 |
gr.HTML("<div id='hero'><span class='live'>β live data</span>"
|
| 547 |
"<h1>Worcestershire Libraries β Live Assistant</h1>"
|
| 548 |
-
"<p>
|
| 549 |
-
"
|
| 550 |
-
"
|
|
|
|
| 551 |
|
| 552 |
chat = gr.Chatbot(height=460, show_label=False,
|
| 553 |
placeholder="π Ask me anything about your local libraryβ¦",
|
|
@@ -561,12 +700,14 @@ def build_demo():
|
|
| 561 |
for _ in range(3)]
|
| 562 |
|
| 563 |
gr.Examples(
|
| 564 |
-
["
|
|
|
|
|
|
|
|
|
|
| 565 |
"Is Malvern library open now?",
|
| 566 |
"A late-opening library with a cafΓ© and meeting rooms",
|
| 567 |
"When does the mobile library visit Abberley?",
|
| 568 |
-
"Can I read newspapers for free?",
|
| 569 |
-
"What do I need to sign up?"],
|
| 570 |
inputs=box, label="Try one")
|
| 571 |
|
| 572 |
if not HF_TOKEN:
|
|
|
|
| 2 |
Worcestershire Libraries β Live Assistant (Build Small Hackathon)
|
| 3 |
|
| 4 |
A small-model (<=32B) civic agent that answers real questions about
|
| 5 |
+
Worcestershire Libraries from official data at EVERY granularity:
|
| 6 |
+
service pages -> every page of the Hive site -> catalogue items -> the
|
| 7 |
+
individual copies on a branch's shelf.
|
| 8 |
|
| 9 |
β’ Live tools β catalogue, mobile library, events, branch hours/facilities
|
| 10 |
+
β’ where_to_get β the full "how do I actually get this title" journey:
|
| 11 |
+
on-shelf copy at a named branch / free reservation / BorrowBox tonight
|
| 12 |
+
β’ hive_info β The Hive (Worcester) page-by-page: archives & archaeology,
|
| 13 |
+
800+ study spaces, room hire, BIPC, Youth Hub, 8:30amβ10pm every day
|
| 14 |
β’ Knowledge graph (GraphRAG-style) β multi-hop "which late library has a cafΓ©?"
|
| 15 |
β’ Curated detail β exactly what you need to sign up for each service
|
| 16 |
β’ Behaviour-change layer β EAST nudges + Β£-saved "value receipt" (DCMS-evidenced)
|
|
|
|
| 60 |
|
| 61 |
TOOLS = {
|
| 62 |
"search_catalogue": {
|
| 63 |
+
"desc": "Browse/search what the library holds (book, eBook, audiobook, DVD). "
|
| 64 |
"args: {\"query\": \"<title / author / subject>\"}",
|
| 65 |
"fn": lambda a: ls.search_catalogue(a.get("query", ""), limit=6)},
|
| 66 |
+
"where_to_get": {
|
| 67 |
+
"desc": "User wants to actually GET/borrow/obtain a specific title β gives "
|
| 68 |
+
"the exact route: which branch has it on the shelf NOW (copy-level), "
|
| 69 |
+
"free reservation steps, or borrow tonight on BorrowBox. "
|
| 70 |
+
"args: {\"query\": \"<title / author>\"}",
|
| 71 |
+
"fn": lambda a: ls.where_to_get(a.get("query") or a.get("title", ""))},
|
| 72 |
+
"hive_info": {
|
| 73 |
+
"desc": "The Hive β Worcester's library (joint university+public): hours "
|
| 74 |
+
"(8:30am-10pm daily), Explore the Past archives & archaeology, study "
|
| 75 |
+
"spaces, room hire, cafΓ©, getting there, children's library, business "
|
| 76 |
+
"support. Page-level detail. args: {\"topic\": \"<optional topic>\"}",
|
| 77 |
+
"fn": lambda a: ls.hive_info(a.get("topic") or a.get("query"))},
|
| 78 |
"whats_new": {
|
| 79 |
"desc": "Newest titles in a genre, for fun recommendations. "
|
| 80 |
"args: {\"genre\": \"<genre/topic>\"}",
|
|
|
|
| 132 |
if w in t)
|
| 133 |
if re.search(r"\b(print|printing|photocopy|photocopies|scan|copier)\b", t):
|
| 134 |
return "printing_help", {}
|
| 135 |
+
# The Hive / its extended offer (archives, archaeology, rooms, Worcester city)
|
| 136 |
+
if re.search(r"\b(the )?hive\b|archiv|archaeolog|explore the past|"
|
| 137 |
+
r"worcester city librar|hire (a |the )?(room|space)|"
|
| 138 |
+
r"(room|space) (for )?hire|book a (room|space|study)", t):
|
| 139 |
+
topic = re.sub(r"\b(the|hive|at|in|about|tell|me|what|can|i|do|you|"
|
| 140 |
+
r"know|is|are|there)\b", " ", t)
|
| 141 |
+
return "hive_info", {"topic": topic.strip(" ?.") or None}
|
| 142 |
+
# "how do I actually GET it" β the full per-copy journey
|
| 143 |
+
if (re.search(r"\b(where|how) (can|do|could|would) i (get|borrow|find|read|"
|
| 144 |
+
r"listen to)\b|\bget hold of\b|\bi want to (read|borrow)\b|"
|
| 145 |
+
r"\bis .{3,60} (available|in stock|on the shelf)\b|"
|
| 146 |
+
r"\b(nearest|closest) copy\b", t)
|
| 147 |
+
and not re.search(r"\bcard|member|join|sign ?up\b", t)):
|
| 148 |
+
q2 = re.sub(r"\b(where|how|can|do|could|would|i|get|borrow|find|read|"
|
| 149 |
+
r"listen|to|hold|of|want|is|available|in stock|on the shelf|"
|
| 150 |
+
r"a copy of|the book|nearest|closest|copy)\b", " ", t)
|
| 151 |
+
q2 = q2.strip(" ?.")
|
| 152 |
+
if q2:
|
| 153 |
+
return "where_to_get", {"query": q2}
|
| 154 |
if (re.search(r"\b(which|what) librar|librar(y|ies) (with|that has)\b", t)
|
| 155 |
or feat_hits >= 2 or "overall" in t):
|
| 156 |
return "graph_search", {"query": q}
|
|
|
|
| 161 |
r"facilit|where is|near me|study space)\b", t):
|
| 162 |
m = re.findall(r"\b([A-Z][a-z]+(?:[ -][A-Z][a-z]+)*)\b", q)
|
| 163 |
return "find_library", {"name": (m[-1] if m else "")}
|
| 164 |
+
if re.search(r"\b(unlocked|8pm|after hours|after work|out of hours|"
|
| 165 |
+
r"evening access|open late|get in (early|late))\b", t):
|
| 166 |
return "libraries_unlocked", {}
|
| 167 |
platform = re.search(r"\b(borrowbox|pressreader|ancestry|espacenet|ebsco|oxford|"
|
| 168 |
r"theory test|bfi|cobra|digital library|online (library )?hub)\b", t)
|
|
|
|
| 401 |
return "\n".join(out)
|
| 402 |
|
| 403 |
|
| 404 |
+
def render_where_to_get(r):
|
| 405 |
+
if not r.get("found"):
|
| 406 |
+
rt = r["routes"][0]
|
| 407 |
+
return (f"I couldn't find **{r['query']}** in the catalogue. {rt['advice']}\n\n"
|
| 408 |
+
f"π [Try the catalogue yourself]({r['search_url']})")
|
| 409 |
+
out = [f"**{r['best_title']}**" + (f" β {r['author']}" if r["author"] else "")
|
| 410 |
+
+ f" Β· held as: {', '.join(r['formats_held'])}\n"]
|
| 411 |
+
for rt in r["routes"]:
|
| 412 |
+
if rt["route"] == "digital":
|
| 413 |
+
link = ("[Open it in BorrowBox]" if rt["direct_link"]
|
| 414 |
+
else "[Open BorrowBox (Worcestershire)]")
|
| 415 |
+
out.append(f"π» **Borrow the {rt['format']} free tonight** β "
|
| 416 |
+
f"{link}({rt['url']})")
|
| 417 |
+
out += [f" {i}. {s}" for i, s in enumerate(rt["steps"], 1)]
|
| 418 |
+
out.append(f" - β
{rt['need']}")
|
| 419 |
+
elif rt["route"] == "shelf":
|
| 420 |
+
out.append(f"π **{rt['format']} β on the shelf right now at:**")
|
| 421 |
+
for c in rt["copies"]:
|
| 422 |
+
call = f" Β· shelf mark `{c['call_number']}`" if c["call_number"] else ""
|
| 423 |
+
out.append(f" - **{c['branch']}**{call} β _{c['status']}_")
|
| 424 |
+
out.append(f" - β
{rt['need']}")
|
| 425 |
+
out.append(f" - π [Item page / all copies]({rt['url']})")
|
| 426 |
+
elif rt["route"] == "reserve":
|
| 427 |
+
out.append(f"π **{rt['format']} β no copy on a shelf just now: "
|
| 428 |
+
f"reserve it free.**")
|
| 429 |
+
if rt.get("copies"):
|
| 430 |
+
spots = "; ".join(f"{c['branch']}: {c['status']}"
|
| 431 |
+
for c in rt["copies"][:5])
|
| 432 |
+
out.append(f" - Current copies β {spots}")
|
| 433 |
+
elif not rt.get("availability_parsed"):
|
| 434 |
+
out.append(" - _(I couldn't read live copy status this time β "
|
| 435 |
+
"the item page below shows it.)_")
|
| 436 |
+
out += [f" {i}. {s}" for i, s in enumerate(rt["steps"], 1)]
|
| 437 |
+
out.append(f" - β
{rt['need']}")
|
| 438 |
+
out.append(f" - π [Reserve here]({rt['url']})")
|
| 439 |
+
else:
|
| 440 |
+
out.append(f"β {rt.get('advice', '')}")
|
| 441 |
+
if r.get("other_matches"):
|
| 442 |
+
out.append(f"\n_Similar in the catalogue:_ {' Β· '.join(r['other_matches'])}")
|
| 443 |
+
out.append(f"\nπ [Full search results]({r['search_url']})")
|
| 444 |
+
return "\n".join(out)
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
def render_hive(r):
|
| 448 |
+
hours = r.get("opening_hours", "")
|
| 449 |
+
out = [f"π **{r['name']}** β Worcester's library, run jointly by the "
|
| 450 |
+
f"University of Worcester and Worcestershire County Council."]
|
| 451 |
+
if hours:
|
| 452 |
+
out.append(f"π **{hours}**")
|
| 453 |
+
if r.get("kind") == "overview":
|
| 454 |
+
out.append(f"{r.get('address', '')}\n")
|
| 455 |
+
out.append("**More than a branch:**")
|
| 456 |
+
for c in r.get("capabilities", [])[:9]:
|
| 457 |
+
if isinstance(c, dict):
|
| 458 |
+
src = f" β [source]({c['source']})" if c.get("source") else ""
|
| 459 |
+
out.append(f"- **{c.get('capability', '')}** β "
|
| 460 |
+
f"{c.get('detail', '')[:160]}{src}")
|
| 461 |
+
out.append(f"\nβ
{ls.ELIGIBILITY['hive_visit']}")
|
| 462 |
+
else:
|
| 463 |
+
for p in r.get("pages", []):
|
| 464 |
+
out.append(f"\n**[{p['title']}]({p['url']})** β {p['summary'][:180]}")
|
| 465 |
+
for o in p.get("offerings", [])[:6]:
|
| 466 |
+
out.append(f" - {o}")
|
| 467 |
+
for n in p.get("what_you_need", [])[:2]:
|
| 468 |
+
out.append(f" - β
{n}")
|
| 469 |
+
d = p.get("details", {})
|
| 470 |
+
for k, label in (("hours", "π"), ("prices", "π·"),
|
| 471 |
+
("location_in_building", "π"), ("contact", "βοΈ")):
|
| 472 |
+
if d.get(k):
|
| 473 |
+
out.append(f" - {label} {d[k][:140]}")
|
| 474 |
+
if p.get("notes"):
|
| 475 |
+
out.append(f" - β οΈ _{p['notes']}_")
|
| 476 |
+
for c in r.get("capabilities", [])[:3]:
|
| 477 |
+
if isinstance(c, dict):
|
| 478 |
+
out.append(f"\nπ **{c.get('capability', '')}** β "
|
| 479 |
+
f"{c.get('detail', '')[:160]}")
|
| 480 |
+
if not r.get("pages") and not r.get("capabilities"):
|
| 481 |
+
out.append("\nI don't have a Hive page on that β try the council "
|
| 482 |
+
f"pages, or browse [thehiveworcester.org]({r['page_url']}).")
|
| 483 |
+
as_of = (r.get("as_of") or "")[:10]
|
| 484 |
+
out.append(f"\n<sub>βΉοΈ From the Hive's own pages (crawled {as_of}). For live "
|
| 485 |
+
"events, membership and prices the council site is the "
|
| 486 |
+
"authority β ask me and I'll check it live.</sub>")
|
| 487 |
+
out.append(f"π [thehiveworcester.org]({r['page_url']})")
|
| 488 |
+
return "\n".join(out)
|
| 489 |
+
|
| 490 |
+
|
| 491 |
RENDER = {
|
| 492 |
"search_catalogue": render_catalogue, "whats_new": render_whats_new,
|
| 493 |
+
"where_to_get": render_where_to_get, "hive_info": render_hive,
|
| 494 |
"find_library": render_find_library, "mobile_library": render_mobile,
|
| 495 |
"library_events": render_events, "online_hub": render_online_hub,
|
| 496 |
"libraries_unlocked": render_unlocked, "membership_help": render_membership,
|
|
|
|
| 505 |
def value_receipt(tool, raw):
|
| 506 |
if tool in ("search_catalogue", "whats_new") and raw.get("items"):
|
| 507 |
return "π· _Borrowing instead of buying β **Β£9βοΏ½οΏ½20 saved** per title._"
|
| 508 |
+
if tool == "where_to_get" and raw.get("found"):
|
| 509 |
+
return "π· _Getting it from the library instead of buying β **Β£9βΒ£20 saved**._"
|
| 510 |
if tool == "online_hub":
|
| 511 |
return ("π· _Free with your card β a newspaper or eBook subscription is "
|
| 512 |
"**~Β£8βΒ£12/month** you don't pay._")
|
|
|
|
| 537 |
["Set up digital membership", "What's the difference?", "What can I borrow online?"]),
|
| 538 |
"graph_search": ("π‘ Tell me what matters (late, cafΓ©, study space) and I'll match a branch.",
|
| 539 |
["Late-opening + cafΓ©", "Find a book", "What's on this week?"]),
|
| 540 |
+
"where_to_get": ("π‘ Reservations are free β collect at any branch, or the van.",
|
| 541 |
+
["Is it on BorrowBox?", "Which branch is nearest?", "How do I join?"]),
|
| 542 |
+
"hive_info": ("π‘ The Hive is open 8:30amβ10pm every single day β and anyone can walk in.",
|
| 543 |
+
["The Hive's archives", "Hire a room at the Hive", "Is the Hive open now?"]),
|
| 544 |
}
|
| 545 |
+
HELP_CHIPS = ["How do I get Wolf Hall?", "What's at The Hive?",
|
| 546 |
"What's on this week?", "How do I print from my phone?"]
|
| 547 |
|
| 548 |
|
|
|
|
| 557 |
"data. Lead with what the person CAN do: if a service meets their need in a "
|
| 558 |
"different way (e.g. they can't print AT home, but can send a job from home "
|
| 559 |
"and collect it at any branch), open with that β never with a bare 'no'. "
|
| 560 |
+
"Keep any provenance footnote (e.g. 'From the Hive's own pages') intact. "
|
| 561 |
"If the data doesn't answer it, say so and point to the source link.")
|
| 562 |
|
| 563 |
|
|
|
|
| 585 |
HELP = (
|
| 586 |
"π I'm the **Worcestershire Libraries assistant**. I check the council site "
|
| 587 |
"and catalogue *live* and can help you:\n\n"
|
| 588 |
+
"- π **Get a specific book** β which branch has it on the shelf *right now*, "
|
| 589 |
+
"or borrow the eBook tonight\n"
|
| 590 |
"- π **Branch hours, 'open now?', toilets, parking**\n"
|
| 591 |
+
"- π **The Hive** (Worcester) β archives & archaeology, study spaces, room "
|
| 592 |
+
"hire, open 8:30amβ10pm daily\n"
|
| 593 |
"- π **Mobile library** times for your village\n"
|
| 594 |
"- π
**What's on** this week\n"
|
| 595 |
"- π» **Free online** β newspapers, magazines, family history\n"
|
| 596 |
"- π¨οΈ **Printing** from your phone\n\n"
|
| 597 |
+
"_Answers come from official sources β the council site and catalogue "
|
| 598 |
+
"checked live, plus every page of the Hive's own site β and each answer "
|
| 599 |
+
"names its source._")
|
| 600 |
|
| 601 |
|
| 602 |
# --------------------------------------------------------------------------- #
|
|
|
|
| 641 |
value = value_receipt(tool, raw)
|
| 642 |
nudge, chips = NUDGES.get(tool, ("", []))
|
| 643 |
checked = raw.get("checked", "")
|
| 644 |
+
label = ("page-level KB" if tool == "hive_info" and raw.get("as_of")
|
| 645 |
+
else "**live**")
|
| 646 |
+
footer = (f"\n\n<sub>π Checked {label}"
|
| 647 |
+ (f" Β· {checked}" if checked else "")
|
| 648 |
+ f" Β· tool `{tool}` ({how})"
|
| 649 |
+ (f" Β· [source]({source})" if source else "") + "</sub>")
|
|
|
|
| 683 |
with gr.Blocks(title="Worcestershire Libraries β Live Assistant") as demo:
|
| 684 |
gr.HTML("<div id='hero'><span class='live'>β live data</span>"
|
| 685 |
"<h1>Worcestershire Libraries β Live Assistant</h1>"
|
| 686 |
+
"<p>Exactly where to get the book you want, everything The Hive "
|
| 687 |
+
"offers, mobile library, events, printing and what's free online β "
|
| 688 |
+
"from the council site, catalogue & every Hive page, with exactly "
|
| 689 |
+
"what you need to sign up.</p></div>")
|
| 690 |
|
| 691 |
chat = gr.Chatbot(height=460, show_label=False,
|
| 692 |
placeholder="π Ask me anything about your local libraryβ¦",
|
|
|
|
| 700 |
for _ in range(3)]
|
| 701 |
|
| 702 |
gr.Examples(
|
| 703 |
+
["How do I get Wolf Hall by Hilary Mantel?",
|
| 704 |
+
"Do you have Harry Potter audiobooks?",
|
| 705 |
+
"What can I do at The Hive?",
|
| 706 |
+
"Tell me about the archives at the Hive",
|
| 707 |
"Is Malvern library open now?",
|
| 708 |
"A late-opening library with a cafΓ© and meeting rooms",
|
| 709 |
"When does the mobile library visit Abberley?",
|
| 710 |
+
"Can I read newspapers for free?"],
|
|
|
|
| 711 |
inputs=box, label="Try one")
|
| 712 |
|
| 713 |
if not HF_TOKEN:
|
build_hive_kb.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
build_hive_kb.py β crawl EVERY page of thehiveworcester.org into hive_kb.json.
|
| 3 |
+
|
| 4 |
+
The Hive is Worcester city's library β Europe's first joint university + public
|
| 5 |
+
library β and its website is the only place much of its extended offer is
|
| 6 |
+
described (Explore the Past archives & archaeology, 800+ study spaces, room
|
| 7 |
+
hire, BIPC business support, the Youth Hub...). We crawl it page-by-page and
|
| 8 |
+
record the EXACT offering of each page, with provenance on every fact.
|
| 9 |
+
|
| 10 |
+
Provenance policy (the re-think of our old "never touch the Hive site" rule):
|
| 11 |
+
β’ every page object carries its source URL + last_seen date
|
| 12 |
+
β’ whats-on.html is flagged static/under-construction by the site itself β
|
| 13 |
+
live events still come from worcestershire.gov.uk
|
| 14 |
+
β’ where the Hive site and the council site conflict (hours, prices,
|
| 15 |
+
membership), the COUNCIL page wins β answers must say which source they used
|
| 16 |
+
|
| 17 |
+
Output: hive_kb.json
|
| 18 |
+
{generated, source, note, page_count, hive_profile, pages: [...]}
|
| 19 |
+
|
| 20 |
+
Each page: {url, title, section, summary, offerings[], what_you_need[],
|
| 21 |
+
details{hours,prices,contact,location_in_building}, links_out[],
|
| 22 |
+
last_seen}
|
| 23 |
+
|
| 24 |
+
`hive_profile.extended_capabilities` is a CURATED list (capability, detail,
|
| 25 |
+
source) β re-crawling refreshes every page but PRESERVES the curated list from
|
| 26 |
+
the existing hive_kb.json unless you pass --rebuild-capabilities, in which case
|
| 27 |
+
a best-effort automatic list is generated for you to re-curate.
|
| 28 |
+
|
| 29 |
+
Run: python build_hive_kb.py (refresh pages, keep curated profile)
|
| 30 |
+
python build_hive_kb.py --rebuild-capabilities
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
from __future__ import annotations
|
| 34 |
+
|
| 35 |
+
import json
|
| 36 |
+
import re
|
| 37 |
+
import sys
|
| 38 |
+
import time
|
| 39 |
+
from datetime import datetime, timezone, date
|
| 40 |
+
from urllib.parse import urljoin, urlparse
|
| 41 |
+
|
| 42 |
+
import requests
|
| 43 |
+
from bs4 import BeautifulSoup
|
| 44 |
+
|
| 45 |
+
HIVE = "https://www.thehiveworcester.org"
|
| 46 |
+
OUT = "hive_kb.json"
|
| 47 |
+
HEADERS = {"User-Agent": "WorcsLibrariesKB/3.0 (Hive pages; charitable project)"}
|
| 48 |
+
TIMEOUT = 20
|
| 49 |
+
DELAY = 0.3
|
| 50 |
+
MAX_PAGES = 90
|
| 51 |
+
|
| 52 |
+
SEEDS = [
|
| 53 |
+
"/", "/collections.html", "/services.html", "/whats-on.html",
|
| 54 |
+
"/plan-your-visit.html", "/learn.html", "/about-the-hive.html",
|
| 55 |
+
"/opening-hours.html", "/getting-here.html", "/contact-us.html",
|
| 56 |
+
"/fines-renewals.html", "/how-to-find-things.html",
|
| 57 |
+
"/borrowing-reservations.html", "/how-to-get-a-library-card.html",
|
| 58 |
+
"/explore-the-past.html", "/book-a-space.html", "/space-for-hire.html",
|
| 59 |
+
"/catalogue-search.html",
|
| 60 |
+
]
|
| 61 |
+
|
| 62 |
+
# URL slug -> nav section. Anything unmatched inherits from its referrer.
|
| 63 |
+
SECTION_HINTS = {
|
| 64 |
+
"collections": "collections", "catalogue": "collections",
|
| 65 |
+
"services": "services", "printing": "services", "computer": "services",
|
| 66 |
+
"whats-on": "whats-on", "event": "whats-on",
|
| 67 |
+
"plan-your-visit": "plan-your-visit", "getting-here": "plan-your-visit",
|
| 68 |
+
"opening-hours": "plan-your-visit", "cafe": "plan-your-visit",
|
| 69 |
+
"learn": "learn", "school": "learn", "student": "learn", "youth": "learn",
|
| 70 |
+
"about": "about", "contact": "about", "facts": "about", "creating": "about",
|
| 71 |
+
"library-card": "using-the-library", "borrowing": "using-the-library",
|
| 72 |
+
"fines": "using-the-library", "using-the-library": "using-the-library",
|
| 73 |
+
"how-to-find": "using-the-library", "explore-the-past": "learn",
|
| 74 |
+
"book-a-space": "plan-your-visit", "space-for-hire": "plan-your-visit",
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
NEED_TRIGGERS = re.compile(
|
| 78 |
+
r"\b(you('| wi)?ll need|you need|to (join|use|access|book|register|sign ?up)|"
|
| 79 |
+
r"who can|available to|free (to|for)|eligible|membership|library card|"
|
| 80 |
+
r"\bPIN\b|register|sign ?up|aged \d+|bring|photo(graphic)? ID|proof of)\b", re.I)
|
| 81 |
+
|
| 82 |
+
HOURS_RE = re.compile(
|
| 83 |
+
r"((?:open\s+)?(?:from\s+)?\d{1,2}[:.]\d{2}\s*(?:am|pm)?\s*(?:-|to|β)\s*"
|
| 84 |
+
r"\d{1,2}(?:[:.]\d{2})?\s*(?:am|pm)[^.]{0,60})", re.I)
|
| 85 |
+
PRICE_RE = re.compile(r"((?:Β£\d+(?:\.\d{2})?|\d+p)\b[^.]{0,80})")
|
| 86 |
+
PHONE_RE = re.compile(r"(0\d{4}\s?\d{6}|\d{5}\s\d{6})")
|
| 87 |
+
EMAIL_RE = re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+")
|
| 88 |
+
LEVEL_RE = re.compile(r"\b([Ll]evel \d)\b")
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def canon(url: str) -> str:
|
| 92 |
+
url = urljoin(HIVE + "/", url).split("#")[0].split("?")[0]
|
| 93 |
+
url = url.replace("http://", "https://").replace("://thehive", "://www.thehive")
|
| 94 |
+
if url.rstrip("/") in (HIVE, HIVE + "/index.html"):
|
| 95 |
+
return HIVE + "/"
|
| 96 |
+
return url
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def is_internal_page(url: str) -> bool:
|
| 100 |
+
p = urlparse(url)
|
| 101 |
+
return (p.netloc == "www.thehiveworcester.org"
|
| 102 |
+
and (p.path.endswith(".html") or p.path in ("", "/")))
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def section_of(url: str, referrer_section: str) -> str:
|
| 106 |
+
slug = urlparse(url).path.lower()
|
| 107 |
+
for hint, sec in SECTION_HINTS.items():
|
| 108 |
+
if hint in slug:
|
| 109 |
+
return sec
|
| 110 |
+
return referrer_section or "other"
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def main_text(soup: BeautifulSoup):
|
| 114 |
+
main = (soup.find("main") or soup.find(id="content")
|
| 115 |
+
or soup.find(class_="container") or soup.body or soup)
|
| 116 |
+
for bad in main.select("nav, header, footer, script, style, .navbar, .carousel"):
|
| 117 |
+
bad.decompose()
|
| 118 |
+
return main
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def extract_page(url: str, html: str, referrer_section: str) -> dict:
|
| 122 |
+
soup = BeautifulSoup(html, "html.parser")
|
| 123 |
+
main = main_text(soup)
|
| 124 |
+
h1 = main.find(["h1", "h2"])
|
| 125 |
+
title = (h1.get_text(" ", strip=True) if h1 else
|
| 126 |
+
(soup.title.get_text(strip=True).strip(" -") if soup.title else url))
|
| 127 |
+
|
| 128 |
+
paras = [re.sub(r"\s+", " ", p.get_text(" ", strip=True))
|
| 129 |
+
for p in main.find_all("p")]
|
| 130 |
+
paras = [p for p in paras if len(p) > 30]
|
| 131 |
+
summary = paras[0][:300] if paras else ""
|
| 132 |
+
|
| 133 |
+
# EXACT offerings: headings + list items + lead sentences, near-verbatim.
|
| 134 |
+
offerings, seen = [], set()
|
| 135 |
+
for el in main.find_all(["h2", "h3", "h4", "li"]):
|
| 136 |
+
t = re.sub(r"\s+", " ", el.get_text(" ", strip=True))
|
| 137 |
+
if 4 < len(t) <= 200 and t.lower() not in seen:
|
| 138 |
+
seen.add(t.lower())
|
| 139 |
+
offerings.append(t)
|
| 140 |
+
for p in paras[:6]:
|
| 141 |
+
first = p.split(". ")[0].strip()
|
| 142 |
+
if 15 < len(first) <= 200 and first.lower() not in seen:
|
| 143 |
+
seen.add(first.lower())
|
| 144 |
+
offerings.append(first)
|
| 145 |
+
|
| 146 |
+
blob = " ".join(paras)
|
| 147 |
+
need = []
|
| 148 |
+
for m in re.finditer(r"[^.!?]*[.!?]", blob):
|
| 149 |
+
seg = m.group(0).strip()
|
| 150 |
+
if 15 <= len(seg) <= 220 and NEED_TRIGGERS.search(seg):
|
| 151 |
+
if seg.lower() not in (n.lower() for n in need):
|
| 152 |
+
need.append(seg)
|
| 153 |
+
if len(need) >= 5:
|
| 154 |
+
break
|
| 155 |
+
|
| 156 |
+
details = {}
|
| 157 |
+
if (m := HOURS_RE.search(blob)):
|
| 158 |
+
details["hours"] = m.group(1).strip()
|
| 159 |
+
prices = PRICE_RE.findall(blob)[:6]
|
| 160 |
+
if prices:
|
| 161 |
+
details["prices"] = "; ".join(p.strip() for p in prices)
|
| 162 |
+
contact = PHONE_RE.findall(blob)[:2] + EMAIL_RE.findall(blob)[:3]
|
| 163 |
+
if contact:
|
| 164 |
+
details["contact"] = ", ".join(dict.fromkeys(contact))
|
| 165 |
+
if (m := LEVEL_RE.search(blob)):
|
| 166 |
+
details["location_in_building"] = m.group(1)
|
| 167 |
+
|
| 168 |
+
links_out = sorted({a["href"] for a in main.find_all("a", href=True)
|
| 169 |
+
if a["href"].startswith("http")
|
| 170 |
+
and "thehiveworcester.org" not in a["href"]})[:15]
|
| 171 |
+
|
| 172 |
+
page = {"url": url, "title": title,
|
| 173 |
+
"section": section_of(url, referrer_section),
|
| 174 |
+
"summary": summary, "offerings": offerings[:30],
|
| 175 |
+
"last_seen": date.today().isoformat()}
|
| 176 |
+
if need:
|
| 177 |
+
page["what_you_need"] = need
|
| 178 |
+
if details:
|
| 179 |
+
page["details"] = details
|
| 180 |
+
if links_out:
|
| 181 |
+
page["links_out"] = links_out
|
| 182 |
+
if "whats-on" in url:
|
| 183 |
+
page["notes"] = ("Site flags this page as static/under construction β "
|
| 184 |
+
"use worcestershire.gov.uk for live events.")
|
| 185 |
+
return page, soup
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def auto_capabilities(pages: list[dict]) -> list[dict]:
|
| 189 |
+
"""Best-effort automatic capability list (for --rebuild-capabilities)."""
|
| 190 |
+
KEY = ("archive", "archaeolog", "study space", "business", "youth",
|
| 191 |
+
"children", "room", "hire", "university", "special collection",
|
| 192 |
+
"research", "cafΓ©", "cafe", "catalogue")
|
| 193 |
+
out = []
|
| 194 |
+
for p in pages:
|
| 195 |
+
hits = [o for o in p.get("offerings", [])
|
| 196 |
+
if any(k in o.lower() for k in KEY)]
|
| 197 |
+
for h in hits[:2]:
|
| 198 |
+
out.append({"capability": p["title"], "detail": h, "source": p["url"]})
|
| 199 |
+
return out[:40]
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def main():
|
| 203 |
+
rebuild_caps = "--rebuild-capabilities" in sys.argv
|
| 204 |
+
|
| 205 |
+
# preserve the curated profile unless told otherwise
|
| 206 |
+
old_profile = {}
|
| 207 |
+
try:
|
| 208 |
+
with open(OUT, encoding="utf-8") as f:
|
| 209 |
+
old_profile = json.load(f).get("hive_profile", {})
|
| 210 |
+
except FileNotFoundError:
|
| 211 |
+
pass
|
| 212 |
+
|
| 213 |
+
queue = [(canon(s), "other") for s in SEEDS]
|
| 214 |
+
done: dict[str, dict] = {}
|
| 215 |
+
while queue and len(done) < MAX_PAGES:
|
| 216 |
+
url, ref_sec = queue.pop(0)
|
| 217 |
+
if url in done:
|
| 218 |
+
continue
|
| 219 |
+
try:
|
| 220 |
+
r = requests.get(url, headers=HEADERS, timeout=TIMEOUT)
|
| 221 |
+
r.raise_for_status()
|
| 222 |
+
except Exception as e:
|
| 223 |
+
done[url] = {"url": url, "error": str(e),
|
| 224 |
+
"last_seen": date.today().isoformat()}
|
| 225 |
+
continue
|
| 226 |
+
page, soup = extract_page(url, r.text, ref_sec)
|
| 227 |
+
done[url] = page
|
| 228 |
+
for a in soup.find_all("a", href=True):
|
| 229 |
+
u = canon(a["href"])
|
| 230 |
+
if is_internal_page(u) and u not in done:
|
| 231 |
+
queue.append((u, page["section"]))
|
| 232 |
+
if len(done) % 10 == 0:
|
| 233 |
+
print(f" ...{len(done)} pages")
|
| 234 |
+
time.sleep(DELAY)
|
| 235 |
+
|
| 236 |
+
pages = sorted(done.values(), key=lambda p: p["url"])
|
| 237 |
+
profile = old_profile if (old_profile and not rebuild_caps) else {
|
| 238 |
+
"name": "The Hive, Worcester",
|
| 239 |
+
"extended_capabilities": auto_capabilities(pages),
|
| 240 |
+
"note": "Auto-generated β please re-curate against the pages.",
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
out = {
|
| 244 |
+
"generated": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
| 245 |
+
"source": HIVE,
|
| 246 |
+
"method": "requests BFS crawl, every internal .html page",
|
| 247 |
+
"note": ("Built page-by-page from thehiveworcester.org. Every fact "
|
| 248 |
+
"carries its source page. Where the Hive site and "
|
| 249 |
+
"worcestershire.gov.uk conflict (hours, prices, membership), "
|
| 250 |
+
"the council page wins; the site itself flags whats-on.html "
|
| 251 |
+
"as static/under-construction."),
|
| 252 |
+
"page_count": len(pages),
|
| 253 |
+
"hive_profile": profile,
|
| 254 |
+
"pages": pages,
|
| 255 |
+
}
|
| 256 |
+
with open(OUT, "w", encoding="utf-8") as f:
|
| 257 |
+
json.dump(out, f, indent=2, ensure_ascii=False)
|
| 258 |
+
print(f"wrote {OUT}: {len(pages)} pages, "
|
| 259 |
+
f"{len(profile.get('extended_capabilities', []))} capabilities "
|
| 260 |
+
f"({'auto' if rebuild_caps or not old_profile else 'curated, preserved'})")
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
if __name__ == "__main__":
|
| 264 |
+
main()
|
graph_build.py
CHANGED
|
@@ -29,9 +29,26 @@ except Exception:
|
|
| 29 |
CURATED_HUB = {}
|
| 30 |
|
| 31 |
KB_PATH = "library_kb.json"
|
|
|
|
| 32 |
OUT_PATH = "library_graph.json"
|
| 33 |
GOV = "https://www.worcestershire.gov.uk"
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
POSTCODE = re.compile(r"[A-Z]{1,2}\d[A-Z\d]?\s*\d[A-Z]{2}")
|
| 36 |
|
| 37 |
|
|
@@ -117,17 +134,76 @@ def build_graph(kb: dict) -> nx.Graph:
|
|
| 117 |
if f"tier::{tier}" in G:
|
| 118 |
G.add_edge(rid, f"tier::{tier}", rel="REQUIRES")
|
| 119 |
|
| 120 |
-
# --- Mobile library villages (
|
| 121 |
mob = add("service::Mobile library", "Service", "Mobile library",
|
| 122 |
category="access",
|
| 123 |
url=f"{GOV}/council-services/libraries/your-library-membership/mobile-library")
|
|
|
|
| 124 |
try:
|
| 125 |
from library_sources import _village_index
|
| 126 |
-
|
| 127 |
-
vid = add(f"village::{name}", "Village", name.title(), url=url)
|
| 128 |
-
G.add_edge(vid, mob, rel="SERVED_BY")
|
| 129 |
except Exception as e:
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
|
| 132 |
return G
|
| 133 |
|
|
@@ -166,7 +242,7 @@ def main():
|
|
| 166 |
|
| 167 |
out = {
|
| 168 |
"generated": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
| 169 |
-
"built_from": KB_PATH,
|
| 170 |
"method": "deterministic GraphRAG-style graph (entities->relationships->"
|
| 171 |
"communities->reports); inspired by microsoft/graphrag",
|
| 172 |
"stats": {"nodes": G.number_of_nodes(), "edges": G.number_of_edges(),
|
|
|
|
| 29 |
CURATED_HUB = {}
|
| 30 |
|
| 31 |
KB_PATH = "library_kb.json"
|
| 32 |
+
HIVE_KB_PATH = "hive_kb.json"
|
| 33 |
OUT_PATH = "library_graph.json"
|
| 34 |
GOV = "https://www.worcestershire.gov.uk"
|
| 35 |
|
| 36 |
+
# Facility signals scanned across the Hive pages (same idea as build_kb.py).
|
| 37 |
+
HIVE_FACILITY_VOCAB = {
|
| 38 |
+
"cafΓ©": r"\bcaf[eΓ©]\b",
|
| 39 |
+
"meeting rooms": r"\bmeeting rooms?|rooms? (for|to) hire|space for hire\b",
|
| 40 |
+
"study space": r"\bstudy (space|area|room)\b",
|
| 41 |
+
"free Wi-Fi": r"\bwi-?fi\b",
|
| 42 |
+
"public computers": r"\b(public )?computers?\b",
|
| 43 |
+
"printing": r"\bprint(ing)?|photocopy",
|
| 44 |
+
"public toilets": r"\btoilets?\b",
|
| 45 |
+
"baby changing": r"\bbaby chang",
|
| 46 |
+
"wheelchair access": r"\bwheelchair|step-?free|level access|accessib",
|
| 47 |
+
"archives": r"\barchives?\b",
|
| 48 |
+
"children's library": r"\bchildren'?s (library|area)\b",
|
| 49 |
+
"exhibition space": r"\bexhibition\b",
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
POSTCODE = re.compile(r"[A-Z]{1,2}\d[A-Z\d]?\s*\d[A-Z]{2}")
|
| 53 |
|
| 54 |
|
|
|
|
| 134 |
if f"tier::{tier}" in G:
|
| 135 |
G.add_edge(rid, f"tier::{tier}", rel="REQUIRES")
|
| 136 |
|
| 137 |
+
# --- Mobile library villages (live fetch; falls back to the last graph) ---
|
| 138 |
mob = add("service::Mobile library", "Service", "Mobile library",
|
| 139 |
category="access",
|
| 140 |
url=f"{GOV}/council-services/libraries/your-library-membership/mobile-library")
|
| 141 |
+
villages: dict[str, str] = {}
|
| 142 |
try:
|
| 143 |
from library_sources import _village_index
|
| 144 |
+
villages = dict(_village_index())
|
|
|
|
|
|
|
| 145 |
except Exception as e:
|
| 146 |
+
try: # offline rebuild: keep the villages from the previous graph
|
| 147 |
+
with open(OUT_PATH, encoding="utf-8") as f:
|
| 148 |
+
for n in json.load(f).get("nodes", []):
|
| 149 |
+
if n.get("type") == "Village":
|
| 150 |
+
villages[n["label"].lower()] = n.get("url", "")
|
| 151 |
+
print(f" (mobile villages: live fetch failed [{e}]; "
|
| 152 |
+
f"kept {len(villages)} from previous graph)")
|
| 153 |
+
except Exception:
|
| 154 |
+
print(f" (mobile villages skipped: {e})")
|
| 155 |
+
for name, url in villages.items():
|
| 156 |
+
vid = add(f"village::{name}", "Village", name.title(), url=url)
|
| 157 |
+
G.add_edge(vid, mob, rel="SERVED_BY")
|
| 158 |
+
|
| 159 |
+
# --- The Hive: page-level KB -> enrich the branch + capability nodes ---
|
| 160 |
+
try:
|
| 161 |
+
with open(HIVE_KB_PATH, encoding="utf-8") as f:
|
| 162 |
+
hive = json.load(f)
|
| 163 |
+
except FileNotFoundError:
|
| 164 |
+
hive = {}
|
| 165 |
+
if hive:
|
| 166 |
+
prof = hive.get("hive_profile", {})
|
| 167 |
+
pages = [p for p in hive.get("pages", []) if not p.get("error")]
|
| 168 |
+
hive_id = next((n for n in G.nodes
|
| 169 |
+
if n.startswith("branch::") and "hive" in n.lower()), None)
|
| 170 |
+
blob = " ".join(o for p in pages for o in p.get("offerings", []))
|
| 171 |
+
facilities = [name for name, pat in HIVE_FACILITY_VOCAB.items()
|
| 172 |
+
if re.search(pat, blob, re.I)]
|
| 173 |
+
hours = (prof.get("opening_hours") or {}).get("building", "")
|
| 174 |
+
attrs = dict(
|
| 175 |
+
open_late=True, # 8:30amβ10pm, seven days β later than Unlocked
|
| 176 |
+
hive_hours=hours, source="thehiveworcester.org",
|
| 177 |
+
partnership=prof.get("partnership", "")[:200],
|
| 178 |
+
summary="Worcester city's library β Europe's first joint university "
|
| 179 |
+
"+ public library, open 8:30amβ10pm every day.",
|
| 180 |
+
)
|
| 181 |
+
if hive_id: # enrich the council-crawled branch node
|
| 182 |
+
G.nodes[hive_id].update(attrs)
|
| 183 |
+
facs = set(G.nodes[hive_id].get("facilities", [])) | set(facilities)
|
| 184 |
+
G.nodes[hive_id]["facilities"] = sorted(facs)
|
| 185 |
+
for fac in facs:
|
| 186 |
+
fid = add(f"facility::{fac}", "Facility", fac)
|
| 187 |
+
G.add_edge(hive_id, fid, rel="HAS_FACILITY")
|
| 188 |
+
else:
|
| 189 |
+
hive_id = add("branch::The Hive", "Branch", "The Hive",
|
| 190 |
+
address=prof.get("address", ""), facilities=facilities,
|
| 191 |
+
url="https://www.thehiveworcester.org", **attrs)
|
| 192 |
+
for fac in facilities:
|
| 193 |
+
fid = add(f"facility::{fac}", "Facility", fac)
|
| 194 |
+
G.add_edge(hive_id, fid, rel="HAS_FACILITY")
|
| 195 |
+
aid = add("area::Worcester", "Area", "Worcester")
|
| 196 |
+
G.add_edge(hive_id, aid, rel="LOCATED_IN")
|
| 197 |
+
for cap in prof.get("extended_capabilities", []):
|
| 198 |
+
if not isinstance(cap, dict):
|
| 199 |
+
continue
|
| 200 |
+
label = cap.get("capability", "")[:80]
|
| 201 |
+
if not label:
|
| 202 |
+
continue
|
| 203 |
+
cid = add(f"hive::{label}", "HiveService", label,
|
| 204 |
+
summary=cap.get("detail", ""), url=cap.get("source", ""),
|
| 205 |
+
source="thehiveworcester.org")
|
| 206 |
+
G.add_edge(hive_id, cid, rel="OFFERS")
|
| 207 |
|
| 208 |
return G
|
| 209 |
|
|
|
|
| 242 |
|
| 243 |
out = {
|
| 244 |
"generated": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
| 245 |
+
"built_from": f"{KB_PATH} + {HIVE_KB_PATH}",
|
| 246 |
"method": "deterministic GraphRAG-style graph (entities->relationships->"
|
| 247 |
"communities->reports); inspired by microsoft/graphrag",
|
| 248 |
"stats": {"nodes": G.number_of_nodes(), "edges": G.number_of_edges(),
|
graph_rag.py
CHANGED
|
@@ -29,6 +29,8 @@ FACILITY_TERMS = {
|
|
| 29 |
"wheelchair": "wheelchair", "accessible": "accessible", "disabled": "accessible",
|
| 30 |
"meeting room": "meeting", "meeting": "meeting", "print": "printing",
|
| 31 |
"photocopy": "printing", "self-service": "self",
|
|
|
|
|
|
|
| 32 |
}
|
| 33 |
LATE_TERMS = ["late", "unlocked", "8pm", "evening", "after work", "after hours",
|
| 34 |
"open late", "out of hours"]
|
|
@@ -80,7 +82,7 @@ def local_search(query: str) -> dict:
|
|
| 80 |
if wanted or want_late or (area and "librar" in q):
|
| 81 |
results = []
|
| 82 |
for b in g["by_type"].get("Branch", []):
|
| 83 |
-
if want_late and not b.get("libraries_unlocked"):
|
| 84 |
continue
|
| 85 |
if area and area.lower() not in (b.get("address", "")).lower():
|
| 86 |
continue
|
|
@@ -93,6 +95,8 @@ def local_search(query: str) -> dict:
|
|
| 93 |
"late": want_late, "area": area,
|
| 94 |
"branches": [{"name": b["label"], "facilities": b.get("facilities", []),
|
| 95 |
"libraries_unlocked": b.get("libraries_unlocked", False),
|
|
|
|
|
|
|
| 96 |
"address": b.get("address", ""), "url": b.get("url", "")}
|
| 97 |
for b in results],
|
| 98 |
"count": len(results),
|
|
|
|
| 29 |
"wheelchair": "wheelchair", "accessible": "accessible", "disabled": "accessible",
|
| 30 |
"meeting room": "meeting", "meeting": "meeting", "print": "printing",
|
| 31 |
"photocopy": "printing", "self-service": "self",
|
| 32 |
+
"archive": "archive", "archives": "archive", "archaeology": "archive",
|
| 33 |
+
"children": "child", "kids": "child", "exhibition": "exhibition",
|
| 34 |
}
|
| 35 |
LATE_TERMS = ["late", "unlocked", "8pm", "evening", "after work", "after hours",
|
| 36 |
"open late", "out of hours"]
|
|
|
|
| 82 |
if wanted or want_late or (area and "librar" in q):
|
| 83 |
results = []
|
| 84 |
for b in g["by_type"].get("Branch", []):
|
| 85 |
+
if want_late and not (b.get("libraries_unlocked") or b.get("open_late")):
|
| 86 |
continue
|
| 87 |
if area and area.lower() not in (b.get("address", "")).lower():
|
| 88 |
continue
|
|
|
|
| 95 |
"late": want_late, "area": area,
|
| 96 |
"branches": [{"name": b["label"], "facilities": b.get("facilities", []),
|
| 97 |
"libraries_unlocked": b.get("libraries_unlocked", False),
|
| 98 |
+
"open_late": b.get("open_late", False),
|
| 99 |
+
"late_hours": b.get("hive_hours", ""),
|
| 100 |
"address": b.get("address", ""), "url": b.get("url", "")}
|
| 101 |
for b in results],
|
| 102 |
"count": len(results),
|
hive_kb.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
index.html
CHANGED
|
@@ -74,9 +74,10 @@
|
|
| 74 |
<header>
|
| 75 |
<span class="live">β live data</span>
|
| 76 |
<h1>Worcestershire Libraries</h1>
|
| 77 |
-
<p>
|
| 78 |
-
|
| 79 |
-
|
|
|
|
| 80 |
</header>
|
| 81 |
|
| 82 |
<main id="log">
|
|
@@ -86,19 +87,19 @@
|
|
| 86 |
</main>
|
| 87 |
|
| 88 |
<div class="chips" id="chips">
|
|
|
|
|
|
|
| 89 |
<div class="chip">Is Malvern library open now?</div>
|
| 90 |
<div class="chip">A late-opening library with a cafΓ© and meeting rooms</div>
|
| 91 |
<div class="chip">Can I read newspapers for free?</div>
|
| 92 |
-
<div class="chip">When does the mobile library visit Abberley?</div>
|
| 93 |
-
<div class="chip">What do I need to sign up?</div>
|
| 94 |
</div>
|
| 95 |
|
| 96 |
<form id="form" autocomplete="off">
|
| 97 |
<input id="box" placeholder="Ask anything about your local libraryβ¦" autofocus />
|
| 98 |
<button id="send" type="submit">Ask</button>
|
| 99 |
</form>
|
| 100 |
-
<footer>
|
| 101 |
-
runs on a β€32B model</footer>
|
| 102 |
</div>
|
| 103 |
|
| 104 |
<script type="module">
|
|
|
|
| 74 |
<header>
|
| 75 |
<span class="live">β live data</span>
|
| 76 |
<h1>Worcestershire Libraries</h1>
|
| 77 |
+
<p>Exactly where to get the book you want, everything The Hive offers,
|
| 78 |
+
the mobile library, events, printing and what's free online β from the
|
| 79 |
+
council site, catalogue & every Hive page, with exactly what you
|
| 80 |
+
need to sign up.</p>
|
| 81 |
</header>
|
| 82 |
|
| 83 |
<main id="log">
|
|
|
|
| 87 |
</main>
|
| 88 |
|
| 89 |
<div class="chips" id="chips">
|
| 90 |
+
<div class="chip">How do I get Wolf Hall?</div>
|
| 91 |
+
<div class="chip">What can I do at The Hive?</div>
|
| 92 |
<div class="chip">Is Malvern library open now?</div>
|
| 93 |
<div class="chip">A late-opening library with a cafΓ© and meeting rooms</div>
|
| 94 |
<div class="chip">Can I read newspapers for free?</div>
|
|
|
|
|
|
|
| 95 |
</div>
|
| 96 |
|
| 97 |
<form id="form" autocomplete="off">
|
| 98 |
<input id="box" placeholder="Ask anything about your local libraryβ¦" autofocus />
|
| 99 |
<button id="send" type="submit">Ask</button>
|
| 100 |
</form>
|
| 101 |
+
<footer>Official sources only β council site & catalogue checked live,
|
| 102 |
+
plus every page of the Hive's own site Β· runs on a β€32B model</footer>
|
| 103 |
</div>
|
| 104 |
|
| 105 |
<script type="module">
|
library_graph.json
CHANGED
|
@@ -1,21 +1,22 @@
|
|
| 1 |
{
|
| 2 |
-
"generated": "2026-06-
|
| 3 |
-
"built_from": "library_kb.json",
|
| 4 |
"method": "deterministic GraphRAG-style graph (entities->relationships->communities->reports); inspired by microsoft/graphrag",
|
| 5 |
"stats": {
|
| 6 |
-
"nodes":
|
| 7 |
-
"edges":
|
| 8 |
-
"communities":
|
| 9 |
"node_types": {
|
| 10 |
"Membership": 3,
|
| 11 |
"Hub": 1,
|
| 12 |
"Branch": 23,
|
| 13 |
-
"Facility":
|
| 14 |
"Area": 17,
|
| 15 |
"Service": 87,
|
| 16 |
"Topic": 7,
|
| 17 |
"Resource": 17,
|
| 18 |
-
"Village": 154
|
|
|
|
| 19 |
}
|
| 20 |
},
|
| 21 |
"nodes": [
|
|
@@ -875,9 +876,26 @@
|
|
| 875 |
"staffed": "8:30am to 10:00pm"
|
| 876 |
}
|
| 877 |
},
|
| 878 |
-
"facilities": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 879 |
"libraries_unlocked": false,
|
| 880 |
-
"url": "https://www.worcestershire.gov.uk/council-services/libraries/find-library/hive"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 881 |
},
|
| 882 |
{
|
| 883 |
"id": "branch::Upton-Upon-Severn Library",
|
|
@@ -2152,7 +2170,7 @@
|
|
| 2152 |
"summary": "Free eBooks & eAudiobooks β latest titles, award winners, non-fiction, classics and children's, plus the curated 'Worcestershire Reads' picks.",
|
| 2153 |
"what_you_need": "Library card number + PIN; choose 'Worcestershire' in the app.",
|
| 2154 |
"access": [
|
| 2155 |
-
"Download the BorrowBox app.",
|
| 2156 |
"Select 'Worcestershire' as your library service.",
|
| 2157 |
"Sign in with your card number + PIN."
|
| 2158 |
],
|
|
@@ -2277,7 +2295,7 @@
|
|
| 2277 |
"summary": "7,000+ full-page newspapers & magazines, 60+ languages, 120+ countries.",
|
| 2278 |
"what_you_need": "Worcestershire library card number + PIN (free full membership, residents). No separate PressReader account needed.",
|
| 2279 |
"access": [
|
| 2280 |
-
"Get the PressReader app (or
|
| 2281 |
"Tap 'Libraries & Groups' and search/select 'Worcestershire'.",
|
| 2282 |
"Sign in with your library card number + PIN.",
|
| 2283 |
"At home you get a 30-day pass (re-confirm monthly); on library Wi-Fi it's 7 days."
|
|
@@ -3252,6 +3270,229 @@
|
|
| 3252 |
"type": "Village",
|
| 3253 |
"label": "Wychbold",
|
| 3254 |
"url": "https://www.worcestershire.gov.uk/council-services/worcestershire-libraries/your-library-membership/mobile-library/wychbold"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3255 |
}
|
| 3256 |
],
|
| 3257 |
"edges": [
|
|
@@ -3720,6 +3961,11 @@
|
|
| 3720 |
"target": "branch::Woodrow Library",
|
| 3721 |
"rel": "HAS_FACILITY"
|
| 3722 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3723 |
{
|
| 3724 |
"source": "facility::accessible toilet",
|
| 3725 |
"target": "branch::Catshill Community Library",
|
|
@@ -3960,6 +4206,11 @@
|
|
| 3960 |
"target": "branch::Wythall Library",
|
| 3961 |
"rel": "HAS_FACILITY"
|
| 3962 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3963 |
{
|
| 3964 |
"source": "facility::free Wi-Fi",
|
| 3965 |
"target": "branch::Bewdley Library",
|
|
@@ -4060,6 +4311,11 @@
|
|
| 4060 |
"target": "branch::Wythall Library",
|
| 4061 |
"rel": "HAS_FACILITY"
|
| 4062 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4063 |
{
|
| 4064 |
"source": "facility::meeting rooms",
|
| 4065 |
"target": "branch::Bromsgrove Library",
|
|
@@ -4115,6 +4371,11 @@
|
|
| 4115 |
"target": "branch::Woodrow Library",
|
| 4116 |
"rel": "HAS_FACILITY"
|
| 4117 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4118 |
{
|
| 4119 |
"source": "facility::printing",
|
| 4120 |
"target": "branch::Bewdley Library",
|
|
@@ -4210,6 +4471,11 @@
|
|
| 4210 |
"target": "branch::Wythall Library",
|
| 4211 |
"rel": "HAS_FACILITY"
|
| 4212 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4213 |
{
|
| 4214 |
"source": "area::Birmingham",
|
| 4215 |
"target": "branch::Rubery Library",
|
|
@@ -4250,6 +4516,11 @@
|
|
| 4250 |
"target": "branch::Malvern Library",
|
| 4251 |
"rel": "HAS_FACILITY"
|
| 4252 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4253 |
{
|
| 4254 |
"source": "area::Bromsgrove",
|
| 4255 |
"target": "branch::Catshill Community Library",
|
|
@@ -4295,6 +4566,11 @@
|
|
| 4295 |
"target": "branch::Warndon Library",
|
| 4296 |
"rel": "HAS_FACILITY"
|
| 4297 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4298 |
{
|
| 4299 |
"source": "branch::Hagley Library",
|
| 4300 |
"target": "area::Stourbridge",
|
|
@@ -4320,6 +4596,11 @@
|
|
| 4320 |
"target": "branch::Wythall Library",
|
| 4321 |
"rel": "HAS_FACILITY"
|
| 4322 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4323 |
{
|
| 4324 |
"source": "branch::Martley Library",
|
| 4325 |
"target": "area::Martley",
|
|
@@ -4365,6 +4646,151 @@
|
|
| 4365 |
"target": "area::Wells",
|
| 4366 |
"rel": "LOCATED_IN"
|
| 4367 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4368 |
{
|
| 4369 |
"source": "branch::Upton-Upon-Severn Library",
|
| 4370 |
"target": "area::Upton-Upon-Severn",
|
|
@@ -5587,382 +6013,419 @@
|
|
| 5587 |
"title": "Cluster 1: 1 service",
|
| 5588 |
"size": 155,
|
| 5589 |
"members": [
|
| 5590 |
-
"village::
|
| 5591 |
-
"village::white ladies aston",
|
| 5592 |
-
"village::crowle",
|
| 5593 |
-
"village::wolverley",
|
| 5594 |
-
"village::guarlford",
|
| 5595 |
-
"village::rock",
|
| 5596 |
-
"village::callow hill redditch",
|
| 5597 |
-
"village::abberley",
|
| 5598 |
-
"village::stoke prior",
|
| 5599 |
-
"village::eldersfield",
|
| 5600 |
-
"village::newnham bridge",
|
| 5601 |
-
"village::kyre",
|
| 5602 |
-
"service::Mobile library",
|
| 5603 |
-
"village::uckinghall",
|
| 5604 |
-
"village::pendock",
|
| 5605 |
"village::sneachill",
|
| 5606 |
-
"village::
|
| 5607 |
-
"village::
|
| 5608 |
-
"village::
|
| 5609 |
-
"village::
|
| 5610 |
-
"village::
|
| 5611 |
-
"village::
|
| 5612 |
-
"village::
|
| 5613 |
-
"village::
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5614 |
"village::aston somerville",
|
| 5615 |
-
"village::
|
| 5616 |
-
"village::
|
| 5617 |
-
"village::
|
| 5618 |
-
"village::
|
| 5619 |
-
"village::
|
| 5620 |
-
"village::astley",
|
| 5621 |
-
"village::netherton",
|
| 5622 |
-
"village::bayton",
|
| 5623 |
-
"village::bliss gate",
|
| 5624 |
-
"village::broad green",
|
| 5625 |
-
"village::leigh",
|
| 5626 |
-
"village::portway",
|
| 5627 |
-
"village::stoke bliss",
|
| 5628 |
"village::dunley",
|
| 5629 |
-
"village::
|
| 5630 |
-
"village::wychbold",
|
| 5631 |
-
"village::astwood bank",
|
| 5632 |
-
"village::bradley green",
|
| 5633 |
-
"village::fladbury",
|
| 5634 |
-
"village::clows top",
|
| 5635 |
-
"village::cookley",
|
| 5636 |
-
"village::drayton",
|
| 5637 |
-
"village::holt heath",
|
| 5638 |
-
"village::littleworth",
|
| 5639 |
"village::ashton under hill",
|
| 5640 |
-
"village::clifton teme",
|
| 5641 |
-
"village::stone",
|
| 5642 |
-
"village::leigh sinton",
|
| 5643 |
-
"village::hunnington",
|
| 5644 |
-
"village::hatfield",
|
| 5645 |
-
"village::bredicot",
|
| 5646 |
-
"village::belbroughton",
|
| 5647 |
-
"village::pensax",
|
| 5648 |
-
"village::little witley",
|
| 5649 |
-
"village::church lench",
|
| 5650 |
-
"village::pebworth",
|
| 5651 |
-
"village::wadborough",
|
| 5652 |
-
"village::shatterford",
|
| 5653 |
-
"village::bickmarsh",
|
| 5654 |
-
"village::overbury",
|
| 5655 |
-
"village::inkberrow",
|
| 5656 |
"village::kemerton",
|
| 5657 |
-
"village::
|
| 5658 |
-
"village::far forest",
|
| 5659 |
-
"village::grimley",
|
| 5660 |
-
"village::sale green",
|
| 5661 |
-
"village::shrawley",
|
| 5662 |
-
"village::south littleton",
|
| 5663 |
-
"village::bishampton",
|
| 5664 |
-
"village::harpley",
|
| 5665 |
-
"village::ombersley",
|
| 5666 |
-
"village::callow hill stourport",
|
| 5667 |
-
"village::ab lench",
|
| 5668 |
-
"village::hanbury",
|
| 5669 |
-
"village::hallow",
|
| 5670 |
-
"village::harvington stourport",
|
| 5671 |
-
"village::ockeridge",
|
| 5672 |
-
"village::bransford",
|
| 5673 |
"village::north littleton",
|
| 5674 |
-
"village::
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5675 |
"village::hook",
|
| 5676 |
-
"village::childswickham",
|
| 5677 |
"village::collets green",
|
| 5678 |
-
"village::
|
| 5679 |
-
"village::
|
| 5680 |
-
"village::eckington",
|
| 5681 |
-
"village::button oak",
|
| 5682 |
"village::newtown",
|
| 5683 |
-
"village::
|
| 5684 |
-
"village::flyford flavell",
|
| 5685 |
-
"village::earls common",
|
| 5686 |
-
"village::broadwas",
|
| 5687 |
"village::bredon",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5688 |
"village::hadley",
|
| 5689 |
-
"village::
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5690 |
"village::summerfield",
|
| 5691 |
-
"village::
|
| 5692 |
-
"village::powick",
|
| 5693 |
"village::mamble",
|
| 5694 |
-
"village::
|
| 5695 |
-
"village::bredons norton",
|
| 5696 |
-
"village::sedgeberrow",
|
| 5697 |
-
"village::hanley childe",
|
| 5698 |
-
"village::cropthorne",
|
| 5699 |
-
"village::woodgate",
|
| 5700 |
-
"village::tibberton",
|
| 5701 |
-
"village::longley green",
|
| 5702 |
-
"village::kinnersley",
|
| 5703 |
-
"village::pinvin",
|
| 5704 |
"village::drakes broughton",
|
| 5705 |
-
"village::
|
| 5706 |
-
"village::
|
| 5707 |
"village::lower broadheath",
|
| 5708 |
-
"village::
|
| 5709 |
-
"village::
|
| 5710 |
-
"village::
|
| 5711 |
-
"village::elmbridge",
|
| 5712 |
-
"village::feckenham",
|
| 5713 |
-
"village::longdon",
|
| 5714 |
"village::bushley",
|
| 5715 |
-
"village::
|
| 5716 |
-
"village::
|
| 5717 |
-
"village::corse lawn",
|
| 5718 |
-
"village::middle littleton",
|
| 5719 |
-
"village::alfrick",
|
| 5720 |
-
"village::menith wood",
|
| 5721 |
-
"village::stoulton",
|
| 5722 |
-
"village::castlemorton",
|
| 5723 |
"village::cookhill",
|
| 5724 |
-
"village::
|
| 5725 |
-
"village::
|
| 5726 |
-
"village::
|
|
|
|
|
|
|
| 5727 |
"village::chaddesley corbett",
|
| 5728 |
-
"village::
|
| 5729 |
-
"village::
|
| 5730 |
-
"village::
|
| 5731 |
-
"village::
|
| 5732 |
-
"village::
|
| 5733 |
-
"village::
|
| 5734 |
-
"village::
|
| 5735 |
-
"village::
|
| 5736 |
-
"village::
|
| 5737 |
-
"village::
|
| 5738 |
-
"village::
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5739 |
"village::bridges stone",
|
| 5740 |
-
"village::
|
| 5741 |
-
"village::
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5742 |
"village::little clevelode",
|
| 5743 |
-
"village::
|
| 5744 |
-
"village::
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5745 |
],
|
| 5746 |
"report": "Service: Mobile library | Village: Ab Lench, Abberley, Alfrick, Ashton Under Hill, Astley, Aston Somerville, Astwood Bank, Bayton, Beckford, Belbroughton, Bickmarsh, Bishampton"
|
| 5747 |
},
|
| 5748 |
{
|
| 5749 |
"id": "community::1",
|
| 5750 |
-
"title": "Cluster 2:
|
| 5751 |
-
"size":
|
| 5752 |
"members": [
|
| 5753 |
-
"
|
| 5754 |
-
"
|
| 5755 |
-
"
|
| 5756 |
-
"branch::
|
|
|
|
| 5757 |
"branch::Martley Library",
|
| 5758 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5759 |
"area::Bewdley",
|
| 5760 |
-
"facility::public toilets",
|
| 5761 |
-
"tier::Libraries Unlocked",
|
| 5762 |
"branch::Malvern Library",
|
| 5763 |
-
"
|
| 5764 |
-
"
|
| 5765 |
-
"area::Birmingham",
|
| 5766 |
-
"area::Bromsgrove",
|
| 5767 |
-
"branch::Broadway Library",
|
| 5768 |
"area::Pershore",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5769 |
"area::Severn",
|
| 5770 |
-
"facility::meeting rooms",
|
| 5771 |
-
"area::Malvern",
|
| 5772 |
-
"area::Redditch",
|
| 5773 |
-
"facility::baby changing",
|
| 5774 |
-
"branch::Bewdley Library",
|
| 5775 |
-
"area::Upton-Upon-Severn",
|
| 5776 |
-
"facility::free Wi-Fi",
|
| 5777 |
-
"area::Broadway",
|
| 5778 |
-
"area::Martley",
|
| 5779 |
"branch::Evesham Library",
|
| 5780 |
-
"branch::Bromsgrove Library",
|
| 5781 |
-
"branch::Upton-Upon-Severn Library",
|
| 5782 |
-
"facility::public computers",
|
| 5783 |
-
"facility::cafΓ©",
|
| 5784 |
-
"branch::Alvechurch Library",
|
| 5785 |
-
"branch::Kidderminster Library",
|
| 5786 |
-
"branch::Droitwich Spa Library",
|
| 5787 |
"branch::St. John's Library",
|
| 5788 |
-
"
|
| 5789 |
-
"facility::
|
| 5790 |
-
"branch::
|
| 5791 |
-
"branch::Hagley Library",
|
| 5792 |
"area::Evesham",
|
| 5793 |
-
"facility::
|
| 5794 |
-
"
|
| 5795 |
-
"
|
| 5796 |
-
"branch::Pershore Library",
|
| 5797 |
"facility::printing",
|
| 5798 |
-
"facility::
|
| 5799 |
-
"
|
| 5800 |
-
"
|
| 5801 |
-
"branch::
|
| 5802 |
-
"
|
| 5803 |
-
"
|
| 5804 |
"area::Stourbridge",
|
| 5805 |
-
"
|
| 5806 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5807 |
],
|
| 5808 |
-
"report": "Area: Bewdley, Birmingham, Broadway, Bromsgrove, Evesham, Kidderminster, Malvern, Martley, Pershore, Redditch, Severn, Spa | Branch: Alvechurch Library, Bewdley Library, Broadway Library, Bromsgrove Library, Catshill Community Library, Droitwich Spa Library, Evesham Library, Hagley Library, Kidderminster Library, Malvern Library, Martley Library, Pershore Library | Facility: accessible toilet, baby changing, cafΓ©, free Wi-Fi, meeting rooms, printing, public computers, public toilets, self-service, study space, wheelchair access
|
| 5809 |
},
|
| 5810 |
{
|
| 5811 |
"id": "community::2",
|
| 5812 |
"title": "Cluster 3: 18 services, 17 resources, 2 memberships",
|
| 5813 |
"size": 40,
|
| 5814 |
"members": [
|
| 5815 |
-
"
|
| 5816 |
-
"
|
| 5817 |
-
"service::
|
| 5818 |
-
"
|
| 5819 |
-
"resource::Oxford Dictionary of National Biography",
|
| 5820 |
-
"resource::The Times Digital Archive",
|
| 5821 |
-
"service::Business database reference resources",
|
| 5822 |
"resource::Oxford English Dictionary",
|
| 5823 |
-
"service::
|
| 5824 |
-
"resource::Oxford Research Encyclopaedias",
|
| 5825 |
-
"resource::Pressreader",
|
| 5826 |
-
"service::Start or grow a business",
|
| 5827 |
"resource::Online events",
|
| 5828 |
-
"
|
| 5829 |
-
"resource::
|
| 5830 |
"service::Login to my library account",
|
| 5831 |
-
"
|
| 5832 |
-
"
|
| 5833 |
-
"
|
| 5834 |
-
"service::
|
| 5835 |
-
"topic::membership",
|
| 5836 |
"resource::British Film Institution (BFI Replay)",
|
| 5837 |
-
"
|
| 5838 |
"hub::online",
|
| 5839 |
-
"service::
|
| 5840 |
-
"resource::COBRA (Business Support)",
|
| 5841 |
"resource::EBSCO",
|
| 5842 |
-
"service::Business facilities at Worcestershire libraries",
|
| 5843 |
-
"tier::Digital membership",
|
| 5844 |
-
"topic::business",
|
| 5845 |
-
"tier::Full membership",
|
| 5846 |
-
"service::Reserve your library books",
|
| 5847 |
-
"service::Book a computer",
|
| 5848 |
"resource::ESPACENET",
|
| 5849 |
-
"
|
| 5850 |
-
"
|
| 5851 |
-
"service::Business networking",
|
| 5852 |
"resource::Digital library membership",
|
| 5853 |
-
"service::
|
| 5854 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5855 |
],
|
| 5856 |
"report": "Hub: Online library hub | Membership: Digital membership, Full membership | Resource: Access to Research, Ancestry, Borrowbox, British Film Institution (BFI Replay), COBRA (Business Support), Digital library membership, EBSCO, ESPACENET, Online events, Oxford Dictionary of National Biography, Oxford English Dictionary, Oxford Reference | Service: About BIPC Worcestershire, Book a computer, Business database reference resources, Business facilities at Worcestershire libraries, Business networking, Emails and updates, Explore free online library resources for learning, research and business, Get business books online, Join the library, Library Service at Home, Login to my library account, Pay fees and charges | Topic: Business, Membership"
|
| 5857 |
},
|
| 5858 |
{
|
| 5859 |
"id": "community::3",
|
| 5860 |
-
"title": "Cluster 4:
|
| 5861 |
-
"size":
|
| 5862 |
"members": [
|
| 5863 |
-
"service::
|
| 5864 |
-
"service::
|
|
|
|
| 5865 |
"service::Spine-tingling events this autumn at your local library",
|
| 5866 |
-
"service::Alvechurch Library - New layout for all library pages V2",
|
| 5867 |
-
"service::Alvechurch Library - New layout for all library pages V1",
|
| 5868 |
"service::Worcestershireβs Libraries inspire local primary school teachers",
|
|
|
|
|
|
|
| 5869 |
"service::Acclaimed Authors Set to Visit Worcestershireβs Libraries This Autumn",
|
| 5870 |
-
"service::Early years information library",
|
| 5871 |
-
"service::Worcestershire libraries get prepared for emergencies this βSave a Life Septemberβ",
|
| 5872 |
"service::Rooms for hire at your local library β book online today",
|
| 5873 |
-
"service::
|
| 5874 |
-
"service::Coming Soon: Free Dementia Events at Worcestershire's Libraries",
|
| 5875 |
-
"topic::general",
|
| 5876 |
"service::Libraries Unlocked Launches Next Week at Tenbury Library!",
|
| 5877 |
-
"service::Worcestershireβs Free Online Library - Perfect for the Festive Season",
|
| 5878 |
-
"service::Survey launched to help shape library services across the county",
|
| 5879 |
-
"service::Library events and activities",
|
| 5880 |
-
"service::Discover the stories that shaped our town this May at Kidderminster Library!",
|
| 5881 |
-
"service::2026 Libraries closing dates",
|
| 5882 |
-
"service::Worcestershireβs Library Service at Home is changing lives",
|
| 5883 |
-
"service::Improve Your Digital Skills at your Local Library",
|
| 5884 |
-
"service::Hire a library meeting room",
|
| 5885 |
-
"service::Libraries Unlocked is coming soon to Tenbury Library!",
|
| 5886 |
-
"service::Exciting STEAM themed event coming to The Hive Library!",
|
| 5887 |
"service::Worcestershire library closures announced over Easter holidays",
|
| 5888 |
-
"service::Library volunteering provides a new chapter for retiree Paul",
|
| 5889 |
"service::Worcestershire Libraries to Host Second βPower of Libraries Conferenceβ for Early Years Professionals",
|
| 5890 |
-
"service::Discover Volunteering Opportunities at your local library",
|
| 5891 |
-
"service::Libraries Unlocked is coming soon to Redditch Library!",
|
| 5892 |
-
"service::Memories and Me",
|
| 5893 |
-
"service::Libraries Unlocked Launches Next Week at Redditch Library!",
|
| 5894 |
"service::Online library hub",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5895 |
"service::New library book collection to guide families through pregnancy and early parenthood",
|
| 5896 |
-
"service::
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5897 |
],
|
| 5898 |
-
"report": "Service: 2026 Libraries closing dates, Acclaimed Authors Set to Visit Worcestershireβs Libraries This Autumn, Alvechurch Library - New layout for all library pages V1, Alvechurch Library - New layout for all library pages V2, Coming Soon: Free Dementia Events at Worcestershire's Libraries, Discover Volunteering Opportunities at your local library, Discover the stories that shaped our town this May at Kidderminster Library!, Early years information library, Exciting STEAM themed event coming to The Hive Library!, Find a library,
|
| 5899 |
},
|
| 5900 |
{
|
| 5901 |
"id": "community::4",
|
| 5902 |
-
"title": "Cluster 5:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5903 |
"size": 14,
|
| 5904 |
"members": [
|
| 5905 |
-
"service::Volunteering, training and work experience",
|
| 5906 |
-
"service::Adult Learning Courses in libraries",
|
| 5907 |
"service::Digital Inclusion - Helping You Online",
|
| 5908 |
"service::Key Stage 1 and 2 (years 1 to 6)",
|
| 5909 |
-
"service::Key Stage 3 and 4 (years 7 to 11)",
|
| 5910 |
-
"service::Learn, upskill and find work",
|
| 5911 |
-
"service::Job clubs",
|
| 5912 |
"service::Learning Outside the Classroom",
|
| 5913 |
-
"service::
|
| 5914 |
-
"service::Online job-seeking support",
|
| 5915 |
-
"service::Sixth form and college library support",
|
| 5916 |
-
"service::Study Happy",
|
| 5917 |
"topic::learning",
|
| 5918 |
-
"service::
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5919 |
],
|
| 5920 |
"report": "Service: Adult Learning Courses in libraries, Book a visit, Digital Inclusion - Helping You Online, Early Years and Reception, Job clubs, Key Stage 1 and 2 (years 1 to 6), Key Stage 3 and 4 (years 7 to 11), Learn, upskill and find work, Learning Outside the Classroom, Online job-seeking support, Sixth form and college library support, Study Happy | Topic: Learning"
|
| 5921 |
},
|
| 5922 |
{
|
| 5923 |
-
"id": "community::
|
| 5924 |
-
"title": "Cluster
|
| 5925 |
"size": 12,
|
| 5926 |
"members": [
|
| 5927 |
-
"service::Get school ready with Worcestershire's libraries",
|
| 5928 |
-
"service::Free family fun this summer with Worcestershire Libraries & National Trust Croome",
|
| 5929 |
-
"service::Celebrate World Book Day at your local library",
|
| 5930 |
-
"service::Libraries βGo All Inβ for the National Year of Reading 2026",
|
| 5931 |
"topic::seasonal",
|
|
|
|
|
|
|
|
|
|
| 5932 |
"service::Young Poet Laureate",
|
|
|
|
|
|
|
| 5933 |
"service::Improve Your Digital Skills at Worcestershire Libraries This Summer!",
|
| 5934 |
"service::Summer Reading Challenge",
|
|
|
|
| 5935 |
"service::Library Christmas & New Year Opening Times",
|
| 5936 |
-
"service::Aged 13β24? Volunteer with Worcestershireβs Libraries this summer"
|
| 5937 |
-
"service::Get involved with the National Year of Reading 2026",
|
| 5938 |
-
"service::Library Christmas and New Year opening times"
|
| 5939 |
],
|
| 5940 |
"report": "Service: Aged 13β24? Volunteer with Worcestershireβs Libraries this summer, Celebrate World Book Day at your local library, Free family fun this summer with Worcestershire Libraries & National Trust Croome, Get involved with the National Year of Reading 2026, Get school ready with Worcestershire's libraries, Improve Your Digital Skills at Worcestershire Libraries This Summer!, Libraries βGo All Inβ for the National Year of Reading 2026, Library Christmas & New Year Opening Times, Library Christmas and New Year opening times, Summer Reading Challenge, Young Poet Laureate | Topic: Seasonal"
|
| 5941 |
},
|
| 5942 |
{
|
| 5943 |
-
"id": "community::
|
| 5944 |
-
"title": "Cluster
|
| 5945 |
"size": 6,
|
| 5946 |
"members": [
|
| 5947 |
-
"service::'Read & Unwind Time' at your local library",
|
| 5948 |
"service::Reading well",
|
| 5949 |
-
"service::Worcestershire Reading Groups Recommend",
|
| 5950 |
"topic::reading",
|
|
|
|
| 5951 |
"service::Read and discover",
|
|
|
|
| 5952 |
"service::Ask for a Book"
|
| 5953 |
],
|
| 5954 |
"report": "Service: 'Read & Unwind Time' at your local library, Ask for a Book, Read and discover, Reading well, Worcestershire Reading Groups Recommend | Topic: Reading"
|
| 5955 |
},
|
| 5956 |
{
|
| 5957 |
-
"id": "community::
|
| 5958 |
-
"title": "Cluster
|
| 5959 |
"size": 5,
|
| 5960 |
"members": [
|
| 5961 |
-
"service::Stay warm and connected at your local library this winter",
|
| 5962 |
-
"service::Warm welcome",
|
| 5963 |
-
"service::Connect with others and boost your wellbeing",
|
| 5964 |
"service::Libraries offer a free warm space to its residents this winter",
|
| 5965 |
-
"
|
|
|
|
|
|
|
|
|
|
| 5966 |
],
|
| 5967 |
"report": "Service: Connect with others and boost your wellbeing, Libraries offer a free warm space to its residents this winter, Stay warm and connected at your local library this winter, Warm welcome | Topic: Wellbeing"
|
| 5968 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"generated": "2026-06-12T21:47:08+00:00",
|
| 3 |
+
"built_from": "library_kb.json + hive_kb.json",
|
| 4 |
"method": "deterministic GraphRAG-style graph (entities->relationships->communities->reports); inspired by microsoft/graphrag",
|
| 5 |
"stats": {
|
| 6 |
+
"nodes": 349,
|
| 7 |
+
"edges": 502,
|
| 8 |
+
"communities": 9,
|
| 9 |
"node_types": {
|
| 10 |
"Membership": 3,
|
| 11 |
"Hub": 1,
|
| 12 |
"Branch": 23,
|
| 13 |
+
"Facility": 14,
|
| 14 |
"Area": 17,
|
| 15 |
"Service": 87,
|
| 16 |
"Topic": 7,
|
| 17 |
"Resource": 17,
|
| 18 |
+
"Village": 154,
|
| 19 |
+
"HiveService": 26
|
| 20 |
}
|
| 21 |
},
|
| 22 |
"nodes": [
|
|
|
|
| 876 |
"staffed": "8:30am to 10:00pm"
|
| 877 |
}
|
| 878 |
},
|
| 879 |
+
"facilities": [
|
| 880 |
+
"archives",
|
| 881 |
+
"cafΓ©",
|
| 882 |
+
"children's library",
|
| 883 |
+
"exhibition space",
|
| 884 |
+
"free Wi-Fi",
|
| 885 |
+
"meeting rooms",
|
| 886 |
+
"printing",
|
| 887 |
+
"public computers",
|
| 888 |
+
"public toilets",
|
| 889 |
+
"study space",
|
| 890 |
+
"wheelchair access"
|
| 891 |
+
],
|
| 892 |
"libraries_unlocked": false,
|
| 893 |
+
"url": "https://www.worcestershire.gov.uk/council-services/libraries/find-library/hive",
|
| 894 |
+
"open_late": true,
|
| 895 |
+
"hive_hours": "The Hive is open from 8:30am - 10pm, Monday to Sunday (opening-hours.html; plan-your-visit.html states 'Monday to Sunday 8.30am - 10.00 pm')",
|
| 896 |
+
"source": "thehiveworcester.org",
|
| 897 |
+
"partnership": "Joint venture of the University of Worcester and Worcestershire County Council - 'The Hive is the first joint public and university library in Europe' (services.html, using-the-library.html). Built wi",
|
| 898 |
+
"summary": "Worcester city's library β Europe's first joint university + public library, open 8:30amβ10pm every day."
|
| 899 |
},
|
| 900 |
{
|
| 901 |
"id": "branch::Upton-Upon-Severn Library",
|
|
|
|
| 2170 |
"summary": "Free eBooks & eAudiobooks β latest titles, award winners, non-fiction, classics and children's, plus the curated 'Worcestershire Reads' picks.",
|
| 2171 |
"what_you_need": "Library card number + PIN; choose 'Worcestershire' in the app.",
|
| 2172 |
"access": [
|
| 2173 |
+
"Download the BorrowBox app β [iPhone/iPad](https://apps.apple.com/app/borrowbox-library/id562843562) or [Android](https://play.google.com/store/apps/details?id=com.bolindadigital.BorrowBoxLibrary).",
|
| 2174 |
"Select 'Worcestershire' as your library service.",
|
| 2175 |
"Sign in with your card number + PIN."
|
| 2176 |
],
|
|
|
|
| 2295 |
"summary": "7,000+ full-page newspapers & magazines, 60+ languages, 120+ countries.",
|
| 2296 |
"what_you_need": "Worcestershire library card number + PIN (free full membership, residents). No separate PressReader account needed.",
|
| 2297 |
"access": [
|
| 2298 |
+
"Get the PressReader app β [iPhone/iPad](https://apps.apple.com/app/pressreader/id313904711) or [Android](https://play.google.com/store/apps/details?id=com.newspaperdirect.pressreader.android) β or read in your browser at [pressreader.com](https://www.pressreader.com).",
|
| 2299 |
"Tap 'Libraries & Groups' and search/select 'Worcestershire'.",
|
| 2300 |
"Sign in with your library card number + PIN.",
|
| 2301 |
"At home you get a 30-day pass (re-confirm monthly); on library Wi-Fi it's 7 days."
|
|
|
|
| 3270 |
"type": "Village",
|
| 3271 |
"label": "Wychbold",
|
| 3272 |
"url": "https://www.worcestershire.gov.uk/council-services/worcestershire-libraries/your-library-membership/mobile-library/wychbold"
|
| 3273 |
+
},
|
| 3274 |
+
{
|
| 3275 |
+
"id": "facility::exhibition space",
|
| 3276 |
+
"type": "Facility",
|
| 3277 |
+
"label": "exhibition space"
|
| 3278 |
+
},
|
| 3279 |
+
{
|
| 3280 |
+
"id": "facility::children's library",
|
| 3281 |
+
"type": "Facility",
|
| 3282 |
+
"label": "children's library"
|
| 3283 |
+
},
|
| 3284 |
+
{
|
| 3285 |
+
"id": "facility::archives",
|
| 3286 |
+
"type": "Facility",
|
| 3287 |
+
"label": "archives"
|
| 3288 |
+
},
|
| 3289 |
+
{
|
| 3290 |
+
"id": "hive::First joint public and university library in Europe",
|
| 3291 |
+
"type": "HiveService",
|
| 3292 |
+
"label": "First joint public and university library in Europe",
|
| 3293 |
+
"summary": "Community members and university students get the same user experience; no segregation - university stock shelved alongside public stock; staff from both university and council work throughout the library; all users access the same services.",
|
| 3294 |
+
"url": "https://www.thehiveworcester.org/using-the-library.html",
|
| 3295 |
+
"source": "thehiveworcester.org"
|
| 3296 |
+
},
|
| 3297 |
+
{
|
| 3298 |
+
"id": "hive::Exceptionally long opening hours",
|
| 3299 |
+
"type": "HiveService",
|
| 3300 |
+
"label": "Exceptionally long opening hours",
|
| 3301 |
+
"summary": "Open 8:30am - 10pm, Monday to Sunday (every day of the week).",
|
| 3302 |
+
"url": "https://www.thehiveworcester.org/opening-hours.html",
|
| 3303 |
+
"source": "thehiveworcester.org"
|
| 3304 |
+
},
|
| 3305 |
+
{
|
| 3306 |
+
"id": "hive::Explore the Past - Worcestershire Archive & Archaeology Service (Level 2)",
|
| 3307 |
+
"type": "HiveService",
|
| 3308 |
+
"label": "Explore the Past - Worcestershire Archive & Archaeology Service (Level 2)",
|
| 3309 |
+
"summary": "Home to the Worcestershire Archive and Archaeology Service; thousands of archaeological sites, historic buildings and landscapes recorded; 12 miles of original archives collected and preserved. Offers: area for consulting original archives on level 2; guidance from knowledgeable staff; facility to talk to archaeologists and access the archaeological record; large self-service area for microform and internet sources; local studies and archaeology library; a conservation room for the repair and preservation of documents; prearranged 'behind the scenes' group visits; programme of history and archaeology events for all ages. Original-document viewings (e.g. Stuart Collection) take place in the original documents examination room within 'access to original archives' opening hours.",
|
| 3310 |
+
"url": "https://www.thehiveworcester.org/explore-the-past.html",
|
| 3311 |
+
"source": "thehiveworcester.org"
|
| 3312 |
+
},
|
| 3313 |
+
{
|
| 3314 |
+
"id": "hive::Historic Environment Record (HER) access",
|
| 3315 |
+
"type": "HiveService",
|
| 3316 |
+
"label": "Historic Environment Record (HER) access",
|
| 3317 |
+
"summary": "Searchable database with digital mapping of all known archaeological sites, historic buildings and landscapes in the county; online search via Heritage Gateway; for detailed enquiries you can book to see an HER officer at The Hive.",
|
| 3318 |
+
"url": "https://www.thehiveworcester.org/catalogue-search.html",
|
| 3319 |
+
"source": "thehiveworcester.org"
|
| 3320 |
+
},
|
| 3321 |
+
{
|
| 3322 |
+
"id": "hive::Three online catalogues",
|
| 3323 |
+
"type": "HiveService",
|
| 3324 |
+
"label": "Three online catalogues",
|
| 3325 |
+
"summary": "Library catalogue search (quarter of a million titles plus all County branches, via wcc.ent.sirsidynix.net.uk), Archaeology report search (Heritage Gateway) and Archives search (CalmView at e-services.worcestershire.gov.uk).",
|
| 3326 |
+
"url": "https://www.thehiveworcester.org/catalogue-search.html",
|
| 3327 |
+
"source": "thehiveworcester.org"
|
| 3328 |
+
},
|
| 3329 |
+
{
|
| 3330 |
+
"id": "hive::Business & IP Centre (BIPC) Worcestershire (Level 2)",
|
| 3331 |
+
"type": "HiveService",
|
| 3332 |
+
"label": "Business & IP Centre (BIPC) Worcestershire (Level 2)",
|
| 3333 |
+
"summary": "The Business Centre at The Hive is home to BIPC Worcestershire, providing free and accessible business advice, support, information and resources, plus well-appointed meeting rooms with the latest technology. Free business databases COBRA (Complete Business Reference Adviser), Statista and FAME are accessible from any public computer in The Hive; start-up support, Intellectual Property information and funding signposted via the BIPC Worcestershire website.",
|
| 3334 |
+
"url": "https://www.thehiveworcester.org/business-centre.html",
|
| 3335 |
+
"source": "thehiveworcester.org"
|
| 3336 |
+
},
|
| 3337 |
+
{
|
| 3338 |
+
"id": "hive::Careers Worcs Youth Hub (Level 1)",
|
| 3339 |
+
"type": "HiveService",
|
| 3340 |
+
"label": "Careers Worcs Youth Hub (Level 1)",
|
| 3341 |
+
"summary": "Dedicated space for young people aged 15-24, open Monday-Friday 9am-4pm, with careers advisors providing 1-1 advice, CV writing, job search, interview preparation, apprenticeships/training routes, plus finance and health & wellbeing services. Careers Helpline 0300 666 3666.",
|
| 3342 |
+
"url": "https://www.thehiveworcester.org/youthhub.html",
|
| 3343 |
+
"source": "thehiveworcester.org"
|
| 3344 |
+
},
|
| 3345 |
+
{
|
| 3346 |
+
"id": "hive::800+ study spaces and bookable study spaces for students",
|
| 3347 |
+
"type": "HiveService",
|
| 3348 |
+
"label": "800+ study spaces and bookable study spaces for students",
|
| 3349 |
+
"summary": "Over 800 study spaces across The Hive available to use for study; Level 0 can be used for group study; Level 4 is the silent study floor; 250 new computers bookable in advance (students page).",
|
| 3350 |
+
"url": "https://www.thehiveworcester.org/book-a-space.html",
|
| 3351 |
+
"source": "thehiveworcester.org"
|
| 3352 |
+
},
|
| 3353 |
+
{
|
| 3354 |
+
"id": "hive::Room hire / Space for hire with online booking and video conferencing",
|
| 3355 |
+
"type": "HiveService",
|
| 3356 |
+
"label": "Room hire / Space for hire with online booking and video conferencing",
|
| 3357 |
+
"summary": "From small huddle rooms to large function rooms at competitive prices; online booking system (librarybookings.worcestershire.gov.uk) with 360-degree virtual tours; new video-conferencing technology in many meeting rooms; concessionary rates for not-for-profit organisations; minimum one-hour charge with additional half hours; all prices include public liability insurance; University of Worcester members can book Hive rooms free for specific purposes; exhibition space also available for hire.",
|
| 3358 |
+
"url": "https://www.thehiveworcester.org/space-for-hire.html",
|
| 3359 |
+
"source": "thehiveworcester.org"
|
| 3360 |
+
},
|
| 3361 |
+
{
|
| 3362 |
+
"id": "hive::On-site hospitality catering (Bellrock)",
|
| 3363 |
+
"type": "HiveService",
|
| 3364 |
+
"label": "On-site hospitality catering (Bellrock)",
|
| 3365 |
+
"summary": "The Hive has its own onsite catering company (also manages the cafe) offering hospitality menus - buffets, platters, canapes, business breakfasts - for meetings, corporate training and conferences; special diets catered for; minimum 2 working days' notice.",
|
| 3366 |
+
"url": "https://www.thehiveworcester.org/catering.html",
|
| 3367 |
+
"source": "thehiveworcester.org"
|
| 3368 |
+
},
|
| 3369 |
+
{
|
| 3370 |
+
"id": "hive::Cafe at The Hive (Level 1)",
|
| 3371 |
+
"type": "HiveService",
|
| 3372 |
+
"label": "Cafe at The Hive (Level 1)",
|
| 3373 |
+
"summary": "Open Monday-Sunday 8.30am-9pm; hot and cold snacks, light meals, deli bar with made-to-order baguettes/sandwiches/salads; 10% student discount; five-star hygiene rating; fully accessible; Urban Eat ranges including vegan, vegetarian, gluten free and halal options.",
|
| 3374 |
+
"url": "https://www.thehiveworcester.org/cafe.html",
|
| 3375 |
+
"source": "thehiveworcester.org"
|
| 3376 |
+
},
|
| 3377 |
+
{
|
| 3378 |
+
"id": "hive::Children's library - one of the largest in the country (Level 1)",
|
| 3379 |
+
"type": "HiveService",
|
| 3380 |
+
"label": "Children's library - one of the largest in the country (Level 1)",
|
| 3381 |
+
"summary": "Designed for children to find their own level; Story Pit recessed seating area for readings, performances and group work; access to 'Story Island', a unique outdoor teaching space for children accessed through the children's library; children-only PCs and printer/copier.",
|
| 3382 |
+
"url": "https://www.thehiveworcester.org/children-young-people-families.html",
|
| 3383 |
+
"source": "thehiveworcester.org"
|
| 3384 |
+
},
|
| 3385 |
+
{
|
| 3386 |
+
"id": "hive::Walk-in academic research databases",
|
| 3387 |
+
"type": "HiveService",
|
| 3388 |
+
"label": "Walk-in academic research databases",
|
| 3389 |
+
"summary": "Free walk-in access inside the building for all library members to ~37 databases including Ancestry Library Edition, JSTOR, Academic Search Complete, Business Source Complete, CINAHL Complete, BMJ Journals, Oxford English Dictionary, Dictionary of National Biography, Times Digital Archive, Gale British newspapers, House of Commons Parliamentary Papers, Cambridge/Oxford/SAGE/Taylor & Francis journals, ScienceDirect, Project Muse, Drama Online and more.",
|
| 3390 |
+
"url": "https://www.thehiveworcester.org/research-databases.html",
|
| 3391 |
+
"source": "thehiveworcester.org"
|
| 3392 |
+
},
|
| 3393 |
+
{
|
| 3394 |
+
"id": "hive::Special collections",
|
| 3395 |
+
"type": "HiveService",
|
| 3396 |
+
"label": "Special collections",
|
| 3397 |
+
"summary": "The Stuart Collection (over one-thousand books on Stuart-period Britain 1603-1714, bequeathed by John Grainger in 1900, many first/last surviving copies); the Palfrey Collection (antiquarian studies and first editions key to Worcestershire study); the Stanbrook Abbey Press Collection (closed access on level 4); extensive Local Studies and Archaeology libraries.",
|
| 3398 |
+
"url": "https://www.thehiveworcester.org/special-collections.html",
|
| 3399 |
+
"source": "thehiveworcester.org"
|
| 3400 |
+
},
|
| 3401 |
+
{
|
| 3402 |
+
"id": "hive::Research collections for family and local history",
|
| 3403 |
+
"type": "HiveService",
|
| 3404 |
+
"label": "Research collections for family and local history",
|
| 3405 |
+
"summary": "Maps and plans (historic Ordnance Survey, tithe, inclosure and estate plans, many digital); census returns, International Genealogical Index, GRO Index of Births Marriages and Deaths, local newspapers, wills and probate; Worcestershire Photographic Survey and aerial photographic collections; archives of local families/businesses including the Earls of Coventry of Croome; Quarter Sessions, County and District Council records and the Worcester City Archive; Diocese of Worcester, parish and Non-conformist records.",
|
| 3406 |
+
"url": "https://www.thehiveworcester.org/research-collections.html",
|
| 3407 |
+
"source": "thehiveworcester.org"
|
| 3408 |
+
},
|
| 3409 |
+
{
|
| 3410 |
+
"id": "hive::Commissioned public art collection",
|
| 3411 |
+
"type": "HiveService",
|
| 3412 |
+
"label": "Commissioned public art collection",
|
| 3413 |
+
"summary": "Kaleidoscope by Robert Orchardson (aluminium, 2012, visible from all three main floors); Rack Alley by Clare Woods (600x500cm enamel and oil on aluminium, 2012, commissioned for the atrium); two John Clare-inspired paintings by James Fisher (2008).",
|
| 3414 |
+
"url": "https://www.thehiveworcester.org/art-collections.html",
|
| 3415 |
+
"source": "thehiveworcester.org"
|
| 3416 |
+
},
|
| 3417 |
+
{
|
| 3418 |
+
"id": "hive::BorrowBox digital library (eBooks/eAudio)",
|
| 3419 |
+
"type": "HiveService",
|
| 3420 |
+
"label": "BorrowBox digital library (eBooks/eAudio)",
|
| 3421 |
+
"summary": "eBook and eAudiobook service free to all library members 24/7 via library.bolindadigital.com/worcestershire or the BorrowBox app; borrow up to six eBooks and six eAudiobooks at a time; loans auto-expire with no late fees; syncs across up to six devices; requires library card number and PIN.",
|
| 3422 |
+
"url": "https://www.thehiveworcester.org/ebooks.html",
|
| 3423 |
+
"source": "thehiveworcester.org"
|
| 3424 |
+
},
|
| 3425 |
+
{
|
| 3426 |
+
"id": "hive::School and college visit packages",
|
| 3427 |
+
"type": "HiveService",
|
| 3428 |
+
"label": "School and college visit packages",
|
| 3429 |
+
"summary": "More than 8,000 students hosted since 2012. Free 'Package 1' visits (library tour plus activities such as Storytime, Book Blitz, Coding with Scratch, research skills, referencing) for Early Years through Sixth Form; charged 'Package 2' Explore the Past history/archive/archaeology workshops from a minimum of Β£40 per hour/class (individual WAAS workshops around Β£80, 90-120 minutes, e.g. Romans in Worcester, Anglo-Saxons, Tudor Worcester, Civil War, Victorian public health, WWI, WWII archaeology); webinars available; classroom resource packs Β£20 each; teacher training days hosted.",
|
| 3430 |
+
"url": "https://www.thehiveworcester.org/10252.html",
|
| 3431 |
+
"source": "thehiveworcester.org"
|
| 3432 |
+
},
|
| 3433 |
+
{
|
| 3434 |
+
"id": "hive::Archaeology planning advice for developers (professional service)",
|
| 3435 |
+
"type": "HiveService",
|
| 3436 |
+
"label": "Archaeology planning advice for developers (professional service)",
|
| 3437 |
+
"summary": "The Archive and Archaeology Service provides advice to landowners, developers and consultants on archaeological implications and opportunities of development in the County; advice on management and conservation of the rural and urban historic environment; workshops and training on heritage topics; plus professional services including digitisation, document conservation and a wide range of professional archaeological services.",
|
| 3438 |
+
"url": "https://www.thehiveworcester.org/supporting-business.html",
|
| 3439 |
+
"source": "thehiveworcester.org"
|
| 3440 |
+
},
|
| 3441 |
+
{
|
| 3442 |
+
"id": "hive::WAAS learning and outreach programme",
|
| 3443 |
+
"type": "HiveService",
|
| 3444 |
+
"label": "WAAS learning and outreach programme",
|
| 3445 |
+
"summary": "Local history research groups, talks, workshops, tours, school activities, community excavations, archive film screenings, family history fairs, guided walks and a geology trail around The Hive.",
|
| 3446 |
+
"url": "https://www.thehiveworcester.org/worcestershire-archive-archaeology.html",
|
| 3447 |
+
"source": "thehiveworcester.org"
|
| 3448 |
+
},
|
| 3449 |
+
{
|
| 3450 |
+
"id": "hive::IT, gaming and printing on every level",
|
| 3451 |
+
"type": "HiveService",
|
| 3452 |
+
"label": "IT, gaming and printing on every level",
|
| 3453 |
+
"summary": "Public access PCs with internet and Microsoft Office on Levels 0-4; group study screen with 46-inch HDMI display (L0); two PS4 gaming zones (L0); high-speed printers/copiers on every level with credit-card top-up kiosks (cash kiosk only at the L1 Hub); A4 scanners on request (L3); privacy filters available (L1 Hub); dedicated catalogue search stations.",
|
| 3454 |
+
"url": "https://www.thehiveworcester.org/it-equipment-printing.html",
|
| 3455 |
+
"source": "thehiveworcester.org"
|
| 3456 |
+
},
|
| 3457 |
+
{
|
| 3458 |
+
"id": "hive::The Studio theatre, exhibition spaces and Worcestershire Hub (Level 1)",
|
| 3459 |
+
"type": "HiveService",
|
| 3460 |
+
"label": "The Studio theatre, exhibition spaces and Worcestershire Hub (Level 1)",
|
| 3461 |
+
"summary": "Level 1 'Discover' holds the children's library, the cafe, exhibition spaces and The Studio theatre as well as The Worcestershire Hub (county council customer services) and quick choice books; Level 2 also has meeting rooms and a Business Centre with a suite of three rooms, informal networking space and 24-hour newsfeed; secure archives on Level 0 with occasional special tours.",
|
| 3462 |
+
"url": "https://www.thehiveworcester.org/inside-the-hive.html",
|
| 3463 |
+
"source": "thehiveworcester.org"
|
| 3464 |
+
},
|
| 3465 |
+
{
|
| 3466 |
+
"id": "hive::Accessibility provision",
|
| 3467 |
+
"type": "HiveService",
|
| 3468 |
+
"label": "Accessibility provision",
|
| 3469 |
+
"summary": "Fully accessible to wheelchairs, mobility scooters and pushchairs; lifts to all five public floors with Braille signage; Meet & Greet service bookable in advance; personal care suite on Level 1 with shower and lifting facilities (access card from staff); height-adjustable workstations and archive/microfilm tables; automated microfilm machine with foot pedal; induction loops in Meeting rooms 2, 3 and The Studio; 21 blue badge bays free for 3 hours in Cattlemarket car park; pre-arranged close access via Level 0; DisabledGo accessibility guide.",
|
| 3470 |
+
"url": "https://www.thehiveworcester.org/physical-accessibility.html",
|
| 3471 |
+
"source": "thehiveworcester.org"
|
| 3472 |
+
},
|
| 3473 |
+
{
|
| 3474 |
+
"id": "hive::Prayer space",
|
| 3475 |
+
"type": "HiveService",
|
| 3476 |
+
"label": "Prayer space",
|
| 3477 |
+
"summary": "Rooms within The Hive can be used by customers to pray - ask a member of staff to be directed to an available room; Worcestershire County Council policy is to be equally supportive of all faiths.",
|
| 3478 |
+
"url": "https://www.thehiveworcester.org/inside-the-hive.html",
|
| 3479 |
+
"source": "thehiveworcester.org"
|
| 3480 |
+
},
|
| 3481 |
+
{
|
| 3482 |
+
"id": "hive::Award-winning sustainable landmark building",
|
| 3483 |
+
"type": "HiveService",
|
| 3484 |
+
"label": "Award-winning sustainable landmark building",
|
| 3485 |
+
"summary": "Biomass boilers, River Severn river-water cooling, rainwater harvesting for WC flushing and archaeological washing, natural ventilation, water meadows for flood attenuation and 4000m2+ of biodiversity landscape; winner of 20+ awards including BREEAM Outstanding (2012), RIBA National Award (2013), SCONUL Library Design Award (2016) and ARA Record Keeping Service of the Year (2017); distinctive golden honeycomb cladding by Feilden Clegg Bradley Studios.",
|
| 3486 |
+
"url": "https://www.thehiveworcester.org/sustainability.html",
|
| 3487 |
+
"source": "thehiveworcester.org"
|
| 3488 |
+
},
|
| 3489 |
+
{
|
| 3490 |
+
"id": "hive::Membership without barriers",
|
| 3491 |
+
"type": "HiveService",
|
| 3492 |
+
"label": "Membership without barriers",
|
| 3493 |
+
"summary": "Instant free digital membership online (eBooks, eAudio, eMagazines and online resources) upgradeable to full membership with proof of name and address; UW students/staff use their university ID as their library card; dedicated Children's library card (designed by Liz Pichon) and Young Person's library card for ages 13-19; instant membership for ages 8-16 allows borrowing 2 books without a parent signature; full members get free computer/Wi-Fi access, DVD/Blu-ray hire, library activities and adult and family learning courses.",
|
| 3494 |
+
"url": "https://www.thehiveworcester.org/how-to-get-a-library-card.html",
|
| 3495 |
+
"source": "thehiveworcester.org"
|
| 3496 |
}
|
| 3497 |
],
|
| 3498 |
"edges": [
|
|
|
|
| 3961 |
"target": "branch::Woodrow Library",
|
| 3962 |
"rel": "HAS_FACILITY"
|
| 3963 |
},
|
| 3964 |
+
{
|
| 3965 |
+
"source": "facility::public toilets",
|
| 3966 |
+
"target": "branch::The Hive",
|
| 3967 |
+
"rel": "HAS_FACILITY"
|
| 3968 |
+
},
|
| 3969 |
{
|
| 3970 |
"source": "facility::accessible toilet",
|
| 3971 |
"target": "branch::Catshill Community Library",
|
|
|
|
| 4206 |
"target": "branch::Wythall Library",
|
| 4207 |
"rel": "HAS_FACILITY"
|
| 4208 |
},
|
| 4209 |
+
{
|
| 4210 |
+
"source": "facility::wheelchair access",
|
| 4211 |
+
"target": "branch::The Hive",
|
| 4212 |
+
"rel": "HAS_FACILITY"
|
| 4213 |
+
},
|
| 4214 |
{
|
| 4215 |
"source": "facility::free Wi-Fi",
|
| 4216 |
"target": "branch::Bewdley Library",
|
|
|
|
| 4311 |
"target": "branch::Wythall Library",
|
| 4312 |
"rel": "HAS_FACILITY"
|
| 4313 |
},
|
| 4314 |
+
{
|
| 4315 |
+
"source": "facility::free Wi-Fi",
|
| 4316 |
+
"target": "branch::The Hive",
|
| 4317 |
+
"rel": "HAS_FACILITY"
|
| 4318 |
+
},
|
| 4319 |
{
|
| 4320 |
"source": "facility::meeting rooms",
|
| 4321 |
"target": "branch::Bromsgrove Library",
|
|
|
|
| 4371 |
"target": "branch::Woodrow Library",
|
| 4372 |
"rel": "HAS_FACILITY"
|
| 4373 |
},
|
| 4374 |
+
{
|
| 4375 |
+
"source": "facility::meeting rooms",
|
| 4376 |
+
"target": "branch::The Hive",
|
| 4377 |
+
"rel": "HAS_FACILITY"
|
| 4378 |
+
},
|
| 4379 |
{
|
| 4380 |
"source": "facility::printing",
|
| 4381 |
"target": "branch::Bewdley Library",
|
|
|
|
| 4471 |
"target": "branch::Wythall Library",
|
| 4472 |
"rel": "HAS_FACILITY"
|
| 4473 |
},
|
| 4474 |
+
{
|
| 4475 |
+
"source": "facility::printing",
|
| 4476 |
+
"target": "branch::The Hive",
|
| 4477 |
+
"rel": "HAS_FACILITY"
|
| 4478 |
+
},
|
| 4479 |
{
|
| 4480 |
"source": "area::Birmingham",
|
| 4481 |
"target": "branch::Rubery Library",
|
|
|
|
| 4516 |
"target": "branch::Malvern Library",
|
| 4517 |
"rel": "HAS_FACILITY"
|
| 4518 |
},
|
| 4519 |
+
{
|
| 4520 |
+
"source": "facility::public computers",
|
| 4521 |
+
"target": "branch::The Hive",
|
| 4522 |
+
"rel": "HAS_FACILITY"
|
| 4523 |
+
},
|
| 4524 |
{
|
| 4525 |
"source": "area::Bromsgrove",
|
| 4526 |
"target": "branch::Catshill Community Library",
|
|
|
|
| 4566 |
"target": "branch::Warndon Library",
|
| 4567 |
"rel": "HAS_FACILITY"
|
| 4568 |
},
|
| 4569 |
+
{
|
| 4570 |
+
"source": "facility::study space",
|
| 4571 |
+
"target": "branch::The Hive",
|
| 4572 |
+
"rel": "HAS_FACILITY"
|
| 4573 |
+
},
|
| 4574 |
{
|
| 4575 |
"source": "branch::Hagley Library",
|
| 4576 |
"target": "area::Stourbridge",
|
|
|
|
| 4596 |
"target": "branch::Wythall Library",
|
| 4597 |
"rel": "HAS_FACILITY"
|
| 4598 |
},
|
| 4599 |
+
{
|
| 4600 |
+
"source": "facility::cafΓ©",
|
| 4601 |
+
"target": "branch::The Hive",
|
| 4602 |
+
"rel": "HAS_FACILITY"
|
| 4603 |
+
},
|
| 4604 |
{
|
| 4605 |
"source": "branch::Martley Library",
|
| 4606 |
"target": "area::Martley",
|
|
|
|
| 4646 |
"target": "area::Wells",
|
| 4647 |
"rel": "LOCATED_IN"
|
| 4648 |
},
|
| 4649 |
+
{
|
| 4650 |
+
"source": "branch::The Hive",
|
| 4651 |
+
"target": "facility::exhibition space",
|
| 4652 |
+
"rel": "HAS_FACILITY"
|
| 4653 |
+
},
|
| 4654 |
+
{
|
| 4655 |
+
"source": "branch::The Hive",
|
| 4656 |
+
"target": "facility::children's library",
|
| 4657 |
+
"rel": "HAS_FACILITY"
|
| 4658 |
+
},
|
| 4659 |
+
{
|
| 4660 |
+
"source": "branch::The Hive",
|
| 4661 |
+
"target": "facility::archives",
|
| 4662 |
+
"rel": "HAS_FACILITY"
|
| 4663 |
+
},
|
| 4664 |
+
{
|
| 4665 |
+
"source": "branch::The Hive",
|
| 4666 |
+
"target": "hive::First joint public and university library in Europe",
|
| 4667 |
+
"rel": "OFFERS"
|
| 4668 |
+
},
|
| 4669 |
+
{
|
| 4670 |
+
"source": "branch::The Hive",
|
| 4671 |
+
"target": "hive::Exceptionally long opening hours",
|
| 4672 |
+
"rel": "OFFERS"
|
| 4673 |
+
},
|
| 4674 |
+
{
|
| 4675 |
+
"source": "branch::The Hive",
|
| 4676 |
+
"target": "hive::Explore the Past - Worcestershire Archive & Archaeology Service (Level 2)",
|
| 4677 |
+
"rel": "OFFERS"
|
| 4678 |
+
},
|
| 4679 |
+
{
|
| 4680 |
+
"source": "branch::The Hive",
|
| 4681 |
+
"target": "hive::Historic Environment Record (HER) access",
|
| 4682 |
+
"rel": "OFFERS"
|
| 4683 |
+
},
|
| 4684 |
+
{
|
| 4685 |
+
"source": "branch::The Hive",
|
| 4686 |
+
"target": "hive::Three online catalogues",
|
| 4687 |
+
"rel": "OFFERS"
|
| 4688 |
+
},
|
| 4689 |
+
{
|
| 4690 |
+
"source": "branch::The Hive",
|
| 4691 |
+
"target": "hive::Business & IP Centre (BIPC) Worcestershire (Level 2)",
|
| 4692 |
+
"rel": "OFFERS"
|
| 4693 |
+
},
|
| 4694 |
+
{
|
| 4695 |
+
"source": "branch::The Hive",
|
| 4696 |
+
"target": "hive::Careers Worcs Youth Hub (Level 1)",
|
| 4697 |
+
"rel": "OFFERS"
|
| 4698 |
+
},
|
| 4699 |
+
{
|
| 4700 |
+
"source": "branch::The Hive",
|
| 4701 |
+
"target": "hive::800+ study spaces and bookable study spaces for students",
|
| 4702 |
+
"rel": "OFFERS"
|
| 4703 |
+
},
|
| 4704 |
+
{
|
| 4705 |
+
"source": "branch::The Hive",
|
| 4706 |
+
"target": "hive::Room hire / Space for hire with online booking and video conferencing",
|
| 4707 |
+
"rel": "OFFERS"
|
| 4708 |
+
},
|
| 4709 |
+
{
|
| 4710 |
+
"source": "branch::The Hive",
|
| 4711 |
+
"target": "hive::On-site hospitality catering (Bellrock)",
|
| 4712 |
+
"rel": "OFFERS"
|
| 4713 |
+
},
|
| 4714 |
+
{
|
| 4715 |
+
"source": "branch::The Hive",
|
| 4716 |
+
"target": "hive::Cafe at The Hive (Level 1)",
|
| 4717 |
+
"rel": "OFFERS"
|
| 4718 |
+
},
|
| 4719 |
+
{
|
| 4720 |
+
"source": "branch::The Hive",
|
| 4721 |
+
"target": "hive::Children's library - one of the largest in the country (Level 1)",
|
| 4722 |
+
"rel": "OFFERS"
|
| 4723 |
+
},
|
| 4724 |
+
{
|
| 4725 |
+
"source": "branch::The Hive",
|
| 4726 |
+
"target": "hive::Walk-in academic research databases",
|
| 4727 |
+
"rel": "OFFERS"
|
| 4728 |
+
},
|
| 4729 |
+
{
|
| 4730 |
+
"source": "branch::The Hive",
|
| 4731 |
+
"target": "hive::Special collections",
|
| 4732 |
+
"rel": "OFFERS"
|
| 4733 |
+
},
|
| 4734 |
+
{
|
| 4735 |
+
"source": "branch::The Hive",
|
| 4736 |
+
"target": "hive::Research collections for family and local history",
|
| 4737 |
+
"rel": "OFFERS"
|
| 4738 |
+
},
|
| 4739 |
+
{
|
| 4740 |
+
"source": "branch::The Hive",
|
| 4741 |
+
"target": "hive::Commissioned public art collection",
|
| 4742 |
+
"rel": "OFFERS"
|
| 4743 |
+
},
|
| 4744 |
+
{
|
| 4745 |
+
"source": "branch::The Hive",
|
| 4746 |
+
"target": "hive::BorrowBox digital library (eBooks/eAudio)",
|
| 4747 |
+
"rel": "OFFERS"
|
| 4748 |
+
},
|
| 4749 |
+
{
|
| 4750 |
+
"source": "branch::The Hive",
|
| 4751 |
+
"target": "hive::School and college visit packages",
|
| 4752 |
+
"rel": "OFFERS"
|
| 4753 |
+
},
|
| 4754 |
+
{
|
| 4755 |
+
"source": "branch::The Hive",
|
| 4756 |
+
"target": "hive::Archaeology planning advice for developers (professional service)",
|
| 4757 |
+
"rel": "OFFERS"
|
| 4758 |
+
},
|
| 4759 |
+
{
|
| 4760 |
+
"source": "branch::The Hive",
|
| 4761 |
+
"target": "hive::WAAS learning and outreach programme",
|
| 4762 |
+
"rel": "OFFERS"
|
| 4763 |
+
},
|
| 4764 |
+
{
|
| 4765 |
+
"source": "branch::The Hive",
|
| 4766 |
+
"target": "hive::IT, gaming and printing on every level",
|
| 4767 |
+
"rel": "OFFERS"
|
| 4768 |
+
},
|
| 4769 |
+
{
|
| 4770 |
+
"source": "branch::The Hive",
|
| 4771 |
+
"target": "hive::The Studio theatre, exhibition spaces and Worcestershire Hub (Level 1)",
|
| 4772 |
+
"rel": "OFFERS"
|
| 4773 |
+
},
|
| 4774 |
+
{
|
| 4775 |
+
"source": "branch::The Hive",
|
| 4776 |
+
"target": "hive::Accessibility provision",
|
| 4777 |
+
"rel": "OFFERS"
|
| 4778 |
+
},
|
| 4779 |
+
{
|
| 4780 |
+
"source": "branch::The Hive",
|
| 4781 |
+
"target": "hive::Prayer space",
|
| 4782 |
+
"rel": "OFFERS"
|
| 4783 |
+
},
|
| 4784 |
+
{
|
| 4785 |
+
"source": "branch::The Hive",
|
| 4786 |
+
"target": "hive::Award-winning sustainable landmark building",
|
| 4787 |
+
"rel": "OFFERS"
|
| 4788 |
+
},
|
| 4789 |
+
{
|
| 4790 |
+
"source": "branch::The Hive",
|
| 4791 |
+
"target": "hive::Membership without barriers",
|
| 4792 |
+
"rel": "OFFERS"
|
| 4793 |
+
},
|
| 4794 |
{
|
| 4795 |
"source": "branch::Upton-Upon-Severn Library",
|
| 4796 |
"target": "area::Upton-Upon-Severn",
|
|
|
|
| 6013 |
"title": "Cluster 1: 1 service",
|
| 6014 |
"size": 155,
|
| 6015 |
"members": [
|
| 6016 |
+
"village::rushock",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6017 |
"village::sneachill",
|
| 6018 |
+
"village::grimley",
|
| 6019 |
+
"village::ripple",
|
| 6020 |
+
"village::new end",
|
| 6021 |
+
"village::ryall",
|
| 6022 |
+
"village::flyford flavell",
|
| 6023 |
+
"village::harvington evesham",
|
| 6024 |
+
"village::hanley childe",
|
| 6025 |
+
"village::earls common",
|
| 6026 |
+
"village::charlton",
|
| 6027 |
+
"village::sytchampton",
|
| 6028 |
+
"village::overbury",
|
| 6029 |
+
"village::shatterford",
|
| 6030 |
+
"village::newnham bridge",
|
| 6031 |
+
"village::peopleton",
|
| 6032 |
+
"village::ombersley",
|
| 6033 |
+
"village::pirton",
|
| 6034 |
+
"village::south littleton",
|
| 6035 |
+
"village::church lench",
|
| 6036 |
+
"village::cleeve prior",
|
| 6037 |
+
"village::sledge green",
|
| 6038 |
"village::aston somerville",
|
| 6039 |
+
"village::stoke prior",
|
| 6040 |
+
"village::norchard",
|
| 6041 |
+
"village::honeybourne",
|
| 6042 |
+
"village::sedgeberrow",
|
| 6043 |
+
"village::throckmorton",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6044 |
"village::dunley",
|
| 6045 |
+
"village::pound green",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6046 |
"village::ashton under hill",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6047 |
"village::kemerton",
|
| 6048 |
+
"village::belbroughton",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6049 |
"village::north littleton",
|
| 6050 |
+
"village::clifton teme",
|
| 6051 |
+
"village::romsley",
|
| 6052 |
+
"village::fernhill heath",
|
| 6053 |
+
"village::middle littleton",
|
| 6054 |
+
"village::crowle",
|
| 6055 |
+
"village::kington",
|
| 6056 |
+
"village::drayton",
|
| 6057 |
+
"village::netherton",
|
| 6058 |
+
"service::Mobile library",
|
| 6059 |
+
"village::bickmarsh",
|
| 6060 |
+
"village::severn stoke",
|
| 6061 |
+
"village::broughton hackett",
|
| 6062 |
+
"village::lower sapey",
|
| 6063 |
+
"village::uckinghall",
|
| 6064 |
+
"village::chatley",
|
| 6065 |
+
"village::kempsey",
|
| 6066 |
+
"village::broadwas",
|
| 6067 |
+
"village::longley green",
|
| 6068 |
+
"village::norton",
|
| 6069 |
+
"village::bretforton",
|
| 6070 |
+
"village::pinvin",
|
| 6071 |
+
"village::littleworth",
|
| 6072 |
+
"village::dormston",
|
| 6073 |
+
"village::feckenham",
|
| 6074 |
+
"village::white ladies aston",
|
| 6075 |
+
"village::bliss gate",
|
| 6076 |
+
"village::broad green",
|
| 6077 |
+
"village::lower moor",
|
| 6078 |
"village::hook",
|
|
|
|
| 6079 |
"village::collets green",
|
| 6080 |
+
"village::westmancote",
|
| 6081 |
+
"village::alfrick",
|
|
|
|
|
|
|
| 6082 |
"village::newtown",
|
| 6083 |
+
"village::elmbridge",
|
|
|
|
|
|
|
|
|
|
| 6084 |
"village::bredon",
|
| 6085 |
+
"village::hunnington",
|
| 6086 |
+
"village::radford",
|
| 6087 |
+
"village::castlemorton",
|
| 6088 |
+
"village::cropthorne",
|
| 6089 |
+
"village::eckington",
|
| 6090 |
+
"village::great witley",
|
| 6091 |
+
"village::astley",
|
| 6092 |
+
"village::eardiston",
|
| 6093 |
+
"village::upton snodsbury",
|
| 6094 |
+
"village::kinnersley",
|
| 6095 |
+
"village::little witley",
|
| 6096 |
+
"village::hanbury",
|
| 6097 |
+
"village::callow hill stourport",
|
| 6098 |
+
"village::ab lench",
|
| 6099 |
+
"village::kinsham",
|
| 6100 |
+
"village::habberley",
|
| 6101 |
+
"village::stock green",
|
| 6102 |
+
"village::callow end",
|
| 6103 |
+
"village::kyre",
|
| 6104 |
+
"village::sale green",
|
| 6105 |
+
"village::wichenford",
|
| 6106 |
+
"village::beckford",
|
| 6107 |
+
"village::bayton",
|
| 6108 |
+
"village::suckley",
|
| 6109 |
"village::hadley",
|
| 6110 |
+
"village::clows top",
|
| 6111 |
+
"village::stoke bliss",
|
| 6112 |
+
"village::portway",
|
| 6113 |
+
"village::tibberton",
|
| 6114 |
+
"village::wadborough",
|
| 6115 |
+
"village::naunton",
|
| 6116 |
+
"village::inkberrow",
|
| 6117 |
"village::summerfield",
|
| 6118 |
+
"village::shrawley",
|
|
|
|
| 6119 |
"village::mamble",
|
| 6120 |
+
"village::bishampton",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6121 |
"village::drakes broughton",
|
| 6122 |
+
"village::caunsall",
|
| 6123 |
+
"village::far forest",
|
| 6124 |
"village::lower broadheath",
|
| 6125 |
+
"village::leigh",
|
| 6126 |
+
"village::stone",
|
| 6127 |
+
"village::harvington stourport",
|
|
|
|
|
|
|
|
|
|
| 6128 |
"village::bushley",
|
| 6129 |
+
"village::hallow",
|
| 6130 |
+
"village::hanley swan",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6131 |
"village::cookhill",
|
| 6132 |
+
"village::pensax",
|
| 6133 |
+
"village::bredicot",
|
| 6134 |
+
"village::cookley",
|
| 6135 |
+
"village::harpley",
|
| 6136 |
+
"village::menith wood",
|
| 6137 |
"village::chaddesley corbett",
|
| 6138 |
+
"village::corse lawn",
|
| 6139 |
+
"village::astwood bank",
|
| 6140 |
+
"village::longdon",
|
| 6141 |
+
"village::himbleton",
|
| 6142 |
+
"village::eldersfield",
|
| 6143 |
+
"village::blakedown",
|
| 6144 |
+
"village::bradley green",
|
| 6145 |
+
"village::pendock",
|
| 6146 |
+
"village::wolverley",
|
| 6147 |
+
"village::bredons norton",
|
| 6148 |
+
"village::guarlford",
|
| 6149 |
+
"village::leigh sinton",
|
| 6150 |
+
"village::pebworth",
|
| 6151 |
+
"village::childswickham",
|
| 6152 |
+
"village::holt heath",
|
| 6153 |
+
"village::rous lench",
|
| 6154 |
"village::bridges stone",
|
| 6155 |
+
"village::callow hill redditch",
|
| 6156 |
+
"village::wychbold",
|
| 6157 |
+
"village::hatfield",
|
| 6158 |
+
"village::lye head",
|
| 6159 |
+
"village::powick",
|
| 6160 |
+
"village::newland",
|
| 6161 |
+
"village::rock",
|
| 6162 |
+
"village::bransford",
|
| 6163 |
+
"village::ockeridge",
|
| 6164 |
"village::little clevelode",
|
| 6165 |
+
"village::grafton flyford",
|
| 6166 |
+
"village::abberley",
|
| 6167 |
+
"village::button oak",
|
| 6168 |
+
"village::woodgate",
|
| 6169 |
+
"village::fladbury",
|
| 6170 |
+
"village::stoulton"
|
| 6171 |
],
|
| 6172 |
"report": "Service: Mobile library | Village: Ab Lench, Abberley, Alfrick, Ashton Under Hill, Astley, Aston Somerville, Astwood Bank, Bayton, Beckford, Belbroughton, Bickmarsh, Bishampton"
|
| 6173 |
},
|
| 6174 |
{
|
| 6175 |
"id": "community::1",
|
| 6176 |
+
"title": "Cluster 2: 22 branchs, 17 areas, 11 facilitys",
|
| 6177 |
+
"size": 50,
|
| 6178 |
"members": [
|
| 6179 |
+
"branch::Tenbury Wells Library",
|
| 6180 |
+
"branch::Kidderminster Library",
|
| 6181 |
+
"facility::public computers",
|
| 6182 |
+
"branch::Bewdley Library",
|
| 6183 |
+
"area::Redditch",
|
| 6184 |
"branch::Martley Library",
|
| 6185 |
+
"facility::self-service",
|
| 6186 |
+
"area::Birmingham",
|
| 6187 |
+
"branch::Pershore Library",
|
| 6188 |
+
"branch::Droitwich Spa Library",
|
| 6189 |
+
"branch::Catshill Community Library",
|
| 6190 |
+
"area::Broadway",
|
| 6191 |
+
"branch::Upton-Upon-Severn Library",
|
| 6192 |
+
"facility::wheelchair access",
|
| 6193 |
"area::Bewdley",
|
|
|
|
|
|
|
| 6194 |
"branch::Malvern Library",
|
| 6195 |
+
"facility::study space",
|
| 6196 |
+
"area::Wells",
|
|
|
|
|
|
|
|
|
|
| 6197 |
"area::Pershore",
|
| 6198 |
+
"branch::Warndon Library",
|
| 6199 |
+
"branch::Stourport-on-Severn Library",
|
| 6200 |
+
"branch::Alvechurch Library",
|
| 6201 |
+
"branch::Bromsgrove Library",
|
| 6202 |
+
"area::Worcester",
|
| 6203 |
+
"area::Welland",
|
| 6204 |
+
"branch::Redditch Library",
|
| 6205 |
"area::Severn",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6206 |
"branch::Evesham Library",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6207 |
"branch::St. John's Library",
|
| 6208 |
+
"area::Spa",
|
| 6209 |
+
"facility::public toilets",
|
| 6210 |
+
"branch::Broadway Library",
|
|
|
|
| 6211 |
"area::Evesham",
|
| 6212 |
+
"facility::free Wi-Fi",
|
| 6213 |
+
"branch::Wythall Library",
|
| 6214 |
+
"branch::Welland Library",
|
|
|
|
| 6215 |
"facility::printing",
|
| 6216 |
+
"facility::baby changing",
|
| 6217 |
+
"facility::meeting rooms",
|
| 6218 |
+
"facility::accessible toilet",
|
| 6219 |
+
"branch::Hagley Library",
|
| 6220 |
+
"facility::cafΓ©",
|
| 6221 |
+
"branch::Woodrow Library",
|
| 6222 |
"area::Stourbridge",
|
| 6223 |
+
"area::Martley",
|
| 6224 |
+
"area::Kidderminster",
|
| 6225 |
+
"area::Malvern",
|
| 6226 |
+
"area::Bromsgrove",
|
| 6227 |
+
"area::Upton-Upon-Severn",
|
| 6228 |
+
"branch::Rubery Library"
|
| 6229 |
],
|
| 6230 |
+
"report": "Area: Bewdley, Birmingham, Broadway, Bromsgrove, Evesham, Kidderminster, Malvern, Martley, Pershore, Redditch, Severn, Spa | Branch: Alvechurch Library, Bewdley Library, Broadway Library, Bromsgrove Library, Catshill Community Library, Droitwich Spa Library, Evesham Library, Hagley Library, Kidderminster Library, Malvern Library, Martley Library, Pershore Library | Facility: accessible toilet, baby changing, cafΓ©, free Wi-Fi, meeting rooms, printing, public computers, public toilets, self-service, study space, wheelchair access"
|
| 6231 |
},
|
| 6232 |
{
|
| 6233 |
"id": "community::2",
|
| 6234 |
"title": "Cluster 3: 18 services, 17 resources, 2 memberships",
|
| 6235 |
"size": 40,
|
| 6236 |
"members": [
|
| 6237 |
+
"service::Renew a loan",
|
| 6238 |
+
"tier::Digital membership",
|
| 6239 |
+
"service::Join the library",
|
| 6240 |
+
"topic::membership",
|
|
|
|
|
|
|
|
|
|
| 6241 |
"resource::Oxford English Dictionary",
|
| 6242 |
+
"service::Business facilities at Worcestershire libraries",
|
|
|
|
|
|
|
|
|
|
| 6243 |
"resource::Online events",
|
| 6244 |
+
"service::Business networking",
|
| 6245 |
+
"resource::Oxford Research Encyclopaedias",
|
| 6246 |
"service::Login to my library account",
|
| 6247 |
+
"resource::Which?",
|
| 6248 |
+
"resource::Access to Research",
|
| 6249 |
+
"resource::Ancestry",
|
| 6250 |
+
"service::Printing and photocopying services",
|
|
|
|
| 6251 |
"resource::British Film Institution (BFI Replay)",
|
| 6252 |
+
"topic::business",
|
| 6253 |
"hub::online",
|
| 6254 |
+
"service::Protect your business",
|
|
|
|
| 6255 |
"resource::EBSCO",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6256 |
"resource::ESPACENET",
|
| 6257 |
+
"service::Pay fees and charges",
|
| 6258 |
+
"resource::Pressreader",
|
|
|
|
| 6259 |
"resource::Digital library membership",
|
| 6260 |
+
"service::Library Service at Home",
|
| 6261 |
+
"resource::Theory Test Pro",
|
| 6262 |
+
"service::Emails and updates",
|
| 6263 |
+
"service::Start or grow a business",
|
| 6264 |
+
"resource::Oxford Dictionary of National Biography",
|
| 6265 |
+
"service::About BIPC Worcestershire",
|
| 6266 |
+
"service::Book a computer",
|
| 6267 |
+
"resource::COBRA (Business Support)",
|
| 6268 |
+
"resource::Borrowbox",
|
| 6269 |
+
"resource::Oxford Reference",
|
| 6270 |
+
"service::Get business books online",
|
| 6271 |
+
"tier::Full membership",
|
| 6272 |
+
"service::Business database reference resources",
|
| 6273 |
+
"resource::The Times Digital Archive",
|
| 6274 |
+
"service::Explore free online library resources for learning, research and business",
|
| 6275 |
+
"service::Your library membership",
|
| 6276 |
+
"service::Reserve your library books"
|
| 6277 |
],
|
| 6278 |
"report": "Hub: Online library hub | Membership: Digital membership, Full membership | Resource: Access to Research, Ancestry, Borrowbox, British Film Institution (BFI Replay), COBRA (Business Support), Digital library membership, EBSCO, ESPACENET, Online events, Oxford Dictionary of National Biography, Oxford English Dictionary, Oxford Reference | Service: About BIPC Worcestershire, Book a computer, Business database reference resources, Business facilities at Worcestershire libraries, Business networking, Emails and updates, Explore free online library resources for learning, research and business, Get business books online, Join the library, Library Service at Home, Login to my library account, Pay fees and charges | Topic: Business, Membership"
|
| 6279 |
},
|
| 6280 |
{
|
| 6281 |
"id": "community::3",
|
| 6282 |
+
"title": "Cluster 4: 35 services, 1 membership",
|
| 6283 |
+
"size": 37,
|
| 6284 |
"members": [
|
| 6285 |
+
"service::Hop into Spring with Easter-themed activities at your local library!",
|
| 6286 |
+
"service::Survey launched to help shape library services across the county",
|
| 6287 |
+
"service::Worcestershireβs Library Service at Home is changing lives",
|
| 6288 |
"service::Spine-tingling events this autumn at your local library",
|
|
|
|
|
|
|
| 6289 |
"service::Worcestershireβs Libraries inspire local primary school teachers",
|
| 6290 |
+
"service::Library volunteering provides a new chapter for retiree Paul",
|
| 6291 |
+
"service::Libraries Unlocked Launches Next Week at Redditch Library!",
|
| 6292 |
"service::Acclaimed Authors Set to Visit Worcestershireβs Libraries This Autumn",
|
|
|
|
|
|
|
| 6293 |
"service::Rooms for hire at your local library β book online today",
|
| 6294 |
+
"service::Early years information library",
|
|
|
|
|
|
|
| 6295 |
"service::Libraries Unlocked Launches Next Week at Tenbury Library!",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6296 |
"service::Worcestershire library closures announced over Easter holidays",
|
|
|
|
| 6297 |
"service::Worcestershire Libraries to Host Second βPower of Libraries Conferenceβ for Early Years Professionals",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6298 |
"service::Online library hub",
|
| 6299 |
+
"service::Discover Volunteering Opportunities at your local library",
|
| 6300 |
+
"service::2026 Libraries closing dates",
|
| 6301 |
+
"service::Improve Your Digital Skills at your Local Library",
|
| 6302 |
+
"service::Libraries Unlocked",
|
| 6303 |
+
"service::No Printer? No Problem. Libraries Launch βPrint Your Wayβ!",
|
| 6304 |
"service::New library book collection to guide families through pregnancy and early parenthood",
|
| 6305 |
+
"service::Memories and Me",
|
| 6306 |
+
"service::Find a library",
|
| 6307 |
+
"tier::Libraries Unlocked",
|
| 6308 |
+
"service::Worcestershireβs Free Online Library - Perfect for the Festive Season",
|
| 6309 |
+
"service::Alvechurch Library - New layout for all library pages V1",
|
| 6310 |
+
"service::Libraries",
|
| 6311 |
+
"service::Coming Soon: Free Dementia Events at Worcestershire's Libraries",
|
| 6312 |
+
"service::Libraries Unlocked is coming soon to Redditch Library!",
|
| 6313 |
+
"service::Discover the stories that shaped our town this May at Kidderminster Library!",
|
| 6314 |
+
"service::Hire a library meeting room",
|
| 6315 |
+
"service::Exciting STEAM themed event coming to The Hive Library!",
|
| 6316 |
+
"service::Alvechurch Library - New layout for all library pages V2",
|
| 6317 |
+
"topic::general",
|
| 6318 |
+
"service::Worcestershire libraries get prepared for emergencies this βSave a Life Septemberβ",
|
| 6319 |
+
"service::Library events and activities",
|
| 6320 |
+
"service::Great news for Malvernβs library members as Libraries Unlocked launches next week",
|
| 6321 |
+
"service::Libraries Unlocked is coming soon to Tenbury Library!"
|
| 6322 |
],
|
| 6323 |
+
"report": "Membership: Libraries Unlocked | Service: 2026 Libraries closing dates, Acclaimed Authors Set to Visit Worcestershireβs Libraries This Autumn, Alvechurch Library - New layout for all library pages V1, Alvechurch Library - New layout for all library pages V2, Coming Soon: Free Dementia Events at Worcestershire's Libraries, Discover Volunteering Opportunities at your local library, Discover the stories that shaped our town this May at Kidderminster Library!, Early years information library, Exciting STEAM themed event coming to The Hive Library!, Find a library, Great news for Malvernβs library members as Libraries Unlocked launches next week, Hire a library meeting room | Topic: General"
|
| 6324 |
},
|
| 6325 |
{
|
| 6326 |
"id": "community::4",
|
| 6327 |
+
"title": "Cluster 5: 1 branch, 3 facilitys",
|
| 6328 |
+
"size": 30,
|
| 6329 |
+
"members": [
|
| 6330 |
+
"hive::Prayer space",
|
| 6331 |
+
"hive::Walk-in academic research databases",
|
| 6332 |
+
"hive::Room hire / Space for hire with online booking and video conferencing",
|
| 6333 |
+
"facility::children's library",
|
| 6334 |
+
"hive::Special collections",
|
| 6335 |
+
"hive::Three online catalogues",
|
| 6336 |
+
"hive::Commissioned public art collection",
|
| 6337 |
+
"facility::exhibition space",
|
| 6338 |
+
"hive::On-site hospitality catering (Bellrock)",
|
| 6339 |
+
"hive::Accessibility provision",
|
| 6340 |
+
"hive::Award-winning sustainable landmark building",
|
| 6341 |
+
"hive::Explore the Past - Worcestershire Archive & Archaeology Service (Level 2)",
|
| 6342 |
+
"hive::Children's library - one of the largest in the country (Level 1)",
|
| 6343 |
+
"hive::Cafe at The Hive (Level 1)",
|
| 6344 |
+
"facility::archives",
|
| 6345 |
+
"hive::Careers Worcs Youth Hub (Level 1)",
|
| 6346 |
+
"hive::Research collections for family and local history",
|
| 6347 |
+
"hive::The Studio theatre, exhibition spaces and Worcestershire Hub (Level 1)",
|
| 6348 |
+
"hive::800+ study spaces and bookable study spaces for students",
|
| 6349 |
+
"hive::First joint public and university library in Europe",
|
| 6350 |
+
"branch::The Hive",
|
| 6351 |
+
"hive::WAAS learning and outreach programme",
|
| 6352 |
+
"hive::Membership without barriers",
|
| 6353 |
+
"hive::Archaeology planning advice for developers (professional service)",
|
| 6354 |
+
"hive::BorrowBox digital library (eBooks/eAudio)",
|
| 6355 |
+
"hive::Business & IP Centre (BIPC) Worcestershire (Level 2)",
|
| 6356 |
+
"hive::Historic Environment Record (HER) access",
|
| 6357 |
+
"hive::Exceptionally long opening hours",
|
| 6358 |
+
"hive::IT, gaming and printing on every level",
|
| 6359 |
+
"hive::School and college visit packages"
|
| 6360 |
+
],
|
| 6361 |
+
"report": "Branch: The Hive | Facility: archives, children's library, exhibition space | HiveService: 800+ study spaces and bookable study spaces for students, Accessibility provision, Archaeology planning advice for developers (professional service), Award-winning sustainable landmark building, BorrowBox digital library (eBooks/eAudio), Business & IP Centre (BIPC) Worcestershire (Level 2), Cafe at The Hive (Level 1), Careers Worcs Youth Hub (Level 1), Children's library - one of the largest in the country (Level 1), Commissioned public art collection, Exceptionally long opening hours, Explore the Past - Worcestershire Archive & Archaeology Service (Level 2)"
|
| 6362 |
+
},
|
| 6363 |
+
{
|
| 6364 |
+
"id": "community::5",
|
| 6365 |
+
"title": "Cluster 6: 13 services",
|
| 6366 |
"size": 14,
|
| 6367 |
"members": [
|
|
|
|
|
|
|
| 6368 |
"service::Digital Inclusion - Helping You Online",
|
| 6369 |
"service::Key Stage 1 and 2 (years 1 to 6)",
|
|
|
|
|
|
|
|
|
|
| 6370 |
"service::Learning Outside the Classroom",
|
| 6371 |
+
"service::Key Stage 3 and 4 (years 7 to 11)",
|
|
|
|
|
|
|
|
|
|
| 6372 |
"topic::learning",
|
| 6373 |
+
"service::Study Happy",
|
| 6374 |
+
"service::Sixth form and college library support",
|
| 6375 |
+
"service::Adult Learning Courses in libraries",
|
| 6376 |
+
"service::Online job-seeking support",
|
| 6377 |
+
"service::Early Years and Reception",
|
| 6378 |
+
"service::Job clubs",
|
| 6379 |
+
"service::Book a visit",
|
| 6380 |
+
"service::Learn, upskill and find work",
|
| 6381 |
+
"service::Volunteering, training and work experience"
|
| 6382 |
],
|
| 6383 |
"report": "Service: Adult Learning Courses in libraries, Book a visit, Digital Inclusion - Helping You Online, Early Years and Reception, Job clubs, Key Stage 1 and 2 (years 1 to 6), Key Stage 3 and 4 (years 7 to 11), Learn, upskill and find work, Learning Outside the Classroom, Online job-seeking support, Sixth form and college library support, Study Happy | Topic: Learning"
|
| 6384 |
},
|
| 6385 |
{
|
| 6386 |
+
"id": "community::6",
|
| 6387 |
+
"title": "Cluster 7: 11 services",
|
| 6388 |
"size": 12,
|
| 6389 |
"members": [
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6390 |
"topic::seasonal",
|
| 6391 |
+
"service::Get involved with the National Year of Reading 2026",
|
| 6392 |
+
"service::Celebrate World Book Day at your local library",
|
| 6393 |
+
"service::Free family fun this summer with Worcestershire Libraries & National Trust Croome",
|
| 6394 |
"service::Young Poet Laureate",
|
| 6395 |
+
"service::Libraries βGo All Inβ for the National Year of Reading 2026",
|
| 6396 |
+
"service::Get school ready with Worcestershire's libraries",
|
| 6397 |
"service::Improve Your Digital Skills at Worcestershire Libraries This Summer!",
|
| 6398 |
"service::Summer Reading Challenge",
|
| 6399 |
+
"service::Library Christmas and New Year opening times",
|
| 6400 |
"service::Library Christmas & New Year Opening Times",
|
| 6401 |
+
"service::Aged 13β24? Volunteer with Worcestershireβs Libraries this summer"
|
|
|
|
|
|
|
| 6402 |
],
|
| 6403 |
"report": "Service: Aged 13β24? Volunteer with Worcestershireβs Libraries this summer, Celebrate World Book Day at your local library, Free family fun this summer with Worcestershire Libraries & National Trust Croome, Get involved with the National Year of Reading 2026, Get school ready with Worcestershire's libraries, Improve Your Digital Skills at Worcestershire Libraries This Summer!, Libraries βGo All Inβ for the National Year of Reading 2026, Library Christmas & New Year Opening Times, Library Christmas and New Year opening times, Summer Reading Challenge, Young Poet Laureate | Topic: Seasonal"
|
| 6404 |
},
|
| 6405 |
{
|
| 6406 |
+
"id": "community::7",
|
| 6407 |
+
"title": "Cluster 8: 5 services",
|
| 6408 |
"size": 6,
|
| 6409 |
"members": [
|
|
|
|
| 6410 |
"service::Reading well",
|
|
|
|
| 6411 |
"topic::reading",
|
| 6412 |
+
"service::'Read & Unwind Time' at your local library",
|
| 6413 |
"service::Read and discover",
|
| 6414 |
+
"service::Worcestershire Reading Groups Recommend",
|
| 6415 |
"service::Ask for a Book"
|
| 6416 |
],
|
| 6417 |
"report": "Service: 'Read & Unwind Time' at your local library, Ask for a Book, Read and discover, Reading well, Worcestershire Reading Groups Recommend | Topic: Reading"
|
| 6418 |
},
|
| 6419 |
{
|
| 6420 |
+
"id": "community::8",
|
| 6421 |
+
"title": "Cluster 9: 4 services",
|
| 6422 |
"size": 5,
|
| 6423 |
"members": [
|
|
|
|
|
|
|
|
|
|
| 6424 |
"service::Libraries offer a free warm space to its residents this winter",
|
| 6425 |
+
"service::Connect with others and boost your wellbeing",
|
| 6426 |
+
"service::Stay warm and connected at your local library this winter",
|
| 6427 |
+
"topic::wellbeing",
|
| 6428 |
+
"service::Warm welcome"
|
| 6429 |
],
|
| 6430 |
"report": "Service: Connect with others and boost your wellbeing, Libraries offer a free warm space to its residents this winter, Stay warm and connected at your local library this winter, Warm welcome | Topic: Wellbeing"
|
| 6431 |
}
|
library_sources.py
CHANGED
|
@@ -1,12 +1,21 @@
|
|
| 1 |
"""
|
| 2 |
-
library_sources.py β
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
No LLM, no Gradio in here β pure functions so they can be unit-tested
|
| 12 |
against the live sites on their own.
|
|
@@ -35,6 +44,9 @@ CATALOGUE_RSS = "https://wcc.ent.sirsidynix.net.uk/client/rss/hitlist/wcc/qu="
|
|
| 35 |
CATALOGUE_SEARCH = (
|
| 36 |
"https://wcc.ent.sirsidynix.net.uk/client/en_GB/wcc/search/results?qu="
|
| 37 |
)
|
|
|
|
|
|
|
|
|
|
| 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"
|
|
@@ -139,6 +151,10 @@ def search_catalogue(query: str, limit: int = 8) -> dict:
|
|
| 139 |
break
|
| 140 |
|
| 141 |
fmt, digital = _classify(fields)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
items.append({
|
| 143 |
"title": title,
|
| 144 |
"author": fields.get("author", ""),
|
|
@@ -147,6 +163,7 @@ def search_catalogue(query: str, limit: int = 8) -> dict:
|
|
| 147 |
"year": fields.get("Publication Date", "").split()[0] if fields.get("Publication Date") else "",
|
| 148 |
"isbn": fields.get("ISBN", ""),
|
| 149 |
"digital": digital,
|
|
|
|
| 150 |
"detail_url": detail,
|
| 151 |
"catalogue_id": cat_id,
|
| 152 |
})
|
|
@@ -391,6 +408,9 @@ ELIGIBILITY = {
|
|
| 391 |
"unlocked": "Full member, aged 15+, after a short one-off staff induction.",
|
| 392 |
"events": "Most events are free β just turn up; a few need booking.",
|
| 393 |
"visit": "Nothing at all β anyone can walk in for Wi-Fi, toilets and study space.",
|
|
|
|
|
|
|
|
|
|
| 394 |
}
|
| 395 |
|
| 396 |
DAY_NAMES = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday",
|
|
@@ -672,6 +692,313 @@ def membership_help(service: str | None = None) -> dict:
|
|
| 672 |
return out
|
| 673 |
|
| 674 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 675 |
def whats_new(genre: str | None = None, limit: int = 6) -> dict:
|
| 676 |
"""Newest catalogue titles for a genre/topic β fuel for a fun 'hot take'."""
|
| 677 |
term = (genre or "fiction").strip()
|
|
@@ -722,3 +1049,30 @@ if __name__ == "__main__":
|
|
| 722 |
res = printing_help()
|
| 723 |
print(" ", res["summary"][:80], "...")
|
| 724 |
print(" pricing:", res["pricing"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
library_sources.py β data-mining tools for Worcestershire Libraries,
|
| 3 |
+
at every granularity:
|
| 4 |
+
|
| 5 |
+
service level β every council library page (library_kb.json, live)
|
| 6 |
+
page level β every page of thehiveworcester.org (hive_kb.json)
|
| 7 |
+
item level β the live SirsiDynix catalogue (Atom feed)
|
| 8 |
+
copy level β per-branch holdings on an item's detail page, so we can
|
| 9 |
+
say WHERE to actually get the book/eBook someone wants
|
| 10 |
+
|
| 11 |
+
Sources & provenance policy:
|
| 12 |
+
- the SirsiDynix Enterprise catalogue (wcc.ent.sirsidynix.net.uk) β live
|
| 13 |
+
- worcestershire.gov.uk library pages β live + crawled KB (canonical)
|
| 14 |
+
- thehiveworcester.org β crawled page-by-page (build_hive_kb.py); every
|
| 15 |
+
fact carries its source page + crawl date. Where Hive and council pages
|
| 16 |
+
conflict (hours, prices, membership), the COUNCIL page wins, and answers
|
| 17 |
+
say which source they used. The Hive site's own events page is static,
|
| 18 |
+
so live events always come from the council.
|
| 19 |
|
| 20 |
No LLM, no Gradio in here β pure functions so they can be unit-tested
|
| 21 |
against the live sites on their own.
|
|
|
|
| 44 |
CATALOGUE_SEARCH = (
|
| 45 |
"https://wcc.ent.sirsidynix.net.uk/client/en_GB/wcc/search/results?qu="
|
| 46 |
)
|
| 47 |
+
HIVE = "https://www.thehiveworcester.org"
|
| 48 |
+
BORROWBOX = "https://library.bolindadigital.com/worcestershire"
|
| 49 |
+
EXPLORE_PAST = "https://www.explorethepast.co.uk"
|
| 50 |
MOBILE_INDEX = f"{GOV}/council-services/libraries/your-library-membership/mobile-library"
|
| 51 |
EVENTS_URL = f"{GOV}/council-services/libraries/library-events-and-activities"
|
| 52 |
PRINTING_URL = f"{GOV}/council-services/libraries/printing-and-photocopying-services"
|
|
|
|
| 151 |
break
|
| 152 |
|
| 153 |
fmt, digital = _classify(fields)
|
| 154 |
+
ea_url = ""
|
| 155 |
+
if fields.get("Electronic Access"):
|
| 156 |
+
m = re.search(r"https?://\S+", fields["Electronic Access"])
|
| 157 |
+
ea_url = m.group(0).rstrip('".,)') if m else ""
|
| 158 |
items.append({
|
| 159 |
"title": title,
|
| 160 |
"author": fields.get("author", ""),
|
|
|
|
| 163 |
"year": fields.get("Publication Date", "").split()[0] if fields.get("Publication Date") else "",
|
| 164 |
"isbn": fields.get("ISBN", ""),
|
| 165 |
"digital": digital,
|
| 166 |
+
"electronic_access": ea_url,
|
| 167 |
"detail_url": detail,
|
| 168 |
"catalogue_id": cat_id,
|
| 169 |
})
|
|
|
|
| 408 |
"unlocked": "Full member, aged 15+, after a short one-off staff induction.",
|
| 409 |
"events": "Most events are free β just turn up; a few need booking.",
|
| 410 |
"visit": "Nothing at all β anyone can walk in for Wi-Fi, toilets and study space.",
|
| 411 |
+
"hive_visit": "Nothing β The Hive is open to everyone, 8:30amβ10pm every day.",
|
| 412 |
+
"archives": "Free to visit Explore the Past (Level 2, The Hive); opening "
|
| 413 |
+
f"times and document-ordering rules are on [Explore the Past]({EXPLORE_PAST}).",
|
| 414 |
}
|
| 415 |
|
| 416 |
DAY_NAMES = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday",
|
|
|
|
| 692 |
return out
|
| 693 |
|
| 694 |
|
| 695 |
+
# --------------------------------------------------------------------------- #
|
| 696 |
+
# 6. Copy-level availability + the "where do I actually GET it" journey
|
| 697 |
+
# --------------------------------------------------------------------------- #
|
| 698 |
+
|
| 699 |
+
_STATUS_AVAILABLE = re.compile(r"\b(available|on shelf|in library)\b", re.I)
|
| 700 |
+
_STATUS_WORDS = re.compile(
|
| 701 |
+
r"\b(available|on shelf|in library|checked out|on loan|due back|due \d|"
|
| 702 |
+
r"in transit|on order|on hold(shelf)?|reserved|missing|lost|reference only|"
|
| 703 |
+
r"not for loan|being catalogued)\b", re.I)
|
| 704 |
+
_CALL_NO = re.compile(r"\b([A-Z]{0,3}\s?\d{3}(?:\.\d+)?\s?[A-Z]{0,4}|[A-Z]{1,3}\s?FIC[A-Z ]*)\b")
|
| 705 |
+
|
| 706 |
+
|
| 707 |
+
def _branch_names() -> list[str]:
|
| 708 |
+
names = [b["name"] for b in kb().get("branches", [])]
|
| 709 |
+
extra = ["The Hive", "Mobile Library"]
|
| 710 |
+
out = names + [n for n in extra if n not in names]
|
| 711 |
+
# also match without the trailing " Library"
|
| 712 |
+
return sorted(out, key=len, reverse=True)
|
| 713 |
+
|
| 714 |
+
|
| 715 |
+
def _item_availability(detail_url: str) -> dict:
|
| 716 |
+
"""
|
| 717 |
+
Copy-level holdings for one catalogue item: which branch, what call number,
|
| 718 |
+
what status β parsed from the SirsiDynix detail page.
|
| 719 |
+
|
| 720 |
+
The page is JS-heavy, so we parse defensively (tables first, then a text
|
| 721 |
+
scan anchored on known branch names) and FAIL SOFT: if we can't parse,
|
| 722 |
+
we say so rather than guess. Returns
|
| 723 |
+
{"copies":[{branch, call_number, status, available}], "parsed": bool}.
|
| 724 |
+
"""
|
| 725 |
+
out = {"copies": [], "parsed": False, "detail_url": detail_url}
|
| 726 |
+
if not detail_url:
|
| 727 |
+
return out
|
| 728 |
+
try:
|
| 729 |
+
soup = BeautifulSoup(_get(detail_url).text, "html.parser")
|
| 730 |
+
except Exception as e:
|
| 731 |
+
out["error"] = str(e)
|
| 732 |
+
return out
|
| 733 |
+
|
| 734 |
+
branches = _branch_names()
|
| 735 |
+
|
| 736 |
+
def add_copy(branch, call_no, status):
|
| 737 |
+
status = re.sub(r"\s+", " ", status).strip()
|
| 738 |
+
out["copies"].append({
|
| 739 |
+
"branch": branch.strip(),
|
| 740 |
+
"call_number": (call_no or "").strip(),
|
| 741 |
+
"status": status,
|
| 742 |
+
"available": bool(_STATUS_AVAILABLE.search(status)),
|
| 743 |
+
})
|
| 744 |
+
|
| 745 |
+
# Strategy 1 β a holdings table (header mentions library/branch + status)
|
| 746 |
+
for table in soup.find_all("table"):
|
| 747 |
+
head = " ".join(th.get_text(" ", strip=True).lower()
|
| 748 |
+
for th in table.find_all(["th"]))
|
| 749 |
+
if not (("librar" in head or "branch" in head or "location" in head)
|
| 750 |
+
and ("status" in head or "avail" in head or "call" in head)):
|
| 751 |
+
continue
|
| 752 |
+
headers = [th.get_text(" ", strip=True).lower()
|
| 753 |
+
for th in table.find_all("th")]
|
| 754 |
+
def col(*words):
|
| 755 |
+
return next((i for i, h in enumerate(headers)
|
| 756 |
+
if any(w in h for w in words)), None)
|
| 757 |
+
c_lib, c_call, c_stat = (col("librar", "branch", "location"),
|
| 758 |
+
col("call", "shelf"), col("status", "avail"))
|
| 759 |
+
for tr in table.find_all("tr")[1:]:
|
| 760 |
+
cells = [td.get_text(" ", strip=True) for td in tr.find_all("td")]
|
| 761 |
+
if not cells:
|
| 762 |
+
continue
|
| 763 |
+
lib = cells[c_lib] if c_lib is not None and c_lib < len(cells) else ""
|
| 764 |
+
stat = cells[c_stat] if c_stat is not None and c_stat < len(cells) else ""
|
| 765 |
+
call = cells[c_call] if c_call is not None and c_call < len(cells) else ""
|
| 766 |
+
if lib and (stat or call):
|
| 767 |
+
add_copy(lib, call, stat or "see catalogue")
|
| 768 |
+
if out["copies"]:
|
| 769 |
+
out["parsed"] = True
|
| 770 |
+
return out
|
| 771 |
+
|
| 772 |
+
# Strategy 2 β text scan anchored on known branch names
|
| 773 |
+
text = re.sub(r"\s+", " ", soup.get_text(" ", strip=True))
|
| 774 |
+
for b in branches:
|
| 775 |
+
for m in re.finditer(re.escape(b), text, re.I):
|
| 776 |
+
window = text[m.end():m.end() + 140]
|
| 777 |
+
sm = _STATUS_WORDS.search(window)
|
| 778 |
+
if sm:
|
| 779 |
+
cm = _CALL_NO.search(window[:sm.start()])
|
| 780 |
+
add_copy(b, cm.group(0) if cm else "", sm.group(0))
|
| 781 |
+
# dedupe
|
| 782 |
+
seen, copies = set(), []
|
| 783 |
+
for c in out["copies"]:
|
| 784 |
+
key = (c["branch"].lower(), c["status"].lower(), c["call_number"])
|
| 785 |
+
if key not in seen:
|
| 786 |
+
seen.add(key); copies.append(c)
|
| 787 |
+
out["copies"] = copies
|
| 788 |
+
out["parsed"] = bool(copies)
|
| 789 |
+
return out
|
| 790 |
+
|
| 791 |
+
|
| 792 |
+
def _title_sim(a: str, b: str) -> float:
|
| 793 |
+
return difflib.SequenceMatcher(None, a.lower(), b.lower()).ratio()
|
| 794 |
+
|
| 795 |
+
|
| 796 |
+
def where_to_get(query: str) -> dict:
|
| 797 |
+
"""
|
| 798 |
+
The full get-it journey for one title: every format the service holds and,
|
| 799 |
+
for each, exactly where/how to get it TODAY β
|
| 800 |
+
on the shelf at a named branch (copy-level, live) β
|
| 801 |
+
reserve it (sign in + Place Hold) β
|
| 802 |
+
borrow tonight on BorrowBox (eBook / eAudiobook) β
|
| 803 |
+
not held: ask staff / check the catalogue yourself.
|
| 804 |
+
"""
|
| 805 |
+
query = (query or "").strip()
|
| 806 |
+
try:
|
| 807 |
+
res = search_catalogue(query, limit=12)
|
| 808 |
+
except Exception as e: # catalogue unreachable β fail soft, keep the journey
|
| 809 |
+
res = {"items": [], "error": f"catalogue unreachable: {e}"}
|
| 810 |
+
base = {"query": query, "search_url": res.get("search_url",
|
| 811 |
+
CATALOGUE_SEARCH + quote_plus(query)), "checked": _now()}
|
| 812 |
+
if res.get("error") or not res.get("items"):
|
| 813 |
+
base.update({"found": False, "routes": [{
|
| 814 |
+
"route": "not_held",
|
| 815 |
+
"advice": "Nothing matched in the catalogue. Staff can often get "
|
| 816 |
+
"titles from other library services β ask in any branch, "
|
| 817 |
+
"or try different search words.",
|
| 818 |
+
"url": base["search_url"],
|
| 819 |
+
}]})
|
| 820 |
+
return base
|
| 821 |
+
|
| 822 |
+
items = sorted(res["items"], key=lambda i: _title_sim(query, i["title"]),
|
| 823 |
+
reverse=True)
|
| 824 |
+
best = items[0]
|
| 825 |
+
cluster = [i for i in items if _title_sim(best["title"], i["title"]) > 0.6]
|
| 826 |
+
routes = []
|
| 827 |
+
|
| 828 |
+
# 1) digital first β instant, tonight, no waiting
|
| 829 |
+
for fmt in ("eBook", "eAudiobook"):
|
| 830 |
+
d = next((i for i in cluster if i["format"] == fmt), None)
|
| 831 |
+
if d:
|
| 832 |
+
routes.append({
|
| 833 |
+
"route": "digital", "format": fmt, "title": d["title"],
|
| 834 |
+
"author": d["author"],
|
| 835 |
+
"url": d.get("electronic_access") or BORROWBOX,
|
| 836 |
+
"direct_link": bool(d.get("electronic_access")),
|
| 837 |
+
"need": ELIGIBILITY["borrow_digital"],
|
| 838 |
+
"steps": ["Open the link (or the BorrowBox app, service "
|
| 839 |
+
"'Worcestershire').",
|
| 840 |
+
"Sign in with your library card number + PIN β or get "
|
| 841 |
+
"instant digital membership with just a postcode.",
|
| 842 |
+
"Borrow it free; it auto-returns, so no fines."],
|
| 843 |
+
})
|
| 844 |
+
|
| 845 |
+
# 2) physical β copy-level: which branch has it on the shelf right now
|
| 846 |
+
phys = next((i for i in cluster if not i["digital"]), None)
|
| 847 |
+
if phys:
|
| 848 |
+
avail = _item_availability(phys["detail_url"])
|
| 849 |
+
on_shelf = [c for c in avail["copies"] if c["available"]]
|
| 850 |
+
if on_shelf:
|
| 851 |
+
routes.append({
|
| 852 |
+
"route": "shelf", "format": phys["format"],
|
| 853 |
+
"title": phys["title"], "author": phys["author"],
|
| 854 |
+
"copies": on_shelf[:8], "all_copies": avail["copies"][:12],
|
| 855 |
+
"need": ELIGIBILITY["borrow_physical"],
|
| 856 |
+
"url": phys["detail_url"],
|
| 857 |
+
})
|
| 858 |
+
else:
|
| 859 |
+
routes.append({
|
| 860 |
+
"route": "reserve", "format": phys["format"],
|
| 861 |
+
"title": phys["title"], "author": phys["author"],
|
| 862 |
+
"copies": avail["copies"][:8],
|
| 863 |
+
"availability_parsed": avail["parsed"],
|
| 864 |
+
"need": ELIGIBILITY["borrow_physical"],
|
| 865 |
+
"url": phys["detail_url"],
|
| 866 |
+
"steps": ["Open the item page and sign in (card number + PIN).",
|
| 867 |
+
"Choose 'Place Hold' and pick the branch you want to "
|
| 868 |
+
"collect from β any Worcestershire library or the van.",
|
| 869 |
+
"You'll be emailed when it's ready to collect. "
|
| 870 |
+
"Reservations are free."],
|
| 871 |
+
})
|
| 872 |
+
|
| 873 |
+
if not routes:
|
| 874 |
+
routes.append({"route": "not_held",
|
| 875 |
+
"advice": "I found similar titles but not that exact one β "
|
| 876 |
+
"check the matches below or ask staff in any branch.",
|
| 877 |
+
"url": base["search_url"]})
|
| 878 |
+
|
| 879 |
+
other = [f'{i["icon"]} {i["title"]} ({i["format"]})'
|
| 880 |
+
for i in items if i not in cluster][:4]
|
| 881 |
+
base.update({
|
| 882 |
+
"found": True, "best_title": best["title"], "author": best["author"],
|
| 883 |
+
"formats_held": sorted({i["format"] for i in cluster}),
|
| 884 |
+
"routes": routes, "other_matches": other,
|
| 885 |
+
})
|
| 886 |
+
return base
|
| 887 |
+
|
| 888 |
+
|
| 889 |
+
# --------------------------------------------------------------------------- #
|
| 890 |
+
# 7. The Hive β page-level KB of thehiveworcester.org (built by build_hive_kb.py)
|
| 891 |
+
# --------------------------------------------------------------------------- #
|
| 892 |
+
|
| 893 |
+
_HIVE_KB = None
|
| 894 |
+
|
| 895 |
+
|
| 896 |
+
def hive_kb() -> dict:
|
| 897 |
+
global _HIVE_KB
|
| 898 |
+
if _HIVE_KB is None:
|
| 899 |
+
path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
| 900 |
+
"hive_kb.json")
|
| 901 |
+
try:
|
| 902 |
+
with open(path, encoding="utf-8") as f:
|
| 903 |
+
_HIVE_KB = json.load(f)
|
| 904 |
+
except FileNotFoundError:
|
| 905 |
+
_HIVE_KB = {"pages": [], "hive_profile": {}, "generated": ""}
|
| 906 |
+
return _HIVE_KB
|
| 907 |
+
|
| 908 |
+
|
| 909 |
+
# question word -> hive page slug fragments that answer it
|
| 910 |
+
_HIVE_INTENTS = {
|
| 911 |
+
"hour": ["opening-hours"], "open": ["opening-hours"],
|
| 912 |
+
"parking": ["getting-here"], "park": ["getting-here"],
|
| 913 |
+
"get there": ["getting-here"], "directions": ["getting-here"],
|
| 914 |
+
"bus": ["getting-here"], "train": ["getting-here"],
|
| 915 |
+
"contact": ["contact-us"], "phone": ["contact-us"], "email": ["contact-us"],
|
| 916 |
+
"archive": ["explore-the-past"], "archaeolog": ["explore-the-past"],
|
| 917 |
+
"family history": ["explore-the-past"], "history": ["explore-the-past"],
|
| 918 |
+
"study": ["book-a-space"], "room": ["space-for-hire", "book-a-space"],
|
| 919 |
+
"hire": ["space-for-hire"], "meeting": ["space-for-hire"],
|
| 920 |
+
"card": ["how-to-get-a-library-card"], "join": ["how-to-get-a-library-card"],
|
| 921 |
+
"fine": ["fines-renewals"], "renew": ["fines-renewals"],
|
| 922 |
+
"borrow": ["borrowing-reservations"], "reserve": ["borrowing-reservations"],
|
| 923 |
+
"cafΓ©": ["cafe"], "cafe": ["cafe"], "coffee": ["cafe"], "food": ["cafe"],
|
| 924 |
+
"children": ["children"], "kids": ["children"],
|
| 925 |
+
"business": ["business"], "student": ["student"], "young": ["youthhub"],
|
| 926 |
+
"career": ["youthhub"], "collection": ["collections"],
|
| 927 |
+
}
|
| 928 |
+
|
| 929 |
+
|
| 930 |
+
def hive_info(topic: str | None = None) -> dict:
|
| 931 |
+
"""
|
| 932 |
+
The Hive (Worcester's library) at PAGE granularity: the exact offering of
|
| 933 |
+
each page of thehiveworcester.org, plus the profile of what makes it more
|
| 934 |
+
than a branch (joint university+public library, Explore the Past archives
|
| 935 |
+
& archaeology, 800+ study spaces, room hire, BIPC, Youth Hub...).
|
| 936 |
+
"""
|
| 937 |
+
hk = hive_kb()
|
| 938 |
+
prof = hk.get("hive_profile", {})
|
| 939 |
+
pages = [p for p in hk.get("pages", []) if not p.get("error")]
|
| 940 |
+
caps = prof.get("extended_capabilities", [])
|
| 941 |
+
out = {
|
| 942 |
+
"name": prof.get("name", "The Hive, Worcester"),
|
| 943 |
+
"partnership": prof.get("partnership", ""),
|
| 944 |
+
"address": prof.get("address", ""),
|
| 945 |
+
"opening_hours": (prof.get("opening_hours") or {}).get("building", ""),
|
| 946 |
+
"as_of": hk.get("generated", ""), "source_note": hk.get("note", ""),
|
| 947 |
+
"page_url": HIVE, "checked": _now(),
|
| 948 |
+
}
|
| 949 |
+
|
| 950 |
+
if not (topic or "").strip():
|
| 951 |
+
out.update({
|
| 952 |
+
"kind": "overview",
|
| 953 |
+
"capabilities": caps[:12],
|
| 954 |
+
"page_count": len(pages),
|
| 955 |
+
"sections": sorted({p.get("section", "") for p in pages}),
|
| 956 |
+
})
|
| 957 |
+
return out
|
| 958 |
+
|
| 959 |
+
t = topic.lower()
|
| 960 |
+
slugs: list[str] = []
|
| 961 |
+
for word, frags in _HIVE_INTENTS.items():
|
| 962 |
+
if word in t:
|
| 963 |
+
slugs += frags
|
| 964 |
+
scored = []
|
| 965 |
+
for p in pages:
|
| 966 |
+
score = 0.0
|
| 967 |
+
if any(s in p["url"] for s in slugs):
|
| 968 |
+
score += 6
|
| 969 |
+
title = p.get("title", "").lower()
|
| 970 |
+
terms = [w for w in re.findall(r"[a-z]{3,}", t)
|
| 971 |
+
if w not in ("the", "hive", "library", "what", "can", "you",
|
| 972 |
+
"about", "tell", "does", "have", "there")]
|
| 973 |
+
for w in terms:
|
| 974 |
+
if w in title:
|
| 975 |
+
score += 3
|
| 976 |
+
score += sum(0.5 for o in p.get("offerings", []) if w in o.lower())
|
| 977 |
+
if w in p.get("summary", "").lower():
|
| 978 |
+
score += 1
|
| 979 |
+
if score:
|
| 980 |
+
scored.append((score, p))
|
| 981 |
+
scored.sort(key=lambda x: -x[0])
|
| 982 |
+
|
| 983 |
+
cap_hits = [c for c in caps
|
| 984 |
+
if isinstance(c, dict) and any(
|
| 985 |
+
w in (c.get("capability", "") + c.get("detail", "")).lower()
|
| 986 |
+
for w in re.findall(r"[a-z]{4,}", t))][:4]
|
| 987 |
+
|
| 988 |
+
out.update({
|
| 989 |
+
"kind": "topic", "topic": topic,
|
| 990 |
+
"pages": [{
|
| 991 |
+
"title": p["title"], "url": p["url"], "summary": p.get("summary", ""),
|
| 992 |
+
"offerings": p.get("offerings", [])[:10],
|
| 993 |
+
"what_you_need": p.get("what_you_need", [])[:3],
|
| 994 |
+
"details": p.get("details", {}),
|
| 995 |
+
"notes": p.get("notes", ""),
|
| 996 |
+
} for _, p in scored[:3]],
|
| 997 |
+
"capabilities": cap_hits,
|
| 998 |
+
})
|
| 999 |
+
return out
|
| 1000 |
+
|
| 1001 |
+
|
| 1002 |
def whats_new(genre: str | None = None, limit: int = 6) -> dict:
|
| 1003 |
"""Newest catalogue titles for a genre/topic β fuel for a fun 'hot take'."""
|
| 1004 |
term = (genre or "fiction").strip()
|
|
|
|
| 1049 |
res = printing_help()
|
| 1050 |
print(" ", res["summary"][:80], "...")
|
| 1051 |
print(" pricing:", res["pricing"])
|
| 1052 |
+
|
| 1053 |
+
print("\n### 5. WHERE TO GET: 'wolf hall' (copy-level availability β LIVE) ###")
|
| 1054 |
+
res = where_to_get("wolf hall")
|
| 1055 |
+
print(f" found={res['found']} best='{res.get('best_title')}' "
|
| 1056 |
+
f"formats={res.get('formats_held')}")
|
| 1057 |
+
for r in res.get("routes", []):
|
| 1058 |
+
if r["route"] == "shelf":
|
| 1059 |
+
spots = ", ".join(f"{c['branch']} ({c['call_number'] or 'ask staff'})"
|
| 1060 |
+
for c in r["copies"][:4])
|
| 1061 |
+
print(f" π ON SHELF now: {spots}")
|
| 1062 |
+
elif r["route"] == "reserve":
|
| 1063 |
+
print(f" π reserve ({len(r.get('copies', []))} copies tracked, "
|
| 1064 |
+
f"parsed={r.get('availability_parsed')})")
|
| 1065 |
+
elif r["route"] == "digital":
|
| 1066 |
+
print(f" π» {r['format']} β direct link: {r['direct_link']}")
|
| 1067 |
+
else:
|
| 1068 |
+
print(f" β {r['route']}: {r.get('advice', '')[:70]}")
|
| 1069 |
+
|
| 1070 |
+
print("\n### 6. HIVE INFO (page-level KB β offline) ###")
|
| 1071 |
+
res = hive_info()
|
| 1072 |
+
print(f" {res['name']} β {res['opening_hours'][:60]}")
|
| 1073 |
+
print(f" pages={res.get('page_count')} capabilities={len(res.get('capabilities', []))} "
|
| 1074 |
+
f"as_of={res.get('as_of', '')[:10]}")
|
| 1075 |
+
for q in ("archives and old documents", "hire a meeting room", "parking"):
|
| 1076 |
+
r = hive_info(q)
|
| 1077 |
+
tops = [p["title"] for p in r.get("pages", [])]
|
| 1078 |
+
print(f" '{q}' -> pages {tops} + {len(r.get('capabilities', []))} capabilities")
|