AI-powered chatbot with Gemini, Rust+PyO3 text processing, and GCP deployment.
Live demo: pawacloud-web.fly.dev · API docs: /docs
Screenshots are available in the public/screenshots directory. Here are some of them showing several functional units:
| Service | URL |
|---|---|
| Frontend | pawacloud-web.fly.dev |
| Backend API | pawacloud-api |
| Swagger | /docs |
| ReDoc | /redoc |
| Status | /status |
Prerequisites: Python 3.12+, pnpm, Docker (for compose method)
Optional: Rust toolchain (PyO3 — falls back to Python), gcloud CLI (Cloud Run deploy), flyctl (frontend deploy), Terraform (IaC), Gemini API key (LLM — app runs without it, returns error on chat)
git clone https://github.com/ericgitangu/pawacloud-assistant.git
cd pawacloud-assistant
cp backend/.env.example backend/.env # add GEMINI_API_KEY
make devStarts PostgreSQL, Redis, backend, and frontend. Hit http://localhost:3000.
# backend
cd backend
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
uvicorn app.main:app --reload --port 8000
# frontend (new terminal)
cd frontend
pnpm install
cp .env.example .env.local
pnpm run devcd backend && python3 -m venv venv && source venv/bin/activate
cd ../rust-core && pip install maturin && maturin develop --release
cd ../backend && uvicorn app.main:app --reload --port 8000make test # backend: 27 pytest cases + doctests
cd frontend && pnpm build # frontend: typecheck + build
cd rust-core && cargo test # rust (runs inside Docker build)| Method | Endpoint | Purpose |
|---|---|---|
POST |
/api/v1/chat |
Query → complete response |
GET |
/api/v1/chat/stream?q= |
Query → SSE stream |
GET |
/api/v1/chat/history |
Paginated history (per user) |
DELETE |
/api/v1/chat/history/{id} |
Delete single conversation |
DELETE |
/api/v1/chat/history |
Clear all history |
POST |
/api/v1/documents/upload |
Multipart upload → ArtifactSummary |
GET |
/api/v1/documents/{id}/process |
SSE stream: summarize or translate |
GET |
/health |
Health + model metadata |
GET |
/health/metrics |
PyO3 benchmarks + Redis stats |
GET |
/health/events |
Event registry introspection |
GET |
/health/infra |
Cloud Run / Fly.io env info |
GET |
/health/llm |
Live Gemini connectivity test |
GET |
/auth/login |
→ Google OAuth consent |
GET |
/auth/callback |
OAuth code → session |
POST |
/auth/signup |
Create email/password account |
POST |
/auth/login |
Email/password login |
GET |
/auth/me |
Current user |
POST |
/auth/logout |
Clear session |
Document pipeline — upload PDF/DOCX/JPG/PNG, summarize or translate via SSE → see docs/documents.md.
All endpoints return typed Pydantic v2 models. Input sanitized via Rust PyO3 (Python fallback). See Swagger for request/response schemas.
Two methods, sessions via HMAC-signed tokens (Redis-independent):
| Method | TTL | Notes |
|---|---|---|
| Google OAuth | 24h | OpenID Connect |
| Email + password | 24h | bcrypt, PostgreSQL |
History is keyed by email, not session ID — persists across sign-out/sign-in and survives Cloud Run cold starts. Chat conversations stored in PostgreSQL (Neon.tech, aws-eu-west-2) with in-memory cache for fast reads.
sequenceDiagram
participant U as User
participant F as Next.js
participant B as FastAPI
participant R as Rust/PyO3
participant PG as Neon PG
participant G as Gemini
U->>F: Submit question
F->>B: GET /api/v1/chat/stream?q=...
B->>R: sanitize_input(query)
B->>G: generate_content(stream=True)
loop SSE chunks
G-->>B: chunk
B-->>F: data: chunk
end
B->>PG: Persist conversation
graph LR
subgraph GCP["GCP — africa-south1"]
CR["Cloud Run<br/>FastAPI + Rust"]
GEM["Gemini 2.5 Flash"]
end
subgraph Fly["Fly.io — JNB"]
WEB["Next.js 16"]
end
UPS["Upstash Redis"]
NEON["Neon PostgreSQL<br/>eu-west-2"]
WEB --> CR
CR --> GEM
CR --> UPS
CR --> NEON
Backend on Cloud Run (africa-south1), frontend on Fly.io (JNB). Terraform IaC in infra/. Details in docs/ARCHITECTURE.md.
Text processing on every request and response. Optional — falls back to pure Python.
| Function | Rust | Python | Speedup |
|---|---|---|---|
sanitize_input (1KB) |
~0.9us | ~45us | ~50x |
estimate_tokens (4KB) |
~1.2us | ~120us | ~100x |
validate_markdown (8KB) |
~2.1us | ~80us | ~38x |
Live benchmarks at /health/metrics.
pawacloud-assistant/
├── backend/
│ ├── app/
│ │ ├── main.py FastAPI + session middleware
│ │ ├── core/ config, decorators, middleware
│ │ ├── models/schemas.py Pydantic v2 models
│ │ ├── routers/ chat, auth, health
│ │ └── services/ llm, history, text processing
│ ├── tests/test_api.py
│ └── Dockerfile
├── rust-core/ PyO3 bindings (7 functions)
├── frontend/
│ ├── app/ pages (chat, status, login, signup)
│ ├── components/ 12 components
│ ├── lib/api.ts fetch + SSE + auth helpers
│ └── Dockerfile
├── infra/ Terraform (Cloud Run + Artifact Registry)
├── public/
│ └── screenshots/ Login, Chat, History, Status PNGs
├── docs/
│ ├── ARCHITECTURE.md
│ ├── PROMPTS.md
│ └── EVALUATION.md Assessment criteria mapping
├── scripts/ deploy-backend.sh, deploy-frontend.sh
├── docker-compose.yml Full stack (Postgres + Redis + backend + frontend)
└── Makefile# backend → Cloud Run
bash scripts/deploy-backend.sh
# frontend → Fly.io
bash scripts/deploy-frontend.sh
# or Terraform
cd infra && terraform init && terraform applySystem prompt in services/llm_service.py. Details in docs/PROMPTS.md.
- Persona: GCP specialist tied to Pawa IT
- Africa-aware: regional availability, cost, connectivity
- Multilingual: responds in user's language, keeps technical terms in English
- Honesty: "if you don't know, say so"
| Issue | Fix |
|---|---|
GEMINI_API_KEY not set |
Copy backend/.env.example to backend/.env and add your key from aistudio.google.com/apikey |
Frontend redirects to /login endlessly |
The pawacloud_auth cookie isn't set — clear cookies and log in again, or check NEXT_PUBLIC_API_URL points to a running backend |
pnpm: command not found |
corepack enable && corepack prepare pnpm@9.15.0 --activate (requires Node 18+) |
| CORS errors in browser console | Ensure ALLOWED_ORIGINS in backend/.env includes your frontend URL (default: http://localhost:3000) |
maturin develop fails |
Rust toolchain missing — curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh. Or skip it entirely; Python fallback handles everything |
| Docker compose: port 5432 in use | Local PostgreSQL is running — brew services stop postgresql or change the port in docker-compose.yml |
| History empty after login | Set DATABASE_URL in backend/.env for persistent history. Without it, history is in-memory and lost on restart. Redis is optional (session cache only) |
| OAuth callback fails locally | Set OAUTH_REDIRECT_URI=http://localhost:8000/auth/callback and FRONTEND_URL=http://localhost:3000 in backend/.env. Google Console must list the redirect URI |
Assessment criteria mapping, what's beyond requirements, and what I'd do with more time: docs/EVALUATION.md.



