Last updated: 30/04/2025
CivicAId is an AI-powered civic chatbot designed to streamline interactions between citizens and government services in Singapore. It integrates an LLM backend for intelligent conversations, local agentic workflows for performance optimisation, supports real-time issue reporting, and delivers customized, localized answers for the Singaporean context.
- Production Stack Overview
- Local Development Setup
- Architecture Overview
- Production Deployment
- Acknowledgments
| Component | Technology Stack |
|---|---|
| Frontend | React.js (Vite) served statically via Nginx |
| Backend | Express.js (Node.js) managed with PM2, reverse proxied by Nginx |
| LLM Server | Flask (Python) serving local AI/NLP models and Deepseek API calls via Openrouter |
| Database | PostgreSQL hosted on NeonDB |
| Hosting | Huawei Cloud Elastic Cloud Server (ECS) |
| Domain & Security | Cloudflare (Domain management and DDoS protection) |
You will need:
- Recent versions of Node.js and Python
- Access to required
.envfiles (contact the project contributors)
cd client
npm install
npm run devFrontend available at http://localhost:5173.
cd server
npm install
npm startBackend available at http://localhost:3000.
Note: The development server connects directly to the live PostgreSQL database once .env is correctly configured.
Please contact the developers to run the LLM server locally because the custom model.safetensors file used in RAG is too large to upload to this repo. Additionally, files containing API keys to call SEA-LION are not provided in this repo. Please contact the developers for the format of the file and how to obtain the API keys.
civic-aid/
├── LICENSE # MIT License
├── client/ # Frontend: Vite / React.js app
│ ├── public/ # Public assets (eg. images)
│ └── src/
│ ├── App.tsx # Main entry point and client-side
│ │ routing
│ └── components/ # React components and custom hooks
├── server/ # Backend: Express.js app
│ ├── config/
│ │ └── db.js # NeonDB PostgreSQL setup
│ ├── controllers/
│ │ ├── chatController.js # Handles chat creation, fetching
│ │ │ modifying, deleting
│ │ ├── commentsController.js # Handles comments
│ │ ├── govController.js # Handles admin functionality
│ │ ├── notificationsController.js # Handles notifications
│ │ ├── queryController.js # Handles query submissions
│ │ ├── reportController.js # Handles fetching, modifying, and
│ │ │ summarizing reports
│ │ └── translatorController.js # Handles translation functionality
│ ├── middleware/
│ │ └── authMiddleware.js # JWT authentication
│ ├── routes/ # API route definitions
│ ├── services/
│ │ ├── cron.js # Monthly rewards system updates
│ │ ├── geocoder.js # Address geocoding
│ │ ├── gov.js # Admin auxiliary functions
│ │ ├── llmservice.js # LLM interactions
│ │ ├── notifications.js # Notifications handler
│ │ ├── parsers.js # Parses LLM responses
│ │ └── promptbook.js # LLM system prompts
│ │ └── translator.js # Translation prompting functionality
│ └── index.js # Main server entry point
└── chatbot/ # Chatbot module
├── llmserver.py # AI server entry point
├── safety.py # LLM input safety filters
├── chatbotmodels/
│ ├── MainChatbotWithSealion.py # SEA-LION caller with RAG and image
│ │ capabilities
│ ├── BasicSealionKoller.py # Basic SEA-LION caller for
│ │ performance optimisation
│ ├── AuxModelCaptioner.py # Auxiliary image recognition and
│ │ captioning model
│ └── ReportCompiler.py # For grouping similar reports
├── Dockerfile # For optional Docker support
└── govsg_crawler_2 # Webcrawler and knowledge base for RAG
Click to expand deployment details
-
Frontend
Built withnpm run buildand served via Nginx. -
Backend
Hosted withpm2and reverse proxied by Nginx (/server/*routes). -
LLM Server
Flask server hosted on the cloud server. Optional Docker support available. -
Database
PostgreSQL database hosted on NeonDB. -
Domain and Security
Managed through Cloudflare for DNS and DDoS protection.
Nginx configuration files available upon request.
model.safetensors file available upon request.
Click to expand routes details
POST /register
Request {
username: string,
email: string,
password: string
}
Response {
success: boolean,
error: undefined | string
}POST /login
Request {
username: string,
password: string
}
Response {
id: number,
username: string,
email: string,
permissions: Array<string>
} | {
error: string
}POST /logout
Request {
null
}
Response {
success: boolean,
error: undefined | string
}GET /protected
Response {
id: number,
username: string,
email: string,
permissions: Array<string>,
iat: string,
exp: string
} | {
error: string
}POST /chats
Request {
chatId: string,
title: string,
type: string,
createdAt: string
}
Response {
success: boolean,
error: undefined | string
}PATCH /chats/:chatId
Request {
title: string
}
Response {
success: boolean,
error: undefined | string
}GET /chats
Response Array<{
id: string,
user_id: number,
type: string,
created_at: string,
title: string
}> | {
error: string
}GET /chats/:chatId
Response Array<{
id: string,
user_id: number,
chat_id: string,
created_at: string,
user_prompt: string,
media_url: Array<string>,
query_address: string,
system_prompt: string,
response: string,
sources: Array<string>,
is_valid: boolean,
to_reply: boolean,
query_confidence: number | null
}> | {
error: string
}DELETE /chats/:chatId
Response {
success: boolean
} | {
error: string
}POST /query
Request {
media: File | undefined,
prompt: string,
latitude: string,
longitude: string,
chatId: string
}
Response {
title: string,
media: string | undefined,
summary: string,
urgency: number,
recommendedSteps: string,
agency: string,
sources: Array<string> | undefined,
valid: true
} | {
answer: string,
sources: Array<string> | undefined,
valid: true
media: string | undefined
} | {
valid: false
} | {
error: string
}GET /reports/:id
Response {
id: string,
user_id: number,
chat_id: string,
title: string,
description: string,
media_url: Array<string>,
incident_address: string,
agency: string,
recommended_steps: string,
urgency: number,
report_confidence: number,
status: 'pending' | 'in progress' | 'resolved' | 'rejected',
created_at: string,
resolved_at: string | null,
is_public: boolean,
upvote_count: number,
remarks: string
} | {
error: string
}GET /reports
Response Array<{
id: string,
user_id: number,
chat_id: string,
title: string,
description: string,
media_url: Array<string>,
longitude: string,
latitude: string,
agency: string,
recommended_steps: string,
urgency: number,
report_confidence: number,
status: 'pending' | 'in progress' | 'resolved' | 'rejected',
created_at: string,
resolved_at: string | null,
is_public: boolean,
upvote_count: number,
remarks: string
}> | {
error: string
}GET /reports/public
Response Array<{
id: string,
user_id: number,
chat_id: string,
title: string,
description: string,
media_url: Array<string>,
longitude: string,
latitude: string,
agency: string,
recommended_steps: string,
urgency: number,
report_confidence: number,
status: 'pending' | 'in progress' | 'resolved' | 'rejected',
created_at: string,
resolved_at: string | null,
is_public: boolean,
upvote_count: number,
remarks: string
}> | {
error: string
}PATCH /reports/:id,
Request {
newStatus: 'pending' | 'in progress' | 'resolved' | 'rejected',
remarks: string
}
Response {
id: string,
user_id: number,
chat_id: string,
title: string,
description: string,
media_url: Array<string>,
incident_address: string,
agency: string,
recommended_steps: string,
urgency: number,
report_confidence: number,
status: 'pending' | 'in progress' | 'resolved' | 'rejected',
created_at: string,
resolved_at: string | null,
is_public: boolean,
upvote_count: number,
remarks: string
} | {
error: string
}POST /reports/set_is_public/:id
Request {
is_public: boolean
}
Response {
success: boolean,
error: undefined | string
}GET /reports/upvote_status
Response Array<string> | {
error: string
}POST /reports/upvote/:id
Request {
}
Response {
upvote_count: number
} | {
error: string
}POST /reports/undo_upvote/:id
Request {
}
Response {
upvote_count: number
} | {
error: string
}POST /comments
Request {
text: string,
report_id: string,
parent_id: undefined | number
}
Response {
success: boolean,
error: undefined | string
}GET /comments/:id
Response Array<Comment>
Comment: {
id: number,
report_id: string,
text: null | string,
upvote_count: number,
parent_id: null | number,
created_at: string,
deleted: boolean,
children: Array<Comment>
}PATCH /comment/:id
Request {
text: string
}
Response {
success: boolean,
error: undefined | string
}DELETE /comment/:id
Response {
success: boolean,
error: undefined | string
}GET /gov/reports
Request {
include_resolved: undefined | 1
}
Response Array<
{
id: string,
user_id: number,
chat_id: string,
title: string,
description: string,
media_url: Array<string>,
incident_address: string,
agency: string,
recommended_steps: string,
urgency: number,
report_confidence: number,
status: 'pending' | 'in progress' | 'resolved' | 'rejected',
created_at: string,
resolved_at: string | null,
is_public: boolean,
upvote_count: number,
remarks: string
} | {
valid: false
}
> | {
error: string
}PATCH /gov/reports/:id
Request {
newStatus: "pending" | "in progress" | "resolved" | "rejected",
remarks: string
}
Response {
id: string,
user_id: number,
chat_id: string,
title: string,
description: string,
media_url: Array<string>,
incident_address: string,
agency: string,
recommended_steps: string,
urgency: number,
report_confidence: number,
status: 'pending' | 'in progress' | 'resolved' | 'rejected',
created_at: string,
resolved_at: string | null,
is_public: boolean,
upvote_count: number,
remarks: string
} | {
error: string
}GET /gov/reports_summary
Response Array<
{
title: string,
summary: string,
description: string,
agency: string,
recommendedSteps: string,
urgency: number,
confidence: number,
sources: Array<string>
valid: true
} | {
valid: false
}
> | {
error: string
}GET /notifications
Response Array<{
id: number,
created_at: string,
user_id: number | null,
target: 'USER' | 'ADMIN' | 'ALL',
text: {
en: string,
cn: string,
ms: string,
ta: string
},
link: string,
read: boolean
}>POST /notifications/:id
Response {
success: boolean,
error: undefined | string
}POST /translate
Request {
text: string,
target:"ENGLISH" | "CHINESE" | "MALAY" | "TAMIL" | undefined
}
Response {
text: string
}