尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证Website knowledge chatbot grounded in 7 PlacidWay pages: cited answers, code-checked prices, honest 'I don't know'. FastAPI + BM25 + Gemini.
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗A chatbot that answers visitor questions only from seven PlacidWay pages (ITC Immunity Therapy Center in Tijuana: clinic profile, three treatment packages, videos, price list, plus the alternative-medicine price comparison). Every answer links the page it came from, prices are checked in code before they reach the user, and when the pages don't say something the bot says so.
python -m venv .venv && .venv\Scripts\activate # macOS/Linux: source .venv/bin/activate
pip install -r requirements-dev.txt
copy .env.example .env # then set LLM_API_KEY (free: aistudio.google.com/apikey)
uvicorn app.main:app --reload # open http://127.0.0.1:8000
The knowledge snapshot (data/knowledge.json) is committed, so the bot works without crawling first.
| Command | What it does |
|---|---|
python -m scripts.ingest | Re-crawl the 7 pages, rebuild the snapshot, print which pages changed |
python -m scripts.run_test_questions | Ask the brief's test questions and write docs/test-results.md (--url to test a deployment) |
python -m pytest | 84 tests: robots.txt rules, extraction, chunking, a 25-question retrieval recall check, price/claim/name checks, model fallback, answer pipeline, API |
flowchart LR
subgraph Ingestion["Ingestion (CLI, admin endpoint or weekly CI)"]
F[Polite fetcher<br/>robots.txt + Crawl-delay,<br/>clear User-Agent] --> E[Extractor<br/>page content only, noise removed]
E --> C[Chunker<br/>heading-aware, ~180 words]
C --> S[(knowledge.json<br/>+ readable data/pages/*.md)]
end
subgraph Question["Each question"]
Q[User message + history] --> R[Router model<br/>language, intent,<br/>standalone English question]
R --> B[BM25 search<br/>top 8 passages]
S --> B
B --> A[Answer model<br/>JSON answer + cited source ids]
A --> G{Checks in code<br/>prices, claims,<br/>doctor names, citations}
G -- pass --> U[Answer + source links]
G -- fail --> A2[One corrected retry] --> G2{Checks} -- fail --> H[Honest fallback<br/>'I don't know' + quote form]
G2 -- pass --> U
end
app/ingest/)app/sources.py). The fetcher identifies itself
(PlacidWayAssessmentBot/1.0 ... contact: ...), retries server errors, and keeps a page's previous
copy if it can't be downloaded during a refresh.app/ingest/robots.py, RFC 9309). PlacidWay's file starts with
Allow: / and lists Disallow rules after it; Python's built-in urllib.robotparser applies the
first matching line and would allow everything. The standard says the longest matching rule wins,
so that's what this parser does. The file also sets Crawl-delay: 10: the brief asks for about one
request per second and robots.txt asks for ten seconds, so the crawler waits ten (the stricter of the
two). A full refresh therefore takes about 80 seconds.Clinic / Surgeon: Stem Health Guadalajara | Standard: $1000 | Procedure: Ozone Therapy), so a row still says what its price is for when read on
its own (without that, "$1,000 at Stem Health" was once quoted as a stem cell price). UI-only columns
such as "Chat with Center" are removed.price-social-block-ctm, so "drop anything with social in the
class" would delete the headline price. Class filters match whole class names.=$priceRange?>"). Blocks
with unrendered template code are skipped and counted.app/ingest/chunk.py)Chunks follow the page's headings and never mix two top-level sections. Small sub-sections (FAQ entries, video cards) are packed together up to ~180 words, and a table split across chunks repeats its header row. 121 chunks in total.
app/knowledge.py)BM25 keyword search, written out in about 25 lines with Lucene's IDF.
tests/test_retrieval_eval.py asks 25 realistic questions with the raw question text
(no rewriting, i.e. the worst case) and checks the answering fact is in the top 8: 25/25.app/chat.py, app/prompts.py, app/guard.py)question, medical_advice, booking,
quote, smalltalk, out_of_scope), standalone question, search keywords. Bookings, small talk and
off-topic messages skip retrieval.answered / partial / not_found / declined), cited source ids, whether the user's own
figure is supported, a medical flag and a short disclaimer.18,995, 18.995, 18 995 and Arabic-Indic digits are normalised).$50 does exist on these pages, as a consultation fee, so "it appears
somewhere" is not enough.not_found answers say so, suggest a free quote, and are logged
(unanswered.jsonl plus the server log) so PlacidWay can see content gaps.| Choice | Why |
|---|---|
| Python, FastAPI | Small, typed API; the same code runs locally and as a Vercel function |
| BeautifulSoup + lxml | Precise, testable control over a known HTML template |
| BM25 (own implementation) | See Retrieval above; zero infrastructure |
| Gemini 3.5 Flash-Lite (answers) and 3.1 Flash-Lite (router) | Free tier, multilingual, fast. Free quotas are per model (Flash-Lite about 500 requests/day; Flash only 20/day), so the two steps use different models, with automatic fallback to other Flash-Lite models when one is busy or out of quota |
| OpenAI-compatible client | Swap to Groq, OpenAI, Claude (via a compatible gateway) or a local model by changing three env vars |
| Vanilla HTML/CSS/JS front end | No build step; works on phones; right-to-left text handled with dir="auto" |
python -m scripts.ingest re-crawls, compares content hashes and reports changed / unchanged / failed.POST /api/admin/refresh with header X-Admin-Token re-crawls and hot-swaps the
knowledge base without a restart (enabled when ADMIN_TOKEN is set). GET /api/admin/unanswered
lists questions the bot couldn't answer..github/workflows/refresh.yml runs weekly: re-crawl, run the tests, commit the new
snapshot if anything changed.| Method | Path | Purpose |
|---|---|---|
POST | /api/chat | {message, history[], disclaimer_shown} → answer, status, sources, disclaimer, offer_quote, debug (retrieved passages, checks, timing) |
POST | /api/lead | Quote request {name, email, question, consent} (validated, logged, optional webhook) |
GET | /api/sources | The pages the bot knows and when they were fetched |
GET | /api/health | Status, model, chunk count, snapshot date |
POST | /api/admin/refresh | Re-crawl and hot-swap (token) |
GET | /api/admin/unanswered | Content-gap log (token) |
Input is validated (max 800 characters, history capped), and each client is limited to 20 messages
per minute to protect the free API key. No secrets live in the code: everything comes from env vars
(.env.example).
| Brief item | Where |
|---|---|
| Content ingestion from the shared URLs only | app/sources.py, app/ingest/fetch.py, app/ingest/robots.py (robots.txt incl. Crawl-delay, User-Agent) |
| Knowledge base that finds the right content | app/ingest/chunk.py, app/knowledge.py |
| Grounded answers citing source URLs | Answer prompt + citation check; links under every answer |
| Honest "I don't know" + free-quote suggestion | not_found status, fallback answer, quote button |
| Prices with currency and context | Prompt rule 3 + code price check against cited sources |
| Simple chat UI | public/ |
| Refresh | CLI, admin endpoint, weekly GitHub Action |
| Live deployment | Vercel (link above) |
| Nice to have: user's language | Router detects language; answers in it (Spanish and Arabic in the test results) |
| Nice to have: follow-ups | Router rewrites follow-ups using history ("What about in Turkey?") |
| Nice to have: medical disclaimer, not on every answer | Shown once per conversation when the topic is medical |
| Nice to have: lead capture | "Request a free quote" form → /api/lead |
| Nice to have: logging unanswered questions | unanswered.jsonl + server log + admin endpoint |
app/
sources.py the 7 allowed URLs
ingest/ fetch.py · robots.py · extract.py · chunk.py · pipeline.py
knowledge.py BM25 index, search, hot-swappable store
prompts.py router and answer prompts
chat.py route -> retrieve -> answer -> verify
guard.py money, user-claim and doctor-name checks
llm.py OpenAI-compatible client with per-model fallback
events.py unanswered-question and lead logs
main.py FastAPI app
api/index.py Vercel entry point
public/ chat page
data/knowledge.json snapshot; data/pages/*.md readable copies
scripts/ ingest.py, run_test_questions.py
tests/ pytest suite
vercel deploy --prod from the repo root. Set LLM_API_KEY (and optionally ADMIN_TOKEN,
LEAD_WEBHOOK_URL) in the project's environment variables. Static files come from public/;
/api/* is rewritten to the Python function in api/index.py, whose time limit is raised to 300 s
so an admin refresh can finish while honouring the 10-second crawl delay.
data/pages/