Loading src/main/java/org/etsi/osl/mcp/backend/configuration/ActiveMQComponentConfig.java 0 → 100644 +41 −0 Viewed Changes for src/main/java/org/etsi/osl/mcp/backend/configuration/ActiveMQComponentConfig.java: 41 added lines, 0 removed lines. Original line number Diff line number Diff line /*- * ========================LICENSE_START================================= * org.etsi.osl.bugzilla * %% * Copyright (C) 2019 openslice.io * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * =========================LICENSE_END================================== */ package org.etsi.osl.mcp.backend.configuration; import org.apache.camel.component.activemq.ActiveMQComponent; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import jakarta.jms.ConnectionFactory; /** * @author ctranoris * */ @Configuration public class ActiveMQComponentConfig { @Bean(name = "activemq") public ActiveMQComponent createComponent(ConnectionFactory factory) { ActiveMQComponent activeMQComponent = new ActiveMQComponent(); activeMQComponent.setConnectionFactory(factory); return activeMQComponent; } } src/main/java/org/etsi/osl/mcp/backend/configuration/McpConfiguration.java 0 → 100644 +46 −0 Viewed Changes for src/main/java/org/etsi/osl/mcp/backend/configuration/McpConfiguration.java: 46 added lines, 0 removed lines. Original line number Diff line number Diff line package org.etsi.osl.mcp.backend.configuration; import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; /** * @author Daniel Garnier-Moiroux */ @Configuration class McpConfiguration { // Disabled OAuth2 customization for MCP client - not needed for JWT-based API // The MCP server connection doesn't need OAuth2 authentication // @Bean // McpSyncHttpClientRequestCustomizer requestCustomizer(OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager, // ClientRegistrationRepository clientRegistrationRepository) { // var registrationId = findUniqueClientRegistration(clientRegistrationRepository); // return new OAuth2AuthorizationCodeSyncHttpRequestCustomizer(oAuth2AuthorizedClientManager, registrationId); // } // @Bean // McpSyncClientCustomizer syncClientCustomizer() { // return (name, syncSpec) -> syncSpec.transportContextProvider(new AuthenticationMcpTransportContextProvider()); // } /** * Returns the ID of the {@code spring.security.oauth2.client.registration}, if * unique. */ private static String findUniqueClientRegistration(ClientRegistrationRepository clientRegistrationRepository) { String registrationId; if (!(clientRegistrationRepository instanceof InMemoryClientRegistrationRepository repo)) { throw new IllegalStateException("Expected an InMemoryClientRegistrationRepository"); } var iterator = repo.iterator(); var firstRegistration = iterator.next(); if (iterator.hasNext()) { throw new IllegalStateException("Expected a single Client Registration"); } registrationId = firstRegistration.getRegistrationId(); return registrationId; } } No newline at end of file src/main/java/org/etsi/osl/mcp/backend/configuration/PartnerRouteBuillder.java 0 → 100644 +53 −0 Viewed Changes for src/main/java/org/etsi/osl/mcp/backend/configuration/PartnerRouteBuillder.java: 53 added lines, 0 removed lines. Original line number Diff line number Diff line package org.etsi.osl.mcp.backend.configuration; import java.io.IOException; import org.apache.camel.LoggingLevel; import org.apache.camel.builder.RouteBuilder; import org.apache.camel.model.dataformat.JsonLibrary; import org.etsi.osl.mcp.backend.ChatController; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; @Configuration @Component public class PartnerRouteBuillder extends RouteBuilder { @Value("${spring.application.name}") private String compname; @Autowired private ChatController chatController; @Override public void configure() throws Exception { String EVENT_CHAT = "jms:queue:agents/"+compname ; from(EVENT_CHAT) .log(LoggingLevel.INFO, log, EVENT_CHAT + " message received!") .to("log:DEBUG?showBody=true&showHeaders=true") .bean( chatController, "simpleAsk( ${headers}, ${body} )") .convertBodyTo( String.class ); } static <T> T toJsonObj(String content, Class<T> valueType) throws IOException { ObjectMapper mapper = new ObjectMapper(); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); return mapper.readValue(content, valueType); } static String toJsonString(Object object) throws IOException { ObjectMapper mapper = new ObjectMapper(); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); return mapper.writeValueAsString(object); } } src/main/java/org/etsi/osl/mcp/backend/AuthController.java 0 → 100644 +129 −0 Viewed Changes for src/main/java/org/etsi/osl/mcp/backend/AuthController.java: 129 added lines, 0 removed lines. Original line number Diff line number Diff line package org.etsi.osl.mcp.backend; import java.io.IOException; import java.util.HashMap; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.UriComponentsBuilder; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @RestController public class AuthController { @Value("${spring.security.oauth2.client.provider.keycloak.issuer-uri:}") private String issuerUri; @Autowired(required = false) private OAuth2AuthorizedClientService authorizedClientService; @GetMapping("/api/user") public Map<String, Object> getUserInfo(Authentication authentication) { Map<String, Object> userInfo = new HashMap<>(); if (authentication != null && authentication.isAuthenticated()) { userInfo.put("authenticated", true); userInfo.put("name", authentication.getName()); String username = authentication.getName(); if (authentication.getPrincipal() instanceof OidcUser oidcUser) { userInfo.put("email", oidcUser.getEmail()); if (oidcUser.getPreferredUsername() != null) { username = oidcUser.getPreferredUsername(); userInfo.put("preferredUsername", oidcUser.getPreferredUsername()); } } userInfo.put("username", username); // Try to get OAuth2AuthorizedClient if available (from OAuth2 login) if (authorizedClientService != null && authentication.getName() != null) { try { OAuth2AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient("keycloak", authentication.getName()); if (authorizedClient != null && authorizedClient.getAccessToken() != null) { userInfo.put("access_token", authorizedClient.getAccessToken().getTokenValue()); userInfo.put("token", authorizedClient.getAccessToken().getTokenValue()); } } catch (Exception e) { // OAuth2 client not available - user authenticated via JWT Bearer token // This is normal for API requests with Bearer tokens } } } else { userInfo.put("authenticated", false); } return userInfo; } @PostMapping("/api/logout") public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { performLogout(request, response, authentication); } @GetMapping("/api/logout") public void logoutGet(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { performLogout(request, response, authentication); } private void performLogout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { if (authentication != null) { new SecurityContextLogoutHandler().logout(request, null, authentication); } if (request.getSession(false) != null) { request.getSession().invalidate(); } String logoutUrl = buildKeycloakLogoutUrl(request, authentication); response.sendRedirect(logoutUrl); } private String buildKeycloakLogoutUrl(HttpServletRequest request, Authentication authentication) { String keycloakIssuer = issuerUri; if (authentication != null && authentication.getPrincipal() instanceof OidcUser oidcUser) { if (oidcUser.getIssuer() != null) { keycloakIssuer = oidcUser.getIssuer().toString(); } } if (keycloakIssuer == null || keycloakIssuer.isEmpty()) { return "/welcome.html"; } String baseUrl = request.getScheme() + "://" + request.getServerName(); if ((request.getScheme().equals("http") && request.getServerPort() != 80) || (request.getScheme().equals("https") && request.getServerPort() != 443)) { baseUrl += ":" + request.getServerPort(); } String postLogoutRedirectUri = baseUrl + "/welcome.html"; String keycloakLogoutUrl = keycloakIssuer + "/protocol/openid-connect/logout"; return UriComponentsBuilder .fromUriString(keycloakLogoutUrl) .queryParam("post_logout_redirect_uri", postLogoutRedirectUri) .build() .toUriString(); } @GetMapping("/api/health") public Map<String, String> health() { Map<String, String> response = new HashMap<>(); response.put("status", "UP"); return response; } } src/main/java/org/etsi/osl/mcp/backend/ChatController.java 0 → 100644 +107 −0 Viewed Changes for src/main/java/org/etsi/osl/mcp/backend/ChatController.java: 107 added lines, 0 removed lines. Original line number Diff line number Diff line package org.etsi.osl.mcp.backend; import java.util.Map; import java.util.UUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.ChatMemoryRepository; import org.springframework.ai.chat.memory.MessageWindowChatMemory; import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.annotation.SessionScope; @RestController @SessionScope public class ChatController { private static final Logger log = LoggerFactory.getLogger(ChatController.class); private final ChatClient chatClient; private final String systemPrompt; private final String conversationId; @Value("${spring.ai.chat.maxMessages}") private final int maxMessages= 100; ChatMemoryRepository chatMemoryRepository; ChatMemory chatMemory = MessageWindowChatMemory.builder() .chatMemoryRepository(chatMemoryRepository) .maxMessages( maxMessages ) .build(); public ChatController(ChatClient.Builder chatClientBuilder, ToolCallbackProvider tools, @Value("${spring.ai.chat.system-prompt}") String systemPrompt, ChatMemory chatMemory) { // Validate that system prompt is not empty if (systemPrompt == null || systemPrompt.trim().isEmpty()) { throw new IllegalArgumentException("System prompt cannot be null or empty. Please configure SPRING_AI_CHAT_SYSTEM_PROMPT with a valid value."); } this.systemPrompt = systemPrompt; this.chatClient = chatClientBuilder .defaultAdvisors(new SimpleLoggerAdvisor(), MessageChatMemoryAdvisor.builder(chatMemory).build()) .defaultToolCallbacks(tools) .defaultSystem(systemPrompt) .build(); log.info("ChatController initialized with system prompt: {}", systemPrompt); this.conversationId = UUID.randomUUID().toString(); } @PostMapping("/ask") public Answer ask(@RequestBody Question question) { var response = chatClient.prompt() .user(question.question()) .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId)) .call() .content(); log.debug("Response from AI: {}", response); String markdownAnswer = formatResponse(question, response); log.debug("Markdown formatted response: {}", markdownAnswer); // var htmlResponse = MarkdownHelper.toHTML(markdownAnswer); // log.debug("HTML formatted response: {}", htmlResponse); return new Answer(markdownAnswer); } public String simpleAsk( Map<String, Object> headers, String question) { var response = chatClient.prompt() .user( question ) .call() .content(); log.debug("Response from AI: {}", response); return response; } private String formatResponse(Question question, String answer) { return answer; // var prompt = """ // Following are the question and answer: // // Question: {question} // // Answer: {answer} // // Format the answer into plain human readable text and return only the formatted response. // """; // return chatClient // .prompt() // .user(u -> u.text(prompt) // .param("question", question.question()) // .param("answer", answer) // ) // .call() // .content(); } public record Question(String question) {} public record Answer(String answer) {} } Loading
src/main/java/org/etsi/osl/mcp/backend/configuration/ActiveMQComponentConfig.java 0 → 100644 +41 −0 Viewed Changes for src/main/java/org/etsi/osl/mcp/backend/configuration/ActiveMQComponentConfig.java: 41 added lines, 0 removed lines. Original line number Diff line number Diff line /*- * ========================LICENSE_START================================= * org.etsi.osl.bugzilla * %% * Copyright (C) 2019 openslice.io * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * =========================LICENSE_END================================== */ package org.etsi.osl.mcp.backend.configuration; import org.apache.camel.component.activemq.ActiveMQComponent; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import jakarta.jms.ConnectionFactory; /** * @author ctranoris * */ @Configuration public class ActiveMQComponentConfig { @Bean(name = "activemq") public ActiveMQComponent createComponent(ConnectionFactory factory) { ActiveMQComponent activeMQComponent = new ActiveMQComponent(); activeMQComponent.setConnectionFactory(factory); return activeMQComponent; } }
src/main/java/org/etsi/osl/mcp/backend/configuration/McpConfiguration.java 0 → 100644 +46 −0 Viewed Changes for src/main/java/org/etsi/osl/mcp/backend/configuration/McpConfiguration.java: 46 added lines, 0 removed lines. Original line number Diff line number Diff line package org.etsi.osl.mcp.backend.configuration; import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; /** * @author Daniel Garnier-Moiroux */ @Configuration class McpConfiguration { // Disabled OAuth2 customization for MCP client - not needed for JWT-based API // The MCP server connection doesn't need OAuth2 authentication // @Bean // McpSyncHttpClientRequestCustomizer requestCustomizer(OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager, // ClientRegistrationRepository clientRegistrationRepository) { // var registrationId = findUniqueClientRegistration(clientRegistrationRepository); // return new OAuth2AuthorizationCodeSyncHttpRequestCustomizer(oAuth2AuthorizedClientManager, registrationId); // } // @Bean // McpSyncClientCustomizer syncClientCustomizer() { // return (name, syncSpec) -> syncSpec.transportContextProvider(new AuthenticationMcpTransportContextProvider()); // } /** * Returns the ID of the {@code spring.security.oauth2.client.registration}, if * unique. */ private static String findUniqueClientRegistration(ClientRegistrationRepository clientRegistrationRepository) { String registrationId; if (!(clientRegistrationRepository instanceof InMemoryClientRegistrationRepository repo)) { throw new IllegalStateException("Expected an InMemoryClientRegistrationRepository"); } var iterator = repo.iterator(); var firstRegistration = iterator.next(); if (iterator.hasNext()) { throw new IllegalStateException("Expected a single Client Registration"); } registrationId = firstRegistration.getRegistrationId(); return registrationId; } } No newline at end of file
src/main/java/org/etsi/osl/mcp/backend/configuration/PartnerRouteBuillder.java 0 → 100644 +53 −0 Viewed Changes for src/main/java/org/etsi/osl/mcp/backend/configuration/PartnerRouteBuillder.java: 53 added lines, 0 removed lines. Original line number Diff line number Diff line package org.etsi.osl.mcp.backend.configuration; import java.io.IOException; import org.apache.camel.LoggingLevel; import org.apache.camel.builder.RouteBuilder; import org.apache.camel.model.dataformat.JsonLibrary; import org.etsi.osl.mcp.backend.ChatController; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; @Configuration @Component public class PartnerRouteBuillder extends RouteBuilder { @Value("${spring.application.name}") private String compname; @Autowired private ChatController chatController; @Override public void configure() throws Exception { String EVENT_CHAT = "jms:queue:agents/"+compname ; from(EVENT_CHAT) .log(LoggingLevel.INFO, log, EVENT_CHAT + " message received!") .to("log:DEBUG?showBody=true&showHeaders=true") .bean( chatController, "simpleAsk( ${headers}, ${body} )") .convertBodyTo( String.class ); } static <T> T toJsonObj(String content, Class<T> valueType) throws IOException { ObjectMapper mapper = new ObjectMapper(); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); return mapper.readValue(content, valueType); } static String toJsonString(Object object) throws IOException { ObjectMapper mapper = new ObjectMapper(); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); return mapper.writeValueAsString(object); } }
src/main/java/org/etsi/osl/mcp/backend/AuthController.java 0 → 100644 +129 −0 Viewed Changes for src/main/java/org/etsi/osl/mcp/backend/AuthController.java: 129 added lines, 0 removed lines. Original line number Diff line number Diff line package org.etsi.osl.mcp.backend; import java.io.IOException; import java.util.HashMap; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.UriComponentsBuilder; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @RestController public class AuthController { @Value("${spring.security.oauth2.client.provider.keycloak.issuer-uri:}") private String issuerUri; @Autowired(required = false) private OAuth2AuthorizedClientService authorizedClientService; @GetMapping("/api/user") public Map<String, Object> getUserInfo(Authentication authentication) { Map<String, Object> userInfo = new HashMap<>(); if (authentication != null && authentication.isAuthenticated()) { userInfo.put("authenticated", true); userInfo.put("name", authentication.getName()); String username = authentication.getName(); if (authentication.getPrincipal() instanceof OidcUser oidcUser) { userInfo.put("email", oidcUser.getEmail()); if (oidcUser.getPreferredUsername() != null) { username = oidcUser.getPreferredUsername(); userInfo.put("preferredUsername", oidcUser.getPreferredUsername()); } } userInfo.put("username", username); // Try to get OAuth2AuthorizedClient if available (from OAuth2 login) if (authorizedClientService != null && authentication.getName() != null) { try { OAuth2AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient("keycloak", authentication.getName()); if (authorizedClient != null && authorizedClient.getAccessToken() != null) { userInfo.put("access_token", authorizedClient.getAccessToken().getTokenValue()); userInfo.put("token", authorizedClient.getAccessToken().getTokenValue()); } } catch (Exception e) { // OAuth2 client not available - user authenticated via JWT Bearer token // This is normal for API requests with Bearer tokens } } } else { userInfo.put("authenticated", false); } return userInfo; } @PostMapping("/api/logout") public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { performLogout(request, response, authentication); } @GetMapping("/api/logout") public void logoutGet(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { performLogout(request, response, authentication); } private void performLogout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { if (authentication != null) { new SecurityContextLogoutHandler().logout(request, null, authentication); } if (request.getSession(false) != null) { request.getSession().invalidate(); } String logoutUrl = buildKeycloakLogoutUrl(request, authentication); response.sendRedirect(logoutUrl); } private String buildKeycloakLogoutUrl(HttpServletRequest request, Authentication authentication) { String keycloakIssuer = issuerUri; if (authentication != null && authentication.getPrincipal() instanceof OidcUser oidcUser) { if (oidcUser.getIssuer() != null) { keycloakIssuer = oidcUser.getIssuer().toString(); } } if (keycloakIssuer == null || keycloakIssuer.isEmpty()) { return "/welcome.html"; } String baseUrl = request.getScheme() + "://" + request.getServerName(); if ((request.getScheme().equals("http") && request.getServerPort() != 80) || (request.getScheme().equals("https") && request.getServerPort() != 443)) { baseUrl += ":" + request.getServerPort(); } String postLogoutRedirectUri = baseUrl + "/welcome.html"; String keycloakLogoutUrl = keycloakIssuer + "/protocol/openid-connect/logout"; return UriComponentsBuilder .fromUriString(keycloakLogoutUrl) .queryParam("post_logout_redirect_uri", postLogoutRedirectUri) .build() .toUriString(); } @GetMapping("/api/health") public Map<String, String> health() { Map<String, String> response = new HashMap<>(); response.put("status", "UP"); return response; } }
src/main/java/org/etsi/osl/mcp/backend/ChatController.java 0 → 100644 +107 −0 Viewed Changes for src/main/java/org/etsi/osl/mcp/backend/ChatController.java: 107 added lines, 0 removed lines. Original line number Diff line number Diff line package org.etsi.osl.mcp.backend; import java.util.Map; import java.util.UUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.ChatMemoryRepository; import org.springframework.ai.chat.memory.MessageWindowChatMemory; import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.annotation.SessionScope; @RestController @SessionScope public class ChatController { private static final Logger log = LoggerFactory.getLogger(ChatController.class); private final ChatClient chatClient; private final String systemPrompt; private final String conversationId; @Value("${spring.ai.chat.maxMessages}") private final int maxMessages= 100; ChatMemoryRepository chatMemoryRepository; ChatMemory chatMemory = MessageWindowChatMemory.builder() .chatMemoryRepository(chatMemoryRepository) .maxMessages( maxMessages ) .build(); public ChatController(ChatClient.Builder chatClientBuilder, ToolCallbackProvider tools, @Value("${spring.ai.chat.system-prompt}") String systemPrompt, ChatMemory chatMemory) { // Validate that system prompt is not empty if (systemPrompt == null || systemPrompt.trim().isEmpty()) { throw new IllegalArgumentException("System prompt cannot be null or empty. Please configure SPRING_AI_CHAT_SYSTEM_PROMPT with a valid value."); } this.systemPrompt = systemPrompt; this.chatClient = chatClientBuilder .defaultAdvisors(new SimpleLoggerAdvisor(), MessageChatMemoryAdvisor.builder(chatMemory).build()) .defaultToolCallbacks(tools) .defaultSystem(systemPrompt) .build(); log.info("ChatController initialized with system prompt: {}", systemPrompt); this.conversationId = UUID.randomUUID().toString(); } @PostMapping("/ask") public Answer ask(@RequestBody Question question) { var response = chatClient.prompt() .user(question.question()) .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId)) .call() .content(); log.debug("Response from AI: {}", response); String markdownAnswer = formatResponse(question, response); log.debug("Markdown formatted response: {}", markdownAnswer); // var htmlResponse = MarkdownHelper.toHTML(markdownAnswer); // log.debug("HTML formatted response: {}", htmlResponse); return new Answer(markdownAnswer); } public String simpleAsk( Map<String, Object> headers, String question) { var response = chatClient.prompt() .user( question ) .call() .content(); log.debug("Response from AI: {}", response); return response; } private String formatResponse(Question question, String answer) { return answer; // var prompt = """ // Following are the question and answer: // // Question: {question} // // Answer: {answer} // // Format the answer into plain human readable text and return only the formatted response. // """; // return chatClient // .prompt() // .user(u -> u.text(prompt) // .param("question", question.question()) // .param("answer", answer) // ) // .call() // .content(); } public record Question(String question) {} public record Answer(String answer) {} }