Secure AI Assistant with RAG Pipeline — A production-grade customer support chatbot powered by Google Gemini and Retrieval-Augmented Generation. Built with ASP.NET Core MVC, Qdrant vector search, and a modern Tailwind CSS interface with persistent dark/light mode.
| Layer | Technology |
|---|---|
| Backend | .NET 10 — ASP.NET Core MVC, ASP.NET Core Identity |
| Database | PostgreSQL (via Npgsql + Entity Framework Core) |
| AI / LLM | Google Gemini API (gemini-2.5-flash) via Microsoft.Extensions.AI |
| Embeddings | Gemini Embedding API (gemini-embedding-001, 3 072-dim vectors) |
| Vector DB | Qdrant (gRPC on port 6334) for RAG retrieval |
| Frontend | Tailwind CSS (Play CDN), Inter typeface, Vanilla JS — zero jQuery |
| Markdown | Markdig for server-side Markdown → HTML rendering |
| Tokenizer | Microsoft.ML.Tokenizers (cl100k_base) for chunk splitting |
| Security | JWT Bearer Authentication (HttpOnly cookie), Role-based access (admin / user), CSRF via ValidateAntiForgeryToken, Global Exception Handling |
ai-support-system/
├── .dockerignore
├── .env # Not added to Git
├── ai-support-system.slnx # Solution file
├── Dockerfile
├── docker-compose.yml
├── LICENSE.txt
├── README.md
│
└── ai-support-system/
├── ai-support-system.csproj # .NET 10 project definition & NuGet packages
├── Program.cs # Application bootstrap, DI, middleware pipeline
├── appsettings.json # Configuration (Gemini, Qdrant, JWT, DB)
├── appsettings.Development.json # Not added to Git
│
├── Controllers/
│ ├── AdminController.cs # [Authorize(Roles="admin")] — User & document management
│ ├── AuthController.cs # Login, Register, Logout (JWT cookie flow)
│ ├── ChatController.cs # [Authorize] — Conversational RAG chat endpoint
│ └── HomeController.cs # Global error page handler
│
├── Services/
│ ├── RagPipeline.cs # Orchestrates ingest (chunk → embed → upsert) and query (embed → search → context)
│ ├── EmbeddingService.cs # Gemini Embedding API client (query & batch document embedding)
│ ├── QdrantService.cs # Qdrant vector CRUD — collection, upsert, search, delete
│ ├── JwtService.cs # JWT token generation with role claims
│ └── TextChunker.cs # Recursive token-aware text splitter with overlap
│
├── Models/
│ ├── DocumentModel .cs # Document entity (Id, Title, Category, Content)
│ ├── DocumentChunkModel.cs # Chunk entity with DocId, index, hash, content
│ └── ErrorViewModel.cs # Error page view model
│
├── DTOs/
│ ├── ChatTurnDto.cs # Lightweight session-serializable chat turn
│ ├── LoginDto.cs # Login form binding
│ └── RegisterDto.cs # Registration form binding
│
├── Data/
│ └── ApplicationDbContext.cs # EF Core Identity DbContext (PostgreSQL)
│
├── Enums/
│ └── ErrorSeverity.cs # Transient | Degraded | Fatal
│
├── Exceptions/
│ ├── DomainException.cs # Abstract base (UserMessage + TechnicalDetail + Severity)
│ ├── AIServiceException.cs # Gemini / LLM errors
│ ├── EmbeddingException.cs # Embedding-specific errors
│ └── VectorDatabaseException.cs # Qdrant connectivity / operation errors
│
├── Extensions/
│ └── SessionExtensions.cs # Generic JSON session get/set helpers
│
├── Filters/
│ └── DomainExceptionFilter.cs # Global MVC filter — catches DomainException, renders user-friendly banners
│
├── Migrations/ # EF Core migration files (PostgreSQL)
│
├── Views/
│ ├── _ViewImports.cshtml
│ ├── _ViewStart.cshtml
│ ├── Shared/
│ │ ├── _Layout.cshtml # Master layout — navbar, dark/light toggle, Tailwind config
│ │ ├── _Layout.cshtml.css # Scoped layout styles
│ │ ├── _ValidationScriptsPartial.cshtml
│ │ └── Error.cshtml # Global 500 error page
│ ├── Chat/
│ │ └── Index.cshtml # Chat UI — message bubbles, input bar, "New Chat" action
│ ├── Admin/
│ │ ├── Index.cshtml # User management — role switching
│ │ └── Documents.cshtml # Document CRUD — add, filter, edit (native <dialog>), delete
│ ├── Auth/
│ │ ├── Login.cshtml # Login form
│ │ └── Register.cshtml # Registration form
│ └── Home/
│ └── Error.cshtml # Fallback error view
│
├── Properties/
│ └── launchSettings.json
│
└── wwwroot/
├── css/
│ └── site.css # Base overrides — animations, dialog backdrop, scrollbar
├── js/
│ └── admin.js # Document edit modal logic (native <dialog> API)
└── favicon.ico
The application implements a full ingest-and-query RAG pipeline:
- Ingest — Admin uploads a document →
TextChunkersplits it into ~500-token chunks with 60-token overlap →EmbeddingServicegenerates 3 072-dim vectors via Gemini'sgemini-embedding-001model →QdrantServiceupserts each chunk as a vector point with metadata payload. - Query — User sends a message → query is embedded with
RETRIEVAL_QUERYtask type → Qdrant returns the top 20 candidates above a 0.65 cosine similarity threshold → a basic MMR (Maximal Marginal Relevance) filter selects the top 5 chunks (max 2 per document for diversity) → the assembled context is injected into the Gemini system prompt. - Deduplication — Documents are SHA-256 hashed before ingestion; duplicates are silently skipped.
- Conversational Memory — Chat history is maintained in-session with a sliding window of the last 10 messages.
- Tailwind CSS via Play CDN with a custom
tailwind.configextending the default theme. - Inter web font for clean, professional typography.
- Persistent Dark / Light Mode toggle — theme preference is stored in
localStorageand applied before first paint (FOUC prevention via inline script in_Layout.cshtml). - Slate-900 color palette for the navbar and chat header; slate-50/slate-800 for content surfaces.
- Fully responsive layout with a collapsible mobile navigation menu.
- Structured Exception Hierarchy —
DomainException→AIServiceException,EmbeddingException,VectorDatabaseException, each carrying a user-facing message, a technical detail string, and anErrorSeverityenum (Transient,Degraded,Fatal). - Global
DomainExceptionFilter— catches domain exceptions in the MVC pipeline and renders non-disruptive in-page error banners with fade-in animation instead of redirecting to a generic 500 page. - Fallback Error Handler —
UseExceptionHandler("/Home/Error")catches any unhandled exception and renders a safe error page (environment-aware messaging).
- User Management (
/Admin) — View all registered users, change roles betweenadminanduser. - Document CRUD (
/Admin/Documents) — Add, edit, and delete knowledge-base documents. Filtering by category and title. Edit uses a native HTML<dialog>modal powered by Vanilla JS (admin.js) — no Bootstrap or jQuery dependency. - Role-Gated Access — Admin controllers are protected with
[Authorize(Roles = "admin")].
- ASP.NET Core Identity with PostgreSQL for user persistence.
- JWT Bearer tokens generated via
JwtService(HMAC-SHA256), stored inHttpOnly,Secure,SameSite=Strictcookies. - JWT is configured as the default authentication scheme, overriding Identity's cookie defaults.
- All mutating endpoints are protected with
[ValidateAntiForgeryToken]. - Chat input is capped at 2 000 characters to prevent abuse.
- .NET 10 SDK
- PostgreSQL
- Qdrant (running on
localhost:6334) - A Google Gemini API Key
Update ai-support-system/appsettings.json (or use User Secrets / environment variables):
cd ai-support-system
# Restore dependencies
dotnet restore
# Apply EF Core migrations
dotnet ef database update
# Build the project
dotnet build
# Run the application
dotnet runThe application will be available at https://localhost:5225 (or the port configured in launchSettings.json).
On first launch, the application automatically creates admin and user roles and initializes the Qdrant support_chunks collection.
Initially, all users are registered with the user role. To promote yourself to Admin, follow these steps:
- Register a new account (e.g.,
test@test.com) via the application UI. - Connect to your PostgreSQL database and execute the following SQL commands to link the user to the admin role:
-- 1. Find your User ID and the Admin Role ID
SELECT "Id", "Email" FROM "AspNetUsers" WHERE "Email" = 'test@test.com';
SELECT "Id" FROM "AspNetRoles" WHERE "Name" = 'admin';
-- 2. Link the user to the admin role
-- Replace 'YOUR_USER_ID' and 'YOUR_ADMIN_ID' with the results from the queries above
INSERT INTO "AspNetUserRoles" ("UserId", "RoleId")
VALUES ('YOUR_USER_ID', 'YOUR_ADMIN_ID');Run the entire stack — ASP.NET Core app, PostgreSQL, and Qdrant — with a single command using Docker Compose.
- Docker (v20.10+)
- Docker Compose (v2.0+ — included with Docker Desktop)
Create a .env file in the project root (next to docker-compose.yml):
# ─── PostgreSQL ───
POSTGRES_DB=aisupportdb
POSTGRES_USER=postgres
POSTGRES_PASSWORD=YOUR_PASSWORD
# ─── Gemini API ───
GEMINI_API_KEY=YOUR_GEMINI_API_KEY
GEMINI_MODEL=gemini-2.5-flash
GEMINI_EMBEDDING_MODEL=gemini-embedding-001
# ─── JWT ───
JWT_SECRET=YOUR_JWT_SECRET_KEY_MIN_32_CHARS
JWT_ISSUER=AiSupportApp
JWT_AUDIENCE=AiSupportAppCaution
Never commit .env to version control. The .gitignore already excludes it. Replace all placeholder values with your own credentials before starting.
# Build and start all services in detached mode
docker compose up -d --buildThe first build will take a few minutes as it downloads base images and restores NuGet packages. Subsequent starts are much faster thanks to Docker layer caching.
| Service | URL |
|---|---|
| Web App | http://localhost:8080 |
| Qdrant Dashboard | http://localhost:6333/dashboard |
| PostgreSQL | localhost:5432 (connect via psql, pgAdmin, etc.) |
Note
The app container waits for both PostgreSQL and Qdrant to pass their health checks before starting. EF Core migrations are applied automatically on first launch.
# View real-time logs (all services)
docker compose logs -f
# View logs for a specific service
docker compose logs -f app
# Stop all services (preserves data volumes)
docker compose down
# Stop and remove all data (fresh start)
docker compose down -v
# Rebuild only the app after code changes
docker compose up -d --build app┌─────────────────────────────────────────────────────┐
│ Docker Compose │
│ │
│ ┌──────────────┐ ┌───────────┐ ┌──────────────┐ │
│ │ app │ │ postgres │ │ qdrant │ │
│ │ ASP.NET Core │ │ PG 17 │ │ Vector DB │ │
│ │ :8080 │→ │ :5432 │ │ :6333/:6334 │ │
│ │ │→ │ │ │ │ │
│ └──────────────┘ └───────────┘ └──────────────┘ │
│ ↓ ↓ ↓ │
│ (host:8080) (host:5432) (host:6333/6334) │
└─────────────────────────────────────────────────────┘
| Container | Image | Ports | Volume |
|---|---|---|---|
aisupport-app |
Built from Dockerfile (multi-stage .NET 10) |
8080:8080 |
— |
aisupport-postgres |
postgres:17-alpine |
5432:5432 |
postgres_data |
aisupport-qdrant |
qdrant/qdrant:latest |
6333:6333, 6334:6334 |
qdrant_data |
| Aspect | Standard |
|---|---|
| Design Language | Modern Corporate — clean lines, generous whitespace, subtle shadows |
| Typography | Inter (400, 500, 600, 700) via Google Fonts |
| Color Palette | Slate-900 navbar/headers, Slate-50 light surfaces, Slate-800 dark surfaces |
| Dark Mode | Class-based (dark: variants), persisted in localStorage, FOUC-free |
| Components | Native HTML <dialog> modals, Tailwind utility classes, zero external UI libraries |
| Animations | CSS fadeIn keyframes for error banners, transition-colors duration-300 for theme switching |
| Scrollbar | Custom WebKit scrollbar styling for chat area (6px, slate tones) |
| Responsiveness | Mobile-first with lg: breakpoint for desktop navigation |
- RAG Pipeline integration with Gemini AI & Qdrant
- Recursive Character Text Splitting with
cl100k_base - Docker Compose setup for local environment
- Interface-based abstractions for full testability
- Server-side HTML sanitization for LLM output
This project is licensed under the MIT License.
{ "ConnectionStrings": { "DefaultConnection": "Host=localhost;Database=aisupportdb;Username=YOUR-USERNAME;Password=YOUR_PASSWORD" }, "Qdrant": { "Host": "localhost", "Port": 6334 }, "Gemini": { "ApiKey": "YOUR_GEMINI_API_KEY", "Model": "gemini-2.5-flash", "EmbeddingModel": "gemini-embedding-001" }, "Jwt": { "Secret": "YOUR_JWT_SECRET_KEY_MIN_32_CHARS", "Issuer": "ai-support-system", "Audience": "ai-support-system" } }