| title | Portfolio profile chatbot |
|---|---|
| emoji | 💬 |
| colorFrom | blue |
| colorTo | indigo |
| sdk | docker |
| app_port | 7860 |
| pinned | false |
Turn your resume or LinkedIn PDF into a small “ask me anything (professional)” widget for your site. This project runs a Gradio chat UI on FastAPI, uses Google Gemini to answer in your voice (first person), and only uses facts from your profile file. A scope gate blocks random questions; rate limits help control API cost.
What you need: Python 3.11+, uv, and a Gemini API key.
At a high level, every message goes through rate limiting, then a quick “is this about my career?” check with Gemini. Only if that passes does the app call Gemini again for the full answer, still grounded in your profile text.
flowchart TB
subgraph visitor [Visitor]
Browser[Browser or iframe]
end
subgraph server [Python server]
Gradio[Gradio chat UI]
FastAPI[FastAPI plus CSP headers]
Limiter[Rate limiter per IP]
Gate[Scope gate small Gemini call]
Chat[Main Gemini reply]
Profile[Profile PDF MD or env text]
end
subgraph gemini [Google Gemini API]
API[Gemini models]
end
Browser --> Gradio
Gradio --> FastAPI
FastAPI --> Limiter
Limiter -->|under limit| Gate
Limiter -->|friendly refusal| Gradio
Gate -->|on topic| Chat
Gate -->|refusal no main call| Gradio
Profile -.->|baked into prompt at startup| Chat
Gate --> API
Chat --> API
Chat --> Gradio
Gradio --> Browser
Notes
- Profile is read when the process starts (from
Profile.pdf, another file, orPROFILE_CONTEXT). Restart after you change it. - Off-topic questions never trigger the big reply model call (saves tokens).
- Deploying (e.g. Hugging Face) uses the
Dockerfile: same app, same flow.
| I want to… | Jump to |
|---|---|
| Run it on my laptop | Local setup |
| Host free on Hugging Face | Hugging Face Spaces |
| Embed it in my portfolio site | Iframe embedding |
-
Clone the repo and open a terminal in the project folder.
-
Install dependencies
uv sync
-
Create your env file
cp .env.example .env
Edit
.envand setGOOGLE_API_KEY(orGEMINI_API_KEY). Tweak models and limits if you like — see Configuration. -
Add your profile
- Put
Profile.pdfin the project root (text-based PDFs work best), or - Set
PROFILE_PATHto another.pdf,.md, or.txt, or - Set
PROFILE_CONTEXTto paste profile text (overrides the file).
- Put
-
Start the app
uv run uvicorn app:app --host 0.0.0.0 --port 7860
Or:
uv run python app.py
Hugging Face hosts the app as a Docker Space. The Gradio “template” Space type is not used here — this app starts with Uvicorn + FastAPI.
- Go to Create a new Space.
- Choose Docker (not the Gradio SDK).
- Template: Blank.
- Hardware: CPU Basic is enough for a portfolio widget.
- Visibility: Public if you want a link and iframe anyone can use.
Your Space gets its own Git repo on Hugging Face (it is not auto-linked to GitHub).
On the Space: Settings → Variables and secrets → add:
GOOGLE_API_KEY— your Gemini key
Optional (same names as .env.example): GEMINI_MODEL, FRAME_ANCESTORS, PROFILE_CONTEXT, etc.
From your computer, in a clone of this repository:
git remote add hf https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
git push hf main-
Git will ask for credentials: use a Hugging Face access token (with write access), not your password.
-
First push often fails with “remote contains work you do not have” because the Space was created with a tiny seed commit. Overwrite only the Space (safe for GitHub):
git push hf main --force
After a successful push, Hugging Face rebuilds the Docker image. Watch Build logs if something fails.
- Commit
Profile.pdf(orprofile.md) in the repo you push to the Space, or - Put the text in
PROFILE_CONTEXTas a Space secret/variable (if multiline is supported for your account).
Rebuild after changing profile or secrets.
- Public URL:
https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME - The container listens on 7860 (see
Dockerfile); the YAML block at the top of this README setsapp_port: 7860for the Hub.
There is no universal “Import from GitHub” button Space-side; mirror from GitHub with one of these:
- Add
hfas a second remote and push to bothoriginandhf(see step 3 above), or - Use GitHub Actions in this repo (details below), following the same Hugging Face ↔ Actions pattern.
Automated sync (this repository)
| File | What it does |
|---|---|
.github/workflows/sync-huggingface.yml |
On every push to main (and on manual Actions → Sync to Hugging Face Space), force-pushes the repo to your Space’s main with Git LFS enabled. |
.github/workflows/huggingface-file-size.yml |
On PRs into main, warns if any file exceeds 10 MB (Spaces limit for non–Git LFS blobs). |
In your GitHub repo: Settings → Secrets and variables → Actions
| Kind | Name | Value |
|---|---|---|
| Secret | HF_TOKEN |
Hugging Face access token with write access. |
| Variable | HF_USERNAME |
Hub user or org that owns the Space (same as YOUR_USERNAME in spaces/YOUR_USERNAME/YOUR_SPACE_NAME). |
| Variable | HF_SPACE_NAME |
Space slug (same as YOUR_SPACE_NAME in that URL). |
Create the Space on Hugging Face first; the workflow only pushes Git history and does not create the Space.
Environment variables drive the app. Locally, copy .env.example to .env. On Hugging Face, use Settings → Variables and secrets.
| Variable | Purpose |
|---|---|
GOOGLE_API_KEY / GEMINI_API_KEY |
Gemini API authentication (GOOGLE_API_KEY wins if both are set). |
GEMINI_MODEL |
Main chat model (default gemini-2.5-flash). |
SCOPE_GATE_MODEL |
Cheaper/smaller model for allow/refuse (defaults to GEMINI_MODEL). |
PROFILE_PATH |
Profile file relative to project root (default Profile.pdf). |
PROFILE_CONTEXT |
If set, used instead of the file. |
MAX_MESSAGE_CHARS |
Max user message length. |
MAX_OUTPUT_TOKENS |
Max length of each assistant reply. |
CHAT_TEMPERATURE |
Creativity for main replies (default 0.65). |
SCOPE_GATE_MAX_OUTPUT_TOKENS |
Cap for gate JSON output. |
RATE_LIMIT_MAX_MESSAGES / RATE_LIMIT_WINDOW_SECONDS |
Sliding-window rate limit per IP. |
FRAME_ANCESTORS |
Sites allowed to embed your app (comma-separated, or * for any). https://huggingface.co is always appended automatically (unless you use *) so the Space App tab on Hugging Face keeps working—Hugging Face loads *.hf.space inside an iframe on huggingface.co. |
HOST / PORT |
Used when running python app.py locally. |
Model names and billing: Gemini models · Pricing.
The app sends Content-Security-Policy: frame-ancestors … and removes X-Frame-Options from your Space responses. Your FRAME_ANCESTORS list is merged with https://huggingface.co / https://www.huggingface.co so the Space App tab on the hub still works.
| URL | Use case |
|---|---|
https://huggingface.co/spaces/USER/SPACE |
Opening in a new tab / sharing a link. Hugging Face’s HTML shell often sets X-Frame-Options: deny — do not use this as your iframe src on another site. |
https://USER-SPACE.hf.space/ |
Iframe src on your portfolio. This hits your container directly; your app’s headers allow embedding from origins you listed in FRAME_ANCESTORS. |
On your Space page, open the “…” / Embed menu if needed; the hf.space address is the one to embed.
-
Set
FRAME_ANCESTORS(Space variables) to your origins, e.g.FRAME_ANCESTORS=http://localhost:3000,http://127.0.0.1:5173,https://srini.fyi
-
Example embed (replace with your real
*.hf.spaceURL):<iframe src="https://srinirk23-srini-chatbot.hf.space/" title="Profile assistant" loading="lazy" referrerpolicy="strict-origin-when-cross-origin" style="width:100%;max-width:900px;height:720px;border:0;border-radius:12px;display:block;margin:0 auto" ></iframe>
If the iframe is blank, check the console for CSP errors and confirm src is *.hf.space, not huggingface.co/spaces/....
| Path | Role |
|---|---|
| app.py | Gradio UI, Gemini calls, FastAPI mount, iframe-friendly headers |
| config.py | Settings; loads profile from PDF / MD / TXT / env |
| guardrails/scope.py | Scope gate (profile-related questions only) |
| limits/ratelimit.py | Sliding-window rate limit |
| Dockerfile | Production / Hugging Face image |
pyproject.toml · uv.lock |
Dependencies (uv) |
.env.example |
Copy to .env locally |
- Two Gemini calls per allowed message (gate + main). Off-topic input stops after the gate.
- Restart the server after changing the profile file or
PROFILE_CONTEXT. - Image-only PDFs may yield almost no text; prefer exported text PDFs or Markdown.
- Do not commit
.envor API keys. TreatProfile.pdfas sensitive if it contains PII.