Skip to main content
Glama

HotelAI

CI License: MIT Python 3.11+

RAG-based hotel concierge assistant: turns a hotel catalogue PDF into structured, natural-language-searchable data, with LLM-assisted extraction, hybrid search on ChromaDB, and an MCP server.

PDF -> structured extraction -> CSV/JSONL -> embeddings + ChromaDB -> hybrid search -> RAG answer with page citations

Exposed both as a web chat (FastAPI + React) and as an MCP tool for compatible clients (e.g. Claude Desktop).

HotelAI chat demo: asking for a pet-friendly hotel near the sea and getting a cited answer

Live demo

Runs on Render's and Netlify's free tiers: the backend spins down after inactivity, so the first request after a while can take up to ~50s to respond.

Related MCP server: Hotels MCP Server

What it includes

  • PDF -> structured data pipeline: dual parser (PyMuPDF for font-size-aware headers, pdfplumber for segmentation and bounding boxes) with HotelRecord (Pydantic) as the canonical schema and per-page traceability for every field.

  • LLM-assisted quality review: low-confidence fields are reviewed by Groq (default), with automatic fallback to Gemini; without any API key the pipeline stays fully deterministic and verifiable offline.

  • Hybrid search: Gemini embeddings on ChromaDB + lexical rerank on structured metadata.

  • Conversational RAG: RAGEngine synthesizes natural-language answers with per-hotel page citations (POST /api/chat).

  • MCP server: the same search engine exposed as a tool (search_hotels) for any MCP-compatible client.

  • Web app: FastAPI backend + React/Vite frontend (dark theme), with live PDF upload and hot re-indexing.

  • 39 tests, fully mocked (no API calls in the standard suite), plus an offline mode verifiable without any keys.

Tech stack

Python 3.11 · FastAPI · Pydantic v2 · ChromaDB · PyMuPDF · pdfplumber · FastMCP · Groq / Google Gemini · React 19 · Vite. Deploy: Render (backend) + Netlify (frontend).

Architecture

flowchart LR
    PDF["Hotel PDF"] --> ING["Ingestion\nPyMuPDF + pdfplumber"]
    ING --> REC["HotelRecord\n(Pydantic)"]
    REC --> FILES[("CSV / JSONL")]
    FILES --> IDX["Gemini embeddings\n+ ChromaDB indexing"]
    IDX --> RET["Retriever\nhybrid search"]
    RET --> RAG["RAGEngine\nGroq -> Gemini"]
    RET --> MCP["MCP Server\nsearch_hotels()"]
    RAG --> API["FastAPI\nPOST /api/chat"]
    API --> UI["React/Vite chat"]
    MCP --> CLIENT["MCP client\n(e.g. Claude Desktop)"]

Indexing re-reads the CSV that was just written instead of reusing the records still held in memory: this is the explicit seam between extraction and search. Every vector document includes the record's structured metadata (excluding source/quality, kept for audit only); results are ranked with a hybrid vector + lexical-overlap score (vector_weight/metadata_weight in .env).

Detailed flow docs, covering both triggers (offline batch and live upload) — currently in Italian: user-doc/architecture-flow.md · user-doc/ingestion-flow.md.

Quick start

1. Setup

python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
Copy-Item .env.example .env
git config core.hooksPath scripts/hooks

.env holds real API keys and is gitignored; it must never be committed. core.hooksPath enables scripts/hooks/pre-commit, which refuses a commit that stages .env or that pastes a key-shaped literal into any other file — it's a local git config, so run that line again after any fresh clone. CI runs a second, repo-wide check (gitleaks) as a backstop.

2. Offline verification (no API key, no external calls)

$env:PYTHONPATH = "src"
python scripts/run_pipeline.py "data\raw\FileHotels.pdf" --offline
pytest -q

With the sample PDF: 19 blocks detected, 19 structured records, zero Groq/Gemini calls. Offline mode only produces CSV/JSONL: it doesn't build the Chroma index, which requires a configured embedding provider.

3. Full pipeline (real LLM and embeddings)

Set GOOGLE_API_KEY and/or GROQ_API_KEY in .env, then:

$env:PYTHONPATH = "src"
python scripts/run_pipeline.py "data\raw\FileHotels.pdf"

4. Web app (chat)

$env:PYTHONPATH = "src"
python -m uvicorn api.main:app --reload --reload-dir src --port 8000
# --reload-dir src limits the watcher to source code: without it, files written
# by /api/ingest under data/ would restart the server mid-request.
cd frontend
npm install
npm run dev

The frontend runs on http://localhost:5173 and proxies /api/* to the backend on http://localhost:8000 (vite.config.js); the backend also keeps CORS open to localhost:5173 as a second line of defense. On Windows, double-click start.bat to launch backend and frontend together.

