Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions spring-ai/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# Spring AI Application

A Spring Boot application powered by **Spring AI** that provides AI-driven capabilities including chat completion, text summarization, translation, code analysis, and image generation via OpenAI.

## Features

- **Chat Completion** - Conversational AI with streaming support
- **Text Summarization** - Condense long text into concise summaries
- **Translation** - Translate text between 10+ languages
- **Code Analysis** - Analyze code for descriptions, issues, and complexity
- **Image Generation** - Generate images from text prompts using DALL-E 3
- **Web UI** - Clean, tabbed browser interface for all features

## Tech Stack

- Java 17
- Spring Boot 3.4.1
- Spring AI 1.0.0-M5 (OpenAI)
- Thymeleaf (Web UI)
- Maven

## Prerequisites

- Java 17+
- Maven 3.6+
- OpenAI API key

## Getting Started

### 1. Navigate to the spring-ai directory

```bash
cd spring-ai
```

### 2. Set your OpenAI API key

```bash
export SPRING_AI_OPENAI_API_KEY=your-api-key-here
```

### 3. Build and run

```bash
mvn clean install
mvn spring-boot:run
```

### 4. Access the application

Open [http://localhost:8080](http://localhost:8080) in your browser.

## API Endpoints

### Chat

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/chat?message=...` | Simple chat |
| `POST` | `/api/chat` | Chat with options (model, temperature) |
| `GET` | `/api/chat/stream?message=...` | Streaming chat (SSE) |
| `POST` | `/api/chat/summarize` | Summarize text |
| `POST` | `/api/chat/translate` | Translate text |
| `POST` | `/api/chat/analyze-code` | Analyze code |

### Image

| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/api/image/generate` | Generate image from prompt |

### Example Requests

**Chat:**
```bash
curl http://localhost:8080/api/chat?message=Hello

curl -X POST http://localhost:8080/api/chat \
-H "Content-Type: application/json" \
-d '{"message": "Explain Spring AI", "model": "gpt-4o-mini", "temperature": 0.7}'
```

**Summarize:**
```bash
curl -X POST http://localhost:8080/api/chat/summarize \
-H "Content-Type: application/json" \
-d '{"text": "Long text to summarize..."}'
```

**Translate:**
```bash
curl -X POST http://localhost:8080/api/chat/translate \
-H "Content-Type: application/json" \
-d '{"text": "Hello world", "targetLanguage": "Spanish"}'
```

**Image Generation:**
```bash
curl -X POST http://localhost:8080/api/image/generate \
-H "Content-Type: application/json" \
-d '{"prompt": "A sunset over mountains", "width": 1024, "height": 1024}'
```

## Configuration

Key properties in `application.properties`:

| Property | Default | Description |
|----------|---------|-------------|
| `spring.ai.openai.api-key` | - | Your OpenAI API key |
| `spring.ai.openai.chat.options.model` | `gpt-4o-mini` | Chat model |
| `spring.ai.openai.chat.options.temperature` | `0.7` | Response creativity |
| `spring.ai.openai.image.options.model` | `dall-e-3` | Image model |
| `server.port` | `8080` | Server port |

## Project Structure

```
spring-ai/
src/main/java/com/example/springai/
├── SpringAiApplication.java # Main application entry point
├── config/
│ └── AiConfig.java # CORS and web configuration
├── controller/
│ ├── ChatController.java # Chat REST API endpoints
│ ├── ImageController.java # Image generation endpoint
│ └── WebController.java # Web UI controller
├── model/
│ ├── ChatRequest.java # Chat request DTO
│ ├── ChatResponse.java # Chat response DTO
│ ├── ImageRequest.java # Image request DTO
│ └── ImageResponse.java # Image response DTO
└── service/
├── ChatService.java # Chat/AI business logic
└── ImageService.java # Image generation logic
```

## Running Tests

```bash
cd spring-ai
mvn test
```
86 changes: 86 additions & 0 deletions spring-ai/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.1</version>
<relativePath/>
</parent>

<groupId>com.example</groupId>
<artifactId>spring-ai-app</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Spring AI Application</name>
<description>A Spring Boot application with Spring AI integration for chat, image generation, and embeddings</description>

<properties>
<java.version>17</java.version>
<spring-ai.version>1.0.0-M5</spring-ai.version>
</properties>

<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- Spring AI - OpenAI -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>

<!-- Spring Boot DevTools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>

<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.springai;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringAiApplication {

public static void main(String[] args) {
SpringApplication.run(SpringAiApplication.class, args);
}
}
17 changes: 17 additions & 0 deletions spring-ai/src/main/java/com/example/springai/config/AiConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.example.springai.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class AiConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.example.springai.controller;

import com.example.springai.model.ChatRequest;
import com.example.springai.model.ChatResponse;
import com.example.springai.service.ChatService;
import jakarta.validation.Valid;
import java.util.Map;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@RestController
@RequestMapping("/api/chat")
public class ChatController {

private final ChatService chatService;

public ChatController(ChatService chatService) {
this.chatService = chatService;
}

@GetMapping
public ChatResponse chat(@RequestParam String message) {
return chatService.chat(message);
}

@PostMapping
public ChatResponse chatPost(@Valid @RequestBody ChatRequest request) {
return chatService.chatWithOptions(
request.getMessage(), request.getModel(), request.getTemperature());
}

@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chatStream(@RequestParam String message) {
return chatService.chatStream(message);
}

@PostMapping("/summarize")
public ChatResponse summarize(@RequestBody Map<String, String> request) {
String text = request.get("text");
return chatService.summarize(text);
Comment on lines +44 to +46

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Missing null validation on /summarize, /translate, /analyze-code endpoints allows null to propagate to AI API

The summarize, translate, and analyzeCode controller methods extract values from a raw Map<String, String> using request.get("text") / request.get("code") without null checks. If a client sends a request with a missing or misnamed key (e.g., {} or {"txt": "hello"}), the value is null. This null propagates through ChatService.chatWithSystemPrompt()new UserMessage(null)chatModel.call(), which will likely throw a NullPointerException or an uninformative 500 error, rather than a proper 400 Bad Request. This contrasts with the chatPost endpoint which properly uses @Valid @RequestBody ChatRequest with @NotBlank validation.

Prompt for agents
The /summarize, /translate, and /analyze-code endpoints in ChatController (lines 43-60) accept Map<String, String> request bodies but never validate that the required keys ("text" for summarize/translate, "code" for analyze-code) are present and non-blank. A missing key causes null to flow through to ChatService.chatWithSystemPrompt and then to new UserMessage(null), resulting in an unhandled NullPointerException (500 error) rather than a 400 Bad Request.

Approach: Either create proper DTO classes (like ChatRequest) with @NotBlank validation for each endpoint, or add null/blank checks in the controller methods before calling the service, throwing a ResponseStatusException(HttpStatus.BAD_REQUEST) if the required field is missing.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}

@PostMapping("/translate")
public ChatResponse translate(@RequestBody Map<String, String> request) {
String text = request.get("text");
String targetLanguage = request.getOrDefault("targetLanguage", "English");
return chatService.translate(text, targetLanguage);
}

@PostMapping("/analyze-code")
public ChatResponse analyzeCode(@RequestBody Map<String, String> request) {
String code = request.get("code");
return chatService.analyzeCode(code);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.example.springai.controller;

import com.example.springai.model.ImageRequest;
import com.example.springai.model.ImageResponse;
import com.example.springai.service.ImageService;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/image")
public class ImageController {

private final ImageService imageService;

public ImageController(ImageService imageService) {
this.imageService = imageService;
}

@PostMapping("/generate")
public ImageResponse generateImage(@Valid @RequestBody ImageRequest request) {
return imageService.generateImage(
request.getPrompt(), request.getWidth(), request.getHeight());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.springai.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class WebController {

@GetMapping("/")
public String index() {
return "index";
}
}
Loading
Loading