package com.vincent.rsf.server.ai.store; import com.vincent.rsf.server.ai.dto.AiObserveStatsDto; import com.vincent.rsf.server.ai.store.support.AiRedisExecutor; import com.vincent.rsf.server.ai.store.support.AiRedisKeys; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Supplier; @Component @RequiredArgsConstructor public class AiObserveStatsStore { private static final String FIELD_CALL_COUNT = "callCount"; private static final String FIELD_SUCCESS_COUNT = "successCount"; private static final String FIELD_FAILURE_COUNT = "failureCount"; private static final String FIELD_ELAPSED_SUM = "elapsedSum"; private static final String FIELD_ELAPSED_COUNT = "elapsedCount"; private static final String FIELD_FIRST_TOKEN_SUM = "firstTokenSum"; private static final String FIELD_FIRST_TOKEN_COUNT = "firstTokenCount"; private static final String FIELD_TOTAL_TOKENS_SUM = "totalTokensSum"; private static final String FIELD_TOTAL_TOKENS_COUNT = "totalTokensCount"; private static final String FIELD_TOOL_CALL_COUNT = "toolCallCount"; private static final String FIELD_TOOL_SUCCESS_COUNT = "toolSuccessCount"; private static final String FIELD_TOOL_FAILURE_COUNT = "toolFailureCount"; private final AiRedisExecutor aiRedisExecutor; private final AiRedisKeys aiRedisKeys; public void recordObserveCallStarted(Long tenantId) { aiRedisExecutor.executeVoid(jedis -> jedis.hincrBy(aiRedisKeys.buildObserveStatsKey(tenantId), FIELD_CALL_COUNT, 1)); } public void recordObserveCallFinished(Long tenantId, String status, Long elapsedMs, Long firstTokenLatencyMs, Integer totalTokens) { aiRedisExecutor.executeVoid(jedis -> { String key = aiRedisKeys.buildObserveStatsKey(tenantId); if ("COMPLETED".equals(status)) { jedis.hincrBy(key, FIELD_SUCCESS_COUNT, 1); } else if ("FAILED".equals(status)) { jedis.hincrBy(key, FIELD_FAILURE_COUNT, 1); } if (elapsedMs != null) { jedis.hincrBy(key, FIELD_ELAPSED_SUM, elapsedMs); jedis.hincrBy(key, FIELD_ELAPSED_COUNT, 1); } if (firstTokenLatencyMs != null) { jedis.hincrBy(key, FIELD_FIRST_TOKEN_SUM, firstTokenLatencyMs); jedis.hincrBy(key, FIELD_FIRST_TOKEN_COUNT, 1); } if (totalTokens != null) { jedis.hincrBy(key, FIELD_TOTAL_TOKENS_SUM, totalTokens.longValue()); jedis.hincrBy(key, FIELD_TOTAL_TOKENS_COUNT, 1); } }); } public void recordObserveToolCall(Long tenantId, String toolName, String status) { aiRedisExecutor.executeVoid(jedis -> { String key = aiRedisKeys.buildObserveStatsKey(tenantId); jedis.hincrBy(key, FIELD_TOOL_CALL_COUNT, 1); if ("COMPLETED".equals(status)) { jedis.hincrBy(key, FIELD_TOOL_SUCCESS_COUNT, 1); } else if ("FAILED".equals(status)) { jedis.hincrBy(key, FIELD_TOOL_FAILURE_COUNT, 1); } if (StringUtils.hasText(toolName)) { jedis.zincrby(aiRedisKeys.buildToolRankKey(tenantId), 1D, toolName); if ("FAILED".equals(status)) { jedis.zincrby(aiRedisKeys.buildToolFailRankKey(tenantId), 1D, toolName); } } }); } public AiObserveStatsDto getObserveStats(Long tenantId, Supplier fallbackLoader) { AiObserveStatsDto cached = readObserveStats(tenantId); if (cached != null) { return cached; } AiObserveStatsDto snapshot = fallbackLoader.get(); if (snapshot != null) { seedObserveStats(tenantId, snapshot); } return snapshot; } private AiObserveStatsDto readObserveStats(Long tenantId) { Map fields = aiRedisExecutor.execute(jedis -> { String key = aiRedisKeys.buildObserveStatsKey(tenantId); if (!jedis.exists(key)) { return null; } return jedis.hgetAll(key); }); if (fields == null || fields.isEmpty()) { return null; } long callCount = parseLong(fields.get(FIELD_CALL_COUNT)); long successCount = parseLong(fields.get(FIELD_SUCCESS_COUNT)); long failureCount = parseLong(fields.get(FIELD_FAILURE_COUNT)); long elapsedSum = parseLong(fields.get(FIELD_ELAPSED_SUM)); long elapsedCount = parseLong(fields.get(FIELD_ELAPSED_COUNT)); long firstTokenSum = parseLong(fields.get(FIELD_FIRST_TOKEN_SUM)); long firstTokenCount = parseLong(fields.get(FIELD_FIRST_TOKEN_COUNT)); long totalTokensSum = parseLong(fields.get(FIELD_TOTAL_TOKENS_SUM)); long totalTokensCount = parseLong(fields.get(FIELD_TOTAL_TOKENS_COUNT)); long toolCallCount = parseLong(fields.get(FIELD_TOOL_CALL_COUNT)); long toolSuccessCount = parseLong(fields.get(FIELD_TOOL_SUCCESS_COUNT)); long toolFailureCount = parseLong(fields.get(FIELD_TOOL_FAILURE_COUNT)); return AiObserveStatsDto.builder() .callCount(callCount) .successCount(successCount) .failureCount(failureCount) .avgElapsedMs(elapsedCount == 0 ? 0L : elapsedSum / elapsedCount) .avgFirstTokenLatencyMs(firstTokenCount == 0 ? 0L : firstTokenSum / firstTokenCount) .totalTokens(totalTokensSum) .avgTotalTokens(totalTokensCount == 0 ? 0L : totalTokensSum / totalTokensCount) .toolCallCount(toolCallCount) .toolSuccessCount(toolSuccessCount) .toolFailureCount(toolFailureCount) .toolSuccessRate(toolCallCount == 0 ? 0D : (toolSuccessCount * 100D) / toolCallCount) .build(); } private void seedObserveStats(Long tenantId, AiObserveStatsDto snapshot) { aiRedisExecutor.executeVoid(jedis -> { String key = aiRedisKeys.buildObserveStatsKey(tenantId); Map values = new LinkedHashMap<>(); values.put(FIELD_CALL_COUNT, String.valueOf(defaultLong(snapshot.getCallCount()))); values.put(FIELD_SUCCESS_COUNT, String.valueOf(defaultLong(snapshot.getSuccessCount()))); values.put(FIELD_FAILURE_COUNT, String.valueOf(defaultLong(snapshot.getFailureCount()))); values.put(FIELD_ELAPSED_SUM, String.valueOf(defaultLong(snapshot.getAvgElapsedMs()) * defaultLong(snapshot.getCallCount()))); values.put(FIELD_ELAPSED_COUNT, String.valueOf(defaultLong(snapshot.getCallCount()))); values.put(FIELD_FIRST_TOKEN_SUM, String.valueOf(defaultLong(snapshot.getAvgFirstTokenLatencyMs()) * defaultLong(snapshot.getCallCount()))); values.put(FIELD_FIRST_TOKEN_COUNT, String.valueOf(defaultLong(snapshot.getCallCount()))); values.put(FIELD_TOTAL_TOKENS_SUM, String.valueOf(defaultLong(snapshot.getTotalTokens()))); values.put(FIELD_TOTAL_TOKENS_COUNT, String.valueOf(defaultLong(snapshot.getCallCount()))); values.put(FIELD_TOOL_CALL_COUNT, String.valueOf(defaultLong(snapshot.getToolCallCount()))); values.put(FIELD_TOOL_SUCCESS_COUNT, String.valueOf(defaultLong(snapshot.getToolSuccessCount()))); values.put(FIELD_TOOL_FAILURE_COUNT, String.valueOf(defaultLong(snapshot.getToolFailureCount()))); jedis.hset(key, values); }); } private long parseLong(String source) { if (!StringUtils.hasText(source)) { return 0L; } try { return Long.parseLong(source); } catch (Exception e) { return 0L; } } private long defaultLong(Long value) { return value == null ? 0L : value; } }