Endpoints: POST /api/chat (RAG answer), GET /api/hotels (canonical catalogue from data/processed/hotels_data.jsonl), POST /api/ingest (PDF upload from the sidebar: regenerates the CSV/index and hot-reloads the retriever), GET /api/health (Chroma connectivity + Groq/Gemini key presence). Assistant replies are rendered as Markdown (react-markdown + remark-gfm, hotel names in bold, lists); page citations [p. x-y] are shown as separate badges and stripped out of the text.

5. MCP server

$env:PYTHONPATH = "src"
python -m mcp_server.server

Exposes the search_hotels(query: str) -> str tool, at most five Markdown results. Config for a desktop MCP client (or MCP Inspector):

{
  "mcpServers": {
    "hotelai": {
      "command": "<absolute path to .venv\\Scripts\\python.exe>",
      "args": ["-m", "mcp_server.server"],
      "cwd": "<absolute path to the repository folder>",
      "env": {"PYTHONPATH": "src"}
    }
  }
}

command must point at the project's own .venv Python executable, not a generic python: many desktop clients (e.g. Claude Desktop on Windows) launch the process without inheriting a shell's activated-venv PATH, so "command": "python" can resolve to a system interpreter missing the project's dependencies (fastmcp, chromadb, ...) and silently fail the connection.

Example queries

  • Looking for a pet-friendly place near the sea.

  • Show me options with full board and a pool.

  • Which places suit a family with children best?

  • Looking for a place with a spa or wellness center.

  • Find places with family rooms and parking.

LLM providers, quotas and fallback

Groq (openai/gpt-oss-120b) is the default provider for reviewing low-confidence records and for RAG answers, with automatic fallback to Gemini (gemini-flash-latest) when Groq isn't configured or exhausts its retries. Without either key, extraction stays deterministic (local fallback) and RAG answers return a fixed fallback message, with no errors. Embeddings only use Gemini (gemini-embedding-001); without GOOGLE_API_KEY, indexing falls back to a deterministic offline embedder that is not semantically equivalent. Models, rate limits and retries (GOOGLE_MODEL, GROQ_MODEL, EMBEDDING_MODEL, REQUEST_DELAY_SECONDS, GEMINI_REQUESTS_PER_MINUTE, GROQ_REQUESTS_PER_MINUTE, MAX_RETRIES) are configurable in .env; Google 404/429/5xx errors are surfaced explicitly, with retry and backoff on 429 and 5xx.

Card grouping detects headers in the PDF text and associates the following page with them; if a catalogue doesn't preserve the expected headers, a structural "intro + page pairs" fallback kicks in. Hotel names aren't kept in hardcoded lists: they're extracted from the header and normalized only to fix obvious OCR artifacts.

Extracted data and quality

The JSONL file is the canonical, auditable store; the CSV is the interface between extraction and indexing. Each record can carry a numeric category, structured ratings (body, type, score/max), qualifiers, original text, source pages and a per-field confidence; unreadable scores stay null instead of being guessed. Deterministic extraction (PyMuPDF for name/locality, text heuristics/regex for the rest) runs first; text-based Groq (Gemini fallback) reviews low-confidence records, and Gemini Vision reads visual ratings when the text doesn't report them as numeric scores.

Testing

$env:PYTHONPATH = "src"
pytest -q

The sample PDF produces 19 CSV rows; source_pages makes every field traceable back to its original pages. Known limits: PDF text extraction order and some OCR artifacts may need visual review; the deterministic fallback is conservative and doesn't replace the model's semantic validation.

RAG retrieval eval

pytest covers plumbing and the deterministic criteria-reranking logic, but the offline embedder it uses for that is hash-based, not semantic — it can't tell you whether a natural-language query actually finds the right hotel. scripts/eval_rag.py does: it runs a small hand-verified set of queries (scripts/eval_queries.json, 12 cases) against the real committed index with real Gemini embeddings, and reports Recall@5.

$env:PYTHONPATH = "src"
python scripts/eval_rag.py

Needs GOOGLE_API_KEY and spends real embedding quota, so it's not part of the automated test suite — run it manually when retrieval logic changes. Currently 12/12.

License

MIT

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

No tool schema history has been recorded yet.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    MCP server that indexes a knowledge base into Chroma and provides search tools for retrieving document fragments via vector embeddings.
    -
  • F
    license
    Not graded
    quality
    B
    maintenance
    Local MCP server that provides a search_documents tool to query a RAG pipeline built with Ollama embeddings and ChromaDB, enabling Claude Desktop to retrieve relevant document chunks.
    -

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/workfromrome/HotelAI'

If you have feedback or need assistance with the MCP directory API, please join our Discord server