Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| sidebar_position: 9 | |
| # Local LLM web scraping (Ollama + Gemma) | |
| Government sites change layout often. CSS selectors and regex break; a **local LLM** can extract **meetings, dates, and contacts** from cleaned page text without sending data to a cloud API. | |
| Open Navigator implements this as an optional layer on top of the existing meeting crawl (httpx / Playwright / BeautifulSoup). | |
| ## Pipeline | |
| ```text | |
| [ Target URL ] | |
| β httpx (Playwright fallback on 403/TLS) | |
| βΌ | |
| [ Raw HTML ] | |
| β BeautifulSoup: drop script/style/nav noise | |
| βΌ | |
| [ Markdown-like text ] | |
| β Pydantic schema + prompt | |
| βΌ | |
| [ Ollama β Gemma 4 (or compatible) ] | |
| βΌ | |
| [ JSON β meetings, contacts, dates ] | |
| ``` | |
| **Do not** paste full raw HTML into the model. Thousands of nested `<motion.div>` nodes waste context and hurt accuracy. The repo converts HTML to compact text first (`scripts/scraping/html_to_markdown.py`), and the jurisdiction crawler already writes `page_*.readable.txt` sidecars. | |
| ## Install Ollama and Gemma | |
| 1. Install [Ollama](https://ollama.com/download) (Linux, macOS, Windows). | |
| 2. Pull a Gemma model (name may vary by Ollama catalog): | |
| ```bash | |
| ./scripts/scraping/setup_ollama_gemma.sh | |
| # or manually: | |
| ollama pull gemma4 | |
| ``` | |
| 3. Optional LangChain Ollama bindings: | |
| ```bash | |
| .venv/bin/pip install -r requirements-ollama-scraping.txt | |
| ``` | |
| 4. Verify: | |
| ```bash | |
| .venv/bin/python scripts/scraping/extract_page_structured.py --check-ollama | |
| ``` | |
| ## Extract from a single URL | |
| ```bash | |
| cd /path/to/open-navigator | |
| .venv/bin/python scripts/scraping/extract_page_structured.py \ | |
| --url "https://www.example-county.gov/meetings/" \ | |
| --markdown-out /tmp/page.md \ | |
| --out /tmp/extraction.json | |
| ``` | |
| Or from an existing crawl sidecar: | |
| ```bash | |
| .venv/bin/python scripts/scraping/extract_page_structured.py \ | |
| --readable-txt data/cache/scraped_meetings/AL/county/.../_crawl_html/page_agenda.readable.txt \ | |
| --out /tmp/extraction.json | |
| ``` | |
| Output shape (Pydantic `JurisdictionPageExtraction`): | |
| - `jurisdiction_name`, `page_summary` | |
| - `meetings[]` β title, `meeting_date`, agenda/minutes URLs | |
| - `contacts[]` β name, role, email, phone | |
| - `contact_email`, `notes` | |
| ## Hook into the meeting crawl | |
| During `comprehensive_discovery_pipeline_jurisdiction`, after each `page_*.readable.txt` is written: | |
| ```bash | |
| export SCRAPED_MEETINGS_OLLAMA_EXTRACT=1 | |
| export SCRAPED_MEETINGS_OLLAMA_EXTRACT_MAX_PAGES=5 # cap per run (slow on CPU) | |
| export SCRAPED_MEETINGS_OLLAMA_MODEL=gemma4 | |
| export OLLAMA_HOST=http://127.0.0.1:11434 | |
| .venv/bin/python -m scripts.discovery.comprehensive_discovery_pipeline_jurisdiction \ | |
| --state AL --geoid 01001 --type county --url "https://..." | |
| ``` | |
| Writes `page_*.ollama.json` next to the readable file under `_crawl_html/`. | |
| ## Environment variables | |
| | Variable | Default | Purpose | | |
| |----------|---------|---------| | |
| | `OLLAMA_HOST` | `http://127.0.0.1:11434` | Ollama API base URL | | |
| | `SCRAPED_MEETINGS_OLLAMA_MODEL` | `gemma4` | Model tag for `ollama pull` | | |
| | `SCRAPED_MEETINGS_OLLAMA_EXTRACT` | off | Enable crawl sidecar | | |
| | `SCRAPED_MEETINGS_OLLAMA_EXTRACT_MAX_PAGES` | `3` | Max LLM calls per crawl run | | |
| | `SCRAPED_MEETINGS_OLLAMA_TIMEOUT_SECONDS` | `300` | Ollama request timeout | | |
| | `SCRAPED_MEETINGS_OLLAMA_USE_LANGCHAIN` | off | Use `langchain-ollama` instead of raw HTTP | | |
| ## Hardware expectations | |
| | Setup | Behavior | | |
| |-------|----------| | |
| | GPU (8GB+ VRAM) | Fast enough for interactive testing | | |
| | CPU-only laptop (16GB RAM) | Works for **batch** jobs; expect seconds per page | | |
| | Thousands of pages/day | Queue locally or sample pages; do not LLM every link | | |
| ## Limitations | |
| - **Hallucinated dates** β prompt asks for YYYY-MM-DD only when explicit; always validate against PDFs. | |
| - **Sequential throughput** β unlike cloud APIs, one local model usually processes one page at a time. | |
| - **Model availability** β if `gemma4` is not in your Ollama library, set `SCRAPED_MEETINGS_OLLAMA_MODEL` to a pulled tag (`ollama list`). | |
| ## Related docs | |
| - [Scraper improvements](./scraper-improvements.md) β Legistar API and platform heuristics | |
| - [Jurisdiction setup](./jurisdiction-setup.md) β discovery and crawl entrypoints | |
| - [Specialized AI models](./specialized-ai-models.md) β cloud and domain-specific models for legislative text | |
| ## Code map | |
| | File | Role | | |
| |------|------| | |
| | `scripts/scraping/html_to_markdown.py` | HTML β clean text | | |
| | `scripts/scraping/schemas.py` | Pydantic output schema | | |
| | `scripts/scraping/ollama_extract.py` | Ollama HTTP + optional LangChain | | |
| | `scripts/scraping/extract_page_structured.py` | CLI for URL / file / readable.txt | | |
| | `scripts/scraping/crawl_llm_sidecar.py` | Optional hook after crawl | | |
| | `scripts/scraping/setup_ollama_gemma.sh` | Install helper | | |