Spring Boot RAG application for PDF Q&A with sandbox isolation, vector search (pgvector), and OpenAI models.
██╗ ██╗ ██╗███╗ ██╗ ██████╗ ██████╗ █████╗ ██╗ ██║ ╚██╗ ██╔╝████╗ ██║██╔═══██╗██╔══██╗██╔══██╗██║ ██║ ╚████╔╝ ██╔██╗ ██║██║ ██║██████╔╝███████║██║ ██║ ╚██╔╝ ██║╚██╗██║██║ ██║██╔══██╗██╔══██║██║ ███████╗ ██║ ██║ ╚████║╚██████╔╝██║ ██║██║ ██║██║ ╚══════╝ ╚═╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝
Implemented and aligned in code:
- Unified bootstrap API: one call to initialize app state
- Sandbox lifecycle with remaining time in seconds
- Document readiness recovery after refresh (
documentReady) - Streaming answer endpoint (SSE)
- PDF upload + async ingestion + dedup by file hash
- Rate limiting for upload/ask
- Flyway migrations for schema evolution
- Test suite updated to run in Docker-restricted environments by default
- Frontend calls
GET /api/bootstrap(with optionalX-Sandbox-Token) - Backend resolves/creates sandbox and returns:
- app version/build metadata
- sandbox token + active flag + remaining seconds
documentReady
- Frontend stores
sandbox.tokenin localStorage - Upload endpoint ingests PDF and builds embeddings/chunks
- Ask endpoint streams answer tokens via SSE, grounded by retrieved chunks
Single initialization endpoint used by frontend.
Response shape:
{
"version": {
"version": "0.0.1",
"commitFull": "abcdef123456...",
"commitShort": "abcdef1",
"commitUrl": "https://github.com/devdao2002/enterprise-rag-assistant/commit/abcdef123456...",
"buildTime": "2026-03-02T00:00:00Z"
},
"sandbox": {
"token": "uuid",
"active": true,
"remainingSeconds": 14399
},
"documentReady": false
}Headers:
X-Sandbox-Token: <uuid>
Body:
- multipart
file(PDF)
Headers:
X-Sandbox-Token: <uuid>
Headers:
X-Sandbox-Token: <uuid>
Produces:
text/event-stream
- Java 17
- Spring Boot 4.0.3
- Spring AI (OpenAI)
- PostgreSQL + pgvector
- Flyway
- Apache PDFBox
- Bucket4j
- JUnit 5 + Mockito + Testcontainers + WireMock
src/main/java/com/ducdo/ai_assistant
├── controller
│ ├── AskController.java
│ ├── BootstrapController.java
│ └── DocumentController.java
├── dto/bootstrap
│ ├── BootstrapResponse.java
│ ├── SandboxDto.java
│ └── VersionDto.java
├── model
├── repository
├── security
│ ├── filter
│ └── resolver
├── service
└── util
src/main/resources
├── application.yaml
├── db/migration
└── static/index.html
src/main/resources/application.yaml expects environment variables:
SPRING_DATASOURCE_URLSPRING_DATASOURCE_USERNAMESPRING_DATASOURCE_PASSWORDOPENAI_API_KEYOPENAI_CHAT_MODEL(optional, defaultgpt-5-nano)OPENAI_EMBEDDING_MODEL(optional, defaulttext-embedding-3-small)APP_VERSION(optional)APP_GIT_COMMIT(optional)APP_BUILD_TIME(optional)
- Set API key:
export OPENAI_API_KEY=your_key_here- Start stack:
docker compose up --build- Open:
http://localhost:8080
Stop:
docker compose downRequirements:
- PostgreSQL with pgvector enabled
- Database/schema accessible by app credentials
Run app:
./mvnw spring-boot:runDefault (CI-like local run, no Docker dependency required for pass):
./mvnw testNotes:
DocumentChunkRepositoryTestuses Testcontainers and is auto-skipped when Docker is unavailable.RagFlowE2ETestis opt-in and disabled by default.
Run integration E2E explicitly:
./mvnw -DrunIntegrationTests=true test- Sandbox token required for protected API routes via
X-Sandbox-Token - Sandbox expiry enforced in filter/service
- Upload and ask rate limits enforced by IP
- LLM responses are grounded through retrieved context pipeline
Planned improvements:
- tighter e2e assertions for SSE payload content
- optional hybrid retrieval (keyword + vector)
- stronger operational metrics/observability
- auth/role model for production multi-organization use