feat: scaffold initial loyalty-agent-service project with OpenAPI specifications and OAuth2-enabled API client configuration
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.example.loyalty.agent.client.model;
|
||||
|
||||
public record Campaign(String campaignId, String name, String targetTierId, String rewardPoolId, String status) {}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.example.loyalty.agent.client.model;
|
||||
|
||||
public record CreateCampaignRequest(String name, String targetTierId, String rewardPoolId) {}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.example.loyalty.agent.client.model;
|
||||
|
||||
public record MemberTier(String tierId, String tierName, Integer requiredPoints) {}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.example.loyalty.agent.client.model;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public record PointPool(String poolId, String poolName, BigDecimal balance) {}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
34
src/main/resources/application.yml
Normal file
34
src/main/resources/application.yml
Normal 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
|
||||
5622
src/main/resources/specs/attribute.json
Normal file
5622
src/main/resources/specs/attribute.json
Normal file
File diff suppressed because it is too large
Load Diff
1
src/main/resources/specs/catalogue.json
Normal file
1
src/main/resources/specs/catalogue.json
Normal file
File diff suppressed because one or more lines are too long
1
src/main/resources/specs/customer.json
Normal file
1
src/main/resources/specs/customer.json
Normal file
File diff suppressed because one or more lines are too long
1
src/main/resources/specs/identity.json
Normal file
1
src/main/resources/specs/identity.json
Normal file
File diff suppressed because one or more lines are too long
1
src/main/resources/specs/marketing.json
Normal file
1
src/main/resources/specs/marketing.json
Normal file
File diff suppressed because one or more lines are too long
1
src/main/resources/specs/master.json
Normal file
1
src/main/resources/specs/master.json
Normal file
File diff suppressed because one or more lines are too long
1
src/main/resources/specs/reward.json
Normal file
1
src/main/resources/specs/reward.json
Normal file
File diff suppressed because one or more lines are too long
1
src/main/resources/specs/transaction.json
Normal file
1
src/main/resources/specs/transaction.json
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user