https://github.com/manziosee/ussd
SmartAssist is an AI-powered USSD assistant that lets any mobile phone user (feature phone or smartphone, no internet required) access AI by simply dialling a shortcode like *123#. Responses are instant and cost the user nothing extra beyond their normal USSD session.
https://github.com/manziosee/ussd
alembic docker fastapi pytest python3 redis-cache sqlalchemy
Last synced: 9 days ago
JSON representation
SmartAssist is an AI-powered USSD assistant that lets any mobile phone user (feature phone or smartphone, no internet required) access AI by simply dialling a shortcode like *123#. Responses are instant and cost the user nothing extra beyond their normal USSD session.
- Host: GitHub
- URL: https://github.com/manziosee/ussd
- Owner: manziosee
- Created: 2026-05-14T19:08:34.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-05-31T19:25:17.000Z (25 days ago)
- Last Synced: 2026-05-31T20:22:26.304Z (25 days ago)
- Topics: alembic, docker, fastapi, pytest, python3, redis-cache, sqlalchemy
- Language: Python
- Homepage:
- Size: 203 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# SmartAssist USSD
> **AI for everyone — including the 3 billion people with no internet.**
SmartAssist is an AI-powered USSD assistant that lets any mobile phone user
(feature phone or smartphone, zero internet required) access AI by dialling
a shortcode like `*123#`. Works with any USSD aggregator, any country,
any mobile operator.
---
## Technology Stack
| | Technology | Role |
|---|---|---|
|  | **Python 3.12** | Core language |
|  | **FastAPI** | Async web framework (USSD app + SMS gateway) |
|  | **Groq — Llama 3.1 8B** | Ultra-fast AI inference (~50 ms) |
|  | **PostgreSQL** | User data, interactions, market prices |
|  | **Redis** | Sessions, AI cache, rate limiting |
|  | **Jasmin SMS Gateway** | Open-source SMPP gateway — own SMS infrastructure, zero per-SMS cost |
|  | **RabbitMQ** | Jasmin internal message queue |
|  | **Docker Compose** | Full-stack deployment |
|  | **SQLAlchemy 2.0** | Async ORM |
|  | **Alembic** | Database migrations |
|  | **pytest-asyncio** | 45-test async suite |
---
## Architecture
```
Any mobile phone dials a shortcode (no internet — any feature phone)
│
▼
┌──────────────────────────────────────┐
│ USSD Aggregator │
│ (your regional USSD network │
│ partner — AT, Comviva, Ericsson, │
│ NTTDATA, local operator, etc.) │
└──────────────┬───────────────────────┘
│ POST /ussd (form-encoded)
│ sessionId · phoneNumber · text
▼
┌──────────────────────────────────────────────────────────────────┐
│ SmartAssist USSD App (port 8000) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ USSD State Machine │ │
│ │ Parses *-separated input → routes to correct handler │ │
│ └─────────────────────┬────────────────────────────────────┘ │
│ ┌──────────────┼──────────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌──────────┐ ┌───────────────┐ │
│ │ Redis │ │ Groq │ │ PostgreSQL │ │
│ │ Sessions │ │ Llama │ │ Users │ │
│ │ AI cache │ │ 3.1 8B │ │ Interactions │ │
│ │ Rate limit│ │ ~50 ms │ │ Market prices│ │
│ └────────────┘ └──────────┘ └───────────────┘ │
│ │
│ Long answer → POST /sms/send │
└──────────────────────┬───────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ SMS Gateway API (port 8001) │
│ │
│ • Validates E.164 phone number │
│ • Extracts country dialing prefix │
│ • Looks up Jasmin connector (or lets Jasmin route globally) │
│ • Detects GSM-7 vs UCS-2 encoding automatically │
│ • Bulk send for daily tip broadcasts │
└──────────────────────┬───────────────────────────────────────────┘
│ POST to Jasmin HTTP API (port 1401)
▼
┌──────────────────────────────────────────────────────────────────┐
│ Jasmin SMPP Gateway (port 1401/2775) │
│ │
│ Connectors are named by ISO country code and point to │
│ whatever SMPP provider the operator chooses. │
│ Add/remove via: telnet localhost 8990 (Jasmin CLI) │
│ │
│ Default mode: empty connector map → Jasmin's MT routing rules │
│ decide automatically — works globally with any provider. │
└──────────────────────┬───────────────────────────────────────────┘
│ SMPP v3.4
▼
Telecom Operator (any country, any network)
│
▼
User's Phone
```
---
## Why Jasmin Instead of Twilio / Third-Party SMS APIs?
| | Third-party SMS API | Jasmin SMPP |
|---|---|---|
| **Cost per SMS** | $0.0075 – $0.05 | ~$0 (direct operator rate) |
| **Infrastructure** | Vendor cloud | Your own server |
| **Latency** | 1–5 s (cloud relay) | < 500 ms (direct SMPP) |
| **Multi-country** | Different API per provider | One gateway, all operators |
| **Carrier lock-in** | Yes | None — swap providers without code changes |
| **Data sovereignty** | Vendor servers | Your servers |
---
## Menu Structure
```
Dial shortcode → Onboarding (new users: language + role) → Main Menu
└── SmartAssist AI
├── 1. Business
│ ├── 1–4. Pricing / Bookkeeping / Marketing / Get customers (AI tip → CON)
│ │ └── 1.More tips · 2.More detail · 3.Send SMS · 4.Helpful · 5.Not helpful
│ ├── 5. My question (free AI question — paginated for long answers)
│ └── 6. Calculator (profit check · loan payment — zero AI cost)
│
├── 2. Farming
│ ├── 1–3. Soil / Pest control / Best crops (AI tip)
│ ├── 4. Market prices (DB-backed, configurable per deployment)
│ ├── 5. My question
│ └── 6. Nearby offices (configurable services directory)
│
├── 3. Health
│ ├── 1–4. Nutrition / Hygiene / Maternal / Child (AI tip)
│ ├── 5. My question
│ ├── 6. Nearby clinics (configurable services directory)
│ └── 7. Emergency numbers (configurable per country)
│
├── 4. Education
│ ├── 1–4. Study / Career / Math / English (AI tip)
│ ├── 5. My question
│ └── 6. Nearby schools (configurable services directory)
│
├── 5. Ask AI (free-form question, paginated for long answers)
│
└── 6. Account
├── 1. My stats · 2. Set name · 3. Set profession
├── 4. Language · 5. SMS alerts · 6. Daily tips
└── 0. Main menu
```
---
## Quick Start
### 1. Clone and configure
```bash
git clone https://github.com/manziosee/ussd.git
cd ussd
```
Edit `.env` (key settings):
```env
# AI — get a free key at console.groq.com
GROQ_API_KEY=gsk_...
GROQ_MODEL=llama-3.1-8b-instant
# Your USSD shortcode (assigned by your USSD aggregator)
USSD_SHORTCODE=*123#
# Database — Neon free tier at neon.tech, or local Docker
DATABASE_URL=postgresql+asyncpg://user:pass@host/dbname
# SMS gateway (Docker service name; use http://localhost:8001 locally)
SMS_GATEWAY_URL=http://sms-gateway:8001
# Jasmin — leave empty to use Jasmin's global routing rules (recommended)
SMS_GW_CONNECTOR_MAP_JSON={}
# USSD webhook security (leave empty for local dev / sandbox)
AT_API_KEY=
AT_WEBHOOK_TOKEN=
```
### 2. Start with Docker Compose
```bash
docker-compose up -d
```
| Service | URL |
|---|---|
| USSD app | http://localhost:8000 |
| API docs | http://localhost:8000/docs |
| Admin dashboard | http://localhost:8000/admin/dashboard?key=`` |
| SMS Gateway | http://localhost:8001 |
| Jasmin CLI | `telnet localhost 8990` (admin / admin) |
| RabbitMQ UI | http://localhost:15672 (jasmin / jasmin) |
### 3. Connect an SMPP provider in Jasmin
Jasmin connects to telecom operators via SMPP. Use the Jasmin CLI to add connectors:
```bash
telnet localhost 8990
# Username: admin Password: admin
```
```
# Add a connector — name it with the ISO country code
smppccm -a
> cid ke
> host smpp.your-provider.com
> port 2775
> username your_smpp_user
> password your_smpp_pass
> ok
# Add a catch-all MT route (sends all numbers through this connector)
mtrouter -a
> order 100
> type DefaultRoute
> connector smppc(ke)
> ok
# Save and start
persist
smppccm -1 ke
quit
```
> **Multiple countries:** add one connector per country (`ke`, `ng`, `gh`, `in`, `pk` …),
> then add MT route filters so Jasmin routes each prefix to the right connector.
> The SMS gateway will honour Jasmin's routing automatically.
> **Test SMPP server:** `smpp.ozekisms.com:9500` (free public test endpoint — no real SMS sent).
### 4. Run without Docker (development)
```bash
# Install main app dependencies
pip install -r requirements.txt
# Start USSD app (auto-uses fakeredis if Redis is unavailable)
uvicorn app.main:app --reload --port 8000
# In a second terminal — start SMS gateway
cd sms_gateway
pip install -r requirements.txt
uvicorn main:app --reload --port 8001
```
### 5. Test with the simulator
```bash
python simulator/cli_sim.py
```
```
══════════════════════════════════════════
SmartAssist USSD Simulator
══════════════════════════════════════════
┌────────────────────────────────────────┐
│ Welcome to SmartAssist! │
│ Choose language: │
│ 1.English │
│ 2.Kinyarwanda │
└────────────────────────────────────────┘
Your input: _
```
### 6. Run tests
```bash
pytest
```
All 45 tests pass with no external services required.
---
## SMS Gateway API
The gateway runs as an independent microservice on port 8001.
### Send a single SMS
```bash
curl -X POST http://localhost:8001/sms/send \
-H "Content-Type: application/json" \
-d '{"to": "+254700000001", "message": "Hello from SmartAssist!"}'
```
```json
{
"success": true,
"message_id": "01234-5678-uuid",
"connector": null,
"country_code": "254",
"error": null
}
```
`connector: null` means Jasmin's own routing rules dispatched the message — the correct global default.
### Bulk send (daily tips broadcast)
```bash
curl -X POST http://localhost:8001/sms/send-bulk \
-H "Content-Type: application/json" \
-d '{
"recipients": ["+254700000001", "+234800000001", "+91900000001"],
"message": "Daily tip: Price = cost + 30% profit minimum.",
"sender_id": "SmartAssist"
}'
```
```json
{"sent": 3, "failed": 0, "results": [...]}
```
### Gateway health
```bash
curl http://localhost:8001/health
```
```json
{"status": "ok", "jasmin_reachable": true, "version": "1.0.0"}
```
---
## SMS Gateway — Country Routing
The connector map is **empty by default**. Jasmin handles routing globally through its own MT route rules — you configure those once in the Jasmin CLI, and the gateway never needs to know about carriers.
To override routing for specific prefixes, set `SMS_GW_CONNECTOR_MAP_JSON`:
```env
# Route by country code prefix → your named Jasmin connector
SMS_GW_CONNECTOR_MAP_JSON={"254":"ke","234":"ng","91":"in","44":"uk"}
```
Connector names are ISO 3166-1 alpha-2 country codes. They map to SMPP connections you configure in Jasmin — entirely carrier-agnostic.
---
## Admin API
All admin routes require `X-Admin-Key: `.
| Method | Path | Description |
|---|---|---|
| `GET` | `/admin/dashboard` | HTML dashboard with charts |
| `GET` | `/admin/stats` | Aggregated analytics |
| `GET` | `/admin/interactions` | Query history (paginated) |
| `GET` | `/admin/users` | User list |
| `GET` | `/admin/market-prices` | Crop price list |
| `PUT` | `/admin/market-prices` | Upsert a single price |
| `POST` | `/admin/market-prices/bulk` | Bulk upsert prices |
| `DELETE` | `/admin/market-prices/{id}` | Delete a price |
| `GET` | `/admin/feedback` | Helpful / not-helpful counts |
| `POST` | `/ussd` | USSD webhook (form-encoded) |
| `POST` | `/simulate` | Local USSD simulator (JSON) |
| `GET` | `/health` | Health check |
---
## Localisation
The system ships with English and Kinyarwanda menus. To add a language:
1. Add entries to `_STRINGS` in `app/services/menu_service.py`
2. Add category labels to `_LABELS` in `app/services/broadcast_service.py`
3. Add the AI language hint in `app/services/ai_service.py`
## Local Services Directory
`app/data/services.py` and `app/data/emergency.py` contain sample data for
one deployment region. Replace these files with your own districts, clinics,
schools, and emergency numbers. The menu system reads them dynamically —
no code changes needed, only data.
---
## Project Structure
```
ussd/
├── app/ # USSD Application (port 8000)
│ ├── main.py # FastAPI app, lifespan, startup
│ ├── config.py # All settings via .env (incl. USSD_SHORTCODE)
│ ├── auth.py # USSD webhook HMAC + token guard
│ ├── models/ # User · Interaction · MarketPrice · Feedback
│ ├── services/
│ │ ├── menu_service.py # USSD state machine
│ │ ├── ai_service.py # Groq Llama 3.1 8B + Redis cache + retry
│ │ ├── session_service.py # Redis sessions, cache, rate limit, pagination
│ │ ├── knowledge_service.py # 15 pre-seeded offline responses
│ │ ├── sms_service.py # HTTP client → SMS Gateway
│ │ └── broadcast_service.py # Daily tip broadcast (uses USSD_SHORTCODE)
│ ├── routes/
│ │ ├── ussd.py # POST /ussd + POST /simulate
│ │ ├── admin.py # /admin/* + HTML dashboard
│ │ └── cron.py # POST /cron/daily-tips
│ └── data/
│ ├── emergency.py # Emergency numbers (replace for your country)
│ └── services.py # Local services directory (replace for your region)
│
├── sms_gateway/ # SMS Gateway microservice (port 8001)
│ ├── main.py # FastAPI app
│ ├── config.py # SMS_GW_ prefixed settings
│ ├── schemas.py # SMSRequest · SMSResponse · BulkSMS*
│ ├── routes/sms.py # POST /sms/send · /sms/send-bulk · GET /health
│ └── services/
│ ├── jasmin_client.py # Jasmin HTTP API + GSM-7/UCS-2 auto-detect
│ └── routing.py # Dialing prefix → connector (100+ countries)
│
├── simulator/cli_sim.py # Terminal USSD simulator
├── tests/
│ ├── conftest.py # Shared fixtures
│ ├── test_ussd_menu.py # 30 menu tests
│ └── test_admin_routes.py # 15 admin route tests
├── alembic/versions/ # 4 DB migrations
├── docker-compose.yml # db · redis · rabbitmq · jasmin · sms-gateway · app
├── Dockerfile
└── requirements.txt
```
---
## Cost Model
| Scenario | Cost |
|---|---|
| Pre-defined topic (any category) | **$0** — knowledge cache |
| Same free question within 24 h | **$0** — Redis response cache |
| New free-form question (Groq) | **< $0.0001** |
| SMS via Jasmin (direct SMPP) | **~$0** — direct operator rate |
| **Blended average per interaction** | **< $0.00005** |
---
## License
MIT — free to use, modify, and deploy.
---
*Built to make AI accessible to everyone, everywhere.*