| src/main/java/com/zy/ai/service/AutoTuneCoordinatorService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/ai/service/impl/AutoTuneCoordinatorServiceImpl.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/ai/timer/AutoTuneScheduler.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/core/enums/RedisKeyType.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/test/java/com/zy/ai/service/AutoTuneCoordinatorServiceImplTest.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/main/java/com/zy/ai/service/AutoTuneCoordinatorService.java
New file @@ -0,0 +1,40 @@ package com.zy.ai.service; import lombok.Data; import java.io.Serializable; public interface AutoTuneCoordinatorService { AutoTuneCoordinatorResult runAutoTuneIfEligible(); @Data class AutoTuneCoordinatorResult implements Serializable { private static final long serialVersionUID = 1L; private Boolean skipped; private String reason; private Boolean triggered; private AutoTuneAgentService.AutoTuneAgentResult agentResult; public static AutoTuneCoordinatorResult skipped(String reason) { AutoTuneCoordinatorResult result = new AutoTuneCoordinatorResult(); result.setSkipped(true); result.setTriggered(false); result.setReason(reason); return result; } public static AutoTuneCoordinatorResult triggered(AutoTuneAgentService.AutoTuneAgentResult agentResult) { AutoTuneCoordinatorResult result = new AutoTuneCoordinatorResult(); result.setSkipped(false); result.setTriggered(true); result.setReason("triggered"); result.setAgentResult(agentResult); return result; } } } src/main/java/com/zy/ai/service/impl/AutoTuneCoordinatorServiceImpl.java
New file @@ -0,0 +1,230 @@ package com.zy.ai.service.impl; import com.alibaba.fastjson.JSON; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.zy.ai.domain.autotune.AutoTuneJobStatus; import com.zy.ai.domain.autotune.AutoTuneTriggerType; import com.zy.ai.entity.AiAutoTuneJob; import com.zy.ai.service.AiAutoTuneJobService; import com.zy.ai.service.AutoTuneAgentService; import com.zy.ai.service.AutoTuneCoordinatorService; import com.zy.asrs.entity.WrkMast; import com.zy.asrs.service.WrkMastService; import com.zy.common.utils.RedisUtil; import com.zy.core.enums.RedisKeyType; import com.zy.core.enums.WrkStsType; import com.zy.system.entity.OperateLog; import com.zy.system.service.ConfigService; import com.zy.system.service.OperateLogService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.Arrays; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.UUID; @Slf4j @Service("autoTuneCoordinatorService") @RequiredArgsConstructor public class AutoTuneCoordinatorServiceImpl implements AutoTuneCoordinatorService { private static final String CONFIG_ENABLED = "aiAutoTuneEnabled"; private static final String CONFIG_INTERVAL_MINUTES = "aiAutoTuneIntervalMinutes"; private static final String DEFAULT_ENABLED = "N"; private static final int DEFAULT_INTERVAL_MINUTES = 10; private static final int MIN_INTERVAL_MINUTES = 5; private static final int MAX_INTERVAL_MINUTES = 60; private static final int RUNNING_LOCK_SECONDS = 20 * 60; private static final long SYSTEM_USER_ID = 9527L; private static final List<Long> FINAL_WRK_STS_LIST = Arrays.asList( WrkStsType.COMPLETE_INBOUND.sts, WrkStsType.SETTLE_INBOUND.sts, WrkStsType.COMPLETE_OUTBOUND.sts, WrkStsType.SETTLE_OUTBOUND.sts, WrkStsType.COMPLETE_LOC_MOVE.sts, WrkStsType.COMPLETE_CRN_MOVE.sts ); private final ConfigService configService; private final WrkMastService wrkMastService; private final AiAutoTuneJobService aiAutoTuneJobService; private final AutoTuneAgentService autoTuneAgentService; private final RedisUtil redisUtil; private final OperateLogService operateLogService; @Override public AutoTuneCoordinatorResult runAutoTuneIfEligible() { if (!isEnabled()) { return AutoTuneCoordinatorResult.skipped("disabled"); } int intervalMinutes = resolveIntervalMinutes(); if (!hasActiveTasks()) { return AutoTuneCoordinatorResult.skipped("no_active_tasks"); } if (isLastTriggerGuardActive()) { return AutoTuneCoordinatorResult.skipped("last_trigger_guard_active"); } AiAutoTuneJob latestSuccessfulJob = latestSuccessfulAutoJob(); if (!isIntervalReached(latestSuccessfulJob, intervalMinutes)) { return AutoTuneCoordinatorResult.skipped("interval_not_reached"); } String lockKey = RedisKeyType.AI_AUTO_TUNE_RUNNING_LOCK.key; String lockToken = UUID.randomUUID().toString(); if (!redisUtil.trySetStringIfAbsent(lockKey, lockToken, RUNNING_LOCK_SECONDS)) { return AutoTuneCoordinatorResult.skipped("running_lock_not_acquired"); } AutoTuneAgentService.AutoTuneAgentResult agentResult = null; try { agentResult = autoTuneAgentService.runAutoTune(AutoTuneTriggerType.AUTO.getCode()); writeOperateLog(agentResult); markNoChangeGuardIfNeeded(latestSuccessfulJob, agentResult, intervalMinutes); return AutoTuneCoordinatorResult.triggered(agentResult); } catch (Exception exception) { log.error("Auto tune coordinator failed to run agent", exception); agentResult = failedAgentResult(exception); writeOperateLog(agentResult); return AutoTuneCoordinatorResult.triggered(agentResult); } finally { redisUtil.compareAndDelete(lockKey, lockToken); } } private boolean isEnabled() { String enabled = configService.getConfigValue(CONFIG_ENABLED, DEFAULT_ENABLED); if (enabled == null) { return false; } String normalized = enabled.trim(); return "Y".equalsIgnoreCase(normalized) || "true".equalsIgnoreCase(normalized) || "1".equals(normalized); } private int resolveIntervalMinutes() { String value = configService.getConfigValue(CONFIG_INTERVAL_MINUTES, String.valueOf(DEFAULT_INTERVAL_MINUTES)); try { int intervalMinutes = Integer.parseInt(value.trim()); if (intervalMinutes < MIN_INTERVAL_MINUTES || intervalMinutes > MAX_INTERVAL_MINUTES) { return DEFAULT_INTERVAL_MINUTES; } return intervalMinutes; } catch (Exception exception) { return DEFAULT_INTERVAL_MINUTES; } } private boolean hasActiveTasks() { QueryWrapper<WrkMast> queryWrapper = new QueryWrapper<>(); queryWrapper.and(wrapper -> wrapper.notIn("wrk_sts", FINAL_WRK_STS_LIST).or().isNull("wrk_sts")); return wrkMastService.count(queryWrapper) > 0; } private boolean isLastTriggerGuardActive() { Object guardValue = redisUtil.get(RedisKeyType.AI_AUTO_TUNE_LAST_TRIGGER_GUARD.key); return guardValue != null; } private AiAutoTuneJob latestSuccessfulAutoJob() { QueryWrapper<AiAutoTuneJob> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("status", AutoTuneJobStatus.SUCCESS.getCode()); queryWrapper.eq("trigger_type", AutoTuneTriggerType.AUTO.getCode()); queryWrapper.last("order by coalesce(finish_time, create_time) desc limit 1"); List<AiAutoTuneJob> jobs = aiAutoTuneJobService.list(queryWrapper); if (jobs == null || jobs.isEmpty()) { return null; } return jobs.get(0); } private boolean isIntervalReached(AiAutoTuneJob latestSuccessfulJob, int intervalMinutes) { if (latestSuccessfulJob == null) { return true; } Date latestFinishTime = latestSuccessfulJob.getFinishTime(); if (latestFinishTime == null) { latestFinishTime = latestSuccessfulJob.getCreateTime(); } if (latestFinishTime == null) { return true; } long intervalMillis = intervalMinutes * 60L * 1000L; return System.currentTimeMillis() - latestFinishTime.getTime() >= intervalMillis; } private void markNoChangeGuardIfNeeded(AiAutoTuneJob beforeJob, AutoTuneAgentService.AutoTuneAgentResult agentResult, int intervalMinutes) { if (agentResult == null || !Boolean.TRUE.equals(agentResult.getSuccess())) { return; } AiAutoTuneJob afterJob = latestSuccessfulAutoJob(); if (!isSameJob(beforeJob, afterJob)) { return; } long expireSeconds = intervalMinutes * 60L; redisUtil.set(RedisKeyType.AI_AUTO_TUNE_LAST_TRIGGER_GUARD.key, String.valueOf(System.currentTimeMillis()), expireSeconds); } private boolean isSameJob(AiAutoTuneJob beforeJob, AiAutoTuneJob afterJob) { if (beforeJob == null || afterJob == null) { return beforeJob == afterJob; } return beforeJob.getId() != null && beforeJob.getId().equals(afterJob.getId()); } private AutoTuneAgentService.AutoTuneAgentResult failedAgentResult(Exception exception) { AutoTuneAgentService.AutoTuneAgentResult result = new AutoTuneAgentService.AutoTuneAgentResult(); result.setSuccess(false); result.setTriggerType(AutoTuneTriggerType.AUTO.getCode()); result.setSummary("自动调参后台任务执行异常: " + exception.getMessage()); result.setToolCallCount(0); result.setLlmCallCount(0); result.setPromptTokens(0L); result.setCompletionTokens(0L); result.setTotalTokens(0L); result.setMaxRoundsReached(false); return result; } private void writeOperateLog(AutoTuneAgentService.AutoTuneAgentResult agentResult) { if (agentResult == null) { return; } OperateLog operateLog = new OperateLog(); operateLog.setAction("ai_auto_tune_background_scheduler"); operateLog.setUserId(SYSTEM_USER_ID); operateLog.setIp("system"); operateLog.setRequest(JSON.toJSONString(buildRequestSummary(agentResult))); operateLog.setResponse(JSON.toJSONString(buildResponseSummary(agentResult))); operateLog.setCreateTime(new Date()); operateLogService.save(operateLog); } private Map<String, Object> buildRequestSummary(AutoTuneAgentService.AutoTuneAgentResult agentResult) { Map<String, Object> request = new LinkedHashMap<>(); request.put("trigger", agentResult.getTriggerType()); return request; } private Map<String, Object> buildResponseSummary(AutoTuneAgentService.AutoTuneAgentResult agentResult) { Map<String, Object> response = new LinkedHashMap<>(); response.put("success", agentResult.getSuccess()); response.put("summary", agentResult.getSummary()); response.put("toolCallCount", agentResult.getToolCallCount()); response.put("llmCallCount", agentResult.getLlmCallCount()); response.put("promptTokens", agentResult.getPromptTokens()); response.put("completionTokens", agentResult.getCompletionTokens()); response.put("totalTokens", agentResult.getTotalTokens()); response.put("maxRoundsReached", agentResult.getMaxRoundsReached()); return response; } } src/main/java/com/zy/ai/timer/AutoTuneScheduler.java
New file @@ -0,0 +1,31 @@ package com.zy.ai.timer; import com.zy.ai.service.AutoTuneCoordinatorService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Slf4j @Component public class AutoTuneScheduler { @Autowired private AutoTuneCoordinatorService autoTuneCoordinatorService; @Scheduled(cron = "0 * * * * ? ") public void runAutoTune() { try { AutoTuneCoordinatorService.AutoTuneCoordinatorResult result = autoTuneCoordinatorService.runAutoTuneIfEligible(); if (Boolean.TRUE.equals(result.getSkipped())) { log.debug("Auto tune scheduler skipped, reason={}", result.getReason()); return; } log.info("Auto tune scheduler triggered, success={}", result.getAgentResult() == null ? null : result.getAgentResult().getSuccess()); } catch (Exception exception) { log.error("Auto tune scheduler failed", exception); } } } src/main/java/com/zy/core/enums/RedisKeyType.java
@@ -78,6 +78,8 @@ CURRENT_CIRCLE_TASK_CRN_NO("current_circle_task_crn_no_"), MAIN_PROCESS_PSEUDOCODE("main_process_pseudocode"), AI_AUTO_TUNE_RUNNING_LOCK("ai_auto_tune_running_lock"), AI_AUTO_TUNE_LAST_TRIGGER_GUARD("ai_auto_tune_last_trigger_guard"), PLANNER_SCHEDULE("planner_schedule_"), HIGH_PRIVILEGE_GRANT("high_privilege_grant_"), ; src/test/java/com/zy/ai/service/AutoTuneCoordinatorServiceImplTest.java
@@ -6,6 +6,7 @@ import com.zy.ai.domain.autotune.AutoTuneApplyResult; import com.zy.ai.domain.autotune.AutoTuneChangeCommand; import com.zy.ai.domain.autotune.AutoTuneSnapshot; import com.zy.ai.domain.autotune.AutoTuneTriggerType; import com.zy.ai.entity.AiAutoTuneChange; import com.zy.ai.entity.AiAutoTuneJob; import com.zy.ai.entity.AiPromptTemplate; @@ -14,6 +15,12 @@ import com.zy.ai.mcp.service.SpringAiMcpToolManager; import com.zy.ai.mcp.tool.AutoTuneMcpTools; import com.zy.ai.service.impl.AutoTuneAgentServiceImpl; import com.zy.ai.service.impl.AutoTuneCoordinatorServiceImpl; import com.zy.asrs.service.WrkMastService; import com.zy.common.utils.RedisUtil; import com.zy.core.enums.RedisKeyType; import com.zy.system.service.ConfigService; import com.zy.system.service.OperateLogService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -24,6 +31,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -39,6 +47,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyDouble; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -63,6 +73,16 @@ private SpringAiMcpToolManager mcpToolManager; @Mock private AiPromptTemplateService aiPromptTemplateService; @Mock private ConfigService configService; @Mock private WrkMastService wrkMastService; @Mock private AutoTuneAgentService autoTuneAgentService; @Mock private RedisUtil redisUtil; @Mock private OperateLogService operateLogService; @BeforeEach void setUp() { @@ -239,6 +259,88 @@ } @Test void coordinatorSkipsWhenDisabled() { when(configService.getConfigValue("aiAutoTuneEnabled", "N")).thenReturn("N"); AutoTuneCoordinatorService.AutoTuneCoordinatorResult result = coordinatorService().runAutoTuneIfEligible(); assertTrue(result.getSkipped()); assertEquals("disabled", result.getReason()); verify(wrkMastService, never()).count(any(Wrapper.class)); verify(autoTuneAgentService, never()).runAutoTune(anyString()); } @Test void coordinatorSkipsWhenNoActiveTasks() { when(configService.getConfigValue("aiAutoTuneEnabled", "N")).thenReturn("Y"); when(configService.getConfigValue("aiAutoTuneIntervalMinutes", "10")).thenReturn("10"); when(wrkMastService.count(any(Wrapper.class))).thenReturn(0L); AutoTuneCoordinatorService.AutoTuneCoordinatorResult result = coordinatorService().runAutoTuneIfEligible(); assertTrue(result.getSkipped()); assertEquals("no_active_tasks", result.getReason()); verify(autoTuneAgentService, never()).runAutoTune(anyString()); } @Test void coordinatorSkipsWhenIntervalNotReached() { AiAutoTuneJob recentJob = new AiAutoTuneJob(); recentJob.setId(11L); recentJob.setFinishTime(new Date()); when(configService.getConfigValue("aiAutoTuneEnabled", "N")).thenReturn("true"); when(configService.getConfigValue("aiAutoTuneIntervalMinutes", "10")).thenReturn("10"); when(wrkMastService.count(any(Wrapper.class))).thenReturn(1L); when(redisUtil.get(RedisKeyType.AI_AUTO_TUNE_LAST_TRIGGER_GUARD.key)).thenReturn(null); when(aiAutoTuneJobService.list(any(Wrapper.class))).thenReturn(Collections.singletonList(recentJob)); AutoTuneCoordinatorService.AutoTuneCoordinatorResult result = coordinatorService().runAutoTuneIfEligible(); assertTrue(result.getSkipped()); assertEquals("interval_not_reached", result.getReason()); verify(autoTuneAgentService, never()).runAutoTune(anyString()); } @Test void coordinatorTriggersAgentWhenEligible() { AutoTuneAgentService.AutoTuneAgentResult agentResult = successfulAgentResult(); when(configService.getConfigValue("aiAutoTuneEnabled", "N")).thenReturn("1"); when(configService.getConfigValue("aiAutoTuneIntervalMinutes", "10")).thenReturn("10"); when(wrkMastService.count(any(Wrapper.class))).thenReturn(1L); when(redisUtil.get(RedisKeyType.AI_AUTO_TUNE_LAST_TRIGGER_GUARD.key)).thenReturn(null); when(aiAutoTuneJobService.list(any(Wrapper.class))).thenReturn(Collections.emptyList(), Collections.emptyList()); when(redisUtil.trySetStringIfAbsent(anyString(), anyString(), anyLong())).thenReturn(true); when(autoTuneAgentService.runAutoTune(AutoTuneTriggerType.AUTO.getCode())).thenReturn(agentResult); AutoTuneCoordinatorService.AutoTuneCoordinatorResult result = coordinatorService().runAutoTuneIfEligible(); assertFalse(result.getSkipped()); assertTrue(result.getTriggered()); assertSame(agentResult, result.getAgentResult()); verify(autoTuneAgentService).runAutoTune(AutoTuneTriggerType.AUTO.getCode()); verify(operateLogService).save(any()); verify(redisUtil).set(anyString(), any(), anyLong()); verify(redisUtil).compareAndDelete(anyString(), anyString()); } @Test void coordinatorSkipsWhenRunningLockIsNotAcquired() { when(configService.getConfigValue("aiAutoTuneEnabled", "N")).thenReturn("Y"); when(configService.getConfigValue("aiAutoTuneIntervalMinutes", "10")).thenReturn("10"); when(wrkMastService.count(any(Wrapper.class))).thenReturn(1L); when(redisUtil.get(RedisKeyType.AI_AUTO_TUNE_LAST_TRIGGER_GUARD.key)).thenReturn(null); when(aiAutoTuneJobService.list(any(Wrapper.class))).thenReturn(Collections.emptyList()); when(redisUtil.trySetStringIfAbsent(anyString(), anyString(), anyLong())).thenReturn(false); AutoTuneCoordinatorService.AutoTuneCoordinatorResult result = coordinatorService().runAutoTuneIfEligible(); assertTrue(result.getSkipped()); assertEquals("running_lock_not_acquired", result.getReason()); verify(autoTuneAgentService, never()).runAutoTune(anyString()); verify(redisUtil, never()).compareAndDelete(anyString(), anyString()); } @Test void agentExecutesSnapshotDryRunAndRealApplyToolSequence() { AutoTuneAgentServiceImpl service = new AutoTuneAgentServiceImpl( llmChatService, @@ -370,6 +472,30 @@ return new AutoTuneAgentServiceImpl(llmChatService, mcpToolManager, aiPromptTemplateService); } private AutoTuneCoordinatorServiceImpl coordinatorService() { return new AutoTuneCoordinatorServiceImpl( configService, wrkMastService, aiAutoTuneJobService, autoTuneAgentService, redisUtil, operateLogService); } private AutoTuneAgentService.AutoTuneAgentResult successfulAgentResult() { AutoTuneAgentService.AutoTuneAgentResult result = new AutoTuneAgentService.AutoTuneAgentResult(); result.setSuccess(true); result.setTriggerType(AutoTuneTriggerType.AUTO.getCode()); result.setSummary("no changes needed"); result.setToolCallCount(1); result.setLlmCallCount(1); result.setPromptTokens(10L); result.setCompletionTokens(5L); result.setTotalTokens(15L); result.setMaxRoundsReached(false); return result; } private AutoTuneChangeCommand change(String targetType, String targetId, String targetKey, String newValue) { AutoTuneChangeCommand command = new AutoTuneChangeCommand(); command.setTargetType(targetType);