|
@@ -0,0 +1,589 @@
|
|
|
|
|
+package com.dtm.storage.service;
|
|
|
|
|
+
|
|
|
|
|
+import com.alibaba.fastjson2.JSON;
|
|
|
|
|
+import com.alibaba.fastjson2.JSONArray;
|
|
|
|
|
+import com.alibaba.fastjson2.JSONObject;
|
|
|
|
|
+import com.dtm.common.utils.StringUtils;
|
|
|
|
|
+import com.dtm.storage.model.AgentChatMessage;
|
|
|
|
|
+import com.dtm.storage.model.AgentChatRequest;
|
|
|
|
|
+import org.slf4j.Logger;
|
|
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
+import org.springframework.http.HttpEntity;
|
|
|
|
|
+import org.springframework.http.HttpHeaders;
|
|
|
|
|
+import org.springframework.http.MediaType;
|
|
|
|
|
+import org.springframework.http.ResponseEntity;
|
|
|
|
|
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+import org.springframework.web.client.RestTemplate;
|
|
|
|
|
+import org.springframework.web.client.HttpStatusCodeException;
|
|
|
|
|
+import org.springframework.web.client.ResourceAccessException;
|
|
|
|
|
+
|
|
|
|
|
+import javax.annotation.PostConstruct;
|
|
|
|
|
+import java.time.LocalDate;
|
|
|
|
|
+import java.time.LocalDateTime;
|
|
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.Collections;
|
|
|
|
|
+import java.util.HashSet;
|
|
|
|
|
+import java.util.LinkedHashMap;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.Locale;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+import java.util.Set;
|
|
|
|
|
+import java.util.regex.Matcher;
|
|
|
|
|
+import java.util.regex.Pattern;
|
|
|
|
|
+
|
|
|
|
|
+@Service
|
|
|
|
|
+public class StorageAgentService {
|
|
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(StorageAgentService.class);
|
|
|
|
|
+ private static final int MAX_HISTORY = 6;
|
|
|
|
|
+ private static final int MAX_MESSAGE_LENGTH = 2000;
|
|
|
|
|
+
|
|
|
|
|
+ private final InventoryService inventoryService;
|
|
|
|
|
+ private final RiskService riskService;
|
|
|
|
|
+ private final ProductService productService;
|
|
|
|
|
+ private final StorageDataLoader dataLoader;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${zhipu.api-key:}")
|
|
|
|
|
+ private String apiKey;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${zhipu.base-url:https://open.bigmodel.cn/api/paas/v4/chat/completions}")
|
|
|
|
|
+ private String baseUrl;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${zhipu.model:glm-4.7-flash}")
|
|
|
|
|
+ private String model;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${zhipu.temperature:0.4}")
|
|
|
|
|
+ private double temperature;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${zhipu.max-tokens:1024}")
|
|
|
|
|
+ private int maxTokens;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${zhipu.timeout-ms:15000}")
|
|
|
|
|
+ private int timeoutMs;
|
|
|
|
|
+
|
|
|
|
|
+ private RestTemplate restTemplate;
|
|
|
|
|
+
|
|
|
|
|
+ public StorageAgentService(InventoryService inventoryService,
|
|
|
|
|
+ RiskService riskService,
|
|
|
|
|
+ ProductService productService,
|
|
|
|
|
+ StorageDataLoader dataLoader) {
|
|
|
|
|
+ this.inventoryService = inventoryService;
|
|
|
|
|
+ this.riskService = riskService;
|
|
|
|
|
+ this.productService = productService;
|
|
|
|
|
+ this.dataLoader = dataLoader;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @PostConstruct
|
|
|
|
|
+ public void init() {
|
|
|
|
|
+ SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
|
|
|
|
+ factory.setConnectTimeout(timeoutMs);
|
|
|
|
|
+ factory.setReadTimeout(timeoutMs);
|
|
|
|
|
+ this.restTemplate = new RestTemplate(factory);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public Map<String, Object> chat(AgentChatRequest request) {
|
|
|
|
|
+ long startedAt = System.currentTimeMillis();
|
|
|
|
|
+ String message = normalizeMessage(request == null ? null : request.getMessage());
|
|
|
|
|
+ if (StringUtils.isEmpty(message)) {
|
|
|
|
|
+ return buildSimpleResponse("请输入问题后再尝试。", null, true, null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String sku = detectSku(message, request == null ? null : request.getSku());
|
|
|
|
|
+ Map<String, Object> context;
|
|
|
|
|
+ try {
|
|
|
|
|
+ context = buildContext(message, sku);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("Storage agent context build failed: {}", e.getMessage(), e);
|
|
|
|
|
+ context = new LinkedHashMap<>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String resolvedKey = resolveApiKey();
|
|
|
|
|
+ if (StringUtils.isEmpty(resolvedKey)) {
|
|
|
|
|
+ String fallback = buildFallbackAnswer(message, context, sku);
|
|
|
|
|
+ return buildSimpleResponse(fallback, sku, true, null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ String reply = callZhipu(message, context, request == null ? null : request.getHistory(), resolvedKey, maxTokens);
|
|
|
|
|
+ if (StringUtils.isEmpty(reply)) {
|
|
|
|
|
+ String fallback = buildFallbackAnswer(message, context, sku);
|
|
|
|
|
+ return buildSimpleResponse(fallback, sku, true, null);
|
|
|
|
|
+ }
|
|
|
|
|
+ return buildSimpleResponse(reply, sku, false, null);
|
|
|
|
|
+ } catch (ResourceAccessException e) {
|
|
|
|
|
+ if (isTimeout(e)) {
|
|
|
|
|
+ log.warn("Zhipu timeout, retry with lite context (sku={})", sku);
|
|
|
|
|
+ try {
|
|
|
|
|
+ Map<String, Object> liteContext = buildLiteContext(context, sku);
|
|
|
|
|
+ String reply = callZhipu(message, liteContext, request == null ? null : request.getHistory(), resolvedKey, 256);
|
|
|
|
|
+ if (!StringUtils.isEmpty(reply)) {
|
|
|
|
|
+ return buildSimpleResponse(reply, sku, false, null);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception retryEx) {
|
|
|
|
|
+ log.error("Storage agent retry failed: {}", retryEx.getMessage(), retryEx);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ log.error("Storage agent call failed: {}", e.getMessage(), e);
|
|
|
|
|
+ String fallback = buildFallbackAnswer(message, context, sku);
|
|
|
|
|
+ return buildSimpleResponse(fallback, sku, true, null);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("Storage agent call failed: {}", e.getMessage(), e);
|
|
|
|
|
+ String fallback = buildFallbackAnswer(message, context, sku);
|
|
|
|
|
+ return buildSimpleResponse(fallback, sku, true, null);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ long cost = System.currentTimeMillis() - startedAt;
|
|
|
|
|
+ if (cost > 5000) {
|
|
|
|
|
+ log.warn("Storage agent request slow: {} ms (sku={})", cost, sku);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ log.info("Storage agent request ok: {} ms (sku={})", cost, sku);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String callZhipu(String message,
|
|
|
|
|
+ Map<String, Object> context,
|
|
|
|
|
+ List<AgentChatMessage> history,
|
|
|
|
|
+ String resolvedKey,
|
|
|
|
|
+ int maxTokensOverride) {
|
|
|
|
|
+ JSONArray messages = new JSONArray();
|
|
|
|
|
+ messages.add(buildMessage("system", buildSystemPrompt()));
|
|
|
|
|
+
|
|
|
|
|
+ List<AgentChatMessage> trimmedHistory = trimHistory(history);
|
|
|
|
|
+ for (AgentChatMessage item : trimmedHistory) {
|
|
|
|
|
+ if (item == null) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ String role = normalizeRole(item.getRole());
|
|
|
|
|
+ String content = normalizeMessage(item.getContent());
|
|
|
|
|
+ if (StringUtils.isEmpty(content) || StringUtils.isEmpty(role)) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ messages.add(buildMessage(role, content));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String contextJson = JSON.toJSONString(context);
|
|
|
|
|
+ String userContent = "User question: " + message + "\n\nAvailable data (JSON):\n" + contextJson;
|
|
|
|
|
+ messages.add(buildMessage("user", userContent));
|
|
|
|
|
+
|
|
|
|
|
+ JSONObject payload = new JSONObject();
|
|
|
|
|
+ payload.put("model", model);
|
|
|
|
|
+ payload.put("messages", messages);
|
|
|
|
|
+ payload.put("temperature", temperature);
|
|
|
|
|
+ payload.put("max_tokens", maxTokensOverride > 0 ? maxTokensOverride : maxTokens);
|
|
|
|
|
+ payload.put("stream", false);
|
|
|
|
|
+
|
|
|
|
|
+ HttpHeaders headers = new HttpHeaders();
|
|
|
|
|
+ headers.setContentType(MediaType.APPLICATION_JSON);
|
|
|
|
|
+ headers.setBearerAuth(resolvedKey);
|
|
|
|
|
+ HttpEntity<String> requestEntity = new HttpEntity<>(payload.toJSONString(), headers);
|
|
|
|
|
+
|
|
|
|
|
+ ResponseEntity<String> response;
|
|
|
|
|
+ try {
|
|
|
|
|
+ response = restTemplate.postForEntity(baseUrl, requestEntity, String.class);
|
|
|
|
|
+ } catch (HttpStatusCodeException e) {
|
|
|
|
|
+ log.error("Zhipu API error: status={} body={}", e.getStatusCode(), e.getResponseBodyAsString());
|
|
|
|
|
+ throw e;
|
|
|
|
|
+ } catch (ResourceAccessException e) {
|
|
|
|
|
+ log.error("Zhipu API access error: {}", e.getMessage());
|
|
|
|
|
+ throw e;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (response == null || response.getBody() == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ JSONObject body = JSON.parseObject(response.getBody());
|
|
|
|
|
+ JSONArray choices = body.getJSONArray("choices");
|
|
|
|
|
+ if (choices == null || choices.isEmpty()) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ JSONObject choice = choices.getJSONObject(0);
|
|
|
|
|
+ if (choice == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ JSONObject messageObj = choice.getJSONObject("message");
|
|
|
|
|
+ if (messageObj == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ String content = messageObj.getString("content");
|
|
|
|
|
+ if (!StringUtils.isEmpty(content)) {
|
|
|
|
|
+ return content;
|
|
|
|
|
+ }
|
|
|
|
|
+ String reasoning = messageObj.getString("reasoning_content");
|
|
|
|
|
+ if (!StringUtils.isEmpty(reasoning)) {
|
|
|
|
|
+ return reasoning;
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private JSONObject buildMessage(String role, String content) {
|
|
|
|
|
+ JSONObject obj = new JSONObject();
|
|
|
|
|
+ obj.put("role", role);
|
|
|
|
|
+ obj.put("content", content);
|
|
|
|
|
+ return obj;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String buildSystemPrompt() {
|
|
|
|
|
+ return "You are an inventory analytics assistant. " +
|
|
|
|
|
+ "Use the provided business data to answer the user's question. " +
|
|
|
|
|
+ "If the data is insufficient, say so and suggest what to check next. " +
|
|
|
|
|
+ "Do not fabricate numbers. Respond in Simplified Chinese. " +
|
|
|
|
|
+ "Be concise and provide actionable suggestions.";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Map<String, Object> buildContext(String message, String sku) {
|
|
|
|
|
+ Map<String, Object> context = new LinkedHashMap<>();
|
|
|
|
|
+ context.put("generatedAt", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
|
|
|
|
+ context.put("overview", inventoryService.getOverviewData());
|
|
|
|
|
+ context.put("healthIndex", inventoryService.getHealthIndex());
|
|
|
|
|
+ context.put("structure", inventoryService.getStructureData());
|
|
|
|
|
+ context.put("lifecycleDistribution", inventoryService.getLifecycleDistribution());
|
|
|
|
|
+ context.put("monthlyComparison", trimMonthly(inventoryService.getMonthlyComparisonData(), 12));
|
|
|
|
|
+ context.put("riskStats", riskService.getRiskStatistics());
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> riskList = riskService.getRiskList(1, 5, null, null);
|
|
|
|
|
+ context.put("topRisks", riskList.getOrDefault("list", Collections.emptyList()));
|
|
|
|
|
+ context.put("optimizationFeedback", riskService.getOptimizationFeedback());
|
|
|
|
|
+
|
|
|
|
|
+ List<Map<String, Object>> skuSummary = inventoryService.getSkuSummaryTable();
|
|
|
|
|
+ context.put("topSkus", sliceList(skuSummary, 5));
|
|
|
|
|
+ List<Map<String, Object>> spuSummary = inventoryService.getSpuSummaryTable();
|
|
|
|
|
+ context.put("topSpus", sliceList(spuSummary, 5));
|
|
|
|
|
+
|
|
|
|
|
+ context.put("dataQuality", buildDataQuality());
|
|
|
|
|
+
|
|
|
|
|
+ if (!StringUtils.isEmpty(sku)) {
|
|
|
|
|
+ context.put("skuFocus", buildSkuFocus(sku));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return context;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Map<String, Object> buildSkuFocus(String sku) {
|
|
|
|
|
+ Map<String, Object> focus = new LinkedHashMap<>();
|
|
|
|
|
+ focus.put("sku", sku);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> skuRow = findSkuRow(sku);
|
|
|
|
|
+ if (skuRow != null) {
|
|
|
|
|
+ focus.put("summary", pickKeys(skuRow, "purchaseQty", "salesQty", "inventory", "turnoverRate", "purchaseAmount"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> riskRow = riskService.getRiskBySku(sku);
|
|
|
|
|
+ if (riskRow != null) {
|
|
|
|
|
+ focus.put("risk", pickKeys(riskRow, "riskLevel", "riskType", "riskScore", "inventory", "avgDailySales", "coverageDays", "turnoverRate", "description", "suggestion"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> trend = productService.getProductTrend(sku);
|
|
|
|
|
+ if (trend != null) {
|
|
|
|
|
+ focus.put("trendSummary", pickKeys(trend, "purchaseQty", "salesQty", "currentInventory", "turnoverRate"));
|
|
|
|
|
+ Object breakdown = trend.get("turnoverBreakdown");
|
|
|
|
|
+ if (breakdown instanceof List) {
|
|
|
|
|
+ focus.put("turnoverBreakdown", sliceList((List<Map<String, Object>>) breakdown, 3));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return focus;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Map<String, Object> findSkuRow(String sku) {
|
|
|
|
|
+ List<Map<String, Object>> table = inventoryService.getSkuSummaryTable();
|
|
|
|
|
+ for (Map<String, Object> row : table) {
|
|
|
|
|
+ Object code = row.get("sku");
|
|
|
|
|
+ if (code != null && sku.equalsIgnoreCase(String.valueOf(code))) {
|
|
|
|
|
+ return row;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Map<String, Object> buildDataQuality() {
|
|
|
|
|
+ Map<String, Object> data = new LinkedHashMap<>();
|
|
|
|
|
+ data.put("purchaseRecords", dataLoader.getPurchaseRecords().size());
|
|
|
|
|
+ data.put("salesRecords", dataLoader.getSalesRecords().size());
|
|
|
|
|
+ data.put("assemblyRecords", dataLoader.getAssemblyRecords().size());
|
|
|
|
|
+ data.put("productInfo", dataLoader.getProductInfo().size());
|
|
|
|
|
+ Map<String, Object> range = buildDateRange();
|
|
|
|
|
+ if (!range.isEmpty()) {
|
|
|
|
|
+ data.put("dateRange", range);
|
|
|
|
|
+ }
|
|
|
|
|
+ return data;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Map<String, Object> buildLiteContext(Map<String, Object> fullContext, String sku) {
|
|
|
|
|
+ Map<String, Object> lite = new LinkedHashMap<>();
|
|
|
|
|
+ lite.put("generatedAt", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
|
|
|
|
+ if (fullContext != null) {
|
|
|
|
|
+ if (fullContext.containsKey("overview")) {
|
|
|
|
|
+ lite.put("overview", fullContext.get("overview"));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (fullContext.containsKey("riskStats")) {
|
|
|
|
|
+ lite.put("riskStats", fullContext.get("riskStats"));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (fullContext.containsKey("optimizationFeedback")) {
|
|
|
|
|
+ lite.put("optimizationFeedback", fullContext.get("optimizationFeedback"));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (fullContext.containsKey("dataQuality")) {
|
|
|
|
|
+ lite.put("dataQuality", fullContext.get("dataQuality"));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (fullContext.containsKey("skuFocus")) {
|
|
|
|
|
+ lite.put("skuFocus", fullContext.get("skuFocus"));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!StringUtils.isEmpty(sku) && !lite.containsKey("skuFocus")) {
|
|
|
|
|
+ lite.put("skuFocus", buildSkuFocus(sku));
|
|
|
|
|
+ }
|
|
|
|
|
+ return lite;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Map<String, Object> buildDateRange() {
|
|
|
|
|
+ List<LocalDate> dates = new ArrayList<>();
|
|
|
|
|
+ dataLoader.getPurchaseRecords().stream().map(r -> r.getDate()).filter(d -> d != null).forEach(dates::add);
|
|
|
|
|
+ dataLoader.getSalesRecords().stream().map(r -> r.getDate()).filter(d -> d != null).forEach(dates::add);
|
|
|
|
|
+ dataLoader.getAssemblyRecords().stream().map(r -> r.getDate()).filter(d -> d != null).forEach(dates::add);
|
|
|
|
|
+ if (dates.isEmpty()) {
|
|
|
|
|
+ return Collections.emptyMap();
|
|
|
|
|
+ }
|
|
|
|
|
+ LocalDate min = dates.stream().min(LocalDate::compareTo).orElse(null);
|
|
|
|
|
+ LocalDate max = dates.stream().max(LocalDate::compareTo).orElse(null);
|
|
|
|
|
+ Map<String, Object> range = new LinkedHashMap<>();
|
|
|
|
|
+ range.put("start", min == null ? null : min.toString());
|
|
|
|
|
+ range.put("end", max == null ? null : max.toString());
|
|
|
|
|
+ return range;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Map<String, Object> trimMonthly(Map<String, Object> data, int max) {
|
|
|
|
|
+ if (data == null || data.isEmpty()) {
|
|
|
|
|
+ return Collections.emptyMap();
|
|
|
|
|
+ }
|
|
|
|
|
+ List<?> months = (List<?>) data.getOrDefault("months", Collections.emptyList());
|
|
|
|
|
+ List<?> purchase = (List<?>) data.getOrDefault("purchase", Collections.emptyList());
|
|
|
|
|
+ List<?> sales = (List<?>) data.getOrDefault("sales", Collections.emptyList());
|
|
|
|
|
+ List<?> inventory = (List<?>) data.getOrDefault("inventory", Collections.emptyList());
|
|
|
|
|
+ int size = months == null ? 0 : months.size();
|
|
|
|
|
+ int start = Math.max(0, size - max);
|
|
|
|
|
+ Map<String, Object> trimmed = new LinkedHashMap<>();
|
|
|
|
|
+ trimmed.put("months", sliceListAny(months, start, size));
|
|
|
|
|
+ trimmed.put("purchase", sliceListAny(purchase, start, size));
|
|
|
|
|
+ trimmed.put("sales", sliceListAny(sales, start, size));
|
|
|
|
|
+ trimmed.put("inventory", sliceListAny(inventory, start, size));
|
|
|
|
|
+ return trimmed;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String buildFallbackAnswer(String message, Map<String, Object> context, String sku) {
|
|
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
|
|
+ sb.append("AI服务暂不可用,先给出基于当前数据的简要建议:\n");
|
|
|
|
|
+
|
|
|
|
|
+ Object overviewObj = context.get("overview");
|
|
|
|
|
+ if (overviewObj instanceof Map) {
|
|
|
|
|
+ Map<?, ?> overview = (Map<?, ?>) overviewObj;
|
|
|
|
|
+ sb.append("1. 总库存约 ").append(valueOrDefault(overview, "totalInventory", "N/A")).append(",")
|
|
|
|
|
+ .append("周转率 ").append(valueOrDefault(overview, "turnoverRate", "N/A")).append("。\n");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Object riskObj = context.get("riskStats");
|
|
|
|
|
+ if (riskObj instanceof Map) {
|
|
|
|
|
+ Map<?, ?> risk = (Map<?, ?>) riskObj;
|
|
|
|
|
+ sb.append("2. 风险概览:critical ")
|
|
|
|
|
+ .append(valueOrDefault(risk, "critical", 0)).append(",warning ")
|
|
|
|
|
+ .append(valueOrDefault(risk, "warning", 0)).append(",info ")
|
|
|
|
|
+ .append(valueOrDefault(risk, "info", 0)).append("。\n");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!StringUtils.isEmpty(sku)) {
|
|
|
|
|
+ Object focusObj = context.get("skuFocus");
|
|
|
|
|
+ if (focusObj instanceof Map) {
|
|
|
|
|
+ Map<?, ?> focus = (Map<?, ?>) focusObj;
|
|
|
|
|
+ Object summaryObj = focus.get("summary");
|
|
|
|
|
+ if (summaryObj instanceof Map) {
|
|
|
|
|
+ Map<?, ?> summary = (Map<?, ?>) summaryObj;
|
|
|
|
|
+ sb.append("3. SKU ").append(sku).append(":库存 ")
|
|
|
|
|
+ .append(valueOrDefault(summary, "inventory", "N/A"))
|
|
|
|
|
+ .append(",采购 ")
|
|
|
|
|
+ .append(valueOrDefault(summary, "purchaseQty", "N/A"))
|
|
|
|
|
+ .append(",销售 ")
|
|
|
|
|
+ .append(valueOrDefault(summary, "salesQty", "N/A"))
|
|
|
|
|
+ .append(",周转率 ")
|
|
|
|
|
+ .append(valueOrDefault(summary, "turnoverRate", "N/A"))
|
|
|
|
|
+ .append("。\n");
|
|
|
|
|
+ }
|
|
|
|
|
+ Object riskRow = focus.get("risk");
|
|
|
|
|
+ if (riskRow instanceof Map) {
|
|
|
|
|
+ Map<?, ?> risk = (Map<?, ?>) riskRow;
|
|
|
|
|
+ sb.append("4. 风险等级 ")
|
|
|
|
|
+ .append(valueOrDefault(risk, "riskLevel", "N/A"))
|
|
|
|
|
+ .append(",建议:")
|
|
|
|
|
+ .append(valueOrDefault(risk, "suggestion", "请关注库存结构与销售变化。"))
|
|
|
|
|
+ .append("\n");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ sb.append("如需更精细的建议,请稍后重试或检查API配置。");
|
|
|
|
|
+ return sb.toString();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Map<String, Object> buildSimpleResponse(String reply, String sku, boolean fallback, String requestId) {
|
|
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
|
|
+ result.put("reply", reply);
|
|
|
|
|
+ result.put("model", model);
|
|
|
|
|
+ result.put("fallback", fallback);
|
|
|
|
|
+ if (!StringUtils.isEmpty(sku)) {
|
|
|
|
|
+ result.put("sku", sku);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!StringUtils.isEmpty(requestId)) {
|
|
|
|
|
+ result.put("requestId", requestId);
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Object valueOrDefault(Map<?, ?> map, String key, Object defaultValue) {
|
|
|
|
|
+ if (map == null) {
|
|
|
|
|
+ return defaultValue;
|
|
|
|
|
+ }
|
|
|
|
|
+ Object value = map.get(key);
|
|
|
|
|
+ return value == null ? defaultValue : value;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean isTimeout(ResourceAccessException e) {
|
|
|
|
|
+ Throwable cause = e.getCause();
|
|
|
|
|
+ while (cause != null) {
|
|
|
|
|
+ if (cause instanceof java.net.SocketTimeoutException) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ cause = cause.getCause();
|
|
|
|
|
+ }
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String resolveApiKey() {
|
|
|
|
|
+ if (!StringUtils.isEmpty(apiKey)) {
|
|
|
|
|
+ return apiKey.trim();
|
|
|
|
|
+ }
|
|
|
|
|
+ String sys = System.getProperty("zhipu.api.key");
|
|
|
|
|
+ if (!StringUtils.isEmpty(sys)) {
|
|
|
|
|
+ return sys.trim();
|
|
|
|
|
+ }
|
|
|
|
|
+ String env = System.getenv("ZHIPU_API_KEY");
|
|
|
|
|
+ if (!StringUtils.isEmpty(env)) {
|
|
|
|
|
+ return env.trim();
|
|
|
|
|
+ }
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String detectSku(String message, String skuHint) {
|
|
|
|
|
+ if (!StringUtils.isEmpty(skuHint)) {
|
|
|
|
|
+ return skuHint.trim();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StringUtils.isEmpty(message)) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String normalized = message.toUpperCase(Locale.ROOT);
|
|
|
|
|
+ Set<String> knownSkus = loadKnownSkus();
|
|
|
|
|
+ for (String sku : knownSkus) {
|
|
|
|
|
+ if (normalized.contains(sku.toUpperCase(Locale.ROOT))) {
|
|
|
|
|
+ return sku;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Pattern pattern = Pattern.compile("(SKU|sku)?\\s*([A-Za-z0-9_-]{3,})");
|
|
|
|
|
+ Matcher matcher = pattern.matcher(message);
|
|
|
|
|
+ while (matcher.find()) {
|
|
|
|
|
+ String candidate = matcher.group(2);
|
|
|
|
|
+ if (candidate != null && knownSkus.contains(candidate)) {
|
|
|
|
|
+ return candidate;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Set<String> loadKnownSkus() {
|
|
|
|
|
+ Set<String> skus = new HashSet<>();
|
|
|
|
|
+ dataLoader.getProductInfo().forEach(info -> {
|
|
|
|
|
+ if (info.getProductCode() != null && !info.getProductCode().trim().isEmpty()) {
|
|
|
|
|
+ skus.add(info.getProductCode().trim());
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ dataLoader.getPurchaseRecords().forEach(record -> {
|
|
|
|
|
+ if (record.getProductCode() != null && !record.getProductCode().trim().isEmpty()) {
|
|
|
|
|
+ skus.add(record.getProductCode().trim());
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ return skus;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String normalizeMessage(String message) {
|
|
|
|
|
+ if (message == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ String trimmed = message.trim();
|
|
|
|
|
+ if (trimmed.length() > MAX_MESSAGE_LENGTH) {
|
|
|
|
|
+ return trimmed.substring(0, MAX_MESSAGE_LENGTH);
|
|
|
|
|
+ }
|
|
|
|
|
+ return trimmed;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String normalizeRole(String role) {
|
|
|
|
|
+ if (StringUtils.isEmpty(role)) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ String normalized = role.trim().toLowerCase(Locale.ROOT);
|
|
|
|
|
+ if ("user".equals(normalized) || "assistant".equals(normalized)) {
|
|
|
|
|
+ return normalized;
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<AgentChatMessage> trimHistory(List<AgentChatMessage> history) {
|
|
|
|
|
+ if (history == null || history.isEmpty()) {
|
|
|
|
|
+ return Collections.emptyList();
|
|
|
|
|
+ }
|
|
|
|
|
+ int size = history.size();
|
|
|
|
|
+ int start = Math.max(0, size - MAX_HISTORY);
|
|
|
|
|
+ return history.subList(start, size);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Map<String, Object> pickKeys(Map<String, Object> source, String... keys) {
|
|
|
|
|
+ Map<String, Object> picked = new LinkedHashMap<>();
|
|
|
|
|
+ if (source == null || keys == null) {
|
|
|
|
|
+ return picked;
|
|
|
|
|
+ }
|
|
|
|
|
+ for (String key : keys) {
|
|
|
|
|
+ if (source.containsKey(key)) {
|
|
|
|
|
+ picked.put(key, source.get(key));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return picked;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private <T> List<T> sliceList(List<T> list, int max) {
|
|
|
|
|
+ if (list == null || list.isEmpty()) {
|
|
|
|
|
+ return Collections.emptyList();
|
|
|
|
|
+ }
|
|
|
|
|
+ return list.subList(0, Math.min(max, list.size()));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private <T> List<T> sliceList(List<T> list, int start, int end) {
|
|
|
|
|
+ if (list == null || list.isEmpty()) {
|
|
|
|
|
+ return Collections.emptyList();
|
|
|
|
|
+ }
|
|
|
|
|
+ int safeStart = Math.max(0, start);
|
|
|
|
|
+ int safeEnd = Math.min(list.size(), end);
|
|
|
|
|
+ if (safeStart >= safeEnd) {
|
|
|
|
|
+ return Collections.emptyList();
|
|
|
|
|
+ }
|
|
|
|
|
+ return new ArrayList<>(list.subList(safeStart, safeEnd));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<?> sliceListAny(List<?> list, int start, int end) {
|
|
|
|
|
+ if (list == null || list.isEmpty()) {
|
|
|
|
|
+ return Collections.emptyList();
|
|
|
|
|
+ }
|
|
|
|
|
+ int safeStart = Math.max(0, start);
|
|
|
|
|
+ int safeEnd = Math.min(list.size(), end);
|
|
|
|
|
+ if (safeStart >= safeEnd) {
|
|
|
|
|
+ return Collections.emptyList();
|
|
|
|
|
+ }
|
|
|
|
|
+ return new ArrayList<>(list.subList(safeStart, safeEnd));
|
|
|
|
|
+ }
|
|
|
|
|
+}
|