feat: scaffold initial loyalty-agent-service project with OpenAPI specifications and OAuth2-enabled API client configuration

This commit is contained in:
SonPhung
2026-07-17 01:49:30 +07:00
commit c3861d6476
30 changed files with 6991 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
package dev.sonpx.loyalty.agent;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class LoyaltyAgentApplication {
public static void main(String[] args) {
SpringApplication.run(LoyaltyAgentApplication.class, args);
}
}

View File

@@ -0,0 +1,19 @@
package dev.sonpx.loyalty.agent.client.api;
import com.example.loyalty.agent.client.model.Campaign;
import com.example.loyalty.agent.client.model.CreateCampaignRequest;
import java.util.UUID;
public class CampaignApi {
public Campaign createCampaign(CreateCampaignRequest request) {
// Mock behavior
return new Campaign(
UUID.randomUUID().toString(),
request.name(),
request.targetTierId(),
request.rewardPoolId(),
"ACTIVE"
);
}
}

View File

@@ -0,0 +1,12 @@
package dev.sonpx.loyalty.agent.client.api;
import com.example.loyalty.agent.client.model.MemberTier;
import java.util.List;
public class MemberTierApi {
public List<MemberTier> listMemberTiers() {
// Mock data to ensure it compiles and works before OpenAPI generation
return List.of(new MemberTier("TIER_GOLD", "Gold", 1000));
}
}

View File

@@ -0,0 +1,13 @@
package dev.sonpx.loyalty.agent.client.api;
import com.example.loyalty.agent.client.model.PointPool;
import java.math.BigDecimal;
import java.util.List;
public class PointPoolApi {
public List<PointPool> listPointPools() {
// Mock data
return List.of(new PointPool("POOL_1", "Main Pool", new BigDecimal("1000000")));
}
}

View File

@@ -0,0 +1,3 @@
package com.example.loyalty.agent.client.model;
public record Campaign(String campaignId, String name, String targetTierId, String rewardPoolId, String status) {}

View File

@@ -0,0 +1,3 @@
package com.example.loyalty.agent.client.model;
public record CreateCampaignRequest(String name, String targetTierId, String rewardPoolId) {}

View File

@@ -0,0 +1,3 @@
package com.example.loyalty.agent.client.model;
public record MemberTier(String tierId, String tierName, Integer requiredPoints) {}

View File

@@ -0,0 +1,5 @@
package com.example.loyalty.agent.client.model;
import java.math.BigDecimal;
public record PointPool(String poolId, String poolName, BigDecimal balance) {}

View File

