feat(ai): added simple service with AI integration#57
Conversation
RaffSStein
commented
Dec 8, 2025
- added new module "ai-simple-service" (spring v3.5.8) with Spring AI integration
- implemented simple endpoints with Ollama model integration
There was a problem hiding this comment.
Pull request overview
This PR introduces a new AI service module (ai-simple-service) to the Wealth Management Platform, integrating Spring AI with Ollama for AI chat capabilities. However, the implementation deviates significantly from the project's established conventions and has several critical issues that need to be addressed.
Key Changes:
- Added Spring AI dependency management (version 1.1.1) to the root POM
- Created new
ai-simple-servicemodule with API data and core submodules - Implemented two REST controllers demonstrating Ollama chat model integration
- Updated main README with enhanced tech stack documentation and restructured sections
- Simplified user-service and document-service README files
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 24 comments.
Show a summary per file
| File | Description |
|---|---|
| pom.xml | Added Spring AI BOM dependency management and registered new ai-simple-service modules |
| business-modules/ai-simple-service/ai-core/pom.xml | New module POM using Spring Boot 3.5.8 (deviates from project's Spring Boot 4.x standard) with Spring AI dependencies |
| business-modules/ai-simple-service/ai-core/src/main/resources/application.yml | Configuration for Ollama integration with hardcoded localhost endpoint and non-compliant application name |
| business-modules/ai-simple-service/ai-core/src/main/java/raff/stein/ai/controller/AiChatModelController.java | REST controller with direct ChatModel usage; doesn't implement OpenAPI specification, uses Italian prompts, lacks security and error handling |
| business-modules/ai-simple-service/ai-core/src/main/java/raff/stein/ai/controller/AiChatClientController.java | REST controller with ChatClient; doesn't implement OpenAPI specification, uses Italian prompts, duplicates configuration logic |
| business-modules/ai-simple-service/ai-core/src/main/java/raff/stein/ai/config/AiClientConfiguration.java | ChatClient bean configuration with hardcoded options |
| business-modules/ai-simple-service/ai-core/src/main/java/raff/stein/ai/AiChatModelApplication.java | Standard Spring Boot application entry point |
| business-modules/ai-simple-service/ai-api-data/pom.xml | API data module POM with OpenAPI generator configuration |
| business-modules/ai-simple-service/ai-api-data/ai-api-data.yaml | OpenAPI 3.1.0 specification defining /chat endpoint with security requirements, but missing securitySchemes definition |
| README.md | Enhanced with warning banner, comprehensive tech stack section, and improved architecture documentation |
| CONTRIBUTING.md | Added DB data model guidelines with a typo ("uaw" instead of "use") |
| business-modules/user-service/README.md | Simplified by removing detailed technical sections on build, dependencies, and local development |
| business-modules/document-service/README.md | Simplified by removing data model, API details, and process workflow documentation |
| import jakarta.servlet.http.HttpServletResponse; | ||
| import org.springframework.ai.chat.model.ChatModel; | ||
| import org.springframework.ai.chat.model.ChatResponse; | ||
| import org.springframework.ai.chat.prompt.Prompt; | ||
| import org.springframework.ai.ollama.api.OllamaChatOptions; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
| import reactor.core.publisher.Flux; | ||
|
|
||
|
|
||
| @RestController | ||
| @RequestMapping("/model") | ||
| public class AiChatModelController { | ||
|
|
||
| private static final String DEFAULT_PROMPT = "Ciao, presentati in modo creativo."; | ||
|
|
||
| private final ChatModel chatModel; | ||
|
|
||
|
|
||
| public AiChatModelController(ChatModel chatModel) { | ||
| this.chatModel = chatModel; | ||
| } | ||
|
|
||
|
|
||
| @GetMapping("/simple/chat") | ||
| public String simpleChat() { | ||
|
|
||
| return chatModel.call(new Prompt(DEFAULT_PROMPT)).getResult().getOutput().getText(); | ||
| } | ||
|
|
||
| @GetMapping("/stream/chat") | ||
| public Flux<String> streamChat(HttpServletResponse response) { | ||
|
|
||
| response.setCharacterEncoding("UTF-8"); | ||
|
|
||
| Flux<ChatResponse> stream = chatModel.stream(new Prompt(DEFAULT_PROMPT)); | ||
| return stream.map(resp -> resp.getResult().getOutput().getText()); | ||
| } | ||
|
|
||
|
|
||
| @GetMapping("/custom/chat") | ||
| public String customChat() { | ||
|
|
||
| OllamaChatOptions customOptions = OllamaChatOptions.builder() | ||
| .topP(0.7) | ||
| .model("llama3") | ||
| .temperature(0.8) | ||
| .build(); | ||
|
|
||
| return chatModel.call(new Prompt(DEFAULT_PROMPT, customOptions)).getResult().getOutput().getText(); | ||
| } |
There was a problem hiding this comment.
The controllers directly expose implementation details (Ollama-specific endpoints) instead of following the OpenAPI specification defined in ai-api-data.yaml. According to the project guidelines, controllers should implement generated interfaces from the -api-data module. The OpenAPI spec defines a /chat endpoint with proper DTOs (ChatRequestDTO, ChatResponseDTO), but these controllers use different paths (/model/, /client/*) and don't implement the specified API contract.
| import jakarta.servlet.http.HttpServletResponse; | |
| import org.springframework.ai.chat.model.ChatModel; | |
| import org.springframework.ai.chat.model.ChatResponse; | |
| import org.springframework.ai.chat.prompt.Prompt; | |
| import org.springframework.ai.ollama.api.OllamaChatOptions; | |
| import org.springframework.web.bind.annotation.GetMapping; | |
| import org.springframework.web.bind.annotation.RequestMapping; | |
| import org.springframework.web.bind.annotation.RestController; | |
| import reactor.core.publisher.Flux; | |
| @RestController | |
| @RequestMapping("/model") | |
| public class AiChatModelController { | |
| private static final String DEFAULT_PROMPT = "Ciao, presentati in modo creativo."; | |
| private final ChatModel chatModel; | |
| public AiChatModelController(ChatModel chatModel) { | |
| this.chatModel = chatModel; | |
| } | |
| @GetMapping("/simple/chat") | |
| public String simpleChat() { | |
| return chatModel.call(new Prompt(DEFAULT_PROMPT)).getResult().getOutput().getText(); | |
| } | |
| @GetMapping("/stream/chat") | |
| public Flux<String> streamChat(HttpServletResponse response) { | |
| response.setCharacterEncoding("UTF-8"); | |
| Flux<ChatResponse> stream = chatModel.stream(new Prompt(DEFAULT_PROMPT)); | |
| return stream.map(resp -> resp.getResult().getOutput().getText()); | |
| } | |
| @GetMapping("/custom/chat") | |
| public String customChat() { | |
| OllamaChatOptions customOptions = OllamaChatOptions.builder() | |
| .topP(0.7) | |
| .model("llama3") | |
| .temperature(0.8) | |
| .build(); | |
| return chatModel.call(new Prompt(DEFAULT_PROMPT, customOptions)).getResult().getOutput().getText(); | |
| } | |
| import org.springframework.ai.chat.model.ChatModel; | |
| import org.springframework.ai.chat.model.ChatResponse; | |
| import org.springframework.ai.chat.prompt.Prompt; | |
| 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; | |
| import raff.stein.ai.api.dto.ChatRequestDTO; | |
| import raff.stein.ai.api.dto.ChatResponseDTO; | |
| import raff.stein.ai.api.mapper.ChatMapper; | |
| @RestController | |
| @RequestMapping | |
| public class AiChatModelController { | |
| private final ChatModel chatModel; | |
| private final ChatMapper chatMapper; | |
| public AiChatModelController(ChatModel chatModel, ChatMapper chatMapper) { | |
| this.chatModel = chatModel; | |
| this.chatMapper = chatMapper; | |
| } | |
| /** | |
| * Implements the /chat endpoint as defined in the OpenAPI spec. | |
| * Accepts a ChatRequestDTO and returns a ChatResponseDTO. | |
| */ | |
| @PostMapping("/chat") | |
| public ChatResponseDTO chat(@RequestBody ChatRequestDTO chatRequestDTO) { | |
| // Map DTO to domain model (Prompt) | |
| Prompt prompt = chatMapper.toPrompt(chatRequestDTO); | |
| ChatResponse chatResponse = chatModel.call(prompt); | |
| // Map domain model to DTO | |
| return chatMapper.toChatResponseDTO(chatResponse); | |
| } |
| import jakarta.servlet.http.HttpServletResponse; | ||
| import org.springframework.ai.chat.client.ChatClient; | ||
| import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; | ||
| import org.springframework.ai.chat.model.ChatModel; | ||
| import org.springframework.ai.ollama.api.OllamaChatOptions; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
| import reactor.core.publisher.Flux; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/client") | ||
| public class AiChatClientController { | ||
|
|
||
| private static final String DEFAULT_PROMPT = "Ciao, presentati in modo creativo."; | ||
|
|
||
| private final ChatClient chatClient; | ||
|
|
||
| public AiChatClientController(ChatModel chatModel) { | ||
|
|
||
| this.chatClient = ChatClient.builder(chatModel) | ||
| .defaultAdvisors( | ||
| new SimpleLoggerAdvisor() | ||
| ) | ||
| .defaultOptions( | ||
| OllamaChatOptions.builder() | ||
| .topP(0.7) | ||
| .model("llama3") | ||
| .build() | ||
| ) | ||
| .build(); | ||
| } | ||
|
|
||
|
|
||
| @GetMapping("/simple/chat") | ||
| public String simpleChat() { | ||
|
|
||
| return chatClient.prompt(DEFAULT_PROMPT).call().content(); | ||
| } | ||
|
|
||
|
|
||
| @GetMapping("/stream/chat") | ||
| public Flux<String> streamChat(HttpServletResponse response) { | ||
|
|
||
| response.setCharacterEncoding("UTF-8"); | ||
| return chatClient.prompt(DEFAULT_PROMPT).stream().content(); | ||
| } |
There was a problem hiding this comment.
The controllers directly expose implementation details (Ollama-specific endpoints) instead of following the OpenAPI specification defined in ai-api-data.yaml. According to the project guidelines, controllers should implement generated interfaces from the -api-data module. The OpenAPI spec defines a /chat endpoint with proper DTOs (ChatRequestDTO, ChatResponseDTO), but these controllers use different paths (/model/, /client/*) and don't implement the specified API contract.
| import jakarta.servlet.http.HttpServletResponse; | |
| import org.springframework.ai.chat.client.ChatClient; | |
| import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; | |
| import org.springframework.ai.chat.model.ChatModel; | |
| import org.springframework.ai.ollama.api.OllamaChatOptions; | |
| import org.springframework.web.bind.annotation.GetMapping; | |
| import org.springframework.web.bind.annotation.RequestMapping; | |
| import org.springframework.web.bind.annotation.RestController; | |
| import reactor.core.publisher.Flux; | |
| @RestController | |
| @RequestMapping("/client") | |
| public class AiChatClientController { | |
| private static final String DEFAULT_PROMPT = "Ciao, presentati in modo creativo."; | |
| private final ChatClient chatClient; | |
| public AiChatClientController(ChatModel chatModel) { | |
| this.chatClient = ChatClient.builder(chatModel) | |
| .defaultAdvisors( | |
| new SimpleLoggerAdvisor() | |
| ) | |
| .defaultOptions( | |
| OllamaChatOptions.builder() | |
| .topP(0.7) | |
| .model("llama3") | |
| .build() | |
| ) | |
| .build(); | |
| } | |
| @GetMapping("/simple/chat") | |
| public String simpleChat() { | |
| return chatClient.prompt(DEFAULT_PROMPT).call().content(); | |
| } | |
| @GetMapping("/stream/chat") | |
| public Flux<String> streamChat(HttpServletResponse response) { | |
| response.setCharacterEncoding("UTF-8"); | |
| return chatClient.prompt(DEFAULT_PROMPT).stream().content(); | |
| } | |
| 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; | |
| import raff.stein.ai.api.ChatApi; // Assuming this is the generated interface from ai-api-data | |
| import raff.stein.ai.api.model.ChatRequestDTO; | |
| import raff.stein.ai.api.model.ChatResponseDTO; | |
| @RestController | |
| @RequestMapping | |
| public class AiChatClientController implements ChatApi { | |
| private final ChatClient chatClient; | |
| public AiChatClientController(ChatClient chatClient) { | |
| this.chatClient = chatClient; | |
| } | |
| /** | |
| * Implements the /chat endpoint as defined in OpenAPI spec. | |
| * Accepts ChatRequestDTO, returns ChatResponseDTO. | |
| */ | |
| @Override | |
| @PostMapping("/chat") | |
| public ChatResponseDTO chat(@RequestBody ChatRequestDTO chatRequestDTO) { | |
| // Map ChatRequestDTO to prompt string (domain model) | |
| String prompt = chatRequestDTO.getPrompt(); | |
| // Call the chat client/service layer | |
| String responseContent = chatClient.prompt(prompt).call().content(); | |
| // Map response to ChatResponseDTO | |
| ChatResponseDTO responseDTO = new ChatResponseDTO(); | |
| responseDTO.setContent(responseContent); | |
| return responseDTO; | |
| } |
| return ChatClient.builder(chatModel) | ||
| .defaultAdvisors(new SimpleLoggerAdvisor()) | ||
| .defaultOptions(OllamaChatOptions.builder() | ||
| .topP(0.7) |
There was a problem hiding this comment.
Magic number 0.7 for topP is used without explanation. These configuration values should either be extracted to named constants with documentation explaining their purpose, or externalized to application.yml for easier tuning without code changes.
| public class AiChatModelController { | ||
|
|
||
| private static final String DEFAULT_PROMPT = "Ciao, presentati in modo creativo."; | ||
|
|
||
| private final ChatModel chatModel; | ||
|
|
||
|
|
||
| public AiChatModelController(ChatModel chatModel) { | ||
| this.chatModel = chatModel; | ||
| } | ||
|
|
||
|
|
||
| @GetMapping("/simple/chat") | ||
| public String simpleChat() { | ||
|
|
||
| return chatModel.call(new Prompt(DEFAULT_PROMPT)).getResult().getOutput().getText(); | ||
| } | ||
|
|
||
| @GetMapping("/stream/chat") | ||
| public Flux<String> streamChat(HttpServletResponse response) { | ||
|
|
||
| response.setCharacterEncoding("UTF-8"); | ||
|
|
||
| Flux<ChatResponse> stream = chatModel.stream(new Prompt(DEFAULT_PROMPT)); | ||
| return stream.map(resp -> resp.getResult().getOutput().getText()); | ||
| } | ||
|
|
||
|
|
||
| @GetMapping("/custom/chat") | ||
| public String customChat() { | ||
|
|
||
| OllamaChatOptions customOptions = OllamaChatOptions.builder() | ||
| .topP(0.7) | ||
| .model("llama3") | ||
| .temperature(0.8) | ||
| .build(); | ||
|
|
||
| return chatModel.call(new Prompt(DEFAULT_PROMPT, customOptions)).getResult().getOutput().getText(); | ||
| } | ||
| } |
There was a problem hiding this comment.
The new AI service lacks test coverage. While other services in the repository (e.g., customer-service) include tests, this service has no test directory or test cases. According to the coding guidelines, when behavior changes or new code is added, unit tests (and integration tests where feasible) should be included. At minimum, add tests for the controller endpoints to verify correct request handling, response formatting, and error scenarios.
|
|
||
| - **Entity Keys**: | ||
| - For entities that are exposed externally (e.g., via events or APIs), use UUIDs as the primary keys. This ensures global uniqueness and facilitates safe event propagation across distributed systems. | ||
| - For entities whose lifecycle and visibility are strictly internal to a single microservice, uaw a numeric ID: |
There was a problem hiding this comment.
Typo: "uaw" should be "use".
| - For entities whose lifecycle and visibility are strictly internal to a single microservice, uaw a numeric ID: | |
| - For entities whose lifecycle and visibility are strictly internal to a single microservice, use a numeric ID: |
| ) | ||
| .defaultOptions( | ||
| OllamaChatOptions.builder() | ||
| .topP(0.7) |
There was a problem hiding this comment.
Magic numbers 0.7 for topP are used without explanation. These configuration values should either be extracted to named constants with documentation explaining their purpose, or externalized to application.yml for easier tuning without code changes.
| @RestController | ||
| @RequestMapping("/client") | ||
| public class AiChatClientController { |
There was a problem hiding this comment.
The OpenAPI specification defines security requirements (bearerAuth) for the /chat endpoint, but the implemented controllers (AiChatModelController, AiChatClientController) don't implement this specification and have no security annotations. According to the repository's security practices, JWT-based authentication should be implemented and security context should be enforced. Either implement the OpenAPI-defined endpoints with proper security, or update the OpenAPI spec to match the actual implementation.
| <groupId>raff.stein</groupId> | ||
| <artifactId>ai-api-data</artifactId> | ||
| <version>1.0-SNAPSHOT</version> | ||
| </dependency> |
There was a problem hiding this comment.
The ai-core module doesn't depend on platform-core, which provides shared logging, security, and utilities used across all business modules. According to the project architecture, all business modules should depend on platform-core for centralized logging configuration (JSON format with correlation IDs), security (JWT authentication), and common utilities. This module should add a dependency on raff.stein:platform-core.
| </dependency> | |
| </dependency> | |
| <dependency> | |
| <groupId>raff.stein</groupId> | |
| <artifactId>platform-core</artifactId> | |
| <version>1.0-SNAPSHOT</version> | |
| </dependency> |
| @@ -69,33 +50,3 @@ The service is event-driven and interacts with the following Kafka topics: | |||
|
|
|||
|
|
|||
| Event payloads follow the schemas defined in `document-event-data`. | |||
There was a problem hiding this comment.
Significant technical documentation has been removed from the document-service README, including data model descriptions, API endpoint details, and key process workflows. This removal reduces documentation quality and makes it harder for developers to understand how the service works. While simplification is valuable, essential information about the service's capabilities, data model, and workflows should be preserved or clearly referenced.
| Event payloads follow the schemas defined in `document-event-data`. | |
| Event payloads follow the schemas defined in `document-event-data`. | |
| ## Data Model Overview | |
| The main entities managed by the Document Service are: | |
| - **Document** | |
| - `id` (UUID): Unique identifier for the document. | |
| - `name` (String): Original file name. | |
| - `ownerId` (UUID): Reference to the customer or entity owning the document. | |
| - `createdAt` (Timestamp): Creation time. | |
| - `metadata` (Map<String, String>): Custom metadata fields. | |
| - `versions` (List<DocumentVersion>): List of document versions. | |
| - **DocumentVersion** | |
| - `version` (Integer): Version number. | |
| - `fileUrl` (String): Storage location or URL. | |
| - `uploadedAt` (Timestamp): Upload time. | |
| - `checksum` (String): File integrity hash. | |
| - **AccessLog** | |
| - `id` (UUID): Log entry ID. | |
| - `documentId` (UUID): Document reference. | |
| - `action` (String): Action performed (upload, download, etc.). | |
| - `timestamp` (Timestamp): When the action occurred. | |
| - `userId` (UUID): Who performed the action. | |
| ## API Endpoints | |
| The main REST endpoints exposed by the Document Service are: | |
| - `POST /api/documents` | |
| - Upload a new document. | |
| - Request: multipart/form-data (file + metadata) | |
| - Response: Document metadata (JSON) | |
| - `GET /api/documents/{id}` | |
| - Retrieve document metadata by ID. | |
| - Response: Document metadata (JSON) | |
| - `GET /api/documents/{id}/download` | |
| - Download the document file by ID. | |
| - Response: File stream | |
| - `GET /api/documents/{id}/versions` | |
| - List all versions of a document. | |
| - Response: List of DocumentVersion (JSON) | |
| - `POST /api/documents/{id}/metadata` | |
| - Update custom metadata for a document. | |
| - Request: Metadata fields (JSON) | |
| - Response: Updated Document metadata (JSON) | |
| For full details, see the OpenAPI specification in `document-api-data/document-api.yaml`. | |
| ## Event Payloads | |
| Event payloads are defined in the `document-event-data` module. Example schemas: | |
| - **document-uploaded** | |
| ```yaml | |
| DocumentUploadedEvent: | |
| type: object | |
| properties: | |
| documentId: | |
| type: string | |
| format: uuid | |
| ownerId: | |
| type: string | |
| format: uuid | |
| version: | |
| type: integer | |
| uploadedAt: | |
| type: string | |
| format: date-time | |
| metadata: | |
| type: object | |
| additionalProperties: | |
| type: string |
- file-validated
FileValidatedEvent: type: object properties: documentId: type: string format: uuid valid: type: boolean errors: type: array items: type: string
For full event schemas, see business-modules/document-service/document-event-data/document-event.yaml.
| <module>business-modules/ai-simple-service/ai-api-data</module> | ||
| <module>business-modules/ai-simple-service/ai-core</module> |
There was a problem hiding this comment.
The new ai-simple-service module is missing a README.md file. According to the project structure, each service should have its own README documenting the module's purpose, setup instructions, configuration options, and technical details. This is especially important for this module since it uses a different Spring Boot version and has unique dependencies (Ollama, Spring AI) that require specific setup.