AI-powered customer service chatbot using DeepSeek (LLM) + SQLite (context database). Built with Node.js, Express, and vanilla frontend styled with Tailwind CSS.
| Layer | Tech |
|---|---|
| Backend | Node.js + Express |
| AI | DeepSeek API (deepseek-chat) |
| Database | SQLite (better-sqlite3) |
| Frontend | Vanilla HTML + CSS + JS |
| Styling | Tailwind CSS (CDN) |
| Markdown | marked.js (CDN) |
.
├── .env # Environment variables
├── .gitignore
├── package.json
├── server.js # Express entry point, routes, static serve
├── cache.js # In-memory TTL response cache
├── db.js # SQLite connection, LIKE search, full-table fetch
├── context.js # Tokenizer, intent detection, context builder
├── ai.js # DeepSeek API client + caching layers
└── public/
├── index.html # Chat UI
├── css/style.css # Custom styles (scrollbar, markdown, typing anim)
└── js/app.js # Frontend chat client
npm installCreate a .env file:
DEEPSEEK_API_KEY=sk-your_key_here
DEEPSEEK_BASE_URL=https://api.deepseek.com
DEEPSEEK_MODEL=deepseek-chat
DB_PATH=./data.db
PORT=3000
CONTACT_EMAIL=cs@example.com
DS_TEMPERATURE=0.7
DS_TOP_P=0.9
DS_TOP_K=40
DS_MAX_TOKENS=2048
DS_PRESENCE_PENALTY=0
DS_FREQUENCY_PENALTY=0Place your SQLite database file at the path specified in DB_PATH, then:
npm startOpen http://localhost:3000.
The chatbot expects a SQLite database with these tables:
| Column | Type | Description |
|---|---|---|
| section_title | TEXT | Section heading (e.g. "Brand", "Vision") |
| content | TEXT | Body content |
| Column | Type | Description |
|---|---|---|
| question | TEXT | Frequently asked question |
| answer | TEXT | Corresponding answer |
| Column | Type | Description |
|---|---|---|
| title | TEXT | Blog/article title |
| content | TEXT | Blog/article body |
| Column | Type | Description |
|---|---|---|
| name | TEXT | Product name |
| short_description | TEXT | Brief product summary |
| long_description | TEXT | Detailed product description |
| tags | TEXT | Comma-separated tags |
| link | TEXT | Product slug or URL |
| price | TEXT | Price as string |
| Column | Type | Description |
|---|---|---|
| name | TEXT | Store name |
| address | TEXT | Store address |
Send a user message and get an AI reply grounded in database context.
Request:
{
"message": "Do you have aloe vera gel?"
}Response:
{
"reply": "Yes! We have Aloe Vera Gel — a soothing gel made from 92% aloe vera, priced at $12.99.",
"hasData": true
}| Field | Type | Description |
|---|---|---|
reply |
string | AI-generated response |
hasData |
boolean | Whether relevant database context was found |
Status codes:
200— Success400— Missingmessagefield500— Internal server error (AI API failure, etc.)
The public/ directory is served as static files. The chat UI sends POST /api/chat requests via fetch() and renders responses with markdown.
- Tokenize — User message is split into keywords (stop words removed)
- LIKE search — Each keyword is matched against all 5 tables via SQL
LIKE - Intent fallback — If a table has zero results but the message hints at it (e.g. "store", "toko", "price", "harga"), all rows from that table are fetched
- Context assembly — Results are formatted into a text block prefixed by section headers
- AI prompt — The context is injected into DeepSeek's system prompt; the AI must answer using only that data
Two layers:
| Layer | What it caches | TTL | Avoids |
|---|---|---|---|
App cache (cache.js) |
Exact Q&A response (by MD5 hash of message + context) | 1 hour | Duplicate API calls entirely |
| DeepSeek auto cache | Prompt prefix (system + context) on server side | Automatic (≥1024 tokens) | Resending cached prompt tokens |
Console logs show cache efficiency: [App Cache] HIT, [DS Auto Cache] hit=X miss=Y.
| Variable | Default | Description |
|---|---|---|
DEEPSEEK_API_KEY |
— | DeepSeek API key (required) |
DEEPSEEK_BASE_URL |
https://api.deepseek.com |
API base URL |
DEEPSEEK_MODEL |
deepseek-chat |
Model name |
DB_PATH |
./data.db |
Path to SQLite database |
PORT |
3000 |
HTTP server port |
DS_TEMPERATURE |
0.7 |
Response randomness |
DS_TOP_P |
0.9 |
Nucleus sampling |
DS_TOP_K |
40 |
Top-K sampling |
DS_MAX_TOKENS |
2048 |
Max response length |
DS_PRESENCE_PENALTY |
0 |
Penalize repeated topics |
DS_FREQUENCY_PENALTY |
0 |
Penalize repeated phrases |
English and Indonesian. Stop words and intent keywords are defined for both languages:
- Intent tables map common query words to database tables:
store/toko→storesproduct/produk→productsabout/tentang→aboutsfaq/retur→faqsblog/artikel→posts