@@ -0,0 +1,105 @@
package dev.sonpx.loyalty.agent.config;
import dev.sonpx.loyalty.agent.client.api.CampaignApi;
import dev.sonpx.loyalty.agent.client.api.MemberTierApi;
import dev.sonpx.loyalty.agent.client.api.PointPoolApi;
import dev.sonpx.loyalty.agent.client.api.ApiClient;
import dev.sonpx.loyalty.agent.client.api.attribute.StaticAttributeResourceApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
/**
* Configures the generated API clients.
* This class mocks the configuration that will wrap the actual classes
* generated by openapi-generator-maven-plugin.
*/
@Configuration
public class ApiClientConfig {
@Value("${loyalty.core.base-url}")
private String coreBaseUrl;
private ApiClient createApiClient(OAuth2AuthorizedClientManager authorizedClientManager, String servicePrefix) {
ApiClient apiClient = new ApiClient();
apiClient.updateBaseUri(coreBaseUrl + servicePrefix);
apiClient.setRequestInterceptor(builder -> {
System.out.println("Executing API Client Interceptor for keycloak...");
org.springframework.security.core.Authentication principal =
new org.springframework.security.authentication.AnonymousAuthenticationToken(
"key", "loyalty-agent-service",
org.springframework.security.core.authority.AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId("keycloak")
.principal(principal)
.build();
try {
OAuth2AuthorizedClient authorizedClient = authorizedClientManager.authorize(authorizeRequest);
if (authorizedClient != null && authorizedClient.getAccessToken() != null) {
System.out.println("Successfully fetched OAuth2 Token! Token length: " + authorizedClient.getAccessToken().getTokenValue().length());
builder.header("Authorization", "Bearer " + authorizedClient.getAccessToken().getTokenValue());
} else {
System.out.println("WARNING: authorizedClient is null or accessToken is null!");
}
} catch (Exception e) {
System.out.println("ERROR during OAuth2 authorize: " + e.getMessage());
e.printStackTrace();
}
});
return apiClient;
}
@Bean
public StaticAttributeResourceApi staticAttributeResourceApi(OAuth2AuthorizedClientManager authorizedClientManager) {
return new StaticAttributeResourceApi(createApiClient(authorizedClientManager, "/svc/attribute"));
}
@Bean
public org.springframework.web.client.RestClient loyaltyRestClient(OAuth2AuthorizedClientManager authorizedClientManager) {
return org.springframework.web.client.RestClient.builder()
.baseUrl(coreBaseUrl + "/svc/attribute")
.requestInterceptor((request, body, execution) -> {
org.springframework.security.core.Authentication principal =
new org.springframework.security.authentication.AnonymousAuthenticationToken(
"key", "loyalty-agent-service",
org.springframework.security.core.authority.AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId("keycloak")
.principal(principal)
.build();
try {
OAuth2AuthorizedClient authorizedClient = authorizedClientManager.authorize(authorizeRequest);
if (authorizedClient != null && authorizedClient.getAccessToken() != null) {
request.getHeaders().setBearerAuth(authorizedClient.getAccessToken().getTokenValue());
}
} catch (Exception e) {
e.printStackTrace();
}
return execution.execute(request, body);
})
.build();
}
@Bean
public MemberTierApi memberTierApi() {
// In reality: return new MemberTierApi(apiClient());
return new MemberTierApi();
}
@Bean
public PointPoolApi pointPoolApi() {
// In reality: return new PointPoolApi(apiClient());
return new PointPoolApi();
}
@Bean
public CampaignApi campaignApi() {
// In reality: return new CampaignApi(apiClient());
return new CampaignApi();
}
}

View File

@@ -0,0 +1,64 @@
package dev.sonpx.loyalty.agent.config;
import dev.sonpx.loyalty.agent.client.api.CampaignApi;
import dev.sonpx.loyalty.agent.client.api.MemberTierApi;
import dev.sonpx.loyalty.agent.client.api.PointPoolApi;
import dev.sonpx.loyalty.agent.client.api.attribute.StaticAttributeResourceApi;
import dev.sonpx.loyalty.agent.client.model.attribute.StaticAttributeViewCriteria;
import dev.sonpx.loyalty.agent.client.model.attribute.StaticAttributeDto;
import dev.sonpx.loyalty.agent.client.model.attribute.StringFilter;
import dev.sonpx.loyalty.agent.client.model.attribute.Pageable;
import com.example.loyalty.agent.client.model.Campaign;
import com.example.loyalty.agent.client.model.CreateCampaignRequest;
import com.example.loyalty.agent.client.model.MemberTier;
import com.example.loyalty.agent.client.model.PointPool;
import org.springframework.ai.tool.annotation.Tool;
import java.util.Arrays;
import java.util.List;
public class LoyaltyAgentTools {
private final MemberTierApi memberTierApi;
private final PointPoolApi pointPoolApi;
private final CampaignApi campaignApi;
private final StaticAttributeResourceApi staticAttributeApi;
private final org.springframework.web.client.RestClient restClient;
public LoyaltyAgentTools(MemberTierApi memberTierApi, PointPoolApi pointPoolApi, CampaignApi campaignApi, StaticAttributeResourceApi staticAttributeApi, org.springframework.web.client.RestClient restClient) {
this.memberTierApi = memberTierApi;
this.pointPoolApi = pointPoolApi;
this.campaignApi = campaignApi;
this.staticAttributeApi = staticAttributeApi;
this.restClient = restClient;
}
@Tool(description = "List all available Member Tiers. Always use this to verify a Tier exists before creating a Campaign.")
public List<MemberTier> listMemberTiers() {
return memberTierApi.listMemberTiers();
}
@Tool(description = "List all available Point Pools. Always use this to verify a Pool exists and has sufficient balance before creating a Campaign.")
public List<PointPool> listPointPools() {
return pointPoolApi.listPointPools();
}
@Tool(description = "Create a new Campaign. This requires a valid targetTierId and rewardPoolId, which MUST be verified beforehand using listMemberTiers and listPointPools.")
public Campaign createCampaign(CreateCampaignRequest request) {
return campaignApi.createCampaign(request);
}
@Tool(description = "List all available Static Attributes. Use this tool when you need to find configurations, properties, or static values like tier types or currencies.")
public List<StaticAttributeDto> listStaticAttributes() {
try {
String url = "/api/static-attribute/csr/view/list?sort=lastApproveDate,desc&sort=lastUpdateDate,desc&page=0&size=10&status.in=A";
return restClient.get()
.uri(url)
.retrieve()
.body(new org.springframework.core.ParameterizedTypeReference<List<StaticAttributeDto>>() {});
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("Failed to fetch static attributes", e);
}
}
}

View File

@@ -0,0 +1,44 @@
package dev.sonpx.loyalty.agent.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
return http.build();
}
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientService authorizedClientService) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager =
new AuthorizedClientServiceOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
}

