Creating 2025Q4 Release

Merge request reports

Loading
+41 −0
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;
    }
}
+46 −0
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
+53 −0
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);
  }


}
+129 −0
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;
    }
}
+107 −0
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
Loading