refactor: overhaul agent workflow engine with scoped tool execution, configurable LLM model profiles, and enhanced state management.

This commit is contained in:
2026-07-22 18:41:45 +07:00
parent 369985ae9c
commit 66385746f6
32 changed files with 1454 additions and 171 deletions

View File

@@ -5,6 +5,7 @@ import dev.sonpx.loyalty.mcp.model.Result;
import dev.sonpx.loyalty.mcp.reward.criteria.CampaignRuleCriteria;
import dev.sonpx.loyalty.mcp.reward.model.CampaignRule;
import dev.sonpx.loyalty.mcp.service.CampaignRuleService;
import dev.sonpx.loyalty.mcp.util.SearchUtils;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.mcp.annotation.McpTool;
@@ -21,19 +22,20 @@ public class CampaignRuleTools {
@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.
Dùng khi người dùng hỏi: "thể lệ", "rule", "quy tắc", "điều kiện", "công thức thưởng", "cách tính điểm".
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.
- Param `search` (String): từ khóa tìm kiếm theo tên hoặc mã rule cụ thể.
- 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`.
- 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.""")
public Result<List<CampaignRule>> searchRules(String search) {
CampaignRuleCriteria criteria = new CampaignRuleCriteria();
criteria.setSearch(search);
criteria.setSearch(SearchUtils.sanitizeSearch(search));
return campaignRuleService.getAll(criteria);
}
@McpTool(description = "Đếm số lượng RULE/THỂ LỆ, không phải chiến dịch. Cho phép tìm kiếm với param search.")
@McpTool(description = "Đếm số lượng RULE/THỂ LỆ, không phải chiến dịch. Để search = null nếu đếm tất cả.")
public Result<Long> countRules(String search) {
CampaignRuleCriteria criteria = new CampaignRuleCriteria();
criteria.setSearch(search);
criteria.setSearch(SearchUtils.sanitizeSearch(search));
return campaignRuleService.count(criteria);
}

View File

@@ -5,6 +5,7 @@ import dev.sonpx.loyalty.mcp.model.Result;
import dev.sonpx.loyalty.mcp.reward.criteria.CampaignCriteria;
import dev.sonpx.loyalty.mcp.reward.model.Campaign;
import dev.sonpx.loyalty.mcp.service.CampaignService;
import dev.sonpx.loyalty.mcp.util.SearchUtils;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.mcp.annotation.McpTool;
@@ -21,18 +22,19 @@ public class CampaignTools {
@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.
Dùng khi người dùng hỏi: "danh sách chiến dịch", "tìm chiến dịch", "chiến dịch nào", "campaign 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.""")
- Param `search` (String): từ khóa tìm kiếm theo tên hoặc mã chiến dịch cụ thể.
- 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`.
- 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<Campaign>> searchCampaigns(String search) {
CampaignCriteria criteria = new CampaignCriteria();
criteria.setSearch(search);
criteria.setSearch(SearchUtils.sanitizeSearch(search));
return campaignService.getAll(criteria);
}
@McpTool(description = "Đếm số lượng CHIẾN DỊCH (Campaign), không phải rule. Cho phép tìm kiếm với param search.")
@McpTool(description = "Đếm số lượng CHIẾN DỊCH (Campaign), không phải rule. Để search = null nếu đếm tất cả.")
public Result<Long> countCampaigns(String search) {
CampaignCriteria criteria = new CampaignCriteria();
criteria.setSearch(search);
criteria.setSearch(SearchUtils.sanitizeSearch(search));
return campaignService.count(criteria);
}

View File

@@ -0,0 +1,64 @@
package dev.sonpx.loyalty.mcp.util;
import java.text.Normalizer;
import java.util.Set;
/**
* Utility for sanitizing search parameters passed to MCP tools.
*/
public class SearchUtils {
private static final Set<String> GENERIC_KEYWORDS = Set.of(
"chien dich", "chiendich",
"danh sach", "danhsach",
"campaign", "campaigns",
"rule", "rules",
"the le", "thele",
"tat ca", "tatca",
"tat ca chien dich",
"danh sach chien dich",
"danh sach rule",
"danh sach the le",
"list", "show", "xem"
);
/**
* Sanitizes the input search string.
* If the search term consists only of generic query noise (e.g. "chiến dịch", "danh sách chiến dịch", "campaign"),
* returns null so the API returns all items rather than filtering by the word "chiến dịch".
*
* @param search the raw search string from LLM tool call
* @return cleaned search string or null if generic
*/
public static String sanitizeSearch(String search) {
if (search == null || search.isBlank()) {
return null;
}
String trimmed = search.trim();
String normalized = removeAccents(trimmed.toLowerCase());
if (GENERIC_KEYWORDS.contains(normalized)) {
return null;
}
// Strip leading/trailing generic intent words
String cleaned = normalized
.replaceAll("^(danh sach|chien dich|campaign|rule|the le|tat ca|xem|list)\\s+", "")
.replaceAll("\\s+(danh sach|chien dich|campaign|rule|the le|tat ca)$", "")
.trim();
if (cleaned.isEmpty() || GENERIC_KEYWORDS.contains(cleaned)) {
return null;
}
return trimmed;
}
private static String removeAccents(String src) {
return Normalizer.normalize(src, Normalizer.Form.NFD)
.replaceAll("\\p{M}", "")
.replace("đ", "d")
.replace("Đ", "d");
}
}

View File

@@ -0,0 +1,29 @@
package dev.sonpx.loyalty.mcp.util;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class SearchUtilsTest {
@Test
void testGenericKeywordsReturnNull() {
assertNull(SearchUtils.sanitizeSearch("chiến dịch"));
assertNull(SearchUtils.sanitizeSearch("danh sách chiến dịch"));
assertNull(SearchUtils.sanitizeSearch("campaign"));
assertNull(SearchUtils.sanitizeSearch("rule"));
assertNull(SearchUtils.sanitizeSearch("thể lệ"));
assertNull(SearchUtils.sanitizeSearch("danh sách rule"));
assertNull(SearchUtils.sanitizeSearch("tất cả chiến dịch"));
assertNull(SearchUtils.sanitizeSearch(""));
assertNull(SearchUtils.sanitizeSearch(" "));
assertNull(SearchUtils.sanitizeSearch(null));
}
@Test
void testSpecificKeywordsPreserved() {
assertEquals("Sinh Nhật", SearchUtils.sanitizeSearch("Sinh Nhật"));
assertEquals("HAPPY BIRTHDAY", SearchUtils.sanitizeSearch("HAPPY BIRTHDAY"));
assertEquals("Chiến dịch Sinh Nhật", SearchUtils.sanitizeSearch("Chiến dịch Sinh Nhật"));
assertEquals("MUA HÀNG TÍCH ĐIỂM", SearchUtils.sanitizeSearch("MUA HÀNG TÍCH ĐIỂM"));
}
}