Compare commits

...

1 Commits

Author SHA1 Message Date
SonPhung
9a26c6ce2d feat: implement MCP tools and services for campaign rules and pool definitions 2026-07-27 23:17:14 +07:00
11 changed files with 208 additions and 14 deletions

View File

@@ -25,4 +25,10 @@ public class Result<T> {
public void instruction(String instruction) { public void instruction(String instruction) {
this.agentInstruction = instruction; this.agentInstruction = instruction;
} }
public static <T> Result<T> failure(String errorMessage) {
Result<T> result = of(null);
result.instruction("VALIDATION_ERROR: " + errorMessage);
return result;
}
} }

View File

@@ -15,4 +15,7 @@ public interface PoolDefinitionClient {
@GetExchange("/api/pool-definition/list-by-ids") @GetExchange("/api/pool-definition/list-by-ids")
List<PoolDefinition> findActiveByIds(@RequestParam("idList") List<String> idList); List<PoolDefinition> findActiveByIds(@RequestParam("idList") List<String> idList);
@GetExchange("/api/pool-definition/csr/list")
List<PoolDefinition> getAll(dev.sonpx.loyalty.mcp.reward.criteria.PoolDefinitionCriteria criteria, org.springframework.data.domain.Pageable pageable);
} }

View File

@@ -5,7 +5,6 @@ package dev.sonpx.loyalty.mcp.reward.criteria;
import dev.sonpx.loyalty.mcp.criteria.AbstractFwCriteria; import dev.sonpx.loyalty.mcp.criteria.AbstractFwCriteria;
import dev.sonpx.loyalty.mcp.filter.StringFilter; import dev.sonpx.loyalty.mcp.filter.StringFilter;
import lombok.Data; import lombok.Data;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
/** /**
* Created by SonPhung on Monday, 20-Jul-2026 * Created by SonPhung on Monday, 20-Jul-2026
@@ -13,18 +12,13 @@ import com.fasterxml.jackson.annotation.JsonPropertyDescription;
@Data @Data
public class CampaignCriteria extends AbstractFwCriteria { public class CampaignCriteria extends AbstractFwCriteria {
@JsonPropertyDescription("Bộ lọc theo Mã chiến dịch (bằng chính xác, chứa từ khóa, v.v.)")
private StringFilter campaignId; private StringFilter campaignId;
@JsonPropertyDescription("Bộ lọc theo Tên người/phòng ban sở hữu chiến dịch")
private StringFilter ownerName; private StringFilter ownerName;
@JsonPropertyDescription("Bộ lọc theo Tên chiến dịch")
private StringFilter name; private StringFilter name;
@JsonPropertyDescription("Bộ lọc theo Mô tả của chiến dịch")
private StringFilter description; private StringFilter description;
@JsonPropertyDescription("Bộ lọc theo Loại chiến dịch (ví dụ: POINT, VOUCHER, v.v.)")
private StringFilter campaignType; private StringFilter campaignType;
} }

View File

@@ -35,4 +35,12 @@ public class PoolDefinitionService {
Không dùng markdown table. Không dùng markdown table.
"""); """);
} }
public Result<List<PoolDefinition>> getAll(dev.sonpx.loyalty.mcp.reward.criteria.PoolDefinitionCriteria criteria) {
List<PoolDefinition> pools = poolDefinitionClient.getAll(criteria, org.springframework.data.domain.Pageable.ofSize(10));
return Result.of(pools, """
Trả lời tự nhiên bằng tiếng Việt. Hiển thị danh sách các Pool, mỗi pool ghi: Tên, Mã, Loại Pool.
Không dùng markdown table.
""");
}
} }

View File

@@ -23,13 +23,58 @@ public class CampaignRuleTools {
@McpTool(description = """ @McpTool(description = """
TÌM KIẾM THỂ LỆ / RULE của chiến dịch. Trả về: tên rule, loại rule, chiến dịch gốc, công thức thưởng. TÌM KIẾM THỂ LỆ / RULE của chiến dịch. Trả về: tên rule, loại rule, chiến dịch gốc, công thức thưởng.
- Param `search` (String): từ khóa tìm kiếm theo tên hoặc mã rule cụ thể. - Param `request`: đối tượng chứa các điều kiện lọc (có thể để null nếu không cần lọc).
- QUAN TRỌNG: Khi người dùng hỏi lấy danh sách chung ("danh sách rule", "xem thể lệ"), KHÔNG truyền param `search` (để null hoặc rỗng ""). Tuyệt đối KHÔNG truyền các từ chung như "rule", "thể lệ", "danh sách" vào `search`. - QUAN TRỌNG: Khi người dùng hỏi lấy danh sách chung ("danh sách rule", "xem thể lệ"), KHÔNG truyền điều kiện nào.
- KHÔNG dùng tool này khi người dùng chỉ hỏi về thông tin cơ bản chiến dịch (tên, owner, loại) → hãy dùng tool searchCampaigns. - KHÔNG dùng tool này khi người dùng chỉ hỏi về thông tin cơ bản chiến dịch (tên, owner, loại) → hãy dùng tool searchCampaigns.
Hiển thị tối đa 10 bản ghi.""") Hiển thị tối đa 10 bản ghi.""")
public Result<List<CampaignRuleSummaryDto>> searchRules(String search) { public Result<List<CampaignRuleSummaryDto>> searchRules(dev.sonpx.loyalty.mcp.tool.request.CampaignRuleSearchRequest request) {
CampaignRuleCriteria criteria = new CampaignRuleCriteria(); CampaignRuleCriteria criteria = new CampaignRuleCriteria();
criteria.setSearch(SearchUtils.sanitizeSearch(search)); if (request != null) {
if (request.campaignId() != null) {
dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter();
filter.setEquals(request.campaignId());
criteria.setCampaignId(filter);
}
if (request.ruleId() != null) {
dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter();
filter.setEquals(request.ruleId());
criteria.setRuleId(filter);
}
if (request.ruleName() != null) {
dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter();
filter.setContains(request.ruleName());
criteria.setRuleName(filter);
}
if (request.description() != null) {
dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter();
filter.setContains(request.description());
criteria.setDescription(filter);
}
if (request.ruleType() != null) {
dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter();
filter.setEquals(request.ruleType());
criteria.setRuleType(filter);
}
if (request.poolId() != null) {
dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter();
filter.setEquals(request.poolId());
criteria.setPoolId(filter);
}
try {
if (request.effectiveFrom() != null) {
dev.sonpx.loyalty.mcp.filter.LocalDateTimeFilter filter = new dev.sonpx.loyalty.mcp.filter.LocalDateTimeFilter();
filter.setGreaterThanOrEqual(java.time.LocalDateTime.parse(request.effectiveFrom()));
criteria.setEffectiveFrom(filter);
}
if (request.effectiveTo() != null) {
dev.sonpx.loyalty.mcp.filter.LocalDateTimeFilter filter = new dev.sonpx.loyalty.mcp.filter.LocalDateTimeFilter();
filter.setLessThanOrEqual(java.time.LocalDateTime.parse(request.effectiveTo()));
criteria.setEffectiveTo(filter);
}
} catch (java.time.format.DateTimeParseException e) {
return Result.failure("Invalid date format. Please use ISO-8601 format (e.g. '2026-07-27T10:00:00').");
}
}
return campaignRuleService.getAll(criteria); return campaignRuleService.getAll(criteria);
} }

View File

@@ -23,12 +23,38 @@ public class CampaignTools {
@McpTool(description = """ @McpTool(description = """
TÌM KIẾM CHIẾN DỊCH (Campaign). Trả về thông tin CƠ BẢN của chiến dịch: tên, mã, owner, loại, thời gian. TÌM KIẾM CHIẾN DỊCH (Campaign). Trả về thông tin CƠ BẢN của chiến dịch: tên, mã, owner, loại, thời gian.
- Param `search` (String): từ khóa tìm kiếm theo tên hoặc mã chiến dịch cụ thể. - Param `request`: đối tượng chứa các điều kiện lọc (có thể để null nếu không cần lọc).
- QUAN TRỌNG: Khi người dùng hỏi lấy danh sách chung ("danh sách chiến dịch", "xem các chiến dịch"), KHÔNG truyền param `search` (để null hoặc rỗng ""). Tuyệt đối KHÔNG truyền các từ chung như "chiến dịch", "danh sách", "campaign" vào `search`. - QUAN TRỌNG: Khi người dùng hỏi lấy danh sách chung ("danh sách chiến dịch", "xem các chiến dịch"), KHÔNG truyền filter nào.
- KHÔNG dùng tool này khi người dùng hỏi về: rule, thể lệ, quy tắc, điều kiện, công thức thưởng → hãy dùng tool searchRules.""") - KHÔNG dùng tool này khi người dùng hỏi về: rule, thể lệ, quy tắc, điều kiện, công thức thưởng → hãy dùng tool searchRules.""")
public Result<List<CampaignSummaryDto>> searchCampaigns(String search) { public Result<List<CampaignSummaryDto>> searchCampaigns(dev.sonpx.loyalty.mcp.tool.request.CampaignSearchRequest request) {
CampaignCriteria criteria = new CampaignCriteria(); CampaignCriteria criteria = new CampaignCriteria();
criteria.setSearch(SearchUtils.sanitizeSearch(search)); if (request != null) {
if (request.campaignId() != null) {
dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter();
filter.setContains(request.campaignId());
criteria.setCampaignId(filter);
}
if (request.ownerName() != null) {
dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter();
filter.setContains(request.ownerName());
criteria.setOwnerName(filter);
}
if (request.name() != null) {
dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter();
filter.setContains(request.name());
criteria.setName(filter);
}
if (request.description() != null) {
dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter();
filter.setContains(request.description());
criteria.setDescription(filter);
}
if (request.campaignType() != null) {
dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter();
filter.setEquals(request.campaignType());
criteria.setCampaignType(filter);
}
}
return campaignService.getAll(criteria); return campaignService.getAll(criteria);
} }

View File

@@ -23,4 +23,42 @@ public class PoolDefinitionTools {
public Result<List<PoolDefinition>> getPoolDefinitionsByIds(String ids) { public Result<List<PoolDefinition>> getPoolDefinitionsByIds(String ids) {
return poolDefinitionService.findActiveByIds(ids); return poolDefinitionService.findActiveByIds(ids);
} }
@McpTool(description = "TÌM KIẾM Point Pool. Trả về thông tin của pool. Param `request`: đối tượng chứa các điều kiện lọc (có thể để null nếu không cần lọc).")
public Result<List<PoolDefinition>> searchPools(dev.sonpx.loyalty.mcp.tool.request.PoolDefinitionSearchRequest request) {
dev.sonpx.loyalty.mcp.reward.criteria.PoolDefinitionCriteria criteria = new dev.sonpx.loyalty.mcp.reward.criteria.PoolDefinitionCriteria();
if (request != null) {
if (request.poolId() != null) {
dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter();
filter.setEquals(request.poolId());
criteria.setPoolId(filter);
}
if (request.poolName() != null) {
dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter();
filter.setContains(request.poolName());
criteria.setPoolName(filter);
}
if (request.poolType() != null) {
dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter();
filter.setEquals(request.poolType());
criteria.setPoolType(filter);
}
if (request.entityLevel() != null) {
dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter();
filter.setEquals(request.entityLevel());
criteria.setEntityLevel(filter);
}
if (request.expiryPolicy() != null) {
dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter();
filter.setEquals(request.expiryPolicy());
criteria.setExpiryPolicy(filter);
}
if (request.atgGroupId() != null) {
dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter();
filter.setEquals(request.atgGroupId());
criteria.setAtgGroupId(filter);
}
}
return poolDefinitionService.getAll(criteria);
}
} }

View File

@@ -0,0 +1,29 @@
package dev.sonpx.loyalty.mcp.tool.request;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
public record CampaignRuleSearchRequest(
@JsonPropertyDescription("Search by Campaign ID (exact match). Leave null if not filtering by Campaign ID.")
String campaignId,
@JsonPropertyDescription("Search by Rule ID (exact match). Leave null if not filtering by Rule ID.")
String ruleId,
@JsonPropertyDescription("Search by Rule Name (partial or exact match). Leave null if not filtering by Rule Name.")
String ruleName,
@JsonPropertyDescription("Search by Rule Description (partial or exact match). Leave null if not filtering by Description.")
String description,
@JsonPropertyDescription("Filter rules effective from this date/time (ISO-8601 format, e.g., '2026-07-27T10:00:00'). Leave null if not filtering by effective from.")
String effectiveFrom,
@JsonPropertyDescription("Filter rules effective to this date/time (ISO-8601 format, e.g., '2026-07-27T10:00:00'). Leave null if not filtering by effective to.")
String effectiveTo,
@JsonPropertyDescription("Search by Rule Type (exact match). Leave null if not filtering by Rule Type.")
String ruleType,
@JsonPropertyDescription("Search by Pool ID (exact match). Leave null if not filtering by Pool ID.")
String poolId
) {}

View File

@@ -0,0 +1,20 @@
package dev.sonpx.loyalty.mcp.tool.request;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
public record CampaignSearchRequest(
@JsonPropertyDescription("Search by Campaign ID (partial or exact match). Leave null if not filtering by ID.")
String campaignId,
@JsonPropertyDescription("Search by Owner Name/Department (partial or exact match). Leave null if not filtering by owner.")
String ownerName,
@JsonPropertyDescription("Search by Campaign Name (partial or exact match). Leave null if not filtering by name.")
String name,
@JsonPropertyDescription("Search by Campaign Description (partial or exact match). Leave null if not filtering by description.")
String description,
@JsonPropertyDescription("Search by Campaign Type (exact match, e.g. POINT, VOUCHER). Leave null if not filtering by type.")
String campaignType
) {}

View File

@@ -0,0 +1,23 @@
package dev.sonpx.loyalty.mcp.tool.request;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
public record PoolDefinitionSearchRequest(
@JsonPropertyDescription("Search by Pool ID (exact match). Leave null if not filtering by Pool ID.")
String poolId,
@JsonPropertyDescription("Search by Pool Name (partial or exact match). Leave null if not filtering by Pool Name.")
String poolName,
@JsonPropertyDescription("Search by Pool Type (exact match). Leave null if not filtering by Pool Type.")
String poolType,
@JsonPropertyDescription("Search by Entity Level (exact match). Leave null if not filtering by Entity Level.")
String entityLevel,
@JsonPropertyDescription("Search by Expiry Policy (exact match). Leave null if not filtering by Expiry Policy.")
String expiryPolicy,
@JsonPropertyDescription("Search by ATG Group ID (exact match). Leave null if not filtering by ATG Group ID.")
String atgGroupId
) {}

View File

@@ -6,6 +6,8 @@ if (-not (Test-Path $LogDir)) {
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
} }
$env:JAVA_TOOL_OPTIONS = "-Duser.timezone=Asia/Ho_Chi_Minh"
if (Test-Path (Join-Path $ProjectRoot ".env")) { if (Test-Path (Join-Path $ProjectRoot ".env")) {
Write-Host "Loading environment variables from .env" Write-Host "Loading environment variables from .env"
Get-Content (Join-Path $ProjectRoot ".env") | Where-Object { $_ -match "^[^#]" -and $_ -match "=" } | ForEach-Object { Get-Content (Join-Path $ProjectRoot ".env") | Where-Object { $_ -match "^[^#]" -and $_ -match "=" } | ForEach-Object {