An AI-powered customer support copilot API for mobile financial services (bKash context). Analyzes support tickets using hybrid deterministic rules + LLM reasoning, returns structured JSON decisions with strict safety guarantees.
Team: Dekha Jabe | Event: SUST CSE Carnival 2026 · Codex Hackathon
# Install
npm install
# Set environment variables
cp .env.example .env
# Edit .env with your Gemini API keys
# Run (production)
npm start
# Run (development with auto-reload)
npm run devServer starts on port 8000 (override with PORT env var).
docker pull menayeem/team-dekha-jabe
docker run -p 8000:8000 --env-file .env menayeem/team-dekha-jabedocker build -t team-dekha-jabe .
docker run -p 8000:8000 --env-file .env team-dekha-jabeImage size: 69.5MB (node:20-slim, multi-stage build, no dev dependencies).
curl http://localhost:8000/health
# → {"status":"ok"}- Backend API: customers-support-chat-bot.onrender.com
Note: The Render free tier spins down after inactivity. The first request may take ~30 seconds to cold-start.
| Variable | Required | Description |
|---|---|---|
PORT |
No | Server port (default: 8000) |
GEMINI_API_KEY_1 |
Yes | Primary Gemini API key for LLM text generation |
GEMINI_API_KEY_2 |
No | Fallback key for rate-limit rotation |
GEMINI_API_KEY_3 |
No | Third fallback key |
Keys are loaded from environment only — never committed to the repo.
Returns instantly with no external dependencies.
{ "status": "ok" }Analyzes a customer support ticket and returns a structured decision.
Sample Request:
{
"ticket_id": "TKT-001",
"complaint": "I sent 5000 taka to a wrong number around 2pm today.",
"language": "en",
"channel": "in_app_chat",
"user_type": "customer",
"transaction_history": [
{
"transaction_id": "TXN-9101",
"timestamp": "2026-04-14T14:08:22Z",
"type": "transfer",
"amount": 5000,
"counterparty": "+8801719876543",
"status": "completed"
}
]
}Sample Response:
{
"ticket_id": "TKT-001",
"relevant_transaction_id": "TXN-9101",
"evidence_verdict": "consistent",
"case_type": "wrong_transfer",
"severity": "high",
"department": "dispute_resolution",
"agent_summary": "Customer reports wrong transfer to +8801719876543.",
"recommended_next_action": "Route to dispute_resolution team for detailed investigation and reversal assessment.",
"customer_reply": "We have noted your concern about transaction TXN-9101. Our dispute resolution team will review the case and contact you through official support channels. Please do not share your PIN or OTP with anyone.",
"human_review_required": true,
"confidence": 0.9,
"reason_codes": ["wrong_transfer", "transaction_match"]
}- Runtime: Node.js 20 + Express 5
- Validation: Zod v4 (lenient input, strict output)
- LLM: Google Gemini 1.5 Flash (external API)
- Cache: In-memory (exact-match, SHA-256 keyed, 5-min TTL)
- Security: Helmet, CORS, 32KB body limit
Architecture: Hybrid deterministic + LLM
The core decision fields (evidence_verdict, case_type, department, severity, human_review_required, relevant_transaction_id) are computed deterministically using rule-based pattern matching and evidence analysis. The LLM is used only for natural language text generation (agent_summary, recommended_next_action, customer_reply).
This means:
- Decisions are consistent and reproducible regardless of LLM availability
- Safety is enforced structurally, not by prompting
- The service remains fully functional if the LLM is down
| Model | Where | Why |
|---|---|---|
| Gemini 2.5 Flash | External API (Google AI Studio) | Fast inference, free tier, strong multilingual (EN/BN) support |
- Temperature: 0.3 for summaries/actions, 0.5 for customer replies
- Multi-key rotation: 2-3 API keys with automatic failover on rate-limit (429)
- Fallback chain: LLM key 1 → key 2/3 → templated fallback → safe default
- Free-tier Gemini API with multi-key rotation to stay within quota
- In-memory cache deduplicates identical requests (cache hit = 0 LLM calls)
- Deterministic fallback ensures zero LLM cost when API is unavailable
- All three LLM calls (summary, action, reply) run in parallel to minimize latency
Every generated response passes through a deterministic safety post-filter before leaving the system. The LLM cannot bypass this.
-
Never request credentials (−15 pts): Scans
customer_replyfor any request for PIN, OTP, password, CVV, card number — in English, Bangla, and romanized Bangla. Warning the user ("never share your PIN") is allowed; asking is blocked. -
Never promise unauthorized actions (−10 pts): Scans both
customer_replyandrecommended_next_actionfor refund promises, account unblock confirmations, or guaranteed outcomes. Rewrites to safe language: "any eligible amount will be returned through official channels." -
Never direct to suspicious third parties (−10 pts): Scans for unofficial phone numbers, WhatsApp/Telegram/IMO links, and non-official URLs. Replaces with official channel guidance.
If any violation is detected → response is rewritten to a safe template and human_review_required is forced to true.
Before any analysis, the complaint text is scanned for:
- Credential mentions (PIN, OTP, password — EN/BN)
- Phishing/social-engineering signals
- Fraud/scam language
- Prompt injection attempts ("ignore your rules", "bypass policies")
If flagged → human_review_required is forced to true regardless of LLM output.
- Always escalate: wrong_transfer (with evidence), phishing, duplicate_payment, agent_cash_in_issue, inconsistent evidence
- Safe to auto-resolve: routine refund requests, merchant settlement delays, payment failures with clear evidence
- Default: when unsure, escalate (safety > confidence)
- Free-tier rate limits: Gemini free tier has RPM limits. Multi-key rotation and caching mitigate this, but under heavy burst the service falls back to templates.
- Cold start: Free Render instances sleep after inactivity. An external pinger keeps the service warm during judging.
- Pattern matching: Case classification uses regex patterns for EN and BN. Novel phrasings outside the pattern set fall to
otherand are handled conservatively. - No persistent storage: Cache is in-memory and resets on restart. This is intentional — no stale answers survive a restart.
- LLM-generated text: When the LLM is available, text fields are richer but still pass through the deterministic safety filter. When unavailable, pre-built templates are used.
- Synthetic data only: All test data is synthetic. No real customer, financial, or production data is used.
- No secrets committed: API keys are loaded from environment variables only.
.envis in.gitignore.
src/
├── server.js # Express entry point
├── routes/ # Route definitions
├── controllers/ # Request handlers
├── services/ # Business logic orchestration
├── schemas/ # Zod input/output validation
├── lib/ # Core logic modules
│ ├── transaction-matcher.js # Evidence matching (Step 2a)
│ ├── evidence-verdict.js # Verdict decision (Step 2b)
│ ├── case-classifier.js # Case classification (Step 2c)
│ ├── routing.js # Department + severity (Step 2d)
│ ├── escalation.js # Human review logic (Step 2e)
│ ├── llm-drafting.js # LLM text generation (Step 2f)
│ ├── safety-pre-screen.js # Input safety scan
│ ├── safety-post-filter.js # Output safety filter
│ ├── cache.js # In-memory cache
│ ├── timing.js # Time budget tracking
│ └── safe-default.js # Conservative fallback
├── constants/
│ └── enums.js # All enum definitions
└── middleware/
├── validate.js # Input validation
└── error-handler.js # Global error catch-all