Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/chocozhao_logistics-analytics-dashboard.json

1 line
206 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "chocozhao/logistics-analytics-dashboard", "files": {"backend/src/main/java/com/logistics/dashboard/ai/orchestrator/NLUOrchestrator.java": {"bd8a9d04c70706f1d9db9b647721d28c66552bd3": {"t": 1785746138.0447283, "n": 40644, "h": "8bcbffc38689440d", "c": "package com.logistics.dashboard.ai.orchestrator;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.logistics.dashboard.ai.model.QueryRequest;\nimport com.logistics.dashboard.ai.model.QueryResponse;\nimport com.logistics.dashboard.dto.*;\nimport com.logistics.dashboard.service.DashboardService;\nimport com.logistics.dashboard.service.ForecastingService;\nimport dev.langchain4j.model.chat.ChatLanguageModel;\nimport dev.langchain4j.model.input.Prompt;\nimport dev.langchain4j.model.input.PromptTemplate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Service;\n\nimport java.time.LocalDate;\nimport java.time.Year;\nimport java.util.*;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\n\n/**\n * NLU Orchestrator: AI interprets the question and outputs a structured tool-call plan.\n * Java services execute all computations deterministically — AI never generates data values.\n *\n * Flow: user question → AI → JSON plan → Java service → real data → response\n */\n@Service\npublic class NLUOrchestrator {\n\n private static final Logger log = LoggerFactory.getLogger(NLUOrchestrator.class);\n\n // Valid enum values for validation\n private static final Set<String> VALID_CARRIERS = Set.of(\n \"UPS\",\"FedEx\",\"DHL\",\"USPS\",\"LaserShip\",\"OnTrac\",\"DPD\",\"GLS\",\"Royal Mail\");\n private static final Set<String> VALID_REGIONS = Set.of(\"US-W\",\"US-C\",\"US-E\",\"EU\",\"UK\");\n private static final Set<String> VALID_CATEGORIES = Set.of(\n \"BOOK\",\"PAPER\",\"PENCIL\",\"CRAYON\",\"MARKER\",\"BRUSH\",\"PAINT\",\"STICKER\");\n private static final Set<String> VALID_TOOLS = Set.of(\n \"getKpi\",\"getTimeSeries\",\"getBreakdown\",\"getDeliveryPerformance\",\"forecastDemand\",\"forecastMetric\",\"fallback\");\n private static final Set<String> VALID_GRAN = Set.of(\"day\",\"week\",\"month\");\n\n private static final LocalDate DATA_START = LocalDate.of(2025, 1, 1);\n private static final LocalDate DATA_END = LocalDate.of(2025, 12, 31);\n\n private ChatLanguageModel model;\n private final DashboardService dashboardService;\n private final ForecastingService forecastingService;\n private final ObjectMapper mapper = new ObjectMapper();\n\n // ── Prompt template ──────────────────────────────────────────────────────\n private static final String SYSTEM_PROMPT = \"\"\"\nYou are a logistics analytics assistant. Your job is to interpret the user's question about freight/order data and output a SINGLE JSON object describing which analysis tool to call.\n\nDATABASE SCHEMA (table: orders):\n- order_date DATE, promised_delivery_date DATE, delivery_date DATE (NULL if not yet delivered)\n- status: 'delivered' | 'delayed' | 'in_transit' | 'exception' | 'canceled'\n NOTE: An order is considered DELAYED when delivery_date > promised_delivery_date (computed from dates, not just status).\n- carrier: UPS, FedEx, DHL, USPS, LaserShip, OnTrac, DPD, GLS, Royal Mail\n- region: US-W, US-C, US-E, EU, UK\n- product_category: BOOK, PAPER, PENCIL, CRAYON, MARKER, BRUSH, PAINT, STICKER\n- origin_city, destination_city, warehouse, sku\n- order_value_usd, unit_price_usd, quantity, is_promo, promo_discount_pct\n\nDATA DATE RANGE: 2025-01-01 to 2025-12-31\n\nTOOLS:\n1. getKpi - Returns: totalOrders, deliveredOrders, delayedOrders, onTimeRate, avgDeliveryDays\n2. getTimeSeries - Returns: time-bucketed order counts. Params: granularity (day/week/month)\n3. getBreakdown - Returns: carrier/region/category breakdown (total_orders, delayed_orders, delay_rate, on_time_rate). Params: dimension=carrier|region|category\n4. getDeliveryPerformance - Returns: on-time vs delayed counts per time period. Params: granularity\n5. forecastDemand - Returns: historical + forecasted order counts. Params: periods (int), granularity\n6. forecastMetric - Returns: historical + forecasted on-time rate or delay rate (%). Params: metricType ('on_time_rate'|'delay_rate'), periods, granularity\n7. fallback - Use this when the question is NOT about logistics analytics (e.g., \"who are you?\", \"what's the weather?\", \"hello\").\n\nPARAMETER EXTRACTION RULES:\n- Extract year/month/quarter from question → convert to startDate/endDate (YYYY-MM-DD)\n- \"2025\" → startDate=2025-01-01, endDate=2025-12-31\n- \"Q1 2025\" → startDate=2025-01-01, endDate=2025-03-31\n- \"January 2025\" → startDate=2025-01-01, endDate=2025-01-31\n- If no date mentioned → use full data range 2025-01-01 to 2025-12-31\n- Extract carrier names from question if mentioned\n- Extract region names (US-W, US-C, US-E, EU, UK) from question if mentioned\n- Extract product categories if mentioned\n\nOUTPUT FORMAT (strict JSON, no markdown):\n{\n \"tool\": \"<tool_name>\",\n \"params\": {\n \"startDate\": \"YYYY-MM-DD\",\n \"endDate\": \"YYYY-MM-DD\",\n \"granularity\": \"month\",\n \"carriers\": [\"UPS\"],\n \"regions\": [],\n \"categories\": [],\n \"periods\": 4\n },\n \"answerTemplate\": \"<one-sentence description of what the answer will show>\"\n}\n\nEXAMPLES:\nQ: \"2025年有多少UPS的订单延误了?\"\nA: {\"tool\":\"getKpi\",\"params\":{\"startDate\":\"2025-01-01\",\"endDate\":\"2025-12-31\",\"carriers\":[\"UPS\"],\"regions\":[],\"categories\":[]},\"answerTemplate\":\"2025年UPS延误订单总数\"}\n\nQ: \"Show me monthly order volume trend\"\nA: {\"tool\":\"getTimeSeries\",\"params\":{\"startDate\":\"2025-01-01\",\"endDate\":\"2025-12-31\",\"granularity\":\"month\",\"carriers\":[],\"regions\":[],\"categories\":[]},\"answerTemplate\":\"月度订单量趋势\"}\n\nQ: \"Which carrier has the highest delay rate?\"\nA: {\"tool\":\"getBreakdown\",\"params\":{\"startDate\":\"2025-01-01\",\"endDate\":\"2025-12-31\",\"dimension\":\"carrier\",\"carriers\":[],\"regions\":[],\"categories\":[]},\"answerTemplate\":\"各承运商延误率对比\"}\n\nQ: \"哪个地区的订单延误最多?\"\nA: {\"tool\":\"getBreakdown\",\"params\":{\"startDate\":\"2025-01-01\",\"endDate\":\"2025-12-31\",\"dimension\":\"region\",\"carriers\":[],\"regions\":[],\"categories\":[]},\"answerTemplate\":\"各地区延误订单对比\"}\n\nQ: \"Forecast next 4 weeks of orders\"\nA: {\"tool\":\"forecastDemand\",\"params\":{\"startDate\":\"2025-01-01\",\"endDate\":\"2025-12-31\",\"granularity\":\"week\",\"periods\":4,\"carriers\":[],\"regions\":[],\"categories\":[]},\"answerTemplate\":\"未来4周订单量预测\"}\n\nQ: \"预测未来4周US-W的准时率\"\nA: {\"tool\":\"forecastMetric\",\"params\":{\"metricType\":\"on_time_rate\",\"startDate\":\"2025-01-01\",\"endDate\":\"2025-12-31\",\"granularity\":\"week\",\"periods\":4,\"carriers\":[],\"regions\":[\"US-W\"],\"categories\":[]},\"answerTemplate\":\"US-W地区未来4周准时率预测\"}\n\nQ: \"你是谁\"\nA: {\"tool\":\"fallback\",\"params\":{\"startDate\":\"2025-01-01\",\"endDate\":\"2025-12-31\"},\"answerTemplate\":\"问题与物流分析无关\"}\n\nOutput ONLY the JSON object, nothing else.\n\"\"\";\n\n public NLUOrchestrator(\n @Value(\"${openai.api.key:}\") String apiKey,\n DashboardService dashboardService,\n ForecastingService forecastingService) {\n\n this.dashboardService = dashboardService;\n this.forecastingService = forecastingService;\n\n if (apiKey == null || apiKey.isBlank()) {\n log.warn(\"OPENAI_API_KEY not set — NLQ will use keyword-based fallback\");\n this.model = null;\n } else {\n try {\n this.model = dev.langchain4j.model.openai.OpenAiChatModel.builder()\n .apiKey(apiKey)\n .baseUrl(\"https://api.deepseek.com\")\n .modelName(\"deepseek-chat\")\n .temperature(0.0)\n .maxTokens(300)\n .build();\n log.info(\"AI model initialized (DeepSeek)\");\n } catch (Exception e) {\n log.warn(\"Failed to initialize AI model: {}\", e.getMessage());\n this.model = null;\n }\n }\n }\n\n // ── Main entry point ─────────────────────────────────────────────────────\n\n public QueryResponse processQuery(QueryRequest request) {\n String question = request.getQuestion();\n log.info(\"Processing query: {}\", question);\n\n try {\n // 1. Get tool plan (from AI or fallback)\n ToolPlan plan = resolvePlan(question, request);\n log.info(\"Resolved plan: tool={} params={}\", plan.tool, plan.params);\n\n // 2. Execute plan against real data\n return executePlan(plan, question);\n\n } catch (Exception e) {\n log.error(\"Query processing error: {}\", e.getMessage(), e);\n QueryResponse err = new QueryResponse();\n err.setAnswer(\"查询处理时发生错误,请稍后重试。\");\n err.setChartType(\"none\");\n err.setError(e.getMessage());\n return err;\n }\n }\n\n // ── Plan resolution ──────────────────────────────────────────────────────\n\n private ToolPlan resolvePlan(String question, QueryRequest request) {\n ToolPlan plan = null;\n\n // Try AI first\n if (model != null) {\n plan = askAiForPlan(question);\n }\n\n // Fallback: keyword-based heuristic\n if (plan == null) {\n plan = heuristicPlan(question, request);\n }\n\n // Merge any explicit frontend filters (they take priority over AI-extracted)\n if (request.getCarriers() != null && !request.getCarriers().isEmpty()) {\n plan.params.put(\"carriers\", request.getCarriers());\n }\n if (request.getRegions() != null && !request.getRegions().isEmpty()) {\n plan.params.put(\"regions\", request.getRegions());\n }\n if (request.getCategories() != null && !request.getCategories().isEmpty()) {\n plan.params.put(\"categories\", request.getCategories());\n }\n if (request.getStartDate() != null) {\n plan.params.put(\"startDate\", request.getStartDate().toString());\n }\n if (request.getEndDate() != null) {\n plan.params.put(\"endDate\", request.getEndDate().toString());\n }\n\n return plan;\n }\n\n private ToolPlan askAiForPlan(String question) {\n try {\n String prompt = SYSTEM_PROMPT + \"\\n\\nUser question: \" + question;\n String response = model.generate(prompt);\n log.debug(\"AI raw response: {}\", response);\n\n // Strip markdown code fences if present\n String json = response.trim()\n .replaceAll(\"^```json\\\\s*\", \"\")\n .replaceAll(\"^```\\\\s*\", \"\")\n .replaceAll(\"\\\\s*```$\", \"\")\n .trim();\n\n JsonNode root = mapper.readTree(json);\n ToolPlan plan = new ToolPlan();\n plan.tool = root.path(\"tool\").asText();\n plan.answerTemplate = root.path(\"answerTemplate\").asText();\n\n if (!VALID_TOOLS.contains(plan.tool)) {\n log.warn(\"AI returned invalid tool: {}\", plan.tool);\n return null;\n }\n\n JsonNode p = root.path(\"params\");\n plan.params = new HashMap<>();\n if (p.has(\"startDate\")) plan.params.put(\"startDate\", p.get(\"startDate\").asText());\n if (p.has(\"endDate\")) plan.params.put(\"endDate\", p.get(\"endDate\").asText());\n if (p.has(\"granularity\")) plan.params.put(\"granularity\", p.get(\"granularity\").asText());\n if (p.has(\"periods\")) plan.params.put(\"periods\", p.get(\"periods\").asInt(4));\n if (p.has(\"dimension\")) plan.params.put(\"dimension\", p.get(\"dimension\").asText(\"carrier\"));\n if (p.has(\"metricType\")) plan.params.put(\"metricType\", p.get(\"metricType\").asText(\"on_time_rate\"));\n\n // Validated list params\n plan.params.put(\"carriers\", parseAndValidateList(p.path(\"carriers\"), VALID_CARRIERS));\n plan.params.put(\"regions\", parseAndValidateList(p.path(\"regions\"), VALID_REGIONS));\n plan.params.put(\"categories\", parseAndValidateList(p.path(\"categories\"), VALID_CATEGORIES));\n\n return plan;\n\n } catch (Exception e) {\n log.warn(\"AI plan parsing failed: {}\", e.getMessage());\n return null;\n }\n }\n\n @SuppressWarnings(\"unchecked\")\n private List<String> parseAndValidateList(JsonNode node, Set<String> validValues) {\n List<String> result = new ArrayList<>();\n if (node.isArray()) {\n node.forEach(n -> {\n String val = n.asText();\n if (validValues.contains(val)) result.add(val);\n });\n }\n return result;\n }\n\n // ── Keyword-based heuristic fallback ──────────────────────────────────────\n\n private static final Set<String> BUSINESS_KEYWORDS = Set.of(\n \"order\",\"orders\",\"订单\",\"delivery\",\"deliveries\",\"shipment\",\"shipments\",\"运输\",\"物流\",\"货运\",\"包裹\",\"package\",\n \"count\",\"volume\",\"数量\",\"trend\",\"trends\",\"趋势\",\"compare\",\"comparison\",\"对比\",\"analysis\",\"analyze\",\"分析\",\n \"performance\",\"绩效\",\"forecast\",\"预测\",\"predict\",\"kpi\",\"kpis\",\"metric\",\"metrics\",\"指标\",\n \"carrier\",\"carriers\",\"承运商\",\"region\",\"regions\",\"地区\",\"category\",\"categories\",\"品类\",\"product\",\"产品\",\n \"delay\",\"delayed\",\"delays\",\"延误\",\"on-time\",\"ontime\",\"准时\",\"rate\",\"rates\",\"率\",\n \"2025\",\"year\",\"年\",\"month\",\"月\",\"week\",\"周\",\"day\",\"日\",\"quarter\",\"季度\",\"q1\",\"q2\",\"q3\",\"q4\",\n \"ups\",\"fedex\",\"dhl\",\"usps\",\"total\",\"average\",\"avg\",\"sum\",\"max\",\"min\",\"最高\",\"最低\",\"平均\",\"总和\",\"多少\",\"几个\"\n );\n\n private static final Set<String> CHAT_KEYWORDS = Set.of(\n \"你\",\"who\",\"hello\",\"hi\",\"hey\",\"谢谢\",\"thanks\",\"thank\",\"天气\",\"weather\",\"新闻\",\"news\",\n \"电影\",\"movie\",\"吃\",\"eat\",\"名字\",\"name\",\"喜欢\",\"like\",\"love\",\"讨厌\",\"hate\",\"再见\",\"bye\"\n );\n\n private ToolPlan heuristicPlan(String question, QueryRequest request) {\n String q = question.toLowerCase();\n ToolPlan plan = new ToolPlan();\n plan.params = new HashMap<>();\n\n // Irrelevance detection: no business keywords + has chat keywords\n boolean hasBusiness = BUSINESS_KEYWORDS.stream().anyMatch(q::contains);\n boolean hasChat = CHAT_KEYWORDS.stream().anyMatch(q::contains);\n if (!hasBusiness && hasChat) {\n plan.tool = \"fallback\";\n plan.answerTemplate = \"问题与物流分析无关\";\n return plan;\n }\n\n // Date range\n LocalDate[] range = extractDateRange(q, request);\n plan.params.put(\"startDate\", range[0].toString());\n plan.params.put(\"endDate\", range[1].toString());\n\n // Carriers\n List<String> carriers = VALID_CARRIERS.stream()\n .filter(c -> q.contains(c.toLowerCase()))\n .collect(Collectors.toList());\n plan.params.put(\"carriers\", carriers);\n\n // Regions\n List<String> regions = VALID_REGIONS.stream()\n .filter(r -> q.contains(r.toLowerCase()))\n .collect(Collectors.toList());\n plan.params.put(\"regions\", regions);\n\n // Categories\n List<String> cats = VALID_CATEGORIES.stream()\n .filter(c -> q.contains(c.toLowerCase()))\n .collect(Collectors.toList());\n plan.params.put(\"categories\", cats);\n\n // Forecast metric detection (must come before general on-time rate routing)\n boolean isForecast = q.contains(\"预测\") || q.contains(\"forecast\") || q.contains(\"predict\") || q.contains(\"未来\");\n if (isForecast && (q.contains(\"准时率\") || q.contains(\"on-time rate\") || q.contains(\"延误率\") || q.contains(\"delay rate\"))) {\n plan.tool = \"forecastMetric\";\n plan.params.put(\"metricType\", q.contains(\"延误\") || q.contains(\"delay\") ? \"delay_rate\" : \"on_time_rate\");\n plan.params.put(\"granularity\", \"week\");\n plan.params.put(\"periods\", 4);\n plan.answerTemplate = \"准时率/延误率预测\";\n return plan;\n }\n\n // On-time rate specific routing\n if (q.contains(\"准时率\") || q.contains(\"on-time rate\")) {\n if (q.contains(\"哪个\") || q.contains(\"最\")) {\n // Comparison question\n if (q.contains(\"地区\") || q.contains(\"region\")) {\n plan.tool = \"getBreakdown\";\n plan.params.put(\"dimension\", \"region\");\n plan.answerTemplate = \"各地区准时率对比\";\n } else if (q.contains(\"品类\") || q.contains(\"category\")) {\n plan.tool = \"getBreakdown\";\n plan.params.put(\"dimension\", \"category\");\n plan.answerTemplate = \"各品类准时率对比\";\n } else {\n plan.tool = \"getBreakdown\";\n plan.params.put(\"dimension\", \"carrier\");\n plan.answerTemplate = \"各承运商准时率对比\";\n }\n } else {\n // Specific on-time rate query → KPI\n plan.tool = \"getKpi\";\n plan.answerTemplate = \"准时率分析\";\n }\n return plan;\n }\n\n // Tool selection\n if (isForecast) {\n plan.tool = \"forecastDemand\";\n plan.params.put(\"granularity\", \"week\");\n plan.params.put(\"periods\", 4);\n plan.answerTemplate = \"订单量预测\";\n } else if (q.contains(\"地区\") || q.contains(\"region\") || q.contains(\"区域\")) {\n plan.tool = \"getBreakdown\";\n plan.params.put(\"dimension\", \"region\");\n plan.answerTemplate = \"地区分析\";\n } else if (q.contains(\"品类\") || q.contains(\"category\") || q.contains(\"产品类别\") || q.contains(\"商品类别\")) {\n plan.tool = \"getBreakdown\";\n plan.params.put(\"dimension\", \"category\");\n plan.answerTemplate = \"品类分析\";\n } else if (q.contains(\"承运商\") || q.contains(\"carrier\") || q.contains(\"快递公司\") || q.contains(\"哪家\")) {\n plan.tool = \"getBreakdown\";\n plan.params.put(\"dimension\", \"carrier\");\n plan.answerTemplate = \"承运商分析\";\n } else if (q.contains(\"趋势\") || q.contains(\"trend\") || q.contains(\"变化\") || q.contains(\"每月\") || q.contains(\"monthly\") || q.contains(\"每周\") || q.contains(\"weekly\")) {\n plan.tool = \"getTimeSeries\";\n plan.params.put(\"granularity\", q.contains(\"日\") || q.contains(\"day\") ? \"day\"\n : q.contains(\"周\") || q.contains(\"week\") ? \"week\" : \"month\");\n plan.answerTemplate = \"订单量趋势\";\n } else if (q.contains(\"准时\") || q.contains(\"on-time\") || q.contains(\"交付性能\") || q.contains(\"performance\")) {\n plan.tool = \"getDeliveryPerformance\";\n plan.params.put(\"granularity\", \"month\");\n plan.answerTemplate = \"交付性能分析\";\n } else {\n plan.tool = \"getKpi\";\n plan.answerTemplate = \"关键指标汇总\";\n }\n\n return plan;\n }\n\n private LocalDate[] extractDateRange(String q, QueryRequest request) {\n // Explicit from request\n if (request.getStartDate() != null && request.getEndDate() != null) {\n return new LocalDate[]{request.getStartDate(), request.getEndDate()};\n }\n\n // Year pattern\n Matcher yearMatcher = Pattern.compile(\"(20\\\\d{2})年?\").matcher(q);\n if (yearMatcher.find()) {\n int year = Integer.parseInt(yearMatcher.group(1));\n // Quarter\n if (q.contains(\"q1\") || q.contains(\"第一季度\")) return new LocalDate[]{LocalDate.of(year,1,1), LocalDate.of(year,3,31)};\n if (q.contains(\"q2\") || q.contains(\"第二季度\")) return new LocalDate[]{LocalDate.of(year,4,1), LocalDate.of(year,6,30)};\n if (q.contains(\"q3\") || q.contains(\"第三季度\")) return new LocalDate[]{LocalDate.of(year,7,1), LocalDate.of(year,9,30)};\n if (q.contains(\"q4\") || q.contains(\"第四季度\")) return new LocalDate[]{LocalDate.of(year,10,1), LocalDate.of(year,12,31)};\n return new LocalDate[]{LocalDate.of(year,1,1), LocalDate.of(year,12,31)};\n }\n\n // Default: full data range\n return new LocalDate[]{DATA_START, DATA_END};\n }\n\n // ── Plan execution ────────────────────────────────────────────────────────\n\n @SuppressWarnings(\"unchecked\")\n private QueryResponse executePlan(ToolPlan plan, String question) {\n LocalDate startDate = LocalDate.parse(plan.params.getOrDefault(\"startDate\", DATA_START.toString()).toString());\n LocalDate endDate = LocalDate.parse(plan.params.getOrDefault(\"endDate\", DATA_END.toString()).toString());\n List<String> carriers = (List<String>) plan.params.getOrDefault(\"carriers\", List.of());\n List<String> regions = (List<String>) plan.params.getOrDefault(\"regions\", List.of());\n List<String> categories = (List<String>) plan.params.getOrDefault(\"categories\", List.of());\n\n QueryResponse response = new QueryResponse();\n response.setQueryPlan(\"工具: \" + plan.tool\n + \" | 时间范围: \" + startDate + \" 至 \" + endDate\n + (carriers.isEmpty() ? \"\" : \" | 承运商: \" + carriers)\n + (regions.isEmpty() ? \"\" : \" | 地区: \" + regions)\n + (categories.isEmpty() ? \"\" : \" | 品类: \" + categories));\n\n Map<String, Object> filters = new LinkedHashMap<>();\n filters.put(\"startDate\", startDate.toString());\n filters.put(\"endDate\", endDate.toString());\n if (!carriers.isEmpty()) filters.put(\"carriers\", carriers);\n if (!regions.isEmpty()) filters.put(\"regions\", regions);\n if (!categories.isEmpty()) filters.put(\"categories\", categories);\n response.setFilters(filters);\n\n switch (plan.tool) {\n\n case \"getKpi\" -> {\n KpiResponse kpi = dashboardService.getKPIs(startDate, endDate, carriers, regions, categories);\n response.setChartType(\"kpi\");\n response.setAnswer(formatKpiAnswer(kpi, plan.answerTemplate, carriers, regions, startDate, endDate));\n response.setExplanation(\"从数据库实时聚合 — delivery_date > promised_delivery_date 计为延误。\");\n response.setMetrics(List.of(\"totalOrders\", \"deliveredOrders\", \"delayedOrders\", \"onTimeRate\", \"avgDeliveryDays\"));\n\n Map<String, Object> chartData = new LinkedHashMap<>();\n chartData.put(\"chartType\", \"kpi\");\n chartData.put(\"labels\", List.of(\"总订单\", \"已送达\", \"延误\"));\n chartData.put(\"values\", List.of(kpi.getTotalOrders(), kpi.getDeliveredOrders(), kpi.getDelayedOrders()));\n chartData.put(\"kpiSummary\", Map.of(\n \"onTimeRate\", kpi.getOnTimeRate() + \"%\",\n \"avgDeliveryDays\", kpi.getAvgDeliveryDays() + \" 天\"\n ));\n response.setChartData(chartData);\n\n List<Map<String, Object>> raw = List.of(Map.of(\n \"总订单\", kpi.getTotalOrders(),\n \"已送达\", kpi.getDeliveredOrders(),\n \"延误\", kpi.getDelayedOrders(),\n \"准时率\", kpi.getOnTimeRate() + \"%\",\n \"平均送达天数\", kpi.getAvgDeliveryDays()\n ));\n response.setRawData(raw);\n }\n\n case \"getTimeSeries\" -> {\n String gran = plan.params.getOrDefault(\"granularity\", \"month\").toString();\n if (!VALID_GRAN.contains(gran)) gran = \"month\";\n TimeSeriesResponse ts = dashboardService.getOrderVolumeTimeSeries(gran, startDate, endDate, carriers, regions, categories);\n response.setChartType(\"time_series\");\n response.setAnswer(plan.answerTemplate + \"(\" + startDate + \" 至 \" + endDate + \",粒度:\" + gran + \")\");\n response.setExplanation(\"按 \" + gran + \" 聚合的订单量趋势,来自实时数据库查询。\");\n response.setMetrics(List.of(\"订单量\", \"时间趋势\"));\n\n List<String> labels = ts.getData().stream().map(d -> d.getDate().toString()).collect(Collectors.toList());\n List<Long> values = ts.getData().stream().map(TimeSeriesData::getCount).collect(Collectors.toList());\n Map<String, Object> chartData = new LinkedHashMap<>();\n chartData.put(\"chartType\", \"line\");\n chartData.put(\"labels\", labels);\n chartData.put(\"values\", values);\n response.setChartData(chartData);\n\n List<Map<String, Object>> raw = ts.getData().stream()\n .map(d -> Map.<String,Object>of(\"date\", d.getDate().toString(), \"count\", d.getCount()))\n .collect(Collectors.toList());\n response.setRawData(raw);\n }\n\n case \"getBreakdown\" -> {\n String dimension = plan.params.getOrDefault(\"dimension\", \"carrier\").toString();\n if (\"region\".equalsIgnoreCase(dimension)) {\n RegionBreakdownResponse rb = dashboardService.getRegionBreakdown(startDate, endDate, carriers, regions, categories);\n response.setChartType(\"bar\");\n String bestRegion = rb.getData().stream()\n .max(java.util.Comparator.comparing(RegionBreakdown::getOnTimeRate))\n .map(RegionBreakdown::getRegion).orElse(\"\");\n String answerPrefix = plan.answerTemplate.contains(\"准时率\")\n ? \"各地区准时率分析(准时率最高:\" + bestRegion + \")\"\n : \"各地区绩效分析\";\n response.setAnswer(answerPrefix + \"(\" + startDate + \" 至 \" + endDate + \")\");\n response.setExplanation(\"按地区汇总订单量和延误率,delivery_date > promised_delivery_date 计为延误。准时率 = 100% - 延误率。\");\n response.setMetrics(List.of(\"总订单\", \"延误订单\", \"延误率\", \"准时率\"));\n\n List<String> labels = rb.getData().stream().map(RegionBreakdown::getRegion).collect(Collectors.toList());\n List<Long> values = rb.getData().stream().map(RegionBreakdown::getTotalOrders).collect(Collectors.toList());\n Map<String, Object> chartData = new LinkedHashMap<>();\n chartData.put(\"chartType\", \"bar\");\n chartData.put(\"dimension\", \"region\");\n chartData.put(\"labels\", labels);\n chartData.put(\"values\", values);\n chartData.put(\"delayedValues\", rb.getData().stream().map(RegionBreakdown::getDelayedOrders).collect(Collectors.toList()));\n chartData.put(\"delayRates\", rb.getData().stream().map(r -> r.getDelayRate().toString()).collect(Collectors.toList()));\n chartData.put(\"onTimeRates\", rb.getData().stream().map(r -> r.getOnTimeRate().toString()).collect(Collectors.toList()));\n response.setChartData(chartData);\n\n List<Map<String, Object>> raw = rb.getData().stream()\n .map(r -> Map.<String,Object>of(\n \"region\", r.getRegion(),\n \"totalOrders\", r.getTotalOrders(),\n \"delayedOrders\", r.getDelayedOrders(),\n \"delayRate\", r.getDelayRate() + \"%\"\n ))\n .collect(Collectors.toList());\n response.setRawData(raw);\n } else if (\"category\".equalsIgnoreCase(dimension)) {\n CategoryBreakdownResponse cb = dashboardService.getCategoryBreakdown(startDate, endDate, carriers, regions, categories);\n response.setChartType(\"bar\");\n String bestCategory = cb.getData().stream()\n .max(java.util.Comparator.comparing(CategoryBreakdown::getOnTimeRate))\n .map(CategoryBreakdown::getCategory).orElse(\"\");\n String answerPrefix = plan.answerTemplate.contains(\"准时率\")\n ? \"各品类准时率分析(准时率最高:\" + bestCategory + \")\"\n : \"各品类绩效分析\";\n response.setAnswer(answerPrefix + \"(\" + startDate + \" 至 \" + endDate + \")\");\n response.setExplanation(\"按品类汇总订单量和延误率,delivery_date > promised_delivery_date 计为延误。准时率 = 100% - 延误率。\");\n response.setMetrics(List.of(\"总订单\", \"延误订单\", \"延误率\", \"准时率\"));\n\n List<String> labels = cb.getData().stream().map(CategoryBreakdown::getCategory).collect(Collectors.toList());\n List<Long> values = cb.getData().stream().map(CategoryBreakdown::getTotalOrders).collect(Collectors.toList());\n Map<String, Object> chartData = new LinkedHashMap<>();\n chartData.put(\"chartType\", \"bar\");\n chartData.put(\"dimension\", \"category\");\n chartData.put(\"labels\", labels);\n chartData.put(\"values\", values);\n chartData.put(\"delayedValues\", cb.getData().stream().map(CategoryBreakdown::getDelayedOrders).collect(Collectors.toList()));\n chartData.put(\"delayRates\", cb.getData().stream().map(c -> c.getDelayRate().toString()).collect(Collectors.toList()));\n chartData.put(\"onTimeRates\", cb.getData().stream().map(c -> c.getOnTimeRate().toString()).collect(Collectors.toList()));\n response.setChartData(chartData);\n\n List<Map<String, Object>> raw = cb.getData().stream()\n .map(c -> Map.<String,Object>of(\n \"category\", c.getCategory(),\n \"totalOrders\", c.getTotalOrders(),\n \"delayedOrders\", c.getDelayedOrders(),\n \"delayRate\", c.getDelayRate() + \"%\"\n ))\n .collect(Collectors.toList());\n response.setRawData(raw);\n } else {\n CarrierBreakdownResponse bd = dashboardService.getCarrierBreakdown(startDate, endDate, carriers, regions, categories);\n response.setChartType(\"bar\");\n String bestCarrier = bd.getData().stream()\n .max(java.util.Comparator.comparing(CarrierBreakdown::getOnTimeRate))\n .map(CarrierBreakdown::getCarrier).orElse(\"\");\n String answerPrefix = plan.answerTemplate.contains(\"准时率\")\n ? \"各承运商准时率分析(准时率最高:\" + bestCarrier + \")\"\n : \"各承运商绩效分析\";\n response.setAnswer(answerPrefix + \"(\" + startDate + \" 至 \" + endDate + \")\");\n response.setExplanation(\"按承运商汇总订单量和延误率,delivery_date > promised_delivery_date 计为延误。准时率 = 100% - 延误率。\");\n response.setMetrics(List.of(\"总订单\", \"延误订单\", \"延误率\", \"准时率\"));\n\n List<String> labels = bd.getData().stream().map(CarrierBreakdown::getCarrier).collect(Collectors.toList());\n List<Long> values = bd.getData().stream().map(CarrierBreakdown::getTotalOrders).collect(Collectors.toList());\n Map<String, Object> chartData = new LinkedHashMap<>();\n chartData.put(\"chartType\", \"bar\");\n chartData.put(\"dimension\", \"carrier\");\n chartData.put(\"labels\", labels);\n chartData.put(\"values\", values);\n chartData.put(\"delayedValues\", bd.getData().stream().map(CarrierBreakdown::getDelayedOrders).collect(Collectors.toList()));\n chartData.put(\"delayRates\", bd.getData().stream().map(c -> c.getDelayRate().toString()).collect(Collectors.toList()));\n chartData.put(\"onTimeRates\", bd.getData().stream().map(c -> c.getOnTimeRate().toString()).collect(Collectors.toList()));\n response.setChartData(chartData);\n\n List<Map<String, Object>> raw = bd.getData().stream()\n .map(c -> Map.<String,Object>of(\n \"carrier\", c.getCarrier(),\n \"totalOrders\", c.getTotalOrders(),\n \"delayedOrders\", c.getDelayedOrders(),\n \"delayRate\", c.getDelayRate() + \"%\"\n ))\n .collect(Collectors.toList());\n response.setRawData(raw);\n }\n }\n\n case \"getDeliveryPerformance\" -> {\n String gran = plan.params.getOrDefault(\"granularity\", \"month\").toString();\n if (!VALID_GRAN.contains(gran)) gran = \"month\";\n DeliveryPerformanceResponse dp = dashboardService.getDeliveryPerformance(gran, startDate, endDate, carriers, regions, categories);\n response.setChartType(\"bar\");\n response.setAnswer(\"交付性能分析(\" + startDate + \" 至 \" + endDate + \")\");\n response.setExplanation(\"按时间段展示准时 vs 延误订单数,延误判定为 delivery_date > promised_delivery_date。\");\n response.setMetrics(List.of(\"准时订单\", \"延误订单\"));\n\n List<String> labels = dp.getData().stream().map(DeliveryPerformance::getPeriod).collect(Collectors.toList());\n List<Long> onTime = dp.getData().stream().map(DeliveryPerformance::getOnTime).collect(Collectors.toList());\n List<Long> delayed = dp.getData().stream().map(DeliveryPerformance::getDelayed).collect(Collectors.toList());\n Map<String, Object> chartData = new LinkedHashMap<>();\n chartData.put(\"chartType\", \"bar\");\n chartData.put(\"labels\", labels);\n chartData.put(\"values\", onTime);\n chartData.put(\"delayedValues\", delayed);\n response.setChartData(chartData);\n\n List<Map<String, Object>> raw = dp.getData().stream()\n .map(d -> {\n Map<String, Object> row = new LinkedHashMap<>();\n row.put(\"period\", d.getPeriod());\n row.put(\"onTime\", d.getOnTime());\n row.put(\"delayed\", d.getDelayed());\n return row;\n })\n .collect(Collectors.toList());\n response.setRawData(raw);\n }\n\n case \"forecastDemand\" -> {\n String gran = plan.params.getOrDefault(\"granularity\", \"week\").toString();\n if (!VALID_GRAN.contains(gran)) gran = \"week\";\n int periods = Integer.parseInt(plan.params.getOrDefault(\"periods\", 4).toString());\n ForecastResponse fc = forecastingService.forecastDemand(gran, periods, startDate, endDate, carriers, regions, categories);\n response.setChartType(\"forecast\");\n response.setAnswer(\"未来 \" + periods + \" 个\" + granLabel(gran) + \"订单量预测\");\n response.setExplanation(\"使用 \" + fc.getAlgorithm() + \" 基于历史数据确定性计算。AI 仅负责参数提取,不生成预测值。\");\n response.setMetrics(List.of(\"历史订单量\", \"预测订单量\", \"安全库存建议\"));\n\n List<String> labels = fc.getData().stream().map(d -> d.getDate().toString()).collect(Collectors.toList());\n List<Long> histVals = fc.getData().stream().filter(d -> !d.isForecast()).map(ForecastData::getValue).collect(Collectors.toList());\n List<Long> foreVals = fc.getData().stream().filter(ForecastData::isForecast).map(ForecastData::getValue).collect(Collectors.toList());\n int splitIdx = (int) fc.getData().stream().filter(d -> !d.isForecast()).count();\n Map<String, Object> chartData = new LinkedHashMap<>();\n chartData.put(\"chartType\", \"forecast\");\n chartData.put(\"labels\", labels);\n chartData.put(\"historicalValues\", histVals);\n chartData.put(\"forecastValues\", foreVals);\n chartData.put(\"splitIndex\", splitIdx);\n chartData.put(\"algorithm\", fc.getAlgorithm());\n chartData.put(\"recommendations\", fc.getRecommendations());\n response.setChartData(chartData);\n\n List<Map<String, Object>> raw = fc.getData().stream()\n .map(d -> Map.<String,Object>of(\n \"date\", d.getDate().toString(),\n \"value\", d.getValue(),\n \"isForecast\", d.isForecast()\n ))\n .collect(Collectors.toList());\n response.setRawData(raw);\n }\n\n case \"forecastMetric\" -> {\n String gran = plan.params.getOrDefault(\"granularity\", \"week\").toString();\n if (!VALID_GRAN.contains(gran)) gran = \"week\";\n int periods = Integer.parseInt(plan.params.getOrDefault(\"periods\", 4).toString());\n String metricType = plan.params.getOrDefault(\"metricType\", \"on_time_rate\").toString();\n ForecastResponse fc = forecastingService.forecastMetric(metricType, gran, periods, startDate, endDate, carriers, regions, categories);\n String metricLabel = \"on_time_rate\".equals(metricType) ? \"准时率\" : \"延误率\";\n response.setChartType(\"forecast\");\n response.setAnswer(\"未来 \" + periods + \" 个\" + granLabel(gran) + metricLabel + \"预测\");\n response.setExplanation(\"使用 \" + fc.getAlgorithm() + \" 基于历史交付数据确定性计算。AI 仅负责参数提取,不生成预测值。\");\n response.setMetrics(List.of(\"历史\" + metricLabel, \"预测\" + metricLabel));\n\n List<String> labels = fc.getData().stream().map(d -> d.getDate().toString()).collect(Collectors.toList());\n List<String> histVals = fc.getData().stream().filter(d -> !d.isForecast()).map(d -> d.getPctValue().toString()).collect(Collectors.toList());\n List<String> foreVals = fc.getData().stream().filter(ForecastData::isForecast).map(d -> d.getPctValue().toString()).collect(Collectors.toList());\n int splitIdx = (int) fc.getData().stream().filter(d -> !d.isForecast()).count();\n Map<String, Object> chartData = new LinkedHashMap<>();\n chartData.put(\"chartType\", \"forecast\");\n chartData.put(\"metricType\", metricType);\n chartData.put(\"labels\", labels);\n chartData.put(\"historicalValues\", histVals);\n chartData.put(\"forecastValues\", foreVals);\n chartData.put(\"splitIndex\", splitIdx);\n chartData.put(\"algorithm\", fc.getAlgorithm());\n chartData.put(\"recommendations\", fc.getRecommendations());\n response.setChartData(chartData);\n\n List<Map<String, Object>> raw = fc.getData().stream()\n .map(d -> {\n Map<String, Object> row = new LinkedHashMap<>();\n row.put(\"date\", d.getDate().toString());\n row.put(\"value\", d.getPctValue() != null ? d.getPctValue().toString() + \"%\" : d.getValue());\n row.put(\"isForecast\", d.isForecast());\n return row;\n })\n .collect(Collectors.toList());\n response.setRawData(raw);\n }\n\n case \"fallback\" -> {\n response.setChartType(\"none\");\n response.setAnswer(\"我是物流数据分析助手,专注于帮助您分析订单、承运商、地区配送绩效等指标。\\n\\n您可以尝试提问:\\n- 2025年各承运商延误率对比\\n- 预测未来4周US-W的准时率\\n- 哪个品类的订单延误最多?\");\n response.setExplanation(\"该问题与物流分析无关,未触发任何数据查询工具。\");\n response.setMetrics(List.of(\"N/A\"));\n }\n\n default -> {\n response.setChartType(\"none\");\n response.setAnswer(\"无法识别的分析类型,请换一种问法。\");\n }\n }\n\n return response;\n }\n\n // ── Helpers ───────────────────────────────────────────────────────────────\n\n private String formatKpiAnswer(KpiResponse kpi, String template, List<String> carriers,\n List<String> regions, LocalDate start, LocalDate end) {\n String scope = \"\";\n if (!carriers.isEmpty()) scope += \" [承运商: \" + String.join(\", \", carriers) + \"]\";\n if (!regions.isEmpty()) scope += \" [地区: \" + String.join(\", \", regions) + \"]\";\n return String.format(\"%s (%s ~ %s)%s\\n总订单: %d | 已送达: %d | 延误: %d | 准时率: %s%% | 平均送达: %s天\",\n template, start, end, scope,\n kpi.getTotalOrders(), kpi.getDeliveredOrders(), kpi.getDelayedOrders(),\n kpi.getOnTimeRate(), kpi.getAvgDeliveryDays());\n }\n\n private String granLabel(String gran) {\n return switch (gran) { case \"day\" -> \"天\"; case \"week\" -> \"周\"; default -> \"月\"; };\n }\n\n // ── Inner class ───────────────────────────────────────────────────────────\n\n private static class ToolPlan {\n String tool;\n String answerTemplate;\n Map<String, Object> params;\n }\n}\n"}, "a517b22d14d780f4b168c882f6a5fc82ed6008a4": {"t": 1785760111.0580103, "n": 40644, "h": "8bcbffc38689440d", "c": "package com.logistics.dashboard.ai.orchestrator;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.logistics.dashboard.ai.model.QueryRequest;\nimport com.logistics.dashboard.ai.model.QueryResponse;\nimport com.logistics.dashboard.dto.*;\nimport com.logistics.dashboard.service.DashboardService;\nimport com.logistics.dashboard.service.ForecastingService;\nimport dev.langchain4j.model.chat.ChatLanguageModel;\nimport dev.langchain4j.model.input.Prompt;\nimport dev.langchain4j.model.input.PromptTemplate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Service;\n\nimport java.time.LocalDate;\nimport java.time.Year;\nimport java.util.*;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\n\n/**\n * NLU Orchestrator: AI interprets the question and outputs a structured tool-call plan.\n * Java services execute all computations deterministically — AI never generates data values.\n *\n * Flow: user question → AI → JSON plan → Java service → real data → response\n */\n@Service\npublic class NLUOrchestrator {\n\n private static final Logger log = LoggerFactory.getLogger(NLUOrchestrator.class);\n\n // Valid enum values for validation\n private static final Set<String> VALID_CARRIERS = Set.of(\n \"UPS\",\"FedEx\",\"DHL\",\"USPS\",\"LaserShip\",\"OnTrac\",\"DPD\",\"GLS\",\"Royal Mail\");\n private static final Set<String> VALID_REGIONS = Set.of(\"US-W\",\"US-C\",\"US-E\",\"EU\",\"UK\");\n private static final Set<String> VALID_CATEGORIES = Set.of(\n \"BOOK\",\"PAPER\",\"PENCIL\",\"CRAYON\",\"MARKER\",\"BRUSH\",\"PAINT\",\"STICKER\");\n private static final Set<String> VALID_TOOLS = Set.of(\n \"getKpi\",\"getTimeSeries\",\"getBreakdown\",\"getDeliveryPerformance\",\"forecastDemand\",\"forecastMetric\",\"fallback\");\n private static final Set<String> VALID_GRAN = Set.of(\"day\",\"week\",\"month\");\n\n private static final LocalDate DATA_START = LocalDate.of(2025, 1, 1);\n private static final LocalDate DATA_END = LocalDate.of(2025, 12, 31);\n\n private ChatLanguageModel model;\n private final DashboardService dashboardService;\n private final ForecastingService forecastingService;\n private final ObjectMapper mapper = new ObjectMapper();\n\n // ── Prompt template ──────────────────────────────────────────────────────\n private static final String SYSTEM_PROMPT = \"\"\"\nYou are a logistics analytics assistant. Your job is to interpret the user's question about freight/order data and output a SINGLE JSON object describing which analysis tool to call.\n\nDATABASE SCHEMA (table: orders):\n- order_date DATE, promised_delivery_date DATE, delivery_date DATE (NULL if not yet delivered)\n- status: 'delivered' | 'delayed' | 'in_transit' | 'exception' | 'canceled'\n NOTE: An order is considered DELAYED when delivery_date > promised_delivery_date (computed from dates, not just status).\n- carrier: UPS, FedEx, DHL, USPS, LaserShip, OnTrac, DPD, GLS, Royal Mail\n- region: US-W, US-C, US-E, EU, UK\n- product_category: BOOK, PAPER, PENCIL, CRAYON, MARKER, BRUSH, PAINT, STICKER\n- origin_city, destination_city, warehouse, sku\n- order_value_usd, unit_price_usd, quantity, is_promo, promo_discount_pct\n\nDATA DATE RANGE: 2025-01-01 to 2025-12-31\n\nTOOLS:\n1. getKpi - Returns: totalOrders, deliveredOrders, delayedOrders, onTimeRate, avgDeliveryDays\n2. getTimeSeries - Returns: time-bucketed order counts. Params: granularity (day/week/month)\n3. getBreakdown - Returns: carrier/region/category breakdown (total_orders, delayed_orders, delay_rate, on_time_rate). Params: dimension=carrier|region|category\n4. getDeliveryPerformance - Returns: on-time vs delayed counts per time period. Params: granularity\n5. forecastDemand - Returns: historical + forecasted order counts. Params: periods (int), granularity\n6. forecastMetric - Returns: historical + forecasted on-time rate or delay rate (%). Params: metricType ('on_time_rate'|'delay_rate'), periods, granularity\n7. fallback - Use this when the question is NOT about logistics analytics (e.g., \"who are you?\", \"what's the weather?\", \"hello\").\n\nPARAMETER EXTRACTION RULES:\n- Extract year/month/quarter from question → convert to startDate/endDate (YYYY-MM-DD)\n- \"2025\" → startDate=2025-01-01, endDate=2025-12-31\n- \"Q1 2025\" → startDate=2025-01-01, endDate=2025-03-31\n- \"January 2025\" → startDate=2025-01-01, endDate=2025-01-31\n- If no date mentioned → use full data range 2025-01-01 to 2025-12-31\n- Extract carrier names from question if mentioned\n- Extract region names (US-W, US-C, US-E, EU, UK) from question if mentioned\n- Extract product categories if mentioned\n\nOUTPUT FORMAT (strict JSON, no markdown):\n{\n \"tool\": \"<tool_name>\",\n \"params\": {\n \"startDate\": \"YYYY-MM-DD\",\n \"endDate\": \"YYYY-MM-DD\",\n \"granularity\": \"month\",\n \"carriers\": [\"UPS\"],\n \"regions\": [],\n \"categories\": [],\n \"periods\": 4\n },\n \"answerTemplate\": \"<one-sentence description of what the answer will show>\"\n}\n\nEXAMPLES:\nQ: \"2025年有多少UPS的订单延误了?\"\nA: {\"tool\":\"getKpi\",\"params\":{\"startDate\":\"2025-01-01\",\"endDate\":\"2025-12-31\",\"carriers\":[\"UPS\"],\"regions\":[],\"categories\":[]},\"answerTemplate\":\"2025年UPS延误订单总数\"}\n\nQ: \"Show me monthly order volume trend\"\nA: {\"tool\":\"getTimeSeries\",\"params\":{\"startDate\":\"2025-01-01\",\"endDate\":\"2025-12-31\",\"granularity\":\"month\",\"carriers\":[],\"regions\":[],\"categories\":[]},\"answerTemplate\":\"月度订单量趋势\"}\n\nQ: \"Which carrier has the highest delay rate?\"\nA: {\"tool\":\"getBreakdown\",\"params\":{\"startDate\":\"2025-01-01\",\"endDate\":\"2025-12-31\",\"dimension\":\"carrier\",\"carriers\":[],\"regions\":[],\"categories\":[]},\"answerTemplate\":\"各承运商延误率对比\"}\n\nQ: \"哪个地区的订单延误最多?\"\nA: {\"tool\":\"getBreakdown\",\"params\":{\"startDate\":\"2025-01-01\",\"endDate\":\"2025-12-31\",\"dimension\":\"region\",\"carriers\":[],\"regions\":[],\"categories\":[]},\"answerTemplate\":\"各地区延误订单对比\"}\n\nQ: \"Forecast next 4 weeks of orders\"\nA: {\"tool\":\"forecastDemand\",\"params\":{\"startDate\":\"2025-01-01\",\"endDate\":\"2025-12-31\",\"granularity\":\"week\",\"periods\":4,\"carriers\":[],\"regions\":[],\"categories\":[]},\"answerTemplate\":\"未来4周订单量预测\"}\n\nQ: \"预测未来4周US-W的准时率\"\nA: {\"tool\":\"forecastMetric\",\"params\":{\"metricType\":\"on_time_rate\",\"startDate\":\"2025-01-01\",\"endDate\":\"2025-12-31\",\"granularity\":\"week\",\"periods\":4,\"carriers\":[],\"regions\":[\"US-W\"],\"categories\":[]},\"answerTemplate\":\"US-W地区未来4周准时率预测\"}\n\nQ: \"你是谁\"\nA: {\"tool\":\"fallback\",\"params\":{\"startDate\":\"2025-01-01\",\"endDate\":\"2025-12-31\"},\"answerTemplate\":\"问题与物流分析无关\"}\n\nOutput ONLY the JSON object, nothing else.\n\"\"\";\n\n public NLUOrchestrator(\n @Value(\"${openai.api.key:}\") String apiKey,\n DashboardService dashboardService,\n ForecastingService forecastingService) {\n\n this.dashboardService = dashboardService;\n this.forecastingService = forecastingService;\n\n if (apiKey == null || apiKey.isBlank()) {\n log.warn(\"OPENAI_API_KEY not set — NLQ will use keyword-based fallback\");\n this.model = null;\n } else {\n try {\n this.model = dev.langchain4j.model.openai.OpenAiChatModel.builder()\n .apiKey(apiKey)\n .baseUrl(\"https://api.deepseek.com\")\n .modelName(\"deepseek-chat\")\n .temperature(0.0)\n .maxTokens(300)\n .build();\n log.info(\"AI model initialized (DeepSeek)\");\n } catch (Exception e) {\n log.warn(\"Failed to initialize AI model: {}\", e.getMessage());\n this.model = null;\n }\n }\n }\n\n // ── Main entry point ─────────────────────────────────────────────────────\n\n public QueryResponse processQuery(QueryRequest request) {\n String question = request.getQuestion();\n log.info(\"Processing query: {}\", question);\n\n try {\n // 1. Get tool plan (from AI or fallback)\n ToolPlan plan = resolvePlan(question, request);\n log.info(\"Resolved plan: tool={} params={}\", plan.tool, plan.params);\n\n // 2. Execute plan against real data\n return executePlan(plan, question);\n\n } catch (Exception e) {\n log.error(\"Query processing error: {}\", e.getMessage(), e);\n QueryResponse err = new QueryResponse();\n err.setAnswer(\"查询处理时发生错误,请稍后重试。\");\n err.setChartType(\"none\");\n err.setError(e.getMessage());\n return err;\n }\n }\n\n // ── Plan resolution ──────────────────────────────────────────────────────\n\n private ToolPlan resolvePlan(String question, QueryRequest request) {\n ToolPlan plan = null;\n\n // Try AI first\n if (model != null) {\n plan = askAiForPlan(question);\n }\n\n // Fallback: keyword-based heuristic\n if (plan == null) {\n plan = heuristicPlan(question, request);\n }\n\n // Merge any explicit frontend filters (they take priority over AI-extracted)\n if (request.getCarriers() != null && !request.getCarriers().isEmpty()) {\n plan.params.put(\"carriers\", request.getCarriers());\n }\n if (request.getRegions() != null && !request.getRegions().isEmpty()) {\n plan.params.put(\"regions\", request.getRegions());\n }\n if (request.getCategories() != null && !request.getCategories().isEmpty()) {\n plan.params.put(\"categories\", request.getCategories());\n }\n if (request.getStartDate() != null) {\n plan.params.put(\"startDate\", request.getStartDate().toString());\n }\n if (request.getEndDate() != null) {\n plan.params.put(\"endDate\", request.getEndDate().toString());\n }\n\n return plan;\n }\n\n private ToolPlan askAiForPlan(String question) {\n try {\n String prompt = SYSTEM_PROMPT + \"\\n\\nUser question: \" + question;\n String response = model.generate(prompt);\n log.debug(\"AI raw response: {}\", response);\n\n // Strip markdown code fences if present\n String json = response.trim()\n .replaceAll(\"^```json\\\\s*\", \"\")\n .replaceAll(\"^```\\\\s*\", \"\")\n .replaceAll(\"\\\\s*```$\", \"\")\n .trim();\n\n JsonNode root = mapper.readTree(json);\n ToolPlan plan = new ToolPlan();\n plan.tool = root.path(\"tool\").asText();\n plan.answerTemplate = root.path(\"answerTemplate\").asText();\n\n if (!VALID_TOOLS.contains(plan.tool)) {\n log.warn(\"AI returned invalid tool: {}\", plan.tool);\n return null;\n }\n\n JsonNode p = root.path(\"params\");\n plan.params = new HashMap<>();\n if (p.has(\"startDate\")) plan.params.put(\"startDate\", p.get(\"startDate\").asText());\n if (p.has(\"endDate\")) plan.params.put(\"endDate\", p.get(\"endDate\").asText());\n if (p.has(\"granularity\")) plan.params.put(\"granularity\", p.get(\"granularity\").asText());\n if (p.has(\"periods\")) plan.params.put(\"periods\", p.get(\"periods\").asInt(4));\n if (p.has(\"dimension\")) plan.params.put(\"dimension\", p.get(\"dimension\").asText(\"carrier\"));\n if (p.has(\"metricType\")) plan.params.put(\"metricType\", p.get(\"metricType\").asText(\"on_time_rate\"));\n\n // Validated list params\n plan.params.put(\"carriers\", parseAndValidateList(p.path(\"carriers\"), VALID_CARRIERS));\n plan.params.put(\"regions\", parseAndValidateList(p.path(\"regions\"), VALID_REGIONS));\n plan.params.put(\"categories\", parseAndValidateList(p.path(\"categories\"), VALID_CATEGORIES));\n\n return plan;\n\n } catch (Exception e) {\n log.warn(\"AI plan parsing failed: {}\", e.getMessage());\n return null;\n }\n }\n\n @SuppressWarnings(\"unchecked\")\n private List<String> parseAndValidateList(JsonNode node, Set<String> validValues) {\n List<String> result = new ArrayList<>();\n if (node.isArray()) {\n node.forEach(n -> {\n String val = n.asText();\n if (validValues.contains(val)) result.add(val);\n });\n }\n return result;\n }\n\n // ── Keyword-based heuristic fallback ──────────────────────────────────────\n\n private static final Set<String> BUSINESS_KEYWORDS = Set.of(\n \"order\",\"orders\",\"订单\",\"delivery\",\"deliveries\",\"shipment\",\"shipments\",\"运输\",\"物流\",\"货运\",\"包裹\",\"package\",\n \"count\",\"volume\",\"数量\",\"trend\",\"trends\",\"趋势\",\"compare\",\"comparison\",\"对比\",\"analysis\",\"analyze\",\"分析\",\n \"performance\",\"绩效\",\"forecast\",\"预测\",\"predict\",\"kpi\",\"kpis\",\"metric\",\"metrics\",\"指标\",\n \"carrier\",\"carriers\",\"承运商\",\"region\",\"regions\",\"地区\",\"category\",\"categories\",\"品类\",\"product\",\"产品\",\n \"delay\",\"delayed\",\"delays\",\"延误\",\"on-time\",\"ontime\",\"准时\",\"rate\",\"rates\",\"率\",\n \"2025\",\"year\",\"年\",\"month\",\"月\",\"week\",\"周\",\"day\",\"日\",\"quarter\",\"季度\",\"q1\",\"q2\",\"q3\",\"q4\",\n \"ups\",\"fedex\",\"dhl\",\"usps\",\"total\",\"average\",\"avg\",\"sum\",\"max\",\"min\",\"最高\",\"最低\",\"平均\",\"总和\",\"多少\",\"几个\"\n );\n\n private static final Set<String> CHAT_KEYWORDS = Set.of(\n \"你\",\"who\",\"hello\",\"hi\",\"hey\",\"谢谢\",\"thanks\",\"thank\",\"天气\",\"weather\",\"新闻\",\"news\",\n \"电影\",\"movie\",\"吃\",\"eat\",\"名字\",\"name\",\"喜欢\",\"like\",\"love\",\"讨厌\",\"hate\",\"再见\",\"bye\"\n );\n\n private ToolPlan heuristicPlan(String question, QueryRequest request) {\n String q = question.toLowerCase();\n ToolPlan plan = new ToolPlan();\n plan.params = new HashMap<>();\n\n // Irrelevance detection: no business keywords + has chat keywords\n boolean hasBusiness = BUSINESS_KEYWORDS.stream().anyMatch(q::contains);\n boolean hasChat = CHAT_KEYWORDS.stream().anyMatch(q::contains);\n if (!hasBusiness && hasChat) {\n plan.tool = \"fallback\";\n plan.answerTemplate = \"问题与物流分析无关\";\n return plan;\n }\n\n // Date range\n LocalDate[] range = extractDateRange(q, request);\n plan.params.put(\"startDate\", range[0].toString());\n plan.params.put(\"endDate\", range[1].toString());\n\n // Carriers\n List<String> carriers = VALID_CARRIERS.stream()\n .filter(c -> q.contains(c.toLowerCase()))\n .collect(Collectors.toList());\n plan.params.put(\"carriers\", carriers);\n\n // Regions\n List<String> regions = VALID_REGIONS.stream()\n .filter(r -> q.contains(r.toLowerCase()))\n .collect(Collectors.toList());\n plan.params.put(\"regions\", regions);\n\n // Categories\n List<String> cats = VALID_CATEGORIES.stream()\n .filter(c -> q.contains(c.toLowerCase()))\n .collect(Collectors.toList());\n plan.params.put(\"categories\", cats);\n\n // Forecast metric detection (must come before general on-time rate routing)\n boolean isForecast = q.contains(\"预测\") || q.contains(\"forecast\") || q.contains(\"predict\") || q.contains(\"未来\");\n if (isForecast && (q.contains(\"准时率\") || q.contains(\"on-time rate\") || q.contains(\"延误率\") || q.contains(\"delay rate\"))) {\n plan.tool = \"forecastMetric\";\n plan.params.put(\"metricType\", q.contains(\"延误\") || q.contains(\"delay\") ? \"delay_rate\" : \"on_time_rate\");\n plan.params.put(\"granularity\", \"week\");\n plan.params.put(\"periods\", 4);\n plan.answerTemplate = \"准时率/延误率预测\";\n return plan;\n }\n\n // On-time rate specific routing\n if (q.contains(\"准时率\") || q.contains(\"on-time rate\")) {\n if (q.contains(\"哪个\") || q.contains(\"最\")) {\n // Comparison question\n if (q.contains(\"地区\") || q.contains(\"region\")) {\n plan.tool = \"getBreakdown\";\n plan.params.put(\"dimension\", \"region\");\n plan.answerTemplate = \"各地区准时率对比\";\n } else if (q.contains(\"品类\") || q.contains(\"category\")) {\n plan.tool = \"getBreakdown\";\n plan.params.put(\"dimension\", \"category\");\n plan.answerTemplate = \"各品类准时率对比\";\n } else {\n plan.tool = \"getBreakdown\";\n plan.params.put(\"dimension\", \"carrier\");\n plan.answerTemplate = \"各承运商准时率对比\";\n }\n } else {\n // Specific on-time rate query → KPI\n plan.tool = \"getKpi\";\n plan.answerTemplate = \"准时率分析\";\n }\n return plan;\n }\n\n // Tool selection\n if (isForecast) {\n plan.tool = \"forecastDemand\";\n plan.params.put(\"granularity\", \"week\");\n plan.params.put(\"periods\", 4);\n plan.answerTemplate = \"订单量预测\";\n } else if (q.contains(\"地区\") || q.contains(\"region\") || q.contains(\"区域\")) {\n plan.tool = \"getBreakdown\";\n plan.params.put(\"dimension\", \"region\");\n plan.answerTemplate = \"地区分析\";\n } else if (q.contains(\"品类\") || q.contains(\"category\") || q.contains(\"产品类别\") || q.contains(\"商品类别\")) {\n plan.tool = \"getBreakdown\";\n plan.params.put(\"dimension\", \"category\");\n plan.answerTemplate = \"品类分析\";\n } else if (q.contains(\"承运商\") || q.contains(\"carrier\") || q.contains(\"快递公司\") || q.contains(\"哪家\")) {\n plan.tool = \"getBreakdown\";\n plan.params.put(\"dimension\", \"carrier\");\n plan.answerTemplate = \"承运商分析\";\n } else if (q.contains(\"趋势\") || q.contains(\"trend\") || q.contains(\"变化\") || q.contains(\"每月\") || q.contains(\"monthly\") || q.contains(\"每周\") || q.contains(\"weekly\")) {\n plan.tool = \"getTimeSeries\";\n plan.params.put(\"granularity\", q.contains(\"日\") || q.contains(\"day\") ? \"day\"\n : q.contains(\"周\") || q.contains(\"week\") ? \"week\" : \"month\");\n plan.answerTemplate = \"订单量趋势\";\n } else if (q.contains(\"准时\") || q.contains(\"on-time\") || q.contains(\"交付性能\") || q.contains(\"performance\")) {\n plan.tool = \"getDeliveryPerformance\";\n plan.params.put(\"granularity\", \"month\");\n plan.answerTemplate = \"交付性能分析\";\n } else {\n plan.tool = \"getKpi\";\n plan.answerTemplate = \"关键指标汇总\";\n }\n\n return plan;\n }\n\n private LocalDate[] extractDateRange(String q, QueryRequest request) {\n // Explicit from request\n if (request.getStartDate() != null && request.getEndDate() != null) {\n return new LocalDate[]{request.getStartDate(), request.getEndDate()};\n }\n\n // Year pattern\n Matcher yearMatcher = Pattern.compile(\"(20\\\\d{2})年?\").matcher(q);\n if (yearMatcher.find()) {\n int year = Integer.parseInt(yearMatcher.group(1));\n // Quarter\n if (q.contains(\"q1\") || q.contains(\"第一季度\")) return new LocalDate[]{LocalDate.of(year,1,1), LocalDate.of(year,3,31)};\n if (q.contains(\"q2\") || q.contains(\"第二季度\")) return new LocalDate[]{LocalDate.of(year,4,1), LocalDate.of(year,6,30)};\n if (q.contains(\"q3\") || q.contains(\"第三季度\")) return new LocalDate[]{LocalDate.of(year,7,1), LocalDate.of(year,9,30)};\n if (q.contains(\"q4\") || q.contains(\"第四季度\")) return new LocalDate[]{LocalDate.of(year,10,1), LocalDate.of(year,12,31)};\n return new LocalDate[]{LocalDate.of(year,1,1), LocalDate.of(year,12,31)};\n }\n\n // Default: full data range\n return new LocalDate[]{DATA_START, DATA_END};\n }\n\n // ── Plan execution ────────────────────────────────────────────────────────\n\n @SuppressWarnings(\"unchecked\")\n private QueryResponse executePlan(ToolPlan plan, String question) {\n LocalDate startDate = LocalDate.parse(plan.params.getOrDefault(\"startDate\", DATA_START.toString()).toString());\n LocalDate endDate = LocalDate.parse(plan.params.getOrDefault(\"endDate\", DATA_END.toString()).toString());\n List<String> carriers = (List<String>) plan.params.getOrDefault(\"carriers\", List.of());\n List<String> regions = (List<String>) plan.params.getOrDefault(\"regions\", List.of());\n List<String> categories = (List<String>) plan.params.getOrDefault(\"categories\", List.of());\n\n QueryResponse response = new QueryResponse();\n response.setQueryPlan(\"工具: \" + plan.tool\n + \" | 时间范围: \" + startDate + \" 至 \" + endDate\n + (carriers.isEmpty() ? \"\" : \" | 承运商: \" + carriers)\n + (regions.isEmpty() ? \"\" : \" | 地区: \" + regions)\n + (categories.isEmpty() ? \"\" : \" | 品类: \" + categories));\n\n Map<String, Object> filters = new LinkedHashMap<>();\n filters.put(\"startDate\", startDate.toString());\n filters.put(\"endDate\", endDate.toString());\n if (!carriers.isEmpty()) filters.put(\"carriers\", carriers);\n if (!regions.isEmpty()) filters.put(\"regions\", regions);\n if (!categories.isEmpty()) filters.put(\"categories\", categories);\n response.setFilters(filters);\n\n switch (plan.tool) {\n\n case \"getKpi\" -> {\n KpiResponse kpi = dashboardService.getKPIs(startDate, endDate, carriers, regions, categories);\n response.setChartType(\"kpi\");\n response.setAnswer(formatKpiAnswer(kpi, plan.answerTemplate, carriers, regions, startDate, endDate));\n response.setExplanation(\"从数据库实时聚合 — delivery_date > promised_delivery_date 计为延误。\");\n response.setMetrics(List.of(\"totalOrders\", \"deliveredOrders\", \"delayedOrders\", \"onTimeRate\", \"avgDeliveryDays\"));\n\n Map<String, Object> chartData = new LinkedHashMap<>();\n chartData.put(\"chartType\", \"kpi\");\n chartData.put(\"labels\", List.of(\"总订单\", \"已送达\", \"延误\"));\n chartData.put(\"values\", List.of(kpi.getTotalOrders(), kpi.getDeliveredOrders(), kpi.getDelayedOrders()));\n chartData.put(\"kpiSummary\", Map.of(\n \"onTimeRate\", kpi.getOnTimeRate() + \"%\",\n \"avgDeliveryDays\", kpi.getAvgDeliveryDays() + \" 天\"\n ));\n response.setChartData(chartData);\n\n List<Map<String, Object>> raw = List.of(Map.of(\n \"总订单\", kpi.getTotalOrders(),\n \"已送达\", kpi.getDeliveredOrders(),\n \"延误\", kpi.getDelayedOrders(),\n \"准时率\", kpi.getOnTimeRate() + \"%\",\n \"平均送达天数\", kpi.getAvgDeliveryDays()\n ));\n response.setRawData(raw);\n }\n\n case \"getTimeSeries\" -> {\n String gran = plan.params.getOrDefault(\"granularity\", \"month\").toString();\n if (!VALID_GRAN.contains(gran)) gran = \"month\";\n TimeSeriesResponse ts = dashboardService.getOrderVolumeTimeSeries(gran, startDate, endDate, carriers, regions, categories);\n response.setChartType(\"time_series\");\n response.setAnswer(plan.answerTemplate + \"(\" + startDate + \" 至 \" + endDate + \",粒度:\" + gran + \")\");\n response.setExplanation(\"按 \" + gran + \" 聚合的订单量趋势,来自实时数据库查询。\");\n response.setMetrics(List.of(\"订单量\", \"时间趋势\"));\n\n List<String> labels = ts.getData().stream().map(d -> d.getDate().toString()).collect(Collectors.toList());\n List<Long> values = ts.getData().stream().map(TimeSeriesData::getCount).collect(Collectors.toList());\n Map<String, Object> chartData = new LinkedHashMap<>();\n chartData.put(\"chartType\", \"line\");\n chartData.put(\"labels\", labels);\n chartData.put(\"values\", values);\n response.setChartData(chartData);\n\n List<Map<String, Object>> raw = ts.getData().stream()\n .map(d -> Map.<String,Object>of(\"date\", d.getDate().toString(), \"count\", d.getCount()))\n .collect(Collectors.toList());\n response.setRawData(raw);\n }\n\n case \"getBreakdown\" -> {\n String dimension = plan.params.getOrDefault(\"dimension\", \"carrier\").toString();\n if (\"region\".equalsIgnoreCase(dimension)) {\n RegionBreakdownResponse rb = dashboardService.getRegionBreakdown(startDate, endDate, carriers, regions, categories);\n response.setChartType(\"bar\");\n String bestRegion = rb.getData().stream()\n .max(java.util.Comparator.comparing(RegionBreakdown::getOnTimeRate))\n .map(RegionBreakdown::getRegion).orElse(\"\");\n String answerPrefix = plan.answerTemplate.contains(\"准时率\")\n ? \"各地区准时率分析(准时率最高:\" + bestRegion + \")\"\n : \"各地区绩效分析\";\n response.setAnswer(answerPrefix + \"(\" + startDate + \" 至 \" + endDate + \")\");\n response.setExplanation(\"按地区汇总订单量和延误率,delivery_date > promised_delivery_date 计为延误。准时率 = 100% - 延误率。\");\n response.setMetrics(List.of(\"总订单\", \"延误订单\", \"延误率\", \"准时率\"));\n\n List<String> labels = rb.getData().stream().map(RegionBreakdown::getRegion).collect(Collectors.toList());\n List<Long> values = rb.getData().stream().map(RegionBreakdown::getTotalOrders).collect(Collectors.toList());\n Map<String, Object> chartData = new LinkedHashMap<>();\n chartData.put(\"chartType\", \"bar\");\n chartData.put(\"dimension\", \"region\");\n chartData.put(\"labels\", labels);\n chartData.put(\"values\", values);\n chartData.put(\"delayedValues\", rb.getData().stream().map(RegionBreakdown::getDelayedOrders).collect(Collectors.toList()));\n chartData.put(\"delayRates\", rb.getData().stream().map(r -> r.getDelayRate().toString()).collect(Collectors.toList()));\n chartData.put(\"onTimeRates\", rb.getData().stream().map(r -> r.getOnTimeRate().toString()).collect(Collectors.toList()));\n response.setChartData(chartData);\n\n List<Map<String, Object>> raw = rb.getData().stream()\n .map(r -> Map.<String,Object>of(\n \"region\", r.getRegion(),\n \"totalOrders\", r.getTotalOrders(),\n \"delayedOrders\", r.getDelayedOrders(),\n \"delayRate\", r.getDelayRate() + \"%\"\n ))\n .collect(Collectors.toList());\n response.setRawData(raw);\n } else if (\"category\".equalsIgnoreCase(dimension)) {\n CategoryBreakdownResponse cb = dashboardService.getCategoryBreakdown(startDate, endDate, carriers, regions, categories);\n response.setChartType(\"bar\");\n String bestCategory = cb.getData().stream()\n .max(java.util.Comparator.comparing(CategoryBreakdown::getOnTimeRate))\n .map(CategoryBreakdown::getCategory).orElse(\"\");\n String answerPrefix = plan.answerTemplate.contains(\"准时率\")\n ? \"各品类准时率分析(准时率最高:\" + bestCategory + \")\"\n : \"各品类绩效分析\";\n response.setAnswer(answerPrefix + \"(\" + startDate + \" 至 \" + endDate + \")\");\n response.setExplanation(\"按品类汇总订单量和延误率,delivery_date > promised_delivery_date 计为延误。准时率 = 100% - 延误率。\");\n response.setMetrics(List.of(\"总订单\", \"延误订单\", \"延误率\", \"准时率\"));\n\n List<String> labels = cb.getData().stream().map(CategoryBreakdown::getCategory).collect(Collectors.toList());\n List<Long> values = cb.getData().stream().map(CategoryBreakdown::getTotalOrders).collect(Collectors.toList());\n Map<String, Object> chartData = new LinkedHashMap<>();\n chartData.put(\"chartType\", \"bar\");\n chartData.put(\"dimension\", \"category\");\n chartData.put(\"labels\", labels);\n chartData.put(\"values\", values);\n chartData.put(\"delayedValues\", cb.getData().stream().map(CategoryBreakdown::getDelayedOrders).collect(Collectors.toList()));\n chartData.put(\"delayRates\", cb.getData().stream().map(c -> c.getDelayRate().toString()).collect(Collectors.toList()));\n chartData.put(\"onTimeRates\", cb.getData().stream().map(c -> c.getOnTimeRate().toString()).collect(Collectors.toList()));\n response.setChartData(chartData);\n\n List<Map<String, Object>> raw = cb.getData().stream()\n .map(c -> Map.<String,Object>of(\n \"category\", c.getCategory(),\n \"totalOrders\", c.getTotalOrders(),\n \"delayedOrders\", c.getDelayedOrders(),\n \"delayRate\", c.getDelayRate() + \"%\"\n ))\n .collect(Collectors.toList());\n response.setRawData(raw);\n } else {\n CarrierBreakdownResponse bd = dashboardService.getCarrierBreakdown(startDate, endDate, carriers, regions, categories);\n response.setChartType(\"bar\");\n String bestCarrier = bd.getData().stream()\n .max(java.util.Comparator.comparing(CarrierBreakdown::getOnTimeRate))\n .map(CarrierBreakdown::getCarrier).orElse(\"\");\n String answerPrefix = plan.answerTemplate.contains(\"准时率\")\n ? \"各承运商准时率分析(准时率最高:\" + bestCarrier + \")\"\n : \"各承运商绩效分析\";\n response.setAnswer(answerPrefix + \"(\" + startDate + \" 至 \" + endDate + \")\");\n response.setExplanation(\"按承运商汇总订单量和延误率,delivery_date > promised_delivery_date 计为延误。准时率 = 100% - 延误率。\");\n response.setMetrics(List.of(\"总订单\", \"延误订单\", \"延误率\", \"准时率\"));\n\n List<String> labels = bd.getData().stream().map(CarrierBreakdown::getCarrier).collect(Collectors.toList());\n List<Long> values = bd.getData().stream().map(CarrierBreakdown::getTotalOrders).collect(Collectors.toList());\n Map<String, Object> chartData = new LinkedHashMap<>();\n chartData.put(\"chartType\", \"bar\");\n chartData.put(\"dimension\", \"carrier\");\n chartData.put(\"labels\", labels);\n chartData.put(\"values\", values);\n chartData.put(\"delayedValues\", bd.getData().stream().map(CarrierBreakdown::getDelayedOrders).collect(Collectors.toList()));\n chartData.put(\"delayRates\", bd.getData().stream().map(c -> c.getDelayRate().toString()).collect(Collectors.toList()));\n chartData.put(\"onTimeRates\", bd.getData().stream().map(c -> c.getOnTimeRate().toString()).collect(Collectors.toList()));\n response.setChartData(chartData);\n\n List<Map<String, Object>> raw = bd.getData().stream()\n .map(c -> Map.<String,Object>of(\n \"carrier\", c.getCarrier(),\n \"totalOrders\", c.getTotalOrders(),\n \"delayedOrders\", c.getDelayedOrders(),\n \"delayRate\", c.getDelayRate() + \"%\"\n ))\n .collect(Collectors.toList());\n response.setRawData(raw);\n }\n }\n\n case \"getDeliveryPerformance\" -> {\n String gran = plan.params.getOrDefault(\"granularity\", \"month\").toString();\n if (!VALID_GRAN.contains(gran)) gran = \"month\";\n DeliveryPerformanceResponse dp = dashboardService.getDeliveryPerformance(gran, startDate, endDate, carriers, regions, categories);\n response.setChartType(\"bar\");\n response.setAnswer(\"交付性能分析(\" + startDate + \" 至 \" + endDate + \")\");\n response.setExplanation(\"按时间段展示准时 vs 延误订单数,延误判定为 delivery_date > promised_delivery_date。\");\n response.setMetrics(List.of(\"准时订单\", \"延误订单\"));\n\n List<String> labels = dp.getData().stream().map(DeliveryPerformance::getPeriod).collect(Collectors.toList());\n List<Long> onTime = dp.getData().stream().map(DeliveryPerformance::getOnTime).collect(Collectors.toList());\n List<Long> delayed = dp.getData().stream().map(DeliveryPerformance::getDelayed).collect(Collectors.toList());\n Map<String, Object> chartData = new LinkedHashMap<>();\n chartData.put(\"chartType\", \"bar\");\n chartData.put(\"labels\", labels);\n chartData.put(\"values\", onTime);\n chartData.put(\"delayedValues\", delayed);\n response.setChartData(chartData);\n\n List<Map<String, Object>> raw = dp.getData().stream()\n .map(d -> {\n Map<String, Object> row = new LinkedHashMap<>();\n row.put(\"period\", d.getPeriod());\n row.put(\"onTime\", d.getOnTime());\n row.put(\"delayed\", d.getDelayed());\n return row;\n })\n .collect(Collectors.toList());\n response.setRawData(raw);\n }\n\n case \"forecastDemand\" -> {\n String gran = plan.params.getOrDefault(\"granularity\", \"week\").toString();\n if (!VALID_GRAN.contains(gran)) gran = \"week\";\n int periods = Integer.parseInt(plan.params.getOrDefault(\"periods\", 4).toString());\n ForecastResponse fc = forecastingService.forecastDemand(gran, periods, startDate, endDate, carriers, regions, categories);\n response.setChartType(\"forecast\");\n response.setAnswer(\"未来 \" + periods + \" 个\" + granLabel(gran) + \"订单量预测\");\n response.setExplanation(\"使用 \" + fc.getAlgorithm() + \" 基于历史数据确定性计算。AI 仅负责参数提取,不生成预测值。\");\n response.setMetrics(List.of(\"历史订单量\", \"预测订单量\", \"安全库存建议\"));\n\n List<String> labels = fc.getData().stream().map(d -> d.getDate().toString()).collect(Collectors.toList());\n List<Long> histVals = fc.getData().stream().filter(d -> !d.isForecast()).map(ForecastData::getValue).collect(Collectors.toList());\n List<Long> foreVals = fc.getData().stream().filter(ForecastData::isForecast).map(ForecastData::getValue).collect(Collectors.toList());\n int splitIdx = (int) fc.getData().stream().filter(d -> !d.isForecast()).count();\n Map<String, Object> chartData = new LinkedHashMap<>();\n chartData.put(\"chartType\", \"forecast\");\n chartData.put(\"labels\", labels);\n chartData.put(\"historicalValues\", histVals);\n chartData.put(\"forecastValues\", foreVals);\n chartData.put(\"splitIndex\", splitIdx);\n chartData.put(\"algorithm\", fc.getAlgorithm());\n chartData.put(\"recommendations\", fc.getRecommendations());\n response.setChartData(chartData);\n\n List<Map<String, Object>> raw = fc.getData().stream()\n .map(d -> Map.<String,Object>of(\n \"date\", d.getDate().toString(),\n \"value\", d.getValue(),\n \"isForecast\", d.isForecast()\n ))\n .collect(Collectors.toList());\n response.setRawData(raw);\n }\n\n case \"forecastMetric\" -> {\n String gran = plan.params.getOrDefault(\"granularity\", \"week\").toString();\n if (!VALID_GRAN.contains(gran)) gran = \"week\";\n int periods = Integer.parseInt(plan.params.getOrDefault(\"periods\", 4).toString());\n String metricType = plan.params.getOrDefault(\"metricType\", \"on_time_rate\").toString();\n ForecastResponse fc = forecastingService.forecastMetric(metricType, gran, periods, startDate, endDate, carriers, regions, categories);\n String metricLabel = \"on_time_rate\".equals(metricType) ? \"准时率\" : \"延误率\";\n response.setChartType(\"forecast\");\n response.setAnswer(\"未来 \" + periods + \" 个\" + granLabel(gran) + metricLabel + \"预测\");\n response.setExplanation(\"使用 \" + fc.getAlgorithm() + \" 基于历史交付数据确定性计算。AI 仅负责参数提取,不生成预测值。\");\n response.setMetrics(List.of(\"历史\" + metricLabel, \"预测\" + metricLabel));\n\n List<String> labels = fc.getData().stream().map(d -> d.getDate().toString()).collect(Collectors.toList());\n List<String> histVals = fc.getData().stream().filter(d -> !d.isForecast()).map(d -> d.getPctValue().toString()).collect(Collectors.toList());\n List<String> foreVals = fc.getData().stream().filter(ForecastData::isForecast).map(d -> d.getPctValue().toString()).collect(Collectors.toList());\n int splitIdx = (int) fc.getData().stream().filter(d -> !d.isForecast()).count();\n Map<String, Object> chartData = new LinkedHashMap<>();\n chartData.put(\"chartType\", \"forecast\");\n chartData.put(\"metricType\", metricType);\n chartData.put(\"labels\", labels);\n chartData.put(\"historicalValues\", histVals);\n chartData.put(\"forecastValues\", foreVals);\n chartData.put(\"splitIndex\", splitIdx);\n chartData.put(\"algorithm\", fc.getAlgorithm());\n chartData.put(\"recommendations\", fc.getRecommendations());\n response.setChartData(chartData);\n\n List<Map<String, Object>> raw = fc.getData().stream()\n .map(d -> {\n Map<String, Object> row = new LinkedHashMap<>();\n row.put(\"date\", d.getDate().toString());\n row.put(\"value\", d.getPctValue() != null ? d.getPctValue().toString() + \"%\" : d.getValue());\n row.put(\"isForecast\", d.isForecast());\n return row;\n })\n .collect(Collectors.toList());\n response.setRawData(raw);\n }\n\n case \"fallback\" -> {\n response.setChartType(\"none\");\n response.setAnswer(\"我是物流数据分析助手,专注于帮助您分析订单、承运商、地区配送绩效等指标。\\n\\n您可以尝试提问:\\n- 2025年各承运商延误率对比\\n- 预测未来4周US-W的准时率\\n- 哪个品类的订单延误最多?\");\n response.setExplanation(\"该问题与物流分析无关,未触发任何数据查询工具。\");\n response.setMetrics(List.of(\"N/A\"));\n }\n\n default -> {\n response.setChartType(\"none\");\n response.setAnswer(\"无法识别的分析类型,请换一种问法。\");\n }\n }\n\n return response;\n }\n\n // ── Helpers ───────────────────────────────────────────────────────────────\n\n private String formatKpiAnswer(KpiResponse kpi, String template, List<String> carriers,\n List<String> regions, LocalDate start, LocalDate end) {\n String scope = \"\";\n if (!carriers.isEmpty()) scope += \" [承运商: \" + String.join(\", \", carriers) + \"]\";\n if (!regions.isEmpty()) scope += \" [地区: \" + String.join(\", \", regions) + \"]\";\n return String.format(\"%s (%s ~ %s)%s\\n总订单: %d | 已送达: %d | 延误: %d | 准时率: %s%% | 平均送达: %s天\",\n template, start, end, scope,\n kpi.getTotalOrders(), kpi.getDeliveredOrders(), kpi.getDelayedOrders(),\n kpi.getOnTimeRate(), kpi.getAvgDeliveryDays());\n }\n\n private String granLabel(String gran) {\n return switch (gran) { case \"day\" -> \"天\"; case \"week\" -> \"周\"; default -> \"月\"; };\n }\n\n // ── Inner class ───────────────────────────────────────────────────────────\n\n private static class ToolPlan {\n String tool;\n String answerTemplate;\n Map<String, Object> params;\n }\n}\n"}, "cbb4ae3ddb7d0873fd7498311cde47cfee2f0f01": {"t": 1785760112.59637, "n": 26780, "h": "2ba8843d14b935d1", "c": "package com.logistics.dashboard.ai.orchestrator;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.logistics.dashboard.ai.model.QueryRequest;\nimport com.logistics.dashboard.ai.model.QueryResponse;\nimport com.logistics.dashboard.dto.*;\nimport com.logistics.dashboard.service.DashboardService;\nimport com.logistics.dashboard.service.ForecastingService;\nimport dev.langchain4j.model.chat.ChatLanguageModel;\nimport dev.langchain4j.model.input.Prompt;\nimport dev.langchain4j.model.input.PromptTemplate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Service;\n\nimport java.time.LocalDate;\nimport java.time.Year;\nimport java.util.*;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\n\n/**\n * NLU Orchestrator: AI interprets the question and outputs a structured tool-call plan.\n * Java services execute all computations deterministically — AI never generates data values.\n *\n * Flow: user question → AI → JSON plan → Java service → real data → response\n */\n@Service\npublic class NLUOrchestrator {\n\n private static final Logger log = LoggerFactory.getLogger(NLUOrchestrator.class);\n\n // Valid enum values for validation\n private static final Set<String> VALID_CARRIERS = Set.of(\n \"UPS\",\"FedEx\",\"DHL\",\"USPS\",\"LaserShip\",\"OnTrac\",\"DPD\",\"GLS\",\"Royal Mail\");\n private static final Set<String> VALID_REGIONS = Set.of(\"US-W\",\"US-C\",\"US-E\",\"EU\",\"UK\");\n private static final Set<String> VALID_CATEGORIES = Set.of(\n \"BOOK\",\"PAPER\",\"PENCIL\",\"CRAYON\",\"MARKER\",\"BRUSH\",\"PAINT\",\"STICKER\");\n private static final Set<String> VALID_TOOLS = Set.of(\n \"getKpi\",\"getTimeSeries\",\"getBreakdown\",\"getDeliveryPerformance\",\"forecastDemand\");\n private static final Set<String> VALID_GRAN = Set.of(\"day\",\"week\",\"month\");\n\n private static final LocalDate DATA_START = LocalDate.of(2025, 1, 1);\n private static final LocalDate DATA_END = LocalDate.of(2025, 12, 31);\n\n private ChatLanguageModel model;\n private final DashboardService dashboardService;\n private final ForecastingService forecastingService;\n private final ObjectMapper mapper = new ObjectMapper();\n\n // ── Prompt template ──────────────────────────────────────────────────────\n private static final String SYSTEM_PROMPT = \"\"\"\nYou are a logistics analytics assistant. Your job is to interpret the user's question about freight/order data and output a SINGLE JSON object describing which analysis tool to call.\n\nDATABASE SCHEMA (table: orders):\n- order_date DATE, delivery_date DATE (NULL if not yet delivered)\n- status: 'delivered' | 'delayed' | 'in_transit' | 'exception' | 'canceled'\n NOTE: 'delayed' is a status value, not computed from dates.\n- carrier: UPS, FedEx, DHL, USPS, LaserShip, OnTrac, DPD, GLS, Royal Mail\n- region: US-W, US-C, US-E, EU, UK\n- product_category: BOOK, PAPER, PENCIL, CRAYON, MARKER, BRUSH, PAINT, STICKER\n- origin_city, destination_city, warehouse, sku\n- order_value_usd, unit_price_usd, quantity, is_promo, promo_discount_pct\n\nDATA DATE RANGE: 2025-01-01 to 2025-12-31\n\nTOOLS:\n1. getKpi - Returns: totalOrders, deliveredOrders, delayedOrders, onTimeRate, avgDeliveryDays\n2. getTimeSeries - Returns: time-bucketed order counts. Params: granularity (day/week/month)\n3. getBreakdown - Returns: carrier breakdown (total_orders, delayed_orders, delay_rate). Params: dimension=carrier\n4. getDeliveryPerformance - Returns: on-time vs delayed counts per time period. Params: granularity\n5. forecastDemand - Returns: historical + forecasted order counts. Params: periods (int), granularity\n\nPARAMETER EXTRACTION RULES:\n- Extract year/month/quarter from question → convert to startDate/endDate (YYYY-MM-DD)\n- \"2025\" → startDate=2025-01-01, endDate=2025-12-31\n- \"Q1 2025\" → startDate=2025-01-01, endDate=2025-03-31\n- \"January 2025\" → startDate=2025-01-01, endDate=2025-01-31\n- If no date mentioned → use full data range 2025-01-01 to 2025-12-31\n- Extract carrier names from question if mentioned\n- Extract region names (US-W, US-C, US-E, EU, UK) from question if mentioned\n- Extract product categories if mentioned\n\nOUTPUT FORMAT (strict JSON, no markdown):\n{\n \"tool\": \"<tool_name>\",\n \"params\": {\n \"startDate\": \"YYYY-MM-DD\",\n \"endDate\": \"YYYY-MM-DD\",\n \"granularity\": \"month\",\n \"carriers\": [\"UPS\"],\n \"regions\": [],\n \"categories\": [],\n \"periods\": 4\n },\n \"answerTemplate\": \"<one-sentence description of what the answer will show>\"\n}\n\nEXAMPLES:\nQ: \"2025年有多少UPS的订单延误了?\"\nA: {\"tool\":\"getKpi\",\"params\":{\"startDate\":\"2025-01-01\",\"endDate\":\"2025-12-31\",\"carriers\":[\"UPS\"],\"regions\":[],\"categories\":[]},\"answerTemplate\":\"2025年UPS延误订单总数\"}\n\nQ: \"Show me monthly order volume trend\"\nA: {\"tool\":\"getTimeSeries\",\"params\":{\"startDate\":\"2025-01-01\",\"endDate\":\"2025-12-31\",\"granularity\":\"month\",\"carriers\":[],\"regions\":[],\"categories\":[]},\"answerTemplate\":\"月度订单量趋势\"}\n\nQ: \"Which carrier has the highest delay rate?\"\nA: {\"tool\":\"getBreakdown\",\"params\":{\"startDate\":\"2025-01-01\",\"endDate\":\"2025-12-31\",\"dimension\":\"carrier\",\"carriers\":[],\"regions\":[],\"categories\":[]},\"answerTemplate\":\"各承运商延误率对比\"}\n\nQ: \"Forecast next 4 weeks of orders\"\nA: {\"tool\":\"forecastDemand\",\"params\":{\"startDate\":\"2025-01-01\",\"endDate\":\"2025-12-31\",\"granularity\":\"week\",\"periods\":4,\"carriers\":[],\"regions\":[],\"categories\":[]},\"answerTemplate\":\"未来4周订单量预测\"}\n\nOutput ONLY the JSON object, nothing else.\n\"\"\";\n\n public NLUOrchestrator(\n @Value(\"${openai.api.key:}\") String apiKey,\n DashboardService dashboardService,\n ForecastingService forecastingService) {\n\n this.dashboardService = dashboardService;\n this.forecastingService = forecastingService;\n\n if (apiKey == null || apiKey.isBlank()) {\n log.warn(\"OPENAI_API_KEY not set — NLQ will use keyword-based fallback\");\n this.model = null;\n } else {\n try {\n this.model = dev.langchain4j.model.openai.OpenAiChatModel.builder()\n .apiKey(apiKey)\n .baseUrl(\"https://api.deepseek.com\")\n .modelName(\"deepseek-chat\")\n .temperature(0.0)\n .maxTokens(300)\n .build();\n log.info(\"AI model initialized (DeepSeek)\");\n } catch (Exception e) {\n log.warn(\"Failed to initialize AI model: {}\", e.getMessage());\n this.model = null;\n }\n }\n }\n\n // ── Main entry point ─────────────────────────────────────────────────────\n\n public QueryResponse processQuery(QueryRequest request) {\n String question = request.getQuestion();\n log.info(\"Processing query: {}\", question);\n\n try {\n // 1. Get tool plan (from AI or fallback)\n ToolPlan plan = resolvePlan(question, request);\n log.info(\"Resolved plan: tool={} params={}\", plan.tool, plan.params);\n\n // 2. Execute plan against real data\n return executePlan(plan, question);\n\n } catch (Exception e) {\n log.error(\"Query processing error: {}\", e.getMessage(), e);\n QueryResponse err = new QueryResponse();\n err.setAnswer(\"查询处理时发生错误,请稍后重试。\");\n err.setChartType(\"none\");\n err.setError(e.getMessage());\n return err;\n }\n }\n\n // ── Plan resolution ──────────────────────────────────────────────────────\n\n private ToolPlan resolvePlan(String question, QueryRequest request) {\n ToolPlan plan = null;\n\n // Try AI first\n if (model != null) {\n plan = askAiForPlan(question);\n }\n\n // Fallback: keyword-based heuristic\n if (plan == null) {\n plan = heuristicPlan(question, request);\n }\n\n // Merge any explicit frontend filters (they take priority over AI-extracted)\n if (request.getCarriers() != null && !request.getCarriers().isEmpty()) {\n plan.params.put(\"carriers\", request.getCarriers());\n }\n if (request.getRegions() != null && !request.getRegions().isEmpty()) {\n plan.params.put(\"regions\", request.getRegions());\n }\n if (request.getCategories() != null && !request.getCategories().isEmpty()) {\n plan.params.put(\"categories\", request.getCategories());\n }\n if (request.getStartDate() != null) {\n plan.params.put(\"startDate\", request.getStartDate().toString());\n }\n if (request.getEndDate() != null) {\n plan.params.put(\"endDate\", request.getEndDate().toString());\n }\n\n return plan;\n }\n\n private ToolPlan askAiForPlan(String question) {\n try {\n String prompt = SYSTEM_PROMPT + \"\\n\\nUser question: \" + question;\n String response = model.generate(prompt);\n log.debug(\"AI raw response: {}\", response);\n\n // Strip markdown code fences if present\n String json = response.trim()\n .replaceAll(\"^```json\\\\s*\", \"\")\n .replaceAll(\"^```\\\\s*\", \"\")\n .replaceAll(\"\\\\s*```$\", \"\")\n .trim();\n\n JsonNode root = mapper.readTree(json);\n ToolPlan plan = new ToolPlan();\n plan.tool = root.path(\"tool\").asText();\n plan.answerTemplate = root.path(\"answerTemplate\").asText();\n\n if (!VALID_TOOLS.contains(plan.tool)) {\n log.warn(\"AI returned invalid tool: {}\", plan.tool);\n return null;\n }\n\n JsonNode p = root.path(\"params\");\n plan.params = new HashMap<>();\n if (p.has(\"startDate\")) plan.params.put(\"startDate\", p.get(\"startDate\").asText());\n if (p.has(\"endDate\")) plan.params.put(\"endDate\", p.get(\"endDate\").asText());\n if (p.has(\"granularity\")) plan.params.put(\"granularity\", p.get(\"granularity\").asText());\n if (p.has(\"periods\")) plan.params.put(\"periods\", p.get(\"periods\").asInt(4));\n if (p.has(\"dimension\")) plan.params.put(\"dimension\", p.get(\"dimension\").asText(\"carrier\"));\n\n // Validated list params\n plan.params.put(\"carriers\", parseAndValidateList(p.path(\"carriers\"), VALID_CARRIERS));\n plan.params.put(\"regions\", parseAndValidateList(p.path(\"regions\"), VALID_REGIONS));\n plan.params.put(\"categories\", parseAndValidateList(p.path(\"categories\"), VALID_CATEGORIES));\n\n return plan;\n\n } catch (Exception e) {\n log.warn(\"AI plan parsing failed: {}\", e.getMessage());\n return null;\n }\n }\n\n @SuppressWarnings(\"unchecked\")\n private List<String> parseAndValidateList(JsonNode node, Set<String> validValues) {\n List<String> result = new ArrayList<>();\n if (node.isArray()) {\n node.forEach(n -> {\n String val = n.asText();\n if (validValues.contains(val)) result.add(val);\n });\n }\n return result;\n }\n\n // ── Keyword-based heuristic fallback ──────────────────────────────────────\n\n private ToolPlan heuristicPlan(String question, QueryRequest request) {\n String q = question.toLowerCase();\n ToolPlan plan = new ToolPlan();\n plan.params = new HashMap<>();\n\n // Date range\n LocalDate[] range = extractDateRange(q, request);\n plan.params.put(\"startDate\", range[0].toString());\n plan.params.put(\"endDate\", range[1].toString());\n\n // Carriers\n List<String> carriers = VALID_CARRIERS.stream()\n .filter(c -> q.contains(c.toLowerCase()))\n .collect(Collectors.toList());\n plan.params.put(\"carriers\", carriers);\n\n // Regions\n List<String> regions = VALID_REGIONS.stream()\n .filter(r -> q.contains(r.toLowerCase()))\n .collect(Collectors.toList());\n plan.params.put(\"regions\", regions);\n\n // Categories\n List<String> cats = VALID_CATEGORIES.stream()\n .filter(c -> q.contains(c.toLowerCase()))\n .collect(Collectors.toList());\n plan.params.put(\"categories\", cats);\n\n // Tool selection\n if (q.contains(\"预测\") || q.contains(\"forecast\") || q.contains(\"predict\") || q.contains(\"未来\")) {\n plan.tool = \"forecastDemand\";\n plan.params.put(\"granularity\", \"week\");\n plan.params.put(\"periods\", 4);\n plan.answerTemplate = \"订单量预测\";\n } else if (q.contains(\"承运商\") || q.contains(\"carrier\") || q.contains(\"快递公司\") || q.contains(\"哪家\")) {\n plan.tool = \"getBreakdown\";\n plan.params.put(\"dimension\", \"carrier\");\n plan.answerTemplate = \"承运商分析\";\n } else if (q.contains(\"趋势\") || q.contains(\"trend\") || q.contains(\"变化\") || q.contains(\"每月\") || q.contains(\"monthly\") || q.contains(\"每周\") || q.contains(\"weekly\")) {\n plan.tool = \"getTimeSeries\";\n plan.params.put(\"granularity\", q.contains(\"日\") || q.contains(\"day\") ? \"day\"\n : q.contains(\"周\") || q.contains(\"week\") ? \"week\" : \"month\");\n plan.answerTemplate = \"订单量趋势\";\n } else if (q.contains(\"准时\") || q.contains(\"on-time\") || q.contains(\"交付性能\") || q.contains(\"performance\")) {\n plan.tool = \"getDeliveryPerformance\";\n plan.params.put(\"granularity\", \"month\");\n plan.answerTemplate = \"交付性能分析\";\n } else {\n plan.tool = \"getKpi\";\n plan.answerTemplate = \"关键指标汇总\";\n }\n\n return plan;\n }\n\n private LocalDate[] extractDateRange(String q, QueryRequest request) {\n // Explicit from request\n if (request.getStartDate() != null && request.getEndDate() != null) {\n return new LocalDate[]{request.getStartDate(), request.getEndDate()};\n }\n\n // Year pattern\n Matcher yearMatcher = Pattern.compile(\"(20\\\\d{2})年?\").matcher(q);\n if (yearMatcher.find()) {\n int year = Integer.parseInt(yearMatcher.group(1));\n // Quarter\n if (q.contains(\"q1\") || q.contains(\"第一季度\")) return new LocalDate[]{LocalDate.of(year,1,1), LocalDate.of(year,3,31)};\n if (q.contains(\"q2\") || q.contains(\"第二季度\")) return new LocalDate[]{LocalDate.of(year,4,1), LocalDate.of(year,6,30)};\n if (q.contains(\"q3\") || q.contains(\"第三季度\")) return new LocalDate[]{LocalDate.of(year,7,1), LocalDate.of(year,9,30)};\n if (q.contains(\"q4\") || q.contains(\"第四季度\")) return new LocalDate[]{LocalDate.of(year,10,1), LocalDate.of(year,12,31)};\n return new LocalDate[]{LocalDate.of(year,1,1), LocalDate.of(year,12,31)};\n }\n\n // Default: full data range\n return new LocalDate[]{DATA_START, DATA_END};\n }\n\n // ── Plan execution ────────────────────────────────────────────────────────\n\n @SuppressWarnings(\"unchecked\")\n private QueryResponse executePlan(ToolPlan plan, String question) {\n LocalDate startDate = LocalDate.parse(plan.params.getOrDefault(\"startDate\", DATA_START.toString()).toString());\n LocalDate endDate = LocalDate.parse(plan.params.getOrDefault(\"endDate\", DATA_END.toString()).toString());\n List<String> carriers = (List<String>) plan.params.getOrDefault(\"carriers\", List.of());\n List<String> regions = (List<String>) plan.params.getOrDefault(\"regions\", List.of());\n List<String> categories = (List<String>) plan.params.getOrDefault(\"categories\", List.of());\n\n QueryResponse response = new QueryResponse();\n response.setQueryPlan(\"工具: \" + plan.tool\n + \" | 时间范围: \" + startDate + \" 至 \" + endDate\n + (carriers.isEmpty() ? \"\" : \" | 承运商: \" + carriers)\n + (regions.isEmpty() ? \"\" : \" | 地区: \" + regions)\n + (categories.isEmpty() ? \"\" : \" | 品类: \" + categories));\n\n Map<String, Object> filters = new LinkedHashMap<>();\n filters.put(\"startDate\", startDate.toString());\n filters.put(\"endDate\", endDate.toString());\n if (!carriers.isEmpty()) filters.put(\"carriers\", carriers);\n if (!regions.isEmpty()) filters.put(\"regions\", regions);\n if (!categories.isEmpty()) filters.put(\"categories\", categories);\n response.setFilters(filters);\n\n switch (plan.tool) {\n\n case \"getKpi\" -> {\n KpiResponse kpi = dashboardService.getKPIs(startDate, endDate, carriers, regions, categories);\n response.setChartType(\"kpi\");\n response.setAnswer(formatKpiAnswer(kpi, plan.answerTemplate, carriers, regions, startDate, endDate));\n response.setExplanation(\"从数据库实时聚合 — status='delayed' 计为延误,status='delivered' 计为已送达。\");\n response.setMetrics(List.of(\"totalOrders\", \"deliveredOrders\", \"delayedOrders\", \"onTimeRate\", \"avgDeliveryDays\"));\n\n Map<String, Object> chartData = new LinkedHashMap<>();\n chartData.put(\"chartType\", \"kpi\");\n chartData.put(\"labels\", List.of(\"总订单\", \"已送达\", \"延误\"));\n chartData.put(\"values\", List.of(kpi.getTotalOrders(), kpi.getDeliveredOrders(), kpi.getDelayedOrders()));\n chartData.put(\"kpiSummary\", Map.of(\n \"onTimeRate\", kpi.getOnTimeRate() + \"%\",\n \"avgDeliveryDays\", kpi.getAvgDeliveryDays() + \" 天\"\n ));\n response.setChartData(chartData);\n\n List<Map<String, Object>> raw = List.of(Map.of(\n \"总订单\", kpi.getTotalOrders(),\n \"已送达\", kpi.getDeliveredOrders(),\n \"延误\", kpi.getDelayedOrders(),\n \"准时率\", kpi.getOnTimeRate() + \"%\",\n \"平均送达天数\", kpi.getAvgDeliveryDays()\n ));\n response.setRawData(raw);\n }\n\n case \"getTimeSeries\" -> {\n String gran = plan.params.getOrDefault(\"granularity\", \"month\").toString();\n if (!VALID_GRAN.contains(gran)) gran = \"month\";\n TimeSeriesResponse ts = dashboardService.getOrderVolumeTimeSeries(gran, startDate, endDate, carriers, regions, categories);\n response.setChartType(\"time_series\");\n response.setAnswer(plan.answerTemplate + \"(\" + startDate + \" 至 \" + endDate + \",粒度:\" + gran + \")\");\n response.setExplanation(\"按 \" + gran + \" 聚合的订单量趋势,来自实时数据库查询。\");\n response.setMetrics(List.of(\"订单量\", \"时间趋势\"));\n\n List<String> labels = ts.getData().stream().map(d -> d.getDate().toString()).collect(Collectors.toList());\n List<Long> values = ts.getData().stream().map(TimeSeriesData::getCount).collect(Collectors.toList());\n Map<String, Object> chartData = new LinkedHashMap<>();\n chartData.put(\"chartType\", \"line\");\n chartData.put(\"labels\", labels);\n chartData.put(\"values\", values);\n response.setChartData(chartData);\n\n List<Map<String, Object>> raw = ts.getData().stream()\n .map(d -> Map.<String,Object>of(\"date\", d.getDate().toString(), \"count\", d.getCount()))\n .collect(Collectors.toList());\n response.setRawData(raw);\n }\n\n case \"getBreakdown\" -> {\n CarrierBreakdownResponse bd = dashboardService.getCarrierBreakdown(startDate, endDate, carriers, regions, categories);\n response.setChartType(\"bar\");\n response.setAnswer(\"各承运商绩效分析(\" + startDate + \" 至 \" + endDate + \")\");\n response.setExplanation(\"按承运商汇总订单量和延误率,status='delayed' 计为延误。\");\n response.setMetrics(List.of(\"总订单\", \"延误订单\", \"延误率\"));\n\n List<String> labels = bd.getData().stream().map(CarrierBreakdown::getCarrier).collect(Collectors.toList());\n List<Long> values = bd.getData().stream().map(CarrierBreakdown::getTotalOrders).collect(Collectors.toList());\n Map<String, Object> chartData = new LinkedHashMap<>();\n chartData.put(\"chartType\", \"bar\");\n chartData.put(\"labels\", labels);\n chartData.put(\"values\", values);\n chartData.put(\"delayedValues\", bd.getData().stream().map(CarrierBreakdown::getDelayedOrders).collect(Collectors.toList()));\n chartData.put(\"delayRates\", bd.getData().stream().map(c -> c.getDelayRate().toString()).collect(Collectors.toList()));\n response.setChartData(chartData);\n\n List<Map<String, Object>> raw = bd.getData().stream()\n .map(c -> Map.<String,Object>of(\n \"carrier\", c.getCarrier(),\n \"totalOrders\", c.getTotalOrders(),\n \"delayedOrders\", c.getDelayedOrders(),\n \"delayRate\", c.getDelayRate() + \"%\"\n ))\n .collect(Collectors.toList());\n response.setRawData(raw);\n }\n\n case \"getDeliveryPerformance\" -> {\n String gran = plan.params.getOrDefault(\"granularity\", \"month\").toString();\n if (!VALID_GRAN.contains(gran)) gran = \"month\";\n DeliveryPerformanceResponse dp = dashboardService.getDeliveryPerformance(gran, startDate, endDate, carriers, regions, categories);\n response.setChartType(\"bar\");\n response.setAnswer(\"交付性能分析(\" + startDate + \" 至 \" + endDate + \")\");\n response.setExplanation(\"按时间段展示准时 vs 延误订单数,直接来自 status 字段统计。\");\n response.setMetrics(List.of(\"准时订单\", \"延误订单\"));\n\n List<String> labels = dp.getData().stream().map(DeliveryPerformance::getPeriod).collect(Collectors.toList());\n List<Long> onTime = dp.getData().stream().map(DeliveryPerformance::getOnTime).collect(Collectors.toList());\n List<Long> delayed = dp.getData().stream().map(DeliveryPerformance::getDelayed).collect(Collectors.toList());\n Map<String, Object> chartData = new LinkedHashMap<>();\n chartData.put(\"chartType\", \"bar\");\n chartData.put(\"labels\", labels);\n chartData.put(\"values\", onTime);\n chartData.put(\"delayedValues\", delayed);\n response.setChartData(chartData);\n\n List<Map<String, Object>> raw = dp.getData().stream()\n .map(d -> {\n Map<String, Object> row = new LinkedHashMap<>();\n row.put(\"period\", d.getPeriod());\n row.put(\"onTime\", d.getOnTime());\n row.put(\"delayed\", d.getDelayed());\n return row;\n })\n .collect(Collectors.toList());\n response.setRawData(raw);\n }\n\n case \"forecastDemand\" -> {\n String gran = plan.params.getOrDefault(\"granularity\", \"week\").toString();\n if (!VALID_GRAN.contains(gran)) gran = \"week\";\n int periods = Integer.parseInt(plan.params.getOrDefault(\"periods\", 4).toString());\n ForecastResponse fc = forecastingService.forecastDemand(gran, periods, startDate, endDate, carriers, regions, categories);\n response.setChartType(\"forecast\");\n response.setAnswer(\"未来 \" + periods + \" 个\" + granLabel(gran) + \"订单量预测\");\n response.setExplanation(\"使用 \" + fc.getAlgorithm() + \" 基于历史数据确定性计算。AI 仅负责参数提取,不生成预测值。\");\n response.setMetrics(List.of(\"历史订单量\", \"预测订单量\", \"安全库存建议\"));\n\n List<String> labels = fc.getData().stream().map(d -> d.getDate().toString()).collect(Collectors.toList());\n List<Long> histVals = fc.getData().stream().filter(d -> !d.isForecast()).map(ForecastData::getValue).collect(Collectors.toList());\n List<Long> foreVals = fc.getData().stream().filter(ForecastData::isForecast).map(ForecastData::getValue).collect(Collectors.toList());\n int splitIdx = (int) fc.getData().stream().filter(d -> !d.isForecast()).count();\n Map<String, Object> chartData = new LinkedHashMap<>();\n chartData.put(\"chartType\", \"forecast\");\n chartData.put(\"labels\", labels);\n chartData.put(\"historicalValues\", histVals);\n chartData.put(\"forecastValues\", foreVals);\n chartData.put(\"splitIndex\", splitIdx);\n chartData.put(\"algorithm\", fc.getAlgorithm());\n chartData.put(\"recommendations\", fc.getRecommendations());\n response.setChartData(chartData);\n\n List<Map<String, Object>> raw = fc.getData().stream()\n .map(d -> Map.<String,Object>of(\n \"date\", d.getDate().toString(),\n \"value\", d.getValue(),\n \"isForecast\", d.isForecast()\n ))\n .collect(Collectors.toList());\n response.setRawData(raw);\n }\n\n default -> {\n response.setChartType(\"none\");\n response.setAnswer(\"无法识别的分析类型,请换一种问法。\");\n }\n }\n\n return response;\n }\n\n // ── Helpers ───────────────────────────────────────────────────────────────\n\n private String formatKpiAnswer(KpiResponse kpi, String template, List<String> carriers,\n List<String> regions, LocalDate start, LocalDate end) {\n String scope = \"\";\n if (!carriers.isEmpty()) scope += \" [承运商: \" + String.join(\", \", carriers) + \"]\";\n if (!regions.isEmpty()) scope += \" [地区: \" + String.join(\", \", regions) + \"]\";\n return String.format(\"%s (%s ~ %s)%s\\n总订单: %d | 已送达: %d | 延误: %d | 准时率: %s%% | 平均送达: %s天\",\n template, start, end, scope,\n kpi.getTotalOrders(), kpi.getDeliveredOrders(), kpi.getDelayedOrders(),\n kpi.getOnTimeRate(), kpi.getAvgDeliveryDays());\n }\n\n private String granLabel(String gran) {\n return switch (gran) { case \"day\" -> \"天\"; case \"week\" -> \"周\"; default -> \"月\"; };\n }\n\n // ── Inner class ───────────────────────────────────────────────────────────\n\n private static class ToolPlan {\n String tool;\n String answerTemplate;\n Map<String, Object> params;\n }\n}\n"}, "300de37ed8a5143af93e2c04610e62ea92d6f0d3": {"t": 1785760114.0103295, "n": 27824, "h": "bcd357b589dfcd98", "c": "package com.logistics.dashboard.ai.orchestrator;\n\nimport com.logistics.dashboard.ai.model.QueryRequest;\nimport com.logistics.dashboard.ai.model.QueryResponse;\nimport com.logistics.dashboard.ai.tools.AiTools;\nimport com.logistics.dashboard.dto.*;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.ChatLanguageModel;\nimport dev.langchain4j.service.AiServices;\nimport dev.langchain4j.service.UserMessage;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Service;\n\nimport java.time.LocalDate;\nimport java.time.format.DateTimeFormatter;\nimport java.util.*;\nimport java.util.concurrent.ThreadLocalRandom;\nimport java.math.BigDecimal;\n\n@Service\npublic class NLUOrchestrator {\n\n // Keywords for data-related questions\n private static final Set<String> DATA_KEYWORDS = Set.of(\n \"订单\", \"order\", \"订购\", \"购买\", \"销量\",\n \"交付\", \"delivery\", \"交货\", \"运输\", \"送达\",\n \"承运商\", \"carrier\", \"快递\", \"物流公司\", \"运输商\",\n \"趋势\", \"trend\", \"变化\", \"增长\", \"下降\",\n \"性能\", \"performance\", \"效率\", \"准时\", \"延迟\",\n \"地区\", \"region\", \"区域\", \"城市\", \"地点\",\n \"预测\", \"forecast\", \"预计\", \"未来\", \"kpi\",\n \"指标\", \"metric\", \"统计\", \"数字\", \"数量\"\n );\n\n // Keywords for unrelated system questions (should not show visualizations)\n private static final Set<String> UNRELATED_KEYWORDS = Set.of(\n \"你是谁\", \"你是什么\", \"你叫什么\", \"你从哪来\",\n \"你好\", \"hello\", \"hi\", \"你好吗\", \"how are you\",\n \"帮助\", \"help\", \"说明\", \"解释\", \"教程\",\n \"天气\", \"time\", \"日期\", \"今天星期几\", \"现在几点\",\n \"who are you\", \"what are you\", \"what is your name\", \"where are you from\",\n \"thanks\", \"thank you\", \"bye\", \"goodbye\", \"see you\"\n );\n\n private ChatLanguageModel model;\n private AiAssistant assistant;\n private final AiTools aiTools;\n\n public NLUOrchestrator(\n @Value(\"${openai.api.key:}\") String apiKey,\n AiTools aiTools) {\n\n this.aiTools = aiTools;\n\n // Check if API key is provided\n if (apiKey == null || apiKey.trim().isEmpty()) {\n System.out.println(\"WARNING: OPENAI_API_KEY not provided. Natural Language Query functionality will be limited.\");\n System.out.println(\"DEBUG: API key is null or empty\");\n // Create a mock model that returns placeholder responses\n this.model = null;\n this.assistant = null;\n } else {\n try {\n System.out.println(\"DEBUG: Attempting to initialize AI model with provided API key\");\n System.out.println(\"DEBUG: API key length: \" + apiKey.length());\n System.out.println(\"DEBUG: API key prefix: \" + (apiKey.length() > 8 ? apiKey.substring(0, 8) + \"...\" : apiKey));\n\n // Create chat model with DeepSeek (OpenAI-compatible API)\n this.model = dev.langchain4j.model.openai.OpenAiChatModel.builder()\n .apiKey(apiKey)\n .baseUrl(\"https://api.deepseek.com\")\n .modelName(\"deepseek-chat\")\n .temperature(0.1)\n .maxTokens(500)\n .build();\n\n // Create chat memory\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n // Create AI assistant with tool support\n this.assistant = AiServices.builder(AiAssistant.class)\n .chatLanguageModel(model)\n .chatMemory(chatMemory)\n .tools(aiTools)\n .build();\n\n System.out.println(\"AI model initialized successfully with DeepSeek API\");\n } catch (Exception e) {\n System.out.println(\"WARNING: Failed to initialize AI model. Natural Language Query functionality will be limited.\");\n System.out.println(\"DEBUG: Error details: \" + e.getClass().getName() + \": \" + e.getMessage());\n e.printStackTrace();\n this.model = null;\n this.assistant = null;\n }\n }\n }\n\n /**\n * Process a natural language query and return structured response\n */\n public QueryResponse processQuery(QueryRequest request) {\n System.out.println(\"DEBUG processQuery: starting with question: \" + request.getQuestion());\n System.out.println(\"DEBUG processQuery: model is \" + (model == null ? \"null\" : \"available\"));\n System.out.println(\"DEBUG processQuery: assistant is \" + (assistant == null ? \"null\" : \"available\"));\n try {\n // Check if AI model is available\n if (model == null || assistant == null) {\n System.out.println(\"DEBUG processQuery: AI model not available, using mock data path\");\n QueryResponse placeholderResponse = new QueryResponse();\n placeholderResponse.setAnswer(\"自然语言查询功能需要配置AI API密钥。请设置OPENAI_API_KEY环境变量。\");\n placeholderResponse.setExplanation(\"此功能使用DeepSeek的AI模型来解释关于物流数据的自然语言问题。\");\n placeholderResponse.setChartType(\"none\");\n\n // You can still use other dashboard features without OpenAI\n if (request.getQuestion() != null && !request.getQuestion().isEmpty()) {\n String question = request.getQuestion().toLowerCase();\n if (question.contains(\"order\") || question.contains(\"delivery\") || question.contains(\"carrier\")) {\n placeholderResponse.setAnswer(\"自然语言查询当前不可用。请使用仪表板的标准分析功能查看订单、交货和承运商数据。\");\n }\n }\n\n // Add mock data for visualization and raw data when AI is not available\n populateResponseWithMockData(placeholderResponse, request, request.getQuestion());\n return placeholderResponse;\n }\n\n // Prepare system message with context\n String systemPrompt = createSystemPrompt(request);\n\n // Get response from AI assistant - combine system prompt and user question\n String fullMessage = \"System: \" + systemPrompt + \"\\n\\nUser: \" + request.getQuestion();\n String aiResponse = assistant.chat(fullMessage);\n\n // For now, return a simple response\n // In a full implementation, we would parse the AI's tool calls and execute them\n QueryResponse response = new QueryResponse();\n response.setAnswer(aiResponse);\n response.setExplanation(\"AI已解读您的问题并选择了合适的分析工具。\");\n response.setChartType(\"none\");\n\n // Add filters if provided\n if (request.getStartDate() != null || request.getEndDate() != null ||\n request.getCarriers() != null || request.getRegions() != null) {\n Map<String, Object> filters = new HashMap<>();\n if (request.getStartDate() != null) filters.put(\"startDate\", request.getStartDate());\n if (request.getEndDate() != null) filters.put(\"endDate\", request.getEndDate());\n if (request.getCarriers() != null) filters.put(\"carriers\", request.getCarriers());\n if (request.getRegions() != null) filters.put(\"regions\", request.getRegions());\n response.setFilters(filters);\n }\n\n // Always add mock data for visualization and raw data for demo purposes\n // This ensures frontend has data to display even when AI only returns text\n populateResponseWithMockData(response, request, request.getQuestion());\n\n return response;\n\n } catch (Exception e) {\n // If AI fails, return a helpful message instead of error\n QueryResponse response = new QueryResponse();\n response.setAnswer(\"自然语言查询功能当前不可用。请使用仪表板的标准分析功能查看订单、交货和承运商数据。\");\n response.setExplanation(\"AI服务暂时不可用,但您仍然可以使用筛选器查看数据。\");\n response.setChartType(\"none\");\n response.setError(null); // Explicitly set error to null\n System.out.println(\"AI query failed: \" + e.getClass().getSimpleName() + \": \" + e.getMessage());\n\n // Add mock data for visualization and raw data\n populateResponseWithMockData(response, request, request.getQuestion());\n return response;\n }\n }\n\n /**\n * Create system prompt with context about available tools and data\n */\n private String createSystemPrompt(QueryRequest request) {\n StringBuilder prompt = new StringBuilder();\n prompt.append(\"You are an AI assistant for a logistics analytics dashboard. \");\n prompt.append(\"Your role is to interpret user questions about logistics data and select appropriate analytical tools. \");\n prompt.append(\"\\n\\n\");\n prompt.append(\"Available data includes: orders, delivery performance, carrier performance, and time series trends. \");\n prompt.append(\"All data is read-only from a PostgreSQL database. \");\n prompt.append(\"\\n\\n\");\n prompt.append(\"You have access to these tools:\\n\");\n prompt.append(\"1. getTimeSeries - Get aggregated values over time (day, week, month)\\n\");\n prompt.append(\"2. getBreakdown - Get aggregated values by categorical dimension (carrier, region)\\n\");\n prompt.append(\"3. getKpi - Get key performance indicators (total orders, delivered, delayed, on-time rate, avg delivery days)\\n\");\n prompt.append(\"4. getDeliveryPerformance - Get delivery performance data (on-time vs delayed)\\n\");\n prompt.append(\"5. forecastDemand - Forecast future order volume (placeholder for now)\\n\");\n prompt.append(\"\\n\");\n prompt.append(\"IMPORTANT RULES:\\n\");\n prompt.append(\"- NEVER make up or guess data values. Always use the tools.\\n\");\n prompt.append(\"- If a user asks about specific dates, use the date range provided.\\n\");\n prompt.append(\"- If no date range is specified, suggest using a reasonable default (e.g., last 30 days).\\n\");\n prompt.append(\"- Always explain what tools you would use and why.\\n\");\n prompt.append(\"\\n\");\n\n // Add context about filters if provided\n if (request.getStartDate() != null || request.getEndDate() != null) {\n prompt.append(\"Context: User has specified date range: \");\n if (request.getStartDate() != null) prompt.append(\"from \").append(request.getStartDate());\n if (request.getEndDate() != null) prompt.append(\" to \").append(request.getEndDate());\n prompt.append(\"\\n\");\n }\n\n if (request.getCarriers() != null && !request.getCarriers().isEmpty()) {\n prompt.append(\"Context: User filtered by carriers: \").append(String.join(\", \", request.getCarriers())).append(\"\\n\");\n }\n\n if (request.getRegions() != null && !request.getRegions().isEmpty()) {\n prompt.append(\"Context: User filtered by regions: \").append(String.join(\", \", request.getRegions())).append(\"\\n\");\n }\n\n prompt.append(\"\\n\");\n prompt.append(\"Your response should be a clear, concise answer to the user's question, \");\n prompt.append(\"mentioning which tools would be used and what the results would show.\");\n\n return prompt.toString();\n }\n\n /**\n * AI Assistant interface for LangChain4j\n */\n interface AiAssistant {\n\n String chat(@UserMessage String message);\n }\n\n /**\n * Check if a question is related to logistics data\n * Returns false for system interaction questions (e.g., \"who are you\")\n * Returns true only if question contains logistics-related keywords\n * Returns false by default for ambiguous questions\n */\n private boolean isDataRelatedQuestion(String question) {\n if (question == null || question.isEmpty()) {\n System.out.println(\"DEBUG isDataRelatedQuestion: null or empty question, returning false\");\n return false;\n }\n\n String lower = question.toLowerCase();\n System.out.println(\"DEBUG isDataRelatedQuestion: checking question: \" + question + \", lower: \" + lower);\n System.out.println(\"DEBUG isDataRelatedQuestion: UNRELATED_KEYWORDS size: \" + UNRELATED_KEYWORDS.size() + \", DATA_KEYWORDS size: \" + DATA_KEYWORDS.size());\n\n // Check for unrelated system interaction keywords first\n for (String keyword : UNRELATED_KEYWORDS) {\n if (lower.contains(keyword)) {\n System.out.println(\"DEBUG isDataRelatedQuestion: found unrelated keyword: \" + keyword + \", returning false\");\n return false;\n }\n }\n\n // Check for logistics data keywords\n for (String keyword : DATA_KEYWORDS) {\n if (lower.contains(keyword)) {\n System.out.println(\"DEBUG isDataRelatedQuestion: found data keyword: \" + keyword + \", returning true\");\n return true;\n }\n }\n\n // Default: assume NOT data-related for ambiguous questions\n // This prevents showing charts for unclear questions\n System.out.println(\"DEBUG isDataRelatedQuestion: no keywords found, returning false (ambiguous question)\");\n return false;\n }\n\n /**\n * Populate basic response fields for non-data-related questions\n */\n private void populateBasicResponseFields(QueryResponse response, QueryRequest request,\n String question, String explanation) {\n response.setExplanation(explanation);\n\n // Set default metrics\n List<String> defaultMetrics = Arrays.asList(\"系统交互\", \"问题分类\");\n response.setMetrics(defaultMetrics);\n\n // Set query plan\n response.setQueryPlan(\"系统检测到此问题与物流数据无关,提供通用响应。\");\n\n // Add filters if present in request\n if (request.getStartDate() != null || request.getEndDate() != null ||\n request.getCarriers() != null || request.getRegions() != null) {\n Map<String, Object> filters = new HashMap<>();\n if (request.getStartDate() != null) filters.put(\"startDate\", request.getStartDate());\n if (request.getEndDate() != null) filters.put(\"endDate\", request.getEndDate());\n if (request.getCarriers() != null) filters.put(\"carriers\", request.getCarriers());\n if (request.getRegions() != null) filters.put(\"regions\", request.getRegions());\n response.setFilters(filters);\n }\n\n // Generate minimal raw data\n List<Map<String, Object>> rawData = new ArrayList<>();\n Map<String, Object> sampleRow = new HashMap<>();\n sampleRow.put(\"question\", question);\n sampleRow.put(\"question_type\", \"system_interaction\");\n sampleRow.put(\"response_type\", \"text_only\");\n sampleRow.put(\"timestamp\", LocalDate.now().toString());\n rawData.add(sampleRow);\n response.setRawData(rawData);\n }\n\n /**\n * Populate response with mock data for visualization and raw data when AI is unavailable\n */\n private void populateResponseWithMockData(QueryResponse response, QueryRequest request, String question) {\n if (question == null || question.isEmpty()) {\n question = \"订单趋势\";\n }\n\n // Check if question is related to logistics data\n boolean isDataRelated = isDataRelatedQuestion(question);\n System.out.println(\"DEBUG populateResponseWithMockData: isDataRelated = \" + isDataRelated + \" for question: \" + question);\n\n if (!isDataRelated) {\n System.out.println(\"DEBUG populateResponseWithMockData: non-data-related question, setting empty chart state\");\n // Non-data-related question: set empty chart state\n response.setChartType(\"none\");\n\n // Create empty state chart data\n Map<String, Object> emptyChartData = new HashMap<>();\n emptyChartData.put(\"labels\", new ArrayList<String>());\n emptyChartData.put(\"values\", new ArrayList<Long>());\n emptyChartData.put(\"chartType\", \"none\");\n emptyChartData.put(\"message\", \"此问题与物流数据无关,无可视化数据。\");\n\n response.setChartData(emptyChartData);\n\n // Set basic response fields (prevents blank query details)\n populateBasicResponseFields(response, request, question, \"This is a system interaction question, not related to logistics data analysis.\");\n return;\n }\n\n String lowerQuestion = question.toLowerCase();\n Random random = ThreadLocalRandom.current();\n\n // Ensure basic fields have default values for data-related questions\n if (response.getExplanation() == null || response.getExplanation().isEmpty()) {\n response.setExplanation(\"根据您的查询生成的分析结果。\");\n }\n\n if (response.getMetrics() == null || response.getMetrics().isEmpty()) {\n List<String> defaultMetrics = Arrays.asList(\"订单量\", \"交货性能\", \"承运商分析\");\n response.setMetrics(defaultMetrics);\n }\n\n if (response.getQueryPlan() == null || response.getQueryPlan().isEmpty()) {\n if (model == null) {\n response.setQueryPlan(\"使用模拟数据生成查询结果。当实际AI功能恢复时,将基于真实数据进行分析。\");\n } else {\n response.setQueryPlan(\"AI功能正常,正在处理您的查询并使用模拟数据进行展示预览。\");\n }\n }\n\n // Determine chart type based on question content\n if (lowerQuestion.contains(\"趋势\") || lowerQuestion.contains(\"时间\") || lowerQuestion.contains(\"order\") || lowerQuestion.contains(\"trend\")) {\n // Time series data\n populateTimeSeriesMockData(response, random);\n } else if (lowerQuestion.contains(\"承运商\") || lowerQuestion.contains(\"carrier\") || lowerQuestion.contains(\"快递\")) {\n // Carrier breakdown data\n populateCarrierBreakdownMockData(response, random);\n } else if (lowerQuestion.contains(\"交付\") || lowerQuestion.contains(\"delivery\") || lowerQuestion.contains(\"performance\")) {\n // Delivery performance data\n populateDeliveryPerformanceMockData(response, random);\n } else if (lowerQuestion.contains(\"kpi\") || lowerQuestion.contains(\"指标\") || lowerQuestion.contains(\"率\") || lowerQuestion.contains(\"rate\")) {\n // KPI data\n populateKpiMockData(response, random);\n } else {\n // Default to time series\n populateTimeSeriesMockData(response, random);\n }\n\n // Add filters from request\n if (request.getStartDate() != null || request.getEndDate() != null ||\n request.getCarriers() != null || request.getRegions() != null) {\n Map<String, Object> filters = new HashMap<>();\n if (request.getStartDate() != null) filters.put(\"startDate\", request.getStartDate());\n if (request.getEndDate() != null) filters.put(\"endDate\", request.getEndDate());\n if (request.getCarriers() != null) filters.put(\"carriers\", request.getCarriers());\n if (request.getRegions() != null) filters.put(\"regions\", request.getRegions());\n response.setFilters(filters);\n }\n\n // Set metrics based on question\n List<String> metrics = new ArrayList<>();\n if (lowerQuestion.contains(\"订单\") || lowerQuestion.contains(\"order\")) {\n metrics.add(\"订单量\");\n metrics.add(\"订单价值\");\n }\n if (lowerQuestion.contains(\"交付\") || lowerQuestion.contains(\"delivery\")) {\n metrics.add(\"准时交货率\");\n metrics.add(\"平均交货天数\");\n metrics.add(\"延迟订单数\");\n }\n if (lowerQuestion.contains(\"承运商\") || lowerQuestion.contains(\"carrier\")) {\n metrics.add(\"承运商分布\");\n metrics.add(\"延迟率对比\");\n }\n if (metrics.isEmpty()) {\n metrics.add(\"订单趋势\");\n metrics.add(\"交货性能\");\n }\n response.setMetrics(metrics);\n\n // Set query plan based on AI availability\n if (model == null) {\n response.setQueryPlan(\"AI服务暂时不可用。系统正在使用模拟数据展示查询结果。当实际功能恢复时,将使用真实数据分析工具处理您的查询。\");\n } else {\n response.setQueryPlan(\"AI功能正常。当前使用模拟数据进行展示预览,实际数据查询功能已就绪。\");\n }\n\n // Generate raw data (sample rows)\n List<Map<String, Object>> rawData = generateRawMockData(random);\n response.setRawData(rawData);\n }\n\n private void populateTimeSeriesMockData(QueryResponse response, Random random) {\n response.setChartType(\"time_series\");\n\n // Create simplified chart data for frontend\n Map<String, Object> chartData = new HashMap<>();\n List<String> labels = new ArrayList<>();\n List<Long> values = new ArrayList<>();\n\n LocalDate startDate = LocalDate.now().minusDays(30);\n\n for (int i = 0; i < 31; i++) {\n LocalDate date = startDate.plusDays(i);\n labels.add(date.format(DateTimeFormatter.ofPattern(\"MM-dd\")));\n values.add(50L + random.nextInt(200)); // Random between 50-250\n }\n\n chartData.put(\"labels\", labels);\n chartData.put(\"values\", values);\n chartData.put(\"chartType\", \"line\");\n\n response.setChartData(chartData);\n // Append mock data explanation to existing explanation if any\n String existingExplanation = response.getExplanation();\n String mockExplanation = \"显示过去30天订单趋势的模拟数据。当实际AI功能恢复时,将根据您的筛选条件提供真实的时间序列分析。\";\n if (existingExplanation == null || existingExplanation.isEmpty()) {\n response.setExplanation(mockExplanation);\n } else {\n response.setExplanation(existingExplanation + \" \" + mockExplanation);\n }\n }\n\n private void populateCarrierBreakdownMockData(QueryResponse response, Random random) {\n response.setChartType(\"bar\");\n\n // Create simplified chart data for frontend\n Map<String, Object> chartData = new HashMap<>();\n List<String> labels = new ArrayList<>();\n List<Long> values = new ArrayList<>();\n\n String[] carriers = {\"UPS\", \"FedEx\", \"DHL\", \"USPS\", \"Amazon Logistics\", \"Regional Carrier\"};\n\n for (String carrier : carriers) {\n labels.add(carrier);\n values.add(500L + random.nextInt(2000)); // 500-2500 orders\n }\n\n chartData.put(\"labels\", labels);\n chartData.put(\"values\", values);\n chartData.put(\"chartType\", \"bar\");\n\n response.setChartData(chartData);\n // Append mock data explanation to existing explanation if any\n String existingExplanation = response.getExplanation();\n String mockExplanation = \"显示各承运商订单分布的模拟数据。当实际AI功能恢复时,将根据您的筛选条件提供真实的承运商性能分析。\";\n if (existingExplanation == null || existingExplanation.isEmpty()) {\n response.setExplanation(mockExplanation);\n } else {\n response.setExplanation(existingExplanation + \" \" + mockExplanation);\n }\n }\n\n private void populateDeliveryPerformanceMockData(QueryResponse response, Random random) {\n response.setChartType(\"bar\");\n\n // Create simplified chart data for frontend\n Map<String, Object> chartData = new HashMap<>();\n List<String> labels = new ArrayList<>();\n List<Long> values = new ArrayList<>();\n\n // Generate weekly data for last 4 weeks\n LocalDate today = LocalDate.now();\n\n for (int i = 3; i >= 0; i--) {\n LocalDate weekStart = today.minusWeeks(i).with(java.time.DayOfWeek.MONDAY);\n String period = weekStart.format(DateTimeFormatter.ofPattern(\"yyyy-'W'ww\"));\n labels.add(period);\n Long onTime = 200L + random.nextInt(300); // 200-500\n values.add(onTime);\n }\n\n chartData.put(\"labels\", labels);\n chartData.put(\"values\", values);\n chartData.put(\"chartType\", \"bar\");\n\n response.setChartData(chartData);\n // Append mock data explanation to existing explanation if any\n String existingExplanation = response.getExplanation();\n String mockExplanation = \"显示过去4周准时交货数量的模拟数据。当实际AI功能恢复时,将根据您的筛选条件提供真实的交货性能分析。\";\n if (existingExplanation == null || existingExplanation.isEmpty()) {\n response.setExplanation(mockExplanation);\n } else {\n response.setExplanation(existingExplanation + \" \" + mockExplanation);\n }\n }\n\n private void populateKpiMockData(QueryResponse response, Random random) {\n response.setChartType(\"kpi\");\n\n // Create simplified chart data for frontend - for KPI we can show a bar chart with metrics\n Map<String, Object> chartData = new HashMap<>();\n List<String> labels = new ArrayList<>();\n List<Long> values = new ArrayList<>();\n\n Long totalOrders = 10000L + random.nextInt(20000); // 10000-30000\n Long deliveredOrders = totalOrders * 8 / 10 + random.nextInt(totalOrders.intValue() / 10); // 80-90% delivered\n Long delayedOrders = deliveredOrders / 10 + random.nextInt(deliveredOrders.intValue() / 5); // 10-30% of delivered delayed\n BigDecimal onTimeRate = BigDecimal.valueOf((deliveredOrders - delayedOrders) * 100.0 / deliveredOrders).setScale(2, BigDecimal.ROUND_HALF_UP);\n BigDecimal avgDeliveryDays = BigDecimal.valueOf(3.5 + random.nextDouble() * 2.5).setScale(2, BigDecimal.ROUND_HALF_UP); // 3.5-6.0 days\n\n // Create KPI summary for chart\n labels.add(\"Total Orders\");\n values.add(totalOrders);\n labels.add(\"Delivered Orders\");\n values.add(deliveredOrders);\n labels.add(\"Delayed Orders\");\n values.add(delayedOrders);\n\n chartData.put(\"labels\", labels);\n chartData.put(\"values\", values);\n chartData.put(\"chartType\", \"bar\");\n chartData.put(\"kpiSummary\", Map.of(\n \"onTimeRate\", onTimeRate.toString() + \"%\",\n \"avgDeliveryDays\", avgDeliveryDays.toString() + \" days\"\n ));\n\n response.setChartData(chartData);\n // Append mock data explanation to existing explanation if any\n String existingExplanation = response.getExplanation();\n String mockExplanation = \"显示关键绩效指标的模拟数据。准时交货率:\" + onTimeRate + \"%,平均交货天数:\" + avgDeliveryDays + \"天。当实际AI功能恢复时,将根据您的筛选条件提供真实的KPI分析。\";\n if (existingExplanation == null || existingExplanation.isEmpty()) {\n response.setExplanation(mockExplanation);\n } else {\n response.setExplanation(existingExplanation + \" \" + mockExplanation);\n }\n }\n\n private List<Map<String, Object>> generateRawMockData(Random random) {\n List<Map<String, Object>> rawData = new ArrayList<>();\n String[] carriers = {\"UPS\", \"FedEx\", \"DHL\", \"USPS\", \"Amazon Logistics\", \"Regional Carrier\"};\n String[] cities = {\"New York\", \"Los Angeles\", \"Chicago\", \"Houston\", \"Phoenix\", \"Philadelphia\"};\n String[] statuses = {\"delivered\", \"in_transit\", \"pending\", \"cancelled\"};\n\n for (int i = 0; i < 10; i++) { // Generate 10 sample rows\n Map<String, Object> row = new HashMap<>();\n row.put(\"id\", i + 1);\n row.put(\"order_date\", LocalDate.now().minusDays(random.nextInt(30)).toString());\n row.put(\"promised_delivery_date\", LocalDate.now().minusDays(random.nextInt(20)).toString());\n row.put(\"actual_delivery_date\", LocalDate.now().minusDays(random.nextInt(15)).toString());\n row.put(\"status\", statuses[random.nextInt(statuses.length)]);\n row.put(\"carrier\", carriers[random.nextInt(carriers.length)]);\n row.put(\"destination_city\", cities[random.nextInt(cities.length)]);\n row.put(\"destination_state\", \"CA\");\n row.put(\"destination_region\", \"West\");\n row.put(\"order_value\", BigDecimal.valueOf(50 + random.nextDouble() * 4950).setScale(2, BigDecimal.ROUND_HALF_UP));\n row.put(\"sku\", \"SKU-\" + String.format(\"%04d\", 1000 + random.nextInt(9000)));\n row.put(\"quantity\", 1 + random.nextInt(50));\n\n rawData.add(row);\n }\n\n return rawData;\n }\n}"}, "2dc3c3593dc5ba2a6af3c96cb948eb812618b66e": {"t": 1785760115.5085433, "n": 23329, "h": "afe60a053dbb2428", "c": "package com.logistics.dashboard.ai.orchestrator;\n\nimport com.logistics.dashboard.ai.model.QueryRequest;\nimport com.logistics.dashboard.ai.model.QueryResponse;\nimport com.logistics.dashboard.ai.tools.AiTools;\nimport com.logistics.dashboard.dto.*;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.ChatLanguageModel;\nimport dev.langchain4j.service.AiServices;\nimport dev.langchain4j.service.UserMessage;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Service;\n\nimport java.time.LocalDate;\nimport java.time.format.DateTimeFormatter;\nimport java.util.*;\nimport java.util.concurrent.ThreadLocalRandom;\nimport java.math.BigDecimal;\n\n@Service\npublic class NLUOrchestrator {\n\n private ChatLanguageModel model;\n private AiAssistant assistant;\n private final AiTools aiTools;\n\n public NLUOrchestrator(\n @Value(\"${openai.api.key:}\") String apiKey,\n AiTools aiTools) {\n\n this.aiTools = aiTools;\n\n // Check if API key is provided\n if (apiKey == null || apiKey.trim().isEmpty()) {\n System.out.println(\"WARNING: OPENAI_API_KEY not provided. Natural Language Query functionality will be limited.\");\n // Create a mock model that returns placeholder responses\n this.model = null;\n this.assistant = null;\n } else {\n try {\n // Create chat model with DeepSeek (OpenAI-compatible API)\n this.model = dev.langchain4j.model.openai.OpenAiChatModel.builder()\n .apiKey(apiKey)\n .baseUrl(\"https://api.deepseek.com\")\n .modelName(\"deepseek-chat\")\n .temperature(0.1)\n .maxTokens(500)\n .build();\n\n // Create chat memory\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n // Create AI assistant with tool support\n this.assistant = AiServices.builder(AiAssistant.class)\n .chatLanguageModel(model)\n .chatMemory(chatMemory)\n .tools(aiTools)\n .build();\n\n System.out.println(\"AI model initialized successfully with DeepSeek API\");\n } catch (Exception e) {\n System.out.println(\"WARNING: Failed to initialize AI model. Natural Language Query functionality will be limited. Error: \" + e.getMessage());\n this.model = null;\n this.assistant = null;\n }\n }\n }\n\n /**\n * Process a natural language query and return structured response\n */\n public QueryResponse processQuery(QueryRequest request) {\n try {\n // Check if AI model is available\n if (model == null || assistant == null) {\n QueryResponse placeholderResponse = new QueryResponse();\n placeholderResponse.setAnswer(\"自然语言查询功能需要配置AI API密钥。请设置OPENAI_API_KEY环境变量。\");\n placeholderResponse.setExplanation(\"此功能使用DeepSeek的AI模型来解释关于物流数据的自然语言问题。\");\n placeholderResponse.setChartType(\"none\");\n\n // You can still use other dashboard features without OpenAI\n if (request.getQuestion() != null && !request.getQuestion().isEmpty()) {\n String question = request.getQuestion().toLowerCase();\n if (question.contains(\"order\") || question.contains(\"delivery\") || question.contains(\"carrier\")) {\n placeholderResponse.setAnswer(\"自然语言查询当前不可用。请使用仪表板的标准分析功能查看订单、交货和承运商数据。\");\n }\n }\n\n // Add mock data for visualization and raw data when AI is not available\n populateResponseWithMockData(placeholderResponse, request, request.getQuestion());\n return placeholderResponse;\n }\n\n // Prepare system message with context\n String systemPrompt = createSystemPrompt(request);\n\n // Get response from AI assistant - combine system prompt and user question\n String fullMessage = \"System: \" + systemPrompt + \"\\n\\nUser: \" + request.getQuestion();\n String aiResponse = assistant.chat(fullMessage);\n\n // For now, return a simple response\n // In a full implementation, we would parse the AI's tool calls and execute them\n QueryResponse response = new QueryResponse();\n response.setAnswer(aiResponse);\n response.setExplanation(\"The AI interpreted your question and selected appropriate analytical tools.\");\n response.setChartType(\"none\");\n\n // Add filters if provided\n if (request.getStartDate() != null || request.getEndDate() != null ||\n request.getCarriers() != null || request.getRegions() != null) {\n Map<String, Object> filters = new HashMap<>();\n if (request.getStartDate() != null) filters.put(\"startDate\", request.getStartDate());\n if (request.getEndDate() != null) filters.put(\"endDate\", request.getEndDate());\n if (request.getCarriers() != null) filters.put(\"carriers\", request.getCarriers());\n if (request.getRegions() != null) filters.put(\"regions\", request.getRegions());\n response.setFilters(filters);\n }\n\n // Always add mock data for visualization and raw data for demo purposes\n // This ensures frontend has data to display even when AI only returns text\n populateResponseWithMockData(response, request, request.getQuestion());\n\n return response;\n\n } catch (Exception e) {\n // If AI fails, return a helpful message instead of error\n QueryResponse response = new QueryResponse();\n response.setAnswer(\"自然语言查询功能当前不可用。请使用仪表板的标准分析功能查看订单、交货和承运商数据。\");\n response.setExplanation(\"AI服务暂时不可用,但您仍然可以使用筛选器查看数据。\");\n response.setChartType(\"none\");\n response.setError(null); // Explicitly set error to null\n System.out.println(\"AI query failed: \" + e.getClass().getSimpleName() + \": \" + e.getMessage());\n\n // Add mock data for visualization and raw data\n populateResponseWithMockData(response, request, request.getQuestion());\n return response;\n }\n }\n\n /**\n * Create system prompt with context about available tools and data\n */\n private String createSystemPrompt(QueryRequest request) {\n StringBuilder prompt = new StringBuilder();\n prompt.append(\"You are an AI assistant for a logistics analytics dashboard. \");\n prompt.append(\"Your role is to interpret user questions about logistics data and select appropriate analytical tools. \");\n prompt.append(\"\\n\\n\");\n prompt.append(\"Available data includes: orders, delivery performance, carrier performance, and time series trends. \");\n prompt.append(\"All data is read-only from a PostgreSQL database. \");\n prompt.append(\"\\n\\n\");\n prompt.append(\"You have access to these tools:\\n\");\n prompt.append(\"1. getTimeSeries - Get aggregated values over time (day, week, month)\\n\");\n prompt.append(\"2. getBreakdown - Get aggregated values by categorical dimension (carrier, region)\\n\");\n prompt.append(\"3. getKpi - Get key performance indicators (total orders, delivered, delayed, on-time rate, avg delivery days)\\n\");\n prompt.append(\"4. getDeliveryPerformance - Get delivery performance data (on-time vs delayed)\\n\");\n prompt.append(\"5. forecastDemand - Forecast future order volume (placeholder for now)\\n\");\n prompt.append(\"\\n\");\n prompt.append(\"IMPORTANT RULES:\\n\");\n prompt.append(\"- NEVER make up or guess data values. Always use the tools.\\n\");\n prompt.append(\"- If a user asks about specific dates, use the date range provided.\\n\");\n prompt.append(\"- If no date range is specified, suggest using a reasonable default (e.g., last 30 days).\\n\");\n prompt.append(\"- Always explain what tools you would use and why.\\n\");\n prompt.append(\"\\n\");\n\n // Add context about filters if provided\n if (request.getStartDate() != null || request.getEndDate() != null) {\n prompt.append(\"Context: User has specified date range: \");\n if (request.getStartDate() != null) prompt.append(\"from \").append(request.getStartDate());\n if (request.getEndDate() != null) prompt.append(\" to \").append(request.getEndDate());\n prompt.append(\"\\n\");\n }\n\n if (request.getCarriers() != null && !request.getCarriers().isEmpty()) {\n prompt.append(\"Context: User filtered by carriers: \").append(String.join(\", \", request.getCarriers())).append(\"\\n\");\n }\n\n if (request.getRegions() != null && !request.getRegions().isEmpty()) {\n prompt.append(\"Context: User filtered by regions: \").append(String.join(\", \", request.getRegions())).append(\"\\n\");\n }\n\n prompt.append(\"\\n\");\n prompt.append(\"Your response should be a clear, concise answer to the user's question, \");\n prompt.append(\"mentioning which tools would be used and what the results would show.\");\n\n return prompt.toString();\n }\n\n /**\n * AI Assistant interface for LangChain4j\n */\n interface AiAssistant {\n\n String chat(@UserMessage String message);\n }\n\n /**\n * Check if a question is related to logistics data\n */\n private boolean isDataRelatedQuestion(String question) {\n if (question == null || question.isEmpty()) return false;\n\n String lower = question.toLowerCase();\n\n // 物流数据相关关键词\n String[] dataKeywords = {\n \"订单\", \"order\", \"订购\", \"购买\", \"销量\",\n \"交付\", \"delivery\", \"交货\", \"运输\", \"送达\",\n \"承运商\", \"carrier\", \"快递\", \"物流公司\", \"运输商\",\n \"趋势\", \"trend\", \"变化\", \"增长\", \"下降\",\n \"性能\", \"performance\", \"效率\", \"准时\", \"延迟\",\n \"地区\", \"region\", \"区域\", \"城市\", \"地点\",\n \"预测\", \"forecast\", \"预计\", \"未来\", \"kpi\",\n \"指标\", \"metric\", \"统计\", \"数字\", \"数量\"\n };\n\n // 系统无关关键词(应跳过可视化)\n String[] unrelatedKeywords = {\n \"你是谁\", \"你是什么\", \"你叫什么\", \"你从哪来\",\n \"你好\", \"hello\", \"hi\", \"你好吗\", \"how are you\",\n \"帮助\", \"help\", \"说明\", \"解释\", \"教程\",\n \"天气\", \"time\", \"日期\", \"今天星期几\", \"现在几点\"\n };\n\n // 检查是否包含无关关键词\n for (String keyword : unrelatedKeywords) {\n if (lower.contains(keyword)) return false;\n }\n\n // 检查是否包含物流关键词\n for (String keyword : dataKeywords) {\n if (lower.contains(keyword)) return true;\n }\n\n // 默认:假设与数据相关(保持现有行为)\n return true;\n }\n\n /**\n * Populate basic response fields for non-data-related questions\n */\n private void populateBasicResponseFields(QueryResponse response, QueryRequest request,\n String question, String explanation) {\n response.setExplanation(explanation);\n\n // 设置默认指标\n List<String> defaultMetrics = Arrays.asList(\"系统交互\", \"问题分类\");\n response.setMetrics(defaultMetrics);\n\n // 设置查询计划\n response.setQueryPlan(\"系统检测到此问题与物流数据无关,提供通用回答。\");\n\n // 添加筛选条件\n if (request.getStartDate() != null || request.getEndDate() != null ||\n request.getCarriers() != null || request.getRegions() != null) {\n Map<String, Object> filters = new HashMap<>();\n if (request.getStartDate() != null) filters.put(\"startDate\", request.getStartDate());\n if (request.getEndDate() != null) filters.put(\"endDate\", request.getEndDate());\n if (request.getCarriers() != null) filters.put(\"carriers\", request.getCarriers());\n if (request.getRegions() != null) filters.put(\"regions\", request.getRegions());\n response.setFilters(filters);\n }\n\n // 生成最小化原始数据\n List<Map<String, Object>> rawData = new ArrayList<>();\n Map<String, Object> sampleRow = new HashMap<>();\n sampleRow.put(\"question\", question);\n sampleRow.put(\"question_type\", \"system_interaction\");\n sampleRow.put(\"response_type\", \"text_only\");\n sampleRow.put(\"timestamp\", LocalDate.now().toString());\n rawData.add(sampleRow);\n response.setRawData(rawData);\n }\n\n /**\n * Populate response with mock data for visualization and raw data when AI is unavailable\n */\n private void populateResponseWithMockData(QueryResponse response, QueryRequest request, String question) {\n if (question == null || question.isEmpty()) {\n question = \"订单趋势\";\n }\n\n String lowerQuestion = question.toLowerCase();\n Random random = ThreadLocalRandom.current();\n\n // Determine chart type based on question content\n if (lowerQuestion.contains(\"趋势\") || lowerQuestion.contains(\"时间\") || lowerQuestion.contains(\"order\") || lowerQuestion.contains(\"trend\")) {\n // Time series data\n populateTimeSeriesMockData(response, random);\n } else if (lowerQuestion.contains(\"承运商\") || lowerQuestion.contains(\"carrier\") || lowerQuestion.contains(\"快递\")) {\n // Carrier breakdown data\n populateCarrierBreakdownMockData(response, random);\n } else if (lowerQuestion.contains(\"交付\") || lowerQuestion.contains(\"delivery\") || lowerQuestion.contains(\"performance\")) {\n // Delivery performance data\n populateDeliveryPerformanceMockData(response, random);\n } else if (lowerQuestion.contains(\"kpi\") || lowerQuestion.contains(\"指标\") || lowerQuestion.contains(\"率\") || lowerQuestion.contains(\"rate\")) {\n // KPI data\n populateKpiMockData(response, random);\n } else {\n // Default to time series\n populateTimeSeriesMockData(response, random);\n }\n\n // Add filters from request\n if (request.getStartDate() != null || request.getEndDate() != null ||\n request.getCarriers() != null || request.getRegions() != null) {\n Map<String, Object> filters = new HashMap<>();\n if (request.getStartDate() != null) filters.put(\"startDate\", request.getStartDate());\n if (request.getEndDate() != null) filters.put(\"endDate\", request.getEndDate());\n if (request.getCarriers() != null) filters.put(\"carriers\", request.getCarriers());\n if (request.getRegions() != null) filters.put(\"regions\", request.getRegions());\n response.setFilters(filters);\n }\n\n // Set metrics based on question\n List<String> metrics = new ArrayList<>();\n if (lowerQuestion.contains(\"订单\") || lowerQuestion.contains(\"order\")) {\n metrics.add(\"订单数量\");\n metrics.add(\"订单价值\");\n }\n if (lowerQuestion.contains(\"交付\") || lowerQuestion.contains(\"delivery\")) {\n metrics.add(\"准时交付率\");\n metrics.add(\"平均交付天数\");\n metrics.add(\"延迟订单数\");\n }\n if (lowerQuestion.contains(\"承运商\") || lowerQuestion.contains(\"carrier\")) {\n metrics.add(\"承运商分布\");\n metrics.add(\"延迟率比较\");\n }\n if (metrics.isEmpty()) {\n metrics.add(\"订单趋势\");\n metrics.add(\"交付表现\");\n }\n response.setMetrics(metrics);\n\n // Set query plan\n response.setQueryPlan(\"由于AI服务暂时不可用,系统使用模拟数据展示查询结果。实际功能恢复后,将使用真实数据分析工具处理您的查询。\");\n\n // Generate raw data (sample rows)\n List<Map<String, Object>> rawData = generateRawMockData(random);\n response.setRawData(rawData);\n }\n\n private void populateTimeSeriesMockData(QueryResponse response, Random random) {\n response.setChartType(\"time_series\");\n\n // Create simplified chart data for frontend\n Map<String, Object> chartData = new HashMap<>();\n List<String> labels = new ArrayList<>();\n List<Long> values = new ArrayList<>();\n\n LocalDate startDate = LocalDate.now().minusDays(30);\n\n for (int i = 0; i < 31; i++) {\n LocalDate date = startDate.plusDays(i);\n labels.add(date.format(DateTimeFormatter.ofPattern(\"MM-dd\")));\n values.add(50L + random.nextInt(200)); // Random between 50-250\n }\n\n chartData.put(\"labels\", labels);\n chartData.put(\"values\", values);\n chartData.put(\"chartType\", \"line\");\n\n response.setChartData(chartData);\n // Append mock data explanation to existing explanation if any\n String existingExplanation = response.getExplanation();\n String mockExplanation = \"显示过去30天的订单趋势模拟数据。实际AI功能恢复后,将基于您的筛选条件提供真实的时间序列分析。\";\n if (existingExplanation == null || existingExplanation.isEmpty()) {\n response.setExplanation(mockExplanation);\n } else {\n response.setExplanation(existingExplanation + \" \" + mockExplanation);\n }\n }\n\n private void populateCarrierBreakdownMockData(QueryResponse response, Random random) {\n response.setChartType(\"bar\");\n\n // Create simplified chart data for frontend\n Map<String, Object> chartData = new HashMap<>();\n List<String> labels = new ArrayList<>();\n List<Long> values = new ArrayList<>();\n\n String[] carriers = {\"UPS\", \"FedEx\", \"DHL\", \"USPS\", \"Amazon Logistics\", \"Regional Carrier\"};\n\n for (String carrier : carriers) {\n labels.add(carrier);\n values.add(500L + random.nextInt(2000)); // 500-2500 orders\n }\n\n chartData.put(\"labels\", labels);\n chartData.put(\"values\", values);\n chartData.put(\"chartType\", \"bar\");\n\n response.setChartData(chartData);\n // Append mock data explanation to existing explanation if any\n String existingExplanation = response.getExplanation();\n String mockExplanation = \"显示各承运商的订单分布模拟数据。实际AI功能恢复后,将基于您的筛选条件提供真实的承运商表现分析。\";\n if (existingExplanation == null || existingExplanation.isEmpty()) {\n response.setExplanation(mockExplanation);\n } else {\n response.setExplanation(existingExplanation + \" \" + mockExplanation);\n }\n }\n\n private void populateDeliveryPerformanceMockData(QueryResponse response, Random random) {\n response.setChartType(\"bar\");\n\n // Create simplified chart data for frontend\n Map<String, Object> chartData = new HashMap<>();\n List<String> labels = new ArrayList<>();\n List<Long> values = new ArrayList<>();\n\n // Generate weekly data for last 4 weeks\n LocalDate today = LocalDate.now();\n\n for (int i = 3; i >= 0; i--) {\n LocalDate weekStart = today.minusWeeks(i).with(java.time.DayOfWeek.MONDAY);\n String period = weekStart.format(DateTimeFormatter.ofPattern(\"yyyy-'W'ww\"));\n labels.add(period);\n Long onTime = 200L + random.nextInt(300); // 200-500\n values.add(onTime);\n }\n\n chartData.put(\"labels\", labels);\n chartData.put(\"values\", values);\n chartData.put(\"chartType\", \"bar\");\n\n response.setChartData(chartData);\n // Append mock data explanation to existing explanation if any\n String existingExplanation = response.getExplanation();\n String mockExplanation = \"显示过去4周准时交付数量的模拟数据。实际AI功能恢复后,将基于您的筛选条件提供真实的交付性能分析。\";\n if (existingExplanation == null || existingExplanation.isEmpty()) {\n response.setExplanation(mockExplanation);\n } else {\n response.setExplanation(existingExplanation + \" \" + mockExplanation);\n }\n }\n\n private void populateKpiMockData(QueryResponse response, Random random) {\n response.setChartType(\"kpi\");\n\n // Create simplified chart data for frontend - for KPI we can show a bar chart with metrics\n Map<String, Object> chartData = new HashMap<>();\n List<String> labels = new ArrayList<>();\n List<Long> values = new ArrayList<>();\n\n Long totalOrders = 10000L + random.nextInt(20000); // 10000-30000\n Long deliveredOrders = totalOrders * 8 / 10 + random.nextInt(totalOrders.intValue() / 10); // 80-90% delivered\n Long delayedOrders = deliveredOrders / 10 + random.nextInt(deliveredOrders.intValue() / 5); // 10-30% of delivered delayed\n BigDecimal onTimeRate = BigDecimal.valueOf((deliveredOrders - delayedOrders) * 100.0 / deliveredOrders).setScale(2, BigDecimal.ROUND_HALF_UP);\n BigDecimal avgDeliveryDays = BigDecimal.valueOf(3.5 + random.nextDouble() * 2.5).setScale(2, BigDecimal.ROUND_HALF_UP); // 3.5-6.0 days\n\n // Create KPI summary for chart\n labels.add(\"总订单\");\n values.add(totalOrders);\n labels.add(\"已交付\");\n values.add(deliveredOrders);\n labels.add(\"延迟订单\");\n values.add(delayedOrders);\n\n chartData.put(\"labels\", labels);\n chartData.put(\"values\", values);\n chartData.put(\"chartType\", \"bar\");\n chartData.put(\"kpiSummary\", Map.of(\n \"onTimeRate\", onTimeRate.toString() + \"%\",\n \"avgDeliveryDays\", avgDeliveryDays.toString() + \" 天\"\n ));\n\n response.setChartData(chartData);\n // Append mock data explanation to existing explanation if any\n String existingExplanation = response.getExplanation();\n String mockExplanation = \"显示关键绩效指标的模拟数据。准时交付率: \" + onTimeRate + \"%,平均交付天数: \" + avgDeliveryDays + \"天。实际AI功能恢复后,将基于您的筛选条件提供真实的KPI分析。\";\n if (existingExplanation == null || existingExplanation.isEmpty()) {\n response.setExplanation(mockExplanation);\n } else {\n response.setExplanation(existingExplanation + \" \" + mockExplanation);\n }\n }\n\n private List<Map<String, Object>> generateRawMockData(Random random) {\n List<Map<String, Object>> rawData = new ArrayList<>();\n String[] carriers = {\"UPS\", \"FedEx\", \"DHL\", \"USPS\", \"Amazon Logistics\", \"Regional Carrier\"};\n String[] cities = {\"New York\", \"Los Angeles\", \"Chicago\", \"Houston\", \"Phoenix\", \"Philadelphia\"};\n String[] statuses = {\"delivered\", \"in_transit\", \"pending\", \"cancelled\"};\n\n for (int i = 0; i < 10; i++) { // Generate 10 sample rows\n Map<String, Object> row = new HashMap<>();\n row.put(\"id\", i + 1);\n row.put(\"order_date\", LocalDate.now().minusDays(random.nextInt(30)).toString());\n row.put(\"promised_delivery_date\", LocalDate.now().minusDays(random.nextInt(20)).toString());\n row.put(\"actual_delivery_date\", LocalDate.now().minusDays(random.nextInt(15)).toString());\n row.put(\"status\", statuses[random.nextInt(statuses.length)]);\n row.put(\"carrier\", carriers[random.nextInt(carriers.length)]);\n row.put(\"destination_city\", cities[random.nextInt(cities.length)]);\n row.put(\"destination_state\", \"CA\");\n row.put(\"destination_region\", \"West\");\n row.put(\"order_value\", BigDecimal.valueOf(50 + random.nextDouble() * 4950).setScale(2, BigDecimal.ROUND_HALF_UP));\n row.put(\"sku\", \"SKU-\" + String.format(\"%04d\", 1000 + random.nextInt(9000)));\n row.put(\"quantity\", 1 + random.nextInt(50));\n\n rawData.add(row);\n }\n\n return rawData;\n }\n}"}, "4030129ce594a55b94897416f341549f41ce625e": {"t": 1785760116.8775403, "n": 8382, "h": "bfb318077cb64e66", "c": "package com.logistics.dashboard.ai.orchestrator;\n\nimport com.logistics.dashboard.ai.model.QueryRequest;\nimport com.logistics.dashboard.ai.model.QueryResponse;\nimport com.logistics.dashboard.ai.tools.AiTools;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.ChatLanguageModel;\nimport dev.langchain4j.service.AiServices;\nimport dev.langchain4j.service.UserMessage;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Service;\n\nimport java.time.LocalDate;\nimport java.util.HashMap;\nimport java.util.Map;\n\n@Service\npublic class NLUOrchestrator {\n\n private final ChatLanguageModel model;\n private final AiAssistant assistant;\n private final AiTools aiTools;\n\n public NLUOrchestrator(\n @Value(\"${openai.api.key:}\") String apiKey,\n AiTools aiTools) {\n\n this.aiTools = aiTools;\n\n // Check if API key is provided\n if (apiKey == null || apiKey.trim().isEmpty()) {\n System.out.println(\"WARNING: OPENAI_API_KEY not provided. Natural Language Query functionality will be limited.\");\n // Create a mock model that returns placeholder responses\n this.model = null;\n this.assistant = null;\n } else {\n // Create chat model with OpenAI\n this.model = dev.langchain4j.model.openai.OpenAiChatModel.builder()\n .apiKey(apiKey)\n .modelName(\"gpt-3.5-turbo\")\n .temperature(0.1)\n .maxTokens(500)\n .build();\n\n // Create chat memory\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n // Create AI assistant with tool support\n this.assistant = AiServices.builder(AiAssistant.class)\n .chatLanguageModel(model)\n .chatMemory(chatMemory)\n .tools(aiTools)\n .build();\n }\n }\n\n /**\n * Process a natural language query and return structured response\n */\n public QueryResponse processQuery(QueryRequest request) {\n try {\n // Check if AI model is available\n if (model == null || assistant == null) {\n QueryResponse placeholderResponse = new QueryResponse();\n placeholderResponse.setAnswer(\"Natural Language Query functionality requires an OpenAI API key to be configured. Please set the OPENAI_API_KEY environment variable.\");\n placeholderResponse.setExplanation(\"This feature uses OpenAI's GPT model to interpret natural language questions about logistics data.\");\n placeholderResponse.setChartType(\"none\");\n\n // You can still use other dashboard features without OpenAI\n if (request.getQuestion() != null && !request.getQuestion().isEmpty()) {\n String question = request.getQuestion().toLowerCase();\n if (question.contains(\"order\") || question.contains(\"delivery\") || question.contains(\"carrier\")) {\n placeholderResponse.setAnswer(\"Natural language queries are currently unavailable. Please use the dashboard's standard analytics features for order, delivery, and carrier data analysis.\");\n }\n }\n\n return placeholderResponse;\n }\n\n // Prepare system message with context\n String systemPrompt = createSystemPrompt(request);\n\n // Get response from AI assistant - combine system prompt and user question\n String fullMessage = \"System: \" + systemPrompt + \"\\n\\nUser: \" + request.getQuestion();\n String aiResponse = assistant.chat(fullMessage);\n\n // For now, return a simple response\n // In a full implementation, we would parse the AI's tool calls and execute them\n QueryResponse response = new QueryResponse();\n response.setAnswer(aiResponse);\n response.setExplanation(\"The AI interpreted your question and selected appropriate analytical tools.\");\n response.setChartType(\"none\");\n\n // Add filters if provided\n if (request.getStartDate() != null || request.getEndDate() != null ||\n request.getCarriers() != null || request.getRegions() != null) {\n Map<String, Object> filters = new HashMap<>();\n if (request.getStartDate() != null) filters.put(\"startDate\", request.getStartDate());\n if (request.getEndDate() != null) filters.put(\"endDate\", request.getEndDate());\n if (request.getCarriers() != null) filters.put(\"carriers\", request.getCarriers());\n if (request.getRegions() != null) filters.put(\"regions\", request.getRegions());\n response.setFilters(filters);\n }\n\n return response;\n\n } catch (Exception e) {\n QueryResponse errorResponse = new QueryResponse();\n errorResponse.setError(\"Failed to process query: \" + e.getMessage());\n errorResponse.setAnswer(\"I encountered an error processing your question. Please try again.\");\n return errorResponse;\n }\n }\n\n /**\n * Create system prompt with context about available tools and data\n */\n private String createSystemPrompt(QueryRequest request) {\n StringBuilder prompt = new StringBuilder();\n prompt.append(\"You are an AI assistant for a logistics analytics dashboard. \");\n prompt.append(\"Your role is to interpret user questions about logistics data and select appropriate analytical tools. \");\n prompt.append(\"\\n\\n\");\n prompt.append(\"Available data includes: orders, delivery performance, carrier performance, and time series trends. \");\n prompt.append(\"All data is read-only from a PostgreSQL database. \");\n prompt.append(\"\\n\\n\");\n prompt.append(\"You have access to these tools:\\n\");\n prompt.append(\"1. getTimeSeries - Get aggregated values over time (day, week, month)\\n\");\n prompt.append(\"2. getBreakdown - Get aggregated values by categorical dimension (carrier, region)\\n\");\n prompt.append(\"3. getKpi - Get key performance indicators (total orders, delivered, delayed, on-time rate, avg delivery days)\\n\");\n prompt.append(\"4. getDeliveryPerformance - Get delivery performance data (on-time vs delayed)\\n\");\n prompt.append(\"5. forecastDemand - Forecast future order volume (placeholder for now)\\n\");\n prompt.append(\"\\n\");\n prompt.append(\"IMPORTANT RULES:\\n\");\n prompt.append(\"- NEVER make up or guess data values. Always use the tools.\\n\");\n prompt.append(\"- If a user asks about specific dates, use the date range provided.\\n\");\n prompt.append(\"- If no date range is specified, suggest using a reasonable default (e.g., last 30 days).\\n\");\n prompt.append(\"- Always explain what tools you would use and why.\\n\");\n prompt.append(\"\\n\");\n\n // Add context about filters if provided\n if (request.getStartDate() != null || request.getEndDate() != null) {\n prompt.append(\"Context: User has specified date range: \");\n if (request.getStartDate() != null) prompt.append(\"from \").append(request.getStartDate());\n if (request.getEndDate() != null) prompt.append(\" to \").append(request.getEndDate());\n prompt.append(\"\\n\");\n }\n\n if (request.getCarriers() != null && !request.getCarriers().isEmpty()) {\n prompt.append(\"Context: User filtered by carriers: \").append(String.join(\", \", request.getCarriers())).append(\"\\n\");\n }\n\n if (request.getRegions() != null && !request.getRegions().isEmpty()) {\n prompt.append(\"Context: User filtered by regions: \").append(String.join(\", \", request.getRegions())).append(\"\\n\");\n }\n\n prompt.append(\"\\n\");\n prompt.append(\"Your response should be a clear, concise answer to the user's question, \");\n prompt.append(\"mentioning which tools would be used and what the results would show.\");\n\n return prompt.toString();\n }\n\n /**\n * AI Assistant interface for LangChain4j\n */\n interface AiAssistant {\n\n String chat(@UserMessage String message);\n }\n}"}, "2743e9e74f8130ccb9603a152c2eb8839cd939a8": {"t": 1785760118.0514407, "n": 8352, "h": "10f481bb3ad4d89c", "c": "package com.logistics.dashboard.ai.orchestrator;\n\nimport com.logistics.dashboard.ai.model.QueryRequest;\nimport com.logistics.dashboard.ai.model.QueryResponse;\nimport com.logistics.dashboard.ai.tools.AiTools;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.ChatLanguageModel;\nimport dev.langchain4j.service.AiServices;\nimport dev.langchain4j.service.SystemMessage;\nimport dev.langchain4j.service.UserMessage;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Service;\n\nimport java.time.LocalDate;\nimport java.util.HashMap;\nimport java.util.Map;\n\n@Service\npublic class NLUOrchestrator {\n\n private final ChatLanguageModel model;\n private final AiAssistant assistant;\n private final AiTools aiTools;\n\n public NLUOrchestrator(\n @Value(\"${openai.api.key:}\") String apiKey,\n AiTools aiTools) {\n\n this.aiTools = aiTools;\n\n // Check if API key is provided\n if (apiKey == null || apiKey.trim().isEmpty()) {\n System.out.println(\"WARNING: OPENAI_API_KEY not provided. Natural Language Query functionality will be limited.\");\n // Create a mock model that returns placeholder responses\n this.model = null;\n this.assistant = null;\n } else {\n // Create chat model with OpenAI\n this.model = dev.langchain4j.model.openai.OpenAiChatModel.builder()\n .apiKey(apiKey)\n .modelName(\"gpt-3.5-turbo\")\n .temperature(0.1)\n .maxTokens(500)\n .build();\n\n // Create chat memory\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n // Create AI assistant with tool support\n this.assistant = AiServices.builder(AiAssistant.class)\n .chatLanguageModel(model)\n .chatMemory(chatMemory)\n .tools(aiTools)\n .build();\n }\n }\n\n /**\n * Process a natural language query and return structured response\n */\n public QueryResponse processQuery(QueryRequest request) {\n try {\n // Check if AI model is available\n if (model == null || assistant == null) {\n QueryResponse placeholderResponse = new QueryResponse();\n placeholderResponse.setAnswer(\"Natural Language Query functionality requires an OpenAI API key to be configured. Please set the OPENAI_API_KEY environment variable.\");\n placeholderResponse.setExplanation(\"This feature uses OpenAI's GPT model to interpret natural language questions about logistics data.\");\n placeholderResponse.setChartType(\"none\");\n\n // You can still use other dashboard features without OpenAI\n if (request.getQuestion() != null && !request.getQuestion().isEmpty()) {\n String question = request.getQuestion().toLowerCase();\n if (question.contains(\"order\") || question.contains(\"delivery\") || question.contains(\"carrier\")) {\n placeholderResponse.setAnswer(\"Natural language queries are currently unavailable. Please use the dashboard's standard analytics features for order, delivery, and carrier data analysis.\");\n }\n }\n\n return placeholderResponse;\n }\n\n // Prepare system message with context\n String systemPrompt = createSystemPrompt(request);\n\n // Get response from AI assistant\n String aiResponse = assistant.chat(systemPrompt, request.getQuestion());\n\n // For now, return a simple response\n // In a full implementation, we would parse the AI's tool calls and execute them\n QueryResponse response = new QueryResponse();\n response.setAnswer(aiResponse);\n response.setExplanation(\"The AI interpreted your question and selected appropriate analytical tools.\");\n response.setChartType(\"none\");\n\n // Add filters if provided\n if (request.getStartDate() != null || request.getEndDate() != null ||\n request.getCarriers() != null || request.getRegions() != null) {\n Map<String, Object> filters = new HashMap<>();\n if (request.getStartDate() != null) filters.put(\"startDate\", request.getStartDate());\n if (request.getEndDate() != null) filters.put(\"endDate\", request.getEndDate());\n if (request.getCarriers() != null) filters.put(\"carriers\", request.getCarriers());\n if (request.getRegions() != null) filters.put(\"regions\", request.getRegions());\n response.setFilters(filters);\n }\n\n return response;\n\n } catch (Exception e) {\n QueryResponse errorResponse = new QueryResponse();\n errorResponse.setError(\"Failed to process query: \" + e.getMessage());\n errorResponse.setAnswer(\"I encountered an error processing your question. Please try again.\");\n return errorResponse;\n }\n }\n\n /**\n * Create system prompt with context about available tools and data\n */\n private String createSystemPrompt(QueryRequest request) {\n StringBuilder prompt = new StringBuilder();\n prompt.append(\"You are an AI assistant for a logistics analytics dashboard. \");\n prompt.append(\"Your role is to interpret user questions about logistics data and select appropriate analytical tools. \");\n prompt.append(\"\\n\\n\");\n prompt.append(\"Available data includes: orders, delivery performance, carrier performance, and time series trends. \");\n prompt.append(\"All data is read-only from a PostgreSQL database. \");\n prompt.append(\"\\n\\n\");\n prompt.append(\"You have access to these tools:\\n\");\n prompt.append(\"1. getTimeSeries - Get aggregated values over time (day, week, month)\\n\");\n prompt.append(\"2. getBreakdown - Get aggregated values by categorical dimension (carrier, region)\\n\");\n prompt.append(\"3. getKpi - Get key performance indicators (total orders, delivered, delayed, on-time rate, avg delivery days)\\n\");\n prompt.append(\"4. getDeliveryPerformance - Get delivery performance data (on-time vs delayed)\\n\");\n prompt.append(\"5. forecastDemand - Forecast future order volume (placeholder for now)\\n\");\n prompt.append(\"\\n\");\n prompt.append(\"IMPORTANT RULES:\\n\");\n prompt.append(\"- NEVER make up or guess data values. Always use the tools.\\n\");\n prompt.append(\"- If a user asks about specific dates, use the date range provided.\\n\");\n prompt.append(\"- If no date range is specified, suggest using a reasonable default (e.g., last 30 days).\\n\");\n prompt.append(\"- Always explain what tools you would use and why.\\n\");\n prompt.append(\"\\n\");\n\n // Add context about filters if provided\n if (request.getStartDate() != null || request.getEndDate() != null) {\n prompt.append(\"Context: User has specified date range: \");\n if (request.getStartDate() != null) prompt.append(\"from \").append(request.getStartDate());\n if (request.getEndDate() != null) prompt.append(\" to \").append(request.getEndDate());\n prompt.append(\"\\n\");\n }\n\n if (request.getCarriers() != null && !request.getCarriers().isEmpty()) {\n prompt.append(\"Context: User filtered by carriers: \").append(String.join(\", \", request.getCarriers())).append(\"\\n\");\n }\n\n if (request.getRegions() != null && !request.getRegions().isEmpty()) {\n prompt.append(\"Context: User filtered by regions: \").append(String.join(\", \", request.getRegions())).append(\"\\n\");\n }\n\n prompt.append(\"\\n\");\n prompt.append(\"Your response should be a clear, concise answer to the user's question, \");\n prompt.append(\"mentioning which tools would be used and what the results would show.\");\n\n return prompt.toString();\n }\n\n /**\n * AI Assistant interface for LangChain4j\n */\n interface AiAssistant {\n\n String chat(@SystemMessage String systemMessage, @UserMessage String userMessage);\n }\n}"}, "9de59b466881b9ca9d63843ddb626d000ca9fe2c": {"t": 1785760119.9060683, "n": 8337, "h": "698190f8f33c3189", "c": "package com.logistics.dashboard.ai.orchestrator;\n\nimport com.logistics.dashboard.ai.model.QueryRequest;\nimport com.logistics.dashboard.ai.model.QueryResponse;\nimport com.logistics.dashboard.ai.tools.AiTools;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.ChatLanguageModel;\nimport dev.langchain4j.service.AiServices;\nimport dev.langchain4j.service.SystemMessage;\nimport dev.langchain4j.service.UserMessage;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Service;\n\nimport java.time.LocalDate;\nimport java.util.HashMap;\nimport java.util.Map;\n\n@Service\npublic class NLUOrchestrator {\n\n private final ChatLanguageModel model;\n private final AiAssistant assistant;\n private final AiTools aiTools;\n\n public NLUOrchestrator(\n @Value(\"${openai.api.key:}\") String apiKey,\n AiTools aiTools) {\n\n this.aiTools = aiTools;\n\n // Check if API key is provided\n if (apiKey == null || apiKey.trim().isEmpty()) {\n System.out.println(\"WARNING: OPENAI_API_KEY not provided. Natural Language Query functionality will be limited.\");\n // Create a mock model that returns placeholder responses\n this.model = null;\n this.assistant = null;\n } else {\n // Create chat model with OpenAI\n this.model = dev.langchain4j.model.openai.OpenAiChatModel.builder()\n .apiKey(apiKey)\n .modelName(\"gpt-3.5-turbo\")\n .temperature(0.1)\n .maxTokens(500)\n .build();\n\n // Create chat memory\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n // Create AI assistant with tool support\n this.assistant = AiServices.builder(AiAssistant.class)\n .chatLanguageModel(model)\n .chatMemory(chatMemory)\n .tools(aiTools)\n .build();\n }\n }\n\n /**\n * Process a natural language query and return structured response\n */\n public QueryResponse processQuery(QueryRequest request) {\n try {\n // Check if AI model is available\n if (model == null || assistant == null) {\n QueryResponse placeholderResponse = new QueryResponse();\n placeholderResponse.setAnswer(\"Natural Language Query functionality requires an OpenAI API key to be configured. Please set the OPENAI_API_KEY environment variable.\");\n placeholderResponse.setExplanation(\"This feature uses OpenAI's GPT model to interpret natural language questions about logistics data.\");\n placeholderResponse.setChartType(\"none\");\n\n // You can still use other dashboard features without OpenAI\n if (request.getQuestion() != null && !request.getQuestion().isEmpty()) {\n String question = request.getQuestion().toLowerCase();\n if (question.contains(\"order\") || question.contains(\"delivery\") || question.contains(\"carrier\")) {\n placeholderResponse.setAnswer(\"Natural language queries are currently unavailable. Please use the dashboard's standard analytics features for order, delivery, and carrier data analysis.\");\n }\n }\n\n return placeholderResponse;\n }\n\n // Prepare system message with context\n String systemPrompt = createSystemPrompt(request);\n\n // Get response from AI assistant\n String aiResponse = assistant.chat(systemPrompt, request.getQuestion());\n\n // For now, return a simple response\n // In a full implementation, we would parse the AI's tool calls and execute them\n QueryResponse response = new QueryResponse();\n response.setAnswer(aiResponse);\n response.setExplanation(\"The AI interpreted your question and selected appropriate analytical tools.\");\n response.setChartType(\"none\");\n\n // Add filters if provided\n if (request.getStartDate() != null || request.getEndDate() != null ||\n request.getCarriers() != null || request.getRegions() != null) {\n Map<String, Object> filters = new HashMap<>();\n if (request.getStartDate() != null) filters.put(\"startDate\", request.getStartDate());\n if (request.getEndDate() != null) filters.put(\"endDate\", request.getEndDate());\n if (request.getCarriers() != null) filters.put(\"carriers\", request.getCarriers());\n if (request.getRegions() != null) filters.put(\"regions\", request.getRegions());\n response.setFilters(filters);\n }\n\n return response;\n\n } catch (Exception e) {\n QueryResponse errorResponse = new QueryResponse();\n errorResponse.setError(\"Failed to process query: \" + e.getMessage());\n errorResponse.setAnswer(\"I encountered an error processing your question. Please try again.\");\n return errorResponse;\n }\n }\n\n /**\n * Create system prompt with context about available tools and data\n */\n private String createSystemPrompt(QueryRequest request) {\n StringBuilder prompt = new StringBuilder();\n prompt.append(\"You are an AI assistant for a logistics analytics dashboard. \");\n prompt.append(\"Your role is to interpret user questions about logistics data and select appropriate analytical tools. \");\n prompt.append(\"\\n\\n\");\n prompt.append(\"Available data includes: orders, delivery performance, carrier performance, and time series trends. \");\n prompt.append(\"All data is read-only from a PostgreSQL database. \");\n prompt.append(\"\\n\\n\");\n prompt.append(\"You have access to these tools:\\n\");\n prompt.append(\"1. getTimeSeries - Get aggregated values over time (day, week, month)\\n\");\n prompt.append(\"2. getBreakdown - Get aggregated values by categorical dimension (carrier, region)\\n\");\n prompt.append(\"3. getKpi - Get key performance indicators (total orders, delivered, delayed, on-time rate, avg delivery days)\\n\");\n prompt.append(\"4. getDeliveryPerformance - Get delivery performance data (on-time vs delayed)\\n\");\n prompt.append(\"5. forecastDemand - Forecast future order volume (placeholder for now)\\n\");\n prompt.append(\"\\n\");\n prompt.append(\"IMPORTANT RULES:\\n\");\n prompt.append(\"- NEVER make up or guess data values. Always use the tools.\\n\");\n prompt.append(\"- If a user asks about specific dates, use the date range provided.\\n\");\n prompt.append(\"- If no date range is specified, suggest using a reasonable default (e.g., last 30 days).\\n\");\n prompt.append(\"- Always explain what tools you would use and why.\\n\");\n prompt.append(\"\\n\");\n\n // Add context about filters if provided\n if (request.getStartDate() != null || request.getEndDate() != null) {\n prompt.append(\"Context: User has specified date range: \");\n if (request.getStartDate() != null) prompt.append(\"from \").append(request.getStartDate());\n if (request.getEndDate() != null) prompt.append(\" to \").append(request.getEndDate());\n prompt.append(\"\\n\");\n }\n\n if (request.getCarriers() != null && !request.getCarriers().isEmpty()) {\n prompt.append(\"Context: User filtered by carriers: \").append(String.join(\", \", request.getCarriers())).append(\"\\n\");\n }\n\n if (request.getRegions() != null && !request.getRegions().isEmpty()) {\n prompt.append(\"Context: User filtered by regions: \").append(String.join(\", \", request.getRegions())).append(\"\\n\");\n }\n\n prompt.append(\"\\n\");\n prompt.append(\"Your response should be a clear, concise answer to the user's question, \");\n prompt.append(\"mentioning which tools would be used and what the results would show.\");\n\n return prompt.toString();\n }\n\n /**\n * AI Assistant interface for LangChain4j\n */\n interface AiAssistant {\n\n String chat(String systemMessage, @UserMessage String userMessage);\n }\n}"}, "6f60be7569439d7f5806cc5d707c58a6a90d8c83": {"t": 1785760121.5634499, "n": 6646, "h": "8de5655b6e4b8804", "c": "package com.logistics.dashboard.ai.orchestrator;\n\nimport com.logistics.dashboard.ai.model.QueryRequest;\nimport com.logistics.dashboard.ai.model.QueryResponse;\nimport com.logistics.dashboard.ai.tools.AiTools;\nimport dev.langchain4j.memory.ChatMemory;\nimport dev.langchain4j.memory.chat.MessageWindowChatMemory;\nimport dev.langchain4j.model.chat.ChatLanguageModel;\nimport dev.langchain4j.service.AiServices;\nimport dev.langchain4j.service.SystemMessage;\nimport dev.langchain4j.service.UserMessage;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Service;\n\nimport java.time.LocalDate;\nimport java.util.HashMap;\nimport java.util.Map;\n\n@Service\npublic class NLUOrchestrator {\n\n private final ChatLanguageModel model;\n private final AiAssistant assistant;\n private final AiTools aiTools;\n\n public NLUOrchestrator(\n @Value(\"${openai.api.key}\") String apiKey,\n AiTools aiTools) {\n\n this.aiTools = aiTools;\n\n // Create chat model with OpenAI\n this.model = dev.langchain4j.model.openai.OpenAiChatModel.builder()\n .apiKey(apiKey)\n .modelName(\"gpt-3.5-turbo\")\n .temperature(0.1)\n .maxTokens(500)\n .build();\n\n // Create chat memory\n ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);\n\n // Create AI assistant with tool support\n this.assistant = AiServices.builder(AiAssistant.class)\n .chatLanguageModel(model)\n .chatMemory(chatMemory)\n .tools(aiTools)\n .build();\n }\n\n /**\n * Process a natural language query and return structured response\n */\n public QueryResponse processQuery(QueryRequest request) {\n try {\n // Prepare system message with context\n String systemPrompt = createSystemPrompt(request);\n\n // Get response from AI assistant\n String aiResponse = assistant.chat(systemPrompt, request.getQuestion());\n\n // For now, return a simple response\n // In a full implementation, we would parse the AI's tool calls and execute them\n QueryResponse response = new QueryResponse();\n response.setAnswer(aiResponse);\n response.setExplanation(\"The AI interpreted your question and selected appropriate analytical tools.\");\n response.setChartType(\"none\");\n\n // Add filters if provided\n if (request.getStartDate() != null || request.getEndDate() != null ||\n request.getCarriers() != null || request.getRegions() != null) {\n Map<String, Object> filters = new HashMap<>();\n if (request.getStartDate() != null) filters.put(\"startDate\", request.getStartDate());\n if (request.getEndDate() != null) filters.put(\"endDate\", request.getEndDate());\n if (request.getCarriers() != null) filters.put(\"carriers\", request.getCarriers());\n if (request.getRegions() != null) filters.put(\"regions\", request.getRegions());\n response.setFilters(filters);\n }\n\n return response;\n\n } catch (Exception e) {\n QueryResponse errorResponse = new QueryResponse();\n errorResponse.setError(\"Failed to process query: \" + e.getMessage());\n errorResponse.setAnswer(\"I encountered an error processing your question. Please try again.\");\n return errorResponse;\n }\n }\n\n /**\n * Create system prompt with context about available tools and data\n */\n private String createSystemPrompt(QueryRequest request) {\n StringBuilder prompt = new StringBuilder();\n prompt.append(\"You are an AI assistant for a logistics analytics dashboard. \");\n prompt.append(\"Your role is to interpret user questions about logistics data and select appropriate analytical tools. \");\n prompt.append(\"\\n\\n\");\n prompt.append(\"Available data includes: orders, delivery performance, carrier performance, and time series trends. \");\n prompt.append(\"All data is read-only from a PostgreSQL database. \");\n prompt.append(\"\\n\\n\");\n prompt.append(\"You have access to these tools:\\n\");\n prompt.append(\"1. getTimeSeries - Get aggregated values over time (day, week, month)\\n\");\n prompt.append(\"2. getBreakdown - Get aggregated values by categorical dimension (carrier, region)\\n\");\n prompt.append(\"3. getKpi - Get key performance indicators (total orders, delivered, delayed, on-time rate, avg delivery days)\\n\");\n prompt.append(\"4. getDeliveryPerformance - Get delivery performance data (on-time vs delayed)\\n\");\n prompt.append(\"5. forecastDemand - Forecast future order volume (placeholder for now)\\n\");\n prompt.append(\"\\n\");\n prompt.append(\"IMPORTANT RULES:\\n\");\n prompt.append(\"- NEVER make up or guess data values. Always use the tools.\\n\");\n prompt.append(\"- If a user asks about specific dates, use the date range provided.\\n\");\n prompt.append(\"- If no date range is specified, suggest using a reasonable default (e.g., last 30 days).\\n\");\n prompt.append(\"- Always explain what tools you would use and why.\\n\");\n prompt.append(\"\\n\");\n\n // Add context about filters if provided\n if (request.getStartDate() != null || request.getEndDate() != null) {\n prompt.append(\"Context: User has specified date range: \");\n if (request.getStartDate() != null) prompt.append(\"from \").append(request.getStartDate());\n if (request.getEndDate() != null) prompt.append(\" to \").append(request.getEndDate());\n prompt.append(\"\\n\");\n }\n\n if (request.getCarriers() != null && !request.getCarriers().isEmpty()) {\n prompt.append(\"Context: User filtered by carriers: \").append(String.join(\", \", request.getCarriers())).append(\"\\n\");\n }\n\n if (request.getRegions() != null && !request.getRegions().isEmpty()) {\n prompt.append(\"Context: User filtered by regions: \").append(String.join(\", \", request.getRegions())).append(\"\\n\");\n }\n\n prompt.append(\"\\n\");\n prompt.append(\"Your response should be a clear, concise answer to the user's question, \");\n prompt.append(\"mentioning which tools would be used and what the results would show.\");\n\n return prompt.toString();\n }\n\n /**\n * AI Assistant interface for LangChain4j\n */\n interface AiAssistant {\n\n String chat(String systemMessage, @UserMessage String userMessage);\n }\n}"}}}}