尚未偵測到更新。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/