View File

@@ -0,0 +1,27 @@
package dev.sonpx.loyalty.agent.controller;
import dev.sonpx.loyalty.agent.service.LoyaltyAgentService;
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;
@RestController
@RequestMapping("/api/v1/agent")
public class AgentController {
private final LoyaltyAgentService agentService;
public AgentController(LoyaltyAgentService agentService) {
this.agentService = agentService;
}
public record ChatRequest(String prompt) {}
public record ChatResponse(String answer) {}
@PostMapping("/chat")
public ChatResponse chat(@RequestBody ChatRequest request) {
String answer = agentService.chat(request.prompt());
return new ChatResponse(answer);
}
}

View File

@@ -0,0 +1,48 @@
package dev.sonpx.loyalty.agent.service;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import dev.sonpx.loyalty.agent.client.api.CampaignApi;
import dev.sonpx.loyalty.agent.client.api.MemberTierApi;
import dev.sonpx.loyalty.agent.client.api.PointPoolApi;
import dev.sonpx.loyalty.agent.client.api.attribute.StaticAttributeResourceApi;
import dev.sonpx.loyalty.agent.config.LoyaltyAgentTools;
@Service
public class LoyaltyAgentService {
private final ChatClient chatClient;
public LoyaltyAgentService(ChatClient.Builder chatClientBuilder,
MemberTierApi memberTierApi, PointPoolApi pointPoolApi,
CampaignApi campaignApi, StaticAttributeResourceApi staticAttributeApi,
org.springframework.web.client.RestClient loyaltyRestClient) {
LoyaltyAgentTools tools = new LoyaltyAgentTools(memberTierApi, pointPoolApi, campaignApi, staticAttributeApi, loyaltyRestClient);
this.chatClient = chatClientBuilder
.defaultSystem("""
You are an expert Loyalty System AI Assistant.
Your primary job is to orchestrate operations for our Loyalty core system.
CRITICAL RULES:
1. DO NOT create a model.client.dev.sonpx.loyalty.agent.Campaign unless you verify that Member Tiers and Point Pools exist first.
2. Use the `listMemberTiers` tool to check for valid tier IDs.
3. Use the `listPointPools` tool to check for valid pool IDs and ensure they have a sufficient balance.
4. Use the `listStaticAttributes` tool when asked about configurations, properties, or static values.
5. Only use the `createCampaign` tool once verification is complete.
6. IMPORTANT: You must use the native tool calling capability to execute tools. NEVER output raw JSON tool calls as your text response. After receiving the tool's result, provide a natural language summary to the user.
7. Always answer the user in the language they used (e.g., Vietnamese).
""")
.defaultTools(tools)
.build();
}
public String chat(String userMessage) {
return this.chatClient.prompt()
.user(userMessage)
.call()
.content();
}
}

View File

@@ -0,0 +1,34 @@
server:
port: 8085
spring:
application:
name: loyalty-agent-service
ai:
ollama:
base-url: http://192.168.10.112:11434
chat:
options:
model: qwen3.5:9b
temperature: 0.3
security:
oauth2:
client:
registration:
keycloak:
client-id: ols-cli
client-secret: 1jV8NSAeybIqmUK0AWhI8hgdo1q5itj7
authorization-grant-type: client_credentials
provider:
keycloak:
token-uri: http://192.168.99.235/auth/realms/ols-cn-sit/protocol/openid-connect/token
loyalty:
core:
base-url: http://192.168.99.242:8081
logging:
level:
org.springframework.ai: DEBUG
org.springframework.web: DEBUG
dev.sonpx.loyalty: DEBUG

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long