Modernize Customer Support chat service

This commit is contained in:
2025-11-13 14:17:25 +01:00
parent 98483338b4
commit 40f91bdacc
9 changed files with 92 additions and 333 deletions

View File

@@ -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]
""";
}

View File

@@ -4,7 +4,6 @@ import com.example.customersupportchatbot.model.ChatRequest;
import com.example.customersupportchatbot.model.ChatResponse; import com.example.customersupportchatbot.model.ChatResponse;
import com.example.customersupportchatbot.model.EnhancedChatResponse; import com.example.customersupportchatbot.model.EnhancedChatResponse;
import com.example.customersupportchatbot.service.GeminiAIService; 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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@@ -17,28 +16,19 @@ public class ChatbotController {
private final GeminiAIService geminiAIService; private final GeminiAIService geminiAIService;
@Autowired
public ChatbotController(GeminiAIService geminiAIService) { public ChatbotController(GeminiAIService geminiAIService) {
this.geminiAIService = geminiAIService; this.geminiAIService = geminiAIService;
} }
@PostMapping("/chat") @PostMapping("/chat")
public Mono<ChatResponse> chat(@RequestBody ChatRequest request) { public Mono<ChatResponse> chat(@RequestBody ChatRequest request) {
if (request.getQuery() == null || request.getQuery().trim().isEmpty()) { return request.valid() ? geminiAIService.generateResponse(request.query()) :
return Mono.just(new ChatResponse("Please provide a valid question or message.")); Mono.just(new ChatResponse("Please provide a valid question or message."));
}
return geminiAIService
.generateResponse(request.getQuery())
.map(ChatResponse::new);
} }
@PostMapping("/enhanced-chat") @PostMapping("/enhanced-chat")
public Mono<EnhancedChatResponse> enhancedChat(@RequestBody ChatRequest request) { public Mono<EnhancedChatResponse> enhancedChat(@RequestBody ChatRequest request) {
if (request.getQuery() == null || request.getQuery().trim().isEmpty()) { return request.valid() ? geminiAIService.generateResponseWithCategory(request.query()) :
return Mono.just(new EnhancedChatResponse("Please provide a valid question or message.", "error")); 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")));
} }
} }

View File

@@ -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!";
}
}

View File

@@ -1,19 +1,8 @@
package com.example.customersupportchatbot.model; package com.example.customersupportchatbot.model;
public class ChatRequest { public record ChatRequest(String query) {
private String query;
public ChatRequest() {} public boolean valid() {
return !(query() == null || query().isBlank());
public ChatRequest(String query) {
this.query = query;
}
public String getQuery() {
return query;
}
public void setQuery(String query) {
this.query = query;
} }
} }

View File

@@ -1,19 +1,3 @@
package com.example.customersupportchatbot.model; package com.example.customersupportchatbot.model;
public class ChatResponse { public record ChatResponse(String response) { }
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;
}
}

View File

@@ -1,29 +1,3 @@
package com.example.customersupportchatbot.model; package com.example.customersupportchatbot.model;
public class EnhancedChatResponse { public record EnhancedChatResponse(String response, String category) { }
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;
}
}

View File

@@ -2,56 +2,17 @@ package com.example.customersupportchatbot.model;
import java.util.List; import java.util.List;
public class GeminiRequest { public record GeminiRequest(List<Content> contents) {
private List<Content> contents;
public GeminiRequest() {} record Part(String text){}
public GeminiRequest(List<Content> contents) { record Content(List<Part> parts, String role){
this.contents = contents; Content(Part part) {
} this(List.of(part), "user");
public List<Content> getContents() {
return contents;
}
public void setContents(List<Content> contents) {
this.contents = contents;
}
public static class Content {
private List<Part> parts;
public Content() {}
public Content(List<Part> parts) {
this.parts = parts;
}
public List<Part> getParts() {
return parts;
}
public void setParts(List<Part> parts) {
this.parts = parts;
} }
} }
public static class Part { public GeminiRequest(String prompt) {
private String text; this(List.of(new Content(new Part(prompt))));
public Part() {}
public Part(String text) {
this.text = text;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
} }
} }

View File

@@ -1,75 +1,29 @@
package com.example.customersupportchatbot.model; package com.example.customersupportchatbot.model;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.util.List; import java.util.List;
import java.util.Map;
public class GeminiResponse { public record GeminiResponse(List<Candidate> candidates, Error error) {
private List<Candidate> candidates; record Candidate(Content content){}
record Content(List<Part> parts){}
record Part(String text){}
record Error(int code, String message, String status, List<Map<String,Object>> details){}
public GeminiResponse() {} public boolean valid() {
if (candidates == null || candidates.isEmpty()) {
public GeminiResponse(List<Candidate> candidates) { return false;
this.candidates = candidates; }
Content content = candidates.getFirst().content();
if (content == null || content.parts().isEmpty()) {
return false;
}
Part part = content.parts().getFirst();
return part.text() != null;
} }
public List<Candidate> getCandidates() { public String text() {
return candidates; return valid() ? candidates.getFirst().content().parts().getFirst().text() : null;
}
public void setCandidates(List<Candidate> 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<Part> parts;
public Content() { }
public Content(List<Part> parts) {
this.parts = parts;
}
public List<Part> getParts() {
return parts;
}
public void setParts(List<Part> 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;
}
} }
} }

View File

@@ -1,5 +1,9 @@
package com.example.customersupportchatbot.service; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; 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 org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import static com.example.customersupportchatbot.Prompts.BASIC_PROMPT;
import static com.example.customersupportchatbot.Prompts.ENHANCED_PROMPT;
@Service @Service
public class GeminiAIService { public class GeminiAIService {
private static final Logger log = LoggerFactory.getLogger(GeminiAIService.class); 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 WebClient webClient;
private final String apiKey;
private final String apiUrl;
public GeminiAIService(@Value("${gemini.api.url}") String url, @Value("${gemini.api.key}") String apiKey) { public GeminiAIService(@Value("${gemini.api.url}") String url, @Value("${gemini.api.key}") String apiKey) {
this.apiKey = apiKey;
this.apiUrl = url;
this.webClient = WebClient.builder() this.webClient = WebClient.builder()
.baseUrl(url + "?key={key}") .baseUrl(url + "?key={key}")
.defaultUriVariables(Map.of("key", apiKey)) .defaultUriVariables(Map.of("key", apiKey))
@@ -58,60 +34,26 @@ public class GeminiAIService {
.build(); .build();
} }
/** public Mono<ChatResponse> generateResponse(String query) {
* Generates a response using the Gemini API
*
* @param query The user's query
* @return The response from the model
*/
public Mono<String> generateResponse(String query) {
// Create the request body with proper structure GeminiRequest request = new GeminiRequest(BASIC_PROMPT.formatted(query));
Map<String, Object> partObject = new HashMap<>();
partObject.put("text", fullPrompt.formatted(query));
Map<String, Object> contentObject = new HashMap<>();
contentObject.put("parts", List.of(partObject));
contentObject.put("role", "user");
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("contents", List.of(contentObject));
// Debug: Print the request being sent
log.info("Sending request to Gemini API:"); log.info("Sending request to Gemini API:");
log.info("URL: {}?key={}...", apiUrl, apiKey.substring(0, 5)); log.info("Request body: {}", request);
log.info("Request body: {}", requestBody);
// Make the API request with more detailed error handling // Make the API request with more detailed error handling
return webClient.post() return webClient.post()
.bodyValue(requestBody) .bodyValue(request)
.retrieve() .retrieve()
.bodyToMono(Map.class) .bodyToMono(GeminiResponse.class)
.map(response -> { .map(response -> {
// Debug: Print response structure
log.info("Response received: {}", response); log.info("Response received: {}", response);
return response.valid() ? response.text() : "I'm sorry, IfullPrompt couldn't process your request.";
// Extract the text from the response
if (response != null && response.containsKey("candidates")) {
List<Map<String, Object>> candidates = (List<Map<String, Object>>) response.get("candidates");
if (!candidates.isEmpty()) {
Map<String, Object> content = (Map<String, Object>) candidates.get(0).get("content");
List<Map<String, Object>> responseParts = (List<Map<String, Object>>) content.get("parts");
if (!responseParts.isEmpty()) {
return (String) responseParts.get(0).get("text");
}
}
}
return "I'm sorry, I couldn't process your request.";
}) })
.onErrorResume(WebClientResponseException.class, e -> { .onErrorResume(WebClientResponseException.class, e -> {
// Handle HTTP error responses specifically // Handle HTTP error responses specifically
log.error("Error calling Gemini API: {} {}", e.getStatusCode(), e.getStatusText()); log.error("Error calling Gemini API: {} {}", e.getStatusCode(), e.getStatusText());
log.error("Response body: {}", e.getResponseBodyAsString()); log.error("Response body: {}", e.getResponseBodyAsString());
// Return a more informative error message // Return a more informative error message
if (e.getStatusCode().value() == 400) { 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."); 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 { } else {
return Mono.just("I'm sorry, there was an error calling the Gemini API: " + e.getStatusCode() + " " + e.getStatusText()); return Mono.just("I'm sorry, there was an error calling the Gemini API: " + e.getStatusCode() + " " + e.getStatusText());
} }
}) })
.onErrorResume(Exception.class, e -> { .onErrorResume(Exception.class, e -> {
// General error handling // General error handling
log.error("Unexpected error calling Gemini API: {}", e.getMessage(), e); 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()); return Mono.just("I'm sorry, there was an unexpected error processing your request: " + e.getMessage());
}); })
.map(ChatResponse::new);
} }
public Mono<Map<String, String>> generateResponseWithCategory(String query) { public Mono<EnhancedChatResponse> generateResponseWithCategory(String query) {
GeminiRequest request = new GeminiRequest(ENHANCED_PROMPT.formatted(query));
// Create the request body with proper structure (same as before) log.info("Sending request to Gemini API:");
Map<String, Object> partObject = new HashMap<>(); log.info("Request body: {}", request);
partObject.put("text", fullPrompt.formatted(query));
Map<String, Object> contentObject = new HashMap<>();
contentObject.put("parts", List.of(partObject));
contentObject.put("role", "user");
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("contents", List.of(contentObject));
// Make the API request (same as before)
return webClient.post() return webClient.post()
.bodyValue(requestBody) .bodyValue(request)
.retrieve() .retrieve()
.bodyToMono(Map.class) .bodyToMono(GeminiResponse.class)
.map(response -> { .mapNotNull(GeminiResponse::text)
.map(responseText -> {
// Extract the text from the response
String responseText = "";
if (response != null && response.containsKey("candidates")) {
List<Map<String, Object>> candidates = (List<Map<String, Object>>) response.get("candidates");
if (!candidates.isEmpty()) {
Map<String, Object> content = (Map<String, Object>) candidates.get(0).get("content");
List<Map<String, Object>> responseParts = (List<Map<String, Object>>) content.get("parts");
if (!responseParts.isEmpty()) {
responseText = (String) responseParts.get(0).get("text");
}
}
}
// Parse the category and response from the text
Map<String, String> result = new HashMap<>();
if (responseText.contains("CATEGORY:") && responseText.contains("RESPONSE:")) { if (responseText.contains("CATEGORY:") && responseText.contains("RESPONSE:")) {
String category = responseText String category = responseText
.substring(responseText.indexOf("CATEGORY:") + 9, responseText.indexOf("RESPONSE:")) .substring(responseText.indexOf("CATEGORY:") + 9, responseText.indexOf("RESPONSE:"))
@@ -177,24 +94,11 @@ public class GeminiAIService {
.substring(responseText.indexOf("RESPONSE:") + 9) .substring(responseText.indexOf("RESPONSE:") + 9)
.trim(); .trim();
result.put("category", category); return new EnhancedChatResponse(chatResponse, 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(responseText, "general");
return result;
}) })
.onErrorResume(e -> Mono.just(new EnhancedChatResponse("error",
.onErrorResume(e -> { "I'm sorry, there was an error processing your request: ")));
// Handle errors (same as before)
Map<String, String> 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);
});
} }
} }