Personal project
AskLK: Bilingual AI Assistant with RAG, Agents and MCP
An assistant a Sri Lankan clinic, school or company can point at its own documents. I wanted it to say "I don't know" rather than guess, so retrieval is hybrid (pgvector plus Postgres full-text, merged with Reciprocal Rank Fusion), every answer cites its sources, the agent waits for a human before any write, and an MCP server lets Claude Code search the same documents.
The problem
- A tuition teacher, a clinic or a company keeps its knowledge in documents, and people ask about it in Sinhala or English. Often the document is in the other language.
- Generic chatbots guess when they don't know, and they invent sources. For a clinic or a school that is worse than no answer.
- Many organisations share one deployment, so one tenant must never see another tenant's documents.
Architecture and key decisions
- NestJS 10 API (auth, RBAC, tenants, the agent loop, SSE streaming), a Next.js 15 dashboard and chat, and an embeddable Shadow DOM widget: 17 KB raw, 5 KB gzipped.
- PostgreSQL 16 with pgvector stores documents, chunks and 768-dimension embeddings. 768 because pgvector's HNSW index stops at 2,000 dimensions, and Gemini's embedding model keeps most of its quality when truncated.
- Retrieval runs two queries in one transaction: a vector search, so a Sinhala question can match an English passage, and a full-text search for exact tokens. Cosine scores and ts_rank can't be added together, so the two lists are fused by rank with RRF (k = 60).
- Four guards against made-up answers: a similarity floor that returns no passages for weak matches, prompt rules, citation validation, and source cards built from the retrieved rows instead of parsed out of the model's text.
- A hand-written agent loop rather than LangChain. It stops after five steps, asks a human to approve any write (opening a ticket, for example), and logs every tool call to an audit table.
- A provider-neutral ChatModel port with Gemini, Claude and OpenAI-compatible adapters, picked per organisation. A fast model classifies the question and a stronger one answers.
- An MCP server over stdio and streamable HTTP, with tenant API keys and per-key rate limits. I've run it from Claude Code; Claude Desktop takes the same config.
- Redis-backed ingestion worker (extract, chunk, embed), OpenTelemetry GenAI traces, a per-tenant LLM cost ledger with a budget, Postgres row-level security on the paths that read content, and OIDC sign-in with PKCE.
- Terraform for AWS: ECS Fargate (ARM64) behind an ALB, RDS and ElastiCache in private subnets, SQS with a dead-letter queue. The GitHub OIDC deploy workflow is written but has never run, because there is no AWS account behind it yet.
Results
- A 46-case golden-set eval runs on every pull request that touches the AI code. It uses a deterministic fake model, so it catches regressions; it doesn't grade a real model. Faithfulness 100%, recall@5 100%, refusal accuracy 91.7%.
- One poisoned document could hijack unrelated answers: injection resistance was 0 of 10 cases. A passage sanitiser plus a stricter prompt took it to 10 of 10, and I wrote a postmortem.
- 358 unit tests, 42 integration tests against a real pgvector database (tenant isolation and RLS included), 17 Vitest tests for the MCP server and 24 Playwright end-to-end tests.
- k6 on my laptop with a stubbed model: p99 346 ms at 20 requests per second. That is platform overhead only, since a real model adds its own time to first token. A 20-page PDF is ingested in 1.76 s.
- Terraform: a 71-resource plan, with 41 resources applied and destroyed on LocalStack. ECS, the ALB, RDS and ElastiCache were planned only.
- 9 row-level security tests. One runs a query with no WHERE clause and still sees only one tenant.
Tech stack
- TypeScript
- NestJS
- Next.js 15
- React 19
- PostgreSQL
- pgvector
- Prisma
- Redis
- RAG
- LLM agents
- MCP
- Claude API
- Gemini API
- OpenTelemetry
- Terraform
- AWS (LocalStack)
- Docker
- GitHub Actions
- Jest
- Vitest
- Playwright
- k6
Links
- Architecture notes and code excerpts: github.com/jani358/asklk-AI-assistant-showcase. The main repo is private. Full source available on request.
- Case study by Janindu Gayanga, full-stack software engineer in Colombo, Sri Lanka
- Profile on GitHub and LinkedIn
Other projects
- BookLK: Event-Driven Service Booking Marketplace: Multi-vendor booking with Kafka, a Go realtime service, payments and Kubernetes
- QueueLK: Real-Time Queue Management for Clinics: Live queue display, phone page and Expo patient app in Sinhala, Tamil and English
- ApplyWise: CV Tailoring Where Every Line Cites a Fact: Each line of a tailored CV cites a fact from your own CV, and a verifier removes the rest
- iGaming Platform: Go Microservices, Payments and Compliance: Real-money gaming platform with 12,299 registered players, moving from a Node.js monolith to Go