From 40f91bdacc88e48c2964b97eeb76548905b2c6d0 Mon Sep 17 00:00:00 2001 From: John Ahlroos Date: Thu, 13 Nov 2025 14:17:25 +0100 Subject: [PATCH] Modernize Customer Support chat service --- .../customersupportchatbot/Prompts.java | 29 ++++ .../controller/ChatbotController.java | 18 +-- .../controller/HealthController.java | 26 --- .../model/ChatRequest.java | 17 +- .../model/ChatResponse.java | 18 +-- .../model/EnhancedChatResponse.java | 28 +--- .../model/GeminiRequest.java | 53 +------ .../model/GeminiResponse.java | 86 +++------- .../service/GeminiAIService.java | 150 ++++-------------- 9 files changed, 92 insertions(+), 333 deletions(-) create mode 100644 CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/Prompts.java delete mode 100644 CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/controller/HealthController.java diff --git a/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/Prompts.java b/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/Prompts.java new file mode 100644 index 0000000..0c378a7 --- /dev/null +++ b/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/Prompts.java @@ -0,0 +1,29 @@ +package com.example.customersupportchatbot; + +public interface Prompts { + + String BASIC_PROMPT = """ + You are a helpful customer support chatbot. + Provide a concise and friendly response to this customer query: + + %s + """; + + String ENHANCED_PROMPT = """ + You are a helpful customer support chatbot. + Provide a response to this customer query: + + %s + + Also classify this query into exactly ONE of these categories: + - account + - billing + - technical + - general + + Format your response as follows: + + CATEGORY: [the category] + RESPONSE: [your helpful response] + """; +} \ No newline at end of file diff --git a/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/controller/ChatbotController.java b/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/controller/ChatbotController.java index bdac2fe..76fb321 100644 --- a/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/controller/ChatbotController.java +++ b/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/controller/ChatbotController.java @@ -4,7 +4,6 @@ import com.example.customersupportchatbot.model.ChatRequest; import com.example.customersupportchatbot.model.ChatResponse; import com.example.customersupportchatbot.model.EnhancedChatResponse; import com.example.customersupportchatbot.service.GeminiAIService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -17,28 +16,19 @@ public class ChatbotController { private final GeminiAIService geminiAIService; - @Autowired public ChatbotController(GeminiAIService geminiAIService) { this.geminiAIService = geminiAIService; } @PostMapping("/chat") public Mono chat(@RequestBody ChatRequest request) { - if (request.getQuery() == null || request.getQuery().trim().isEmpty()) { - return Mono.just(new ChatResponse("Please provide a valid question or message.")); - } - return geminiAIService - .generateResponse(request.getQuery()) - .map(ChatResponse::new); + return request.valid() ? geminiAIService.generateResponse(request.query()) : + Mono.just(new ChatResponse("Please provide a valid question or message.")); } @PostMapping("/enhanced-chat") public Mono enhancedChat(@RequestBody ChatRequest request) { - if (request.getQuery() == null || request.getQuery().trim().isEmpty()) { - return Mono.just(new EnhancedChatResponse("Please provide a valid question or message.", "error")); - } - return geminiAIService - .generateResponseWithCategory(request.getQuery()) - .map(result -> new EnhancedChatResponse(result.get("response"), result.get("category"))); + return request.valid() ? geminiAIService.generateResponseWithCategory(request.query()) : + Mono.just(new EnhancedChatResponse("Please provide a valid question or message.", "error")); } } \ No newline at end of file diff --git a/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/controller/HealthController.java b/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/controller/HealthController.java deleted file mode 100644 index fa063c7..0000000 --- a/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/controller/HealthController.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.customersupportchatbot.controller; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/health") -public class HealthController { - - // Constructor injection of our service - @Autowired - public HealthController() { - } - - /** - * Simple health check endpoint to verify the API is running - * - * @return A message indicating the API is up and running - */ - @GetMapping(value = {"", "/"}) - public String health() { - return "Chatbot API is up and running!"; - } -} \ No newline at end of file diff --git a/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/model/ChatRequest.java b/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/model/ChatRequest.java index ad573ab..ece703f 100644 --- a/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/model/ChatRequest.java +++ b/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/model/ChatRequest.java @@ -1,19 +1,8 @@ package com.example.customersupportchatbot.model; -public class ChatRequest { - private String query; +public record ChatRequest(String query) { - public ChatRequest() {} - - public ChatRequest(String query) { - this.query = query; - } - - public String getQuery() { - return query; - } - - public void setQuery(String query) { - this.query = query; + public boolean valid() { + return !(query() == null || query().isBlank()); } } \ No newline at end of file diff --git a/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/model/ChatResponse.java b/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/model/ChatResponse.java index 940c5e5..067d40e 100644 --- a/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/model/ChatResponse.java +++ b/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/model/ChatResponse.java @@ -1,19 +1,3 @@ package com.example.customersupportchatbot.model; -public class ChatResponse { - private String response; - - public ChatResponse() {} - - public ChatResponse(String response) { - this.response = response; - } - - public String getResponse() { - return response; - } - - public void setResponse(String response) { - this.response = response; - } -} \ No newline at end of file +public record ChatResponse(String response) { } \ No newline at end of file diff --git a/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/model/EnhancedChatResponse.java b/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/model/EnhancedChatResponse.java index 0044497..f6da872 100644 --- a/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/model/EnhancedChatResponse.java +++ b/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/model/EnhancedChatResponse.java @@ -1,29 +1,3 @@ package com.example.customersupportchatbot.model; -public class EnhancedChatResponse { - private String response; - private String category; - - public EnhancedChatResponse() {} - - public EnhancedChatResponse(String response, String category) { - this.response = response; - this.category = category; - } - - public String getResponse() { - return response; - } - - public void setResponse(String response) { - this.response = response; - } - - public String getCategory() { - return category; - } - - public void setCategory(String category) { - this.category = category; - } -} \ No newline at end of file +public record EnhancedChatResponse(String response, String category) { } \ No newline at end of file diff --git a/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/model/GeminiRequest.java b/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/model/GeminiRequest.java index 66a67fb..e4b92b2 100644 --- a/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/model/GeminiRequest.java +++ b/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/model/GeminiRequest.java @@ -2,56 +2,17 @@ package com.example.customersupportchatbot.model; import java.util.List; -public class GeminiRequest { - private List contents; +public record GeminiRequest(List contents) { - public GeminiRequest() {} + record Part(String text){} - public GeminiRequest(List contents) { - this.contents = contents; - } - - public List getContents() { - return contents; - } - - public void setContents(List contents) { - this.contents = contents; - } - - public static class Content { - private List parts; - - public Content() {} - - public Content(List parts) { - this.parts = parts; - } - - public List getParts() { - return parts; - } - - public void setParts(List parts) { - this.parts = parts; + record Content(List parts, String role){ + Content(Part part) { + this(List.of(part), "user"); } } - public static class Part { - private String text; - - public Part() {} - - public Part(String text) { - this.text = text; - } - - public String getText() { - return text; - } - - public void setText(String text) { - this.text = text; - } + public GeminiRequest(String prompt) { + this(List.of(new Content(new Part(prompt)))); } } \ No newline at end of file diff --git a/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/model/GeminiResponse.java b/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/model/GeminiResponse.java index fd6ebc4..5c838b8 100644 --- a/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/model/GeminiResponse.java +++ b/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/model/GeminiResponse.java @@ -1,75 +1,29 @@ package com.example.customersupportchatbot.model; +import com.fasterxml.jackson.annotation.JsonInclude; + import java.util.List; +import java.util.Map; -public class GeminiResponse { - private List candidates; +public record GeminiResponse(List candidates, Error error) { + record Candidate(Content content){} + record Content(List parts){} + record Part(String text){} + record Error(int code, String message, String status, List> details){} - public GeminiResponse() {} - - public GeminiResponse(List candidates) { - this.candidates = candidates; + public boolean valid() { + if (candidates == null || candidates.isEmpty()) { + return false; + } + Content content = candidates.getFirst().content(); + if (content == null || content.parts().isEmpty()) { + return false; + } + Part part = content.parts().getFirst(); + return part.text() != null; } - public List getCandidates() { - return candidates; - } - - public void setCandidates(List candidates) { - this.candidates = candidates; - } - - public static class Candidate { - private Content content; - - public Candidate() {} - - public Candidate(Content content) { - this.content = content; - } - - public Content getContent() { - return content; - } - - public void setContent(Content content) { - this.content = content; - } - } - - public static class Content { - private List parts; - - public Content() { } - - public Content(List parts) { - this.parts = parts; - } - - public List getParts() { - return parts; - } - - public void setParts(List parts) { - this.parts = parts; - } - } - - public static class Part { - private String text; - - public Part() { } - - public Part(String text) { - this.text = text; - } - - public String getText() { - return text; - } - - public void setText(String text) { - this.text = text; - } + public String text() { + return valid() ? candidates.getFirst().content().parts().getFirst().text() : null; } } \ No newline at end of file diff --git a/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/service/GeminiAIService.java b/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/service/GeminiAIService.java index c559d55..0ab009a 100644 --- a/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/service/GeminiAIService.java +++ b/CustomerSupportChatBot/src/main/java/com/example/customersupportchatbot/service/GeminiAIService.java @@ -1,5 +1,9 @@ package com.example.customersupportchatbot.service; +import com.example.customersupportchatbot.model.ChatResponse; +import com.example.customersupportchatbot.model.EnhancedChatResponse; +import com.example.customersupportchatbot.model.GeminiRequest; +import com.example.customersupportchatbot.model.GeminiResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -10,47 +14,19 @@ import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.core.publisher.Mono; -import java.util.HashMap; -import java.util.List; import java.util.Map; +import static com.example.customersupportchatbot.Prompts.BASIC_PROMPT; +import static com.example.customersupportchatbot.Prompts.ENHANCED_PROMPT; + @Service public class GeminiAIService { private static final Logger log = LoggerFactory.getLogger(GeminiAIService.class); - private static final String fullPrompt = """ - You are a helpful customer support chatbot. - Provide a concise and friendly response to this customer query: - - %s - """; - - private static final String fullPromptEnhanced = """ - You are a helpful customer support chatbot. - Provide a response to this customer query: - - %s - - Also classify this query into exactly ONE of these categories: - - account - - billing - - technical - - general - - Format your response as follows: - - CATEGORY: [the category] - RESPONSE: [your helpful response] - """; - private final WebClient webClient; - private final String apiKey; - private final String apiUrl; public GeminiAIService(@Value("${gemini.api.url}") String url, @Value("${gemini.api.key}") String apiKey) { - this.apiKey = apiKey; - this.apiUrl = url; this.webClient = WebClient.builder() .baseUrl(url + "?key={key}") .defaultUriVariables(Map.of("key", apiKey)) @@ -58,60 +34,26 @@ public class GeminiAIService { .build(); } - /** - * Generates a response using the Gemini API - * - * @param query The user's query - * @return The response from the model - */ - public Mono generateResponse(String query) { + public Mono generateResponse(String query) { - // Create the request body with proper structure - Map partObject = new HashMap<>(); - partObject.put("text", fullPrompt.formatted(query)); + GeminiRequest request = new GeminiRequest(BASIC_PROMPT.formatted(query)); - Map contentObject = new HashMap<>(); - contentObject.put("parts", List.of(partObject)); - contentObject.put("role", "user"); - - Map requestBody = new HashMap<>(); - requestBody.put("contents", List.of(contentObject)); - - // Debug: Print the request being sent log.info("Sending request to Gemini API:"); - log.info("URL: {}?key={}...", apiUrl, apiKey.substring(0, 5)); - log.info("Request body: {}", requestBody); + log.info("Request body: {}", request); // Make the API request with more detailed error handling return webClient.post() - .bodyValue(requestBody) + .bodyValue(request) .retrieve() - .bodyToMono(Map.class) + .bodyToMono(GeminiResponse.class) .map(response -> { - - // Debug: Print response structure log.info("Response received: {}", response); - - // Extract the text from the response - if (response != null && response.containsKey("candidates")) { - List> candidates = (List>) response.get("candidates"); - if (!candidates.isEmpty()) { - Map content = (Map) candidates.get(0).get("content"); - List> responseParts = (List>) content.get("parts"); - if (!responseParts.isEmpty()) { - return (String) responseParts.get(0).get("text"); - } - } - } - - return "I'm sorry, I couldn't process your request."; + return response.valid() ? response.text() : "I'm sorry, IfullPrompt couldn't process your request."; }) - .onErrorResume(WebClientResponseException.class, e -> { // Handle HTTP error responses specifically log.error("Error calling Gemini API: {} {}", e.getStatusCode(), e.getStatusText()); log.error("Response body: {}", e.getResponseBodyAsString()); - // Return a more informative error message if (e.getStatusCode().value() == 400) { return Mono.just("I'm sorry, there was an error with the request format (400 Bad Request). Please ensure your API key is correct and try again."); @@ -122,52 +64,27 @@ public class GeminiAIService { } else { return Mono.just("I'm sorry, there was an error calling the Gemini API: " + e.getStatusCode() + " " + e.getStatusText()); } - }) - .onErrorResume(Exception.class, e -> { // General error handling log.error("Unexpected error calling Gemini API: {}", e.getMessage(), e); return Mono.just("I'm sorry, there was an unexpected error processing your request: " + e.getMessage()); - }); + }) + .map(ChatResponse::new); } - public Mono> generateResponseWithCategory(String query) { + public Mono generateResponseWithCategory(String query) { + GeminiRequest request = new GeminiRequest(ENHANCED_PROMPT.formatted(query)); - // Create the request body with proper structure (same as before) - Map partObject = new HashMap<>(); - partObject.put("text", fullPrompt.formatted(query)); + log.info("Sending request to Gemini API:"); + log.info("Request body: {}", request); - Map contentObject = new HashMap<>(); - contentObject.put("parts", List.of(partObject)); - contentObject.put("role", "user"); - - Map requestBody = new HashMap<>(); - requestBody.put("contents", List.of(contentObject)); - - // Make the API request (same as before) return webClient.post() - .bodyValue(requestBody) + .bodyValue(request) .retrieve() - .bodyToMono(Map.class) - .map(response -> { - - // Extract the text from the response - String responseText = ""; - if (response != null && response.containsKey("candidates")) { - List> candidates = (List>) response.get("candidates"); - if (!candidates.isEmpty()) { - Map content = (Map) candidates.get(0).get("content"); - List> responseParts = (List>) content.get("parts"); - if (!responseParts.isEmpty()) { - responseText = (String) responseParts.get(0).get("text"); - } - } - } - - // Parse the category and response from the text - Map result = new HashMap<>(); - + .bodyToMono(GeminiResponse.class) + .mapNotNull(GeminiResponse::text) + .map(responseText -> { if (responseText.contains("CATEGORY:") && responseText.contains("RESPONSE:")) { String category = responseText .substring(responseText.indexOf("CATEGORY:") + 9, responseText.indexOf("RESPONSE:")) @@ -177,24 +94,11 @@ public class GeminiAIService { .substring(responseText.indexOf("RESPONSE:") + 9) .trim(); - result.put("category", category); - result.put("response", chatResponse); - } else { - // Fallback if the format is not as expected - result.put("category", "general"); - result.put("response", responseText); + return new EnhancedChatResponse(chatResponse, category); } - - return result; - + return new EnhancedChatResponse(responseText, "general"); }) - - .onErrorResume(e -> { - // Handle errors (same as before) - Map errorResult = new HashMap<>(); - errorResult.put("category", "error"); - errorResult.put("response", "I'm sorry, there was an error processing your request: " + e.getMessage()); - return Mono.just(errorResult); - }); + .onErrorResume(e -> Mono.just(new EnhancedChatResponse("error", + "I'm sorry, there was an error processing your request: "))); } } \ No newline at end of file