A Spring Boot middleware service that connects users to external AI APIs via OpenRouter, with features like selectable personalities and conversation memory.
Built with Java 25, Spring Boot 4, Maven, and OpenRouter.
- Minimalistic Chat UI: Simple web interface with Markdown rendering and syntax-highlighted code blocks. Vibe-coded with Claude.
- RESTful API: Clean API endpoints for integration.
- Session-based Memory: Maintains conversation context using unique session IDs.
- Selectable Features and Personalities: Choose between different AI behaviors (Assistant, Coder, Ron Burgundy).
- Resiliency: Automatic retry with exponential backoff for network errors and rate limits.
- Thread-safe: Robust session handling using
ConcurrentHashMap. - API Documentation: Integrated OpenAPI/Swagger UI.
- Java 25
- Maven
- OpenRouter API Key: Obtain one for free at openrouter.ai.
git clone https://github.com/simonforsberg/promptly.git
cd promptlyThe application requires an AI_API_KEY to communicate with OpenRouter.
Terminal:
export AI_API_KEY=your_api_key_hereIntelliJ IDEA:
- Go to
Run->Edit Configurations. - Select
PromptlyApplication. - Add
AI_API_KEY=your_api_key_hereto Environment variables.
Maven Wrapper:
./mvnw spring-boot:runIDE:
Run the PromptlyApplication class directly.
- Chat UI: http://localhost:8080
- Swagger Documentation: http://localhost:8080/swagger-ui/index.html
You can override default settings in src/main/resources/application.properties or via environment variables:
| Property | Environment Variable | Description | Default |
|---|---|---|---|
ai.base-url |
AI_BASE_URL |
LLM provider base URL | https://openrouter.ai/api/v1 |
ai.api-key |
AI_API_KEY |
OpenRouter API key | (required) |
ai.model |
AI_MODEL |
AI model to use | openai/gpt-4o-mini |
The personality field in the API request changes the system prompt:
| Key | Role | Behavior |
|---|---|---|
assistant |
Assistant | Helpful and supportive assistant, answers clearly and concisely. |
coder |
Coder | Software engineer and coding assistant, gives technical advice. |
ron-burgundy |
Ron Burgundy | Chat with the legendary news anchor. Stay classy! |
Promptly features a modern, responsive chat interface built with lightweight web technologies and tightly integrated with the Spring Boot backend. The UI is rendered with HTML5, CSS3, Thymeleaf, and Vanilla JavaScript (ES6+), with Marked.js for Markdown rendering, Highlight.js for syntax highlighting, and DOMPurify for secure HTML sanitization.
- Markdown Support: Real-time rendering of Markdown for AI responses.
- Syntax Highlighting: Properly formatted code blocks using
highlight.js. - Personality Selector: Dynamic switching between different AI personas directly from the UI.
- Theme Switching: Support for both Dark 🌙 and Light 🔆 modes.
- Clear History: Easily reset the conversation context with the "Clear" button.
- Typography: Uses Google Fonts with Inter for UI text and JetBrains Mono for code blocks.
The project includes unit and integration tests using JUnit 5, Mockito, and MockMvc.
To execute all tests, run:
./mvnw test- Unit Tests: Business logic, personality mapping, and conversation memory.
- Integration Tests: API controllers and exception handling.
- Mocking: Unit tests use Mockito to mock dependencies. External API calls in integration tests are stubbed using WireMock.
Promptly uses a centralized exception handling mechanism with GlobalExceptionHandler to ensure consistent API responses.
All errors follow a standard structure:
{
"timestamp": "2026-05-10T12:45:00Z",
"status": 400,
"error": "Bad Request",
"message": "Detailed error message",
"path": "/api/v1/chat"
}- 400 Bad Request: Validation errors (e.g., missing message or personality).
- 503 Service Unavailable: AI service connection issues, communication errors, or circuit breaker activation.
- 500 Internal Server Error: Unexpected application errors.
The service implements automatic retries with exponential backoff for transient network errors and rate limits (429 Too Many Requests) when communicating with the AI provider.
POST /api/v1/chat
Request Body:
{
"sessionId": "unique-session-id",
"personality": "ron-burgundy",
"message": "Who are you?"
}Success Response (200 OK):
{
"sessionId": "unique-session-id",
"reply": "I don't know how to tell you this, but I'm kind of a big deal."
}Error Response (400 Bad Request):
{
"timestamp": "2026-05-08T17:25:00Z",
"status": 400,
"error": "Bad Request",
"message": "message: must not be blank",
"path": "/api/v1/chat"
}Error Response (503 Service Unavailable): Occurs when the AI service is unreachable, communication fails, or when the circuit breaker is open.
{
"timestamp": "2026-05-08T17:25:00Z",
"status": 503,
"error": "AI Service Unavailable",
"message": "AI service is currently unavailable. Please try again later.",
"path": "/api/v1/chat"
}
