|
@@ -1,19 +1,28 @@
|
|
|
package com.etotem.cfc.controller.ai;
|
|
package com.etotem.cfc.controller.ai;
|
|
|
|
|
|
|
|
import com.etotem.cfc.common.Result;
|
|
import com.etotem.cfc.common.Result;
|
|
|
|
|
+import com.etotem.cfc.dto.HealthAnalysisResult;
|
|
|
|
|
+import com.etotem.cfc.dto.RecommendationQuery;
|
|
|
|
|
+import com.etotem.cfc.dto.RecommendationResult;
|
|
|
import com.etotem.cfc.service.AIService;
|
|
import com.etotem.cfc.service.AIService;
|
|
|
import com.etotem.cfc.service.FamilyContextService;
|
|
import com.etotem.cfc.service.FamilyContextService;
|
|
|
|
|
+import com.etotem.cfc.service.HealthAnalysisService;
|
|
|
|
|
+import com.etotem.cfc.service.RecommendationService;
|
|
|
import io.swagger.v3.oas.annotations.Operation;
|
|
import io.swagger.v3.oas.annotations.Operation;
|
|
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.web.bind.annotation.*;
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
import javax.annotation.Resource;
|
|
|
import java.util.*;
|
|
import java.util.*;
|
|
|
|
|
+import java.util.regex.Matcher;
|
|
|
|
|
+import java.util.regex.Pattern;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* AI 聊天接口
|
|
* AI 聊天接口
|
|
|
* 对接 Dify Chatbot API,自动注入家庭上下文
|
|
* 对接 Dify Chatbot API,自动注入家庭上下文
|
|
|
*/
|
|
*/
|
|
|
|
|
+@Slf4j
|
|
|
@Tag(name = "AI聊天", description = "AI家庭助手聊天接口")
|
|
@Tag(name = "AI聊天", description = "AI家庭助手聊天接口")
|
|
|
@RestController
|
|
@RestController
|
|
|
@RequestMapping("/api/ai")
|
|
@RequestMapping("/api/ai")
|
|
@@ -25,6 +34,12 @@ public class AIChatController {
|
|
|
@Resource
|
|
@Resource
|
|
|
private FamilyContextService familyContextService;
|
|
private FamilyContextService familyContextService;
|
|
|
|
|
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private HealthAnalysisService healthAnalysisService;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private RecommendationService recommendationService;
|
|
|
|
|
+
|
|
|
@Operation(summary = "发送聊天消息(支持传入reportId以解读报告)")
|
|
@Operation(summary = "发送聊天消息(支持传入reportId以解读报告)")
|
|
|
@PostMapping("/chat/send")
|
|
@PostMapping("/chat/send")
|
|
|
public Result<Map<String, Object>> sendMessage(
|
|
public Result<Map<String, Object>> sendMessage(
|
|
@@ -97,4 +112,151 @@ public class AIChatController {
|
|
|
aiService.deleteConversation(conversationId, String.valueOf(userId));
|
|
aiService.deleteConversation(conversationId, String.valueOf(userId));
|
|
|
return Result.success(null);
|
|
return Result.success(null);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // ── 精准营养助手 ──
|
|
|
|
|
+
|
|
|
|
|
+ private static final Pattern RECOMMEND_PATTERN =
|
|
|
|
|
+ Pattern.compile("\\[RECOMMEND:\\s*(\\{.*?\\})\\]", Pattern.DOTALL);
|
|
|
|
|
+
|
|
|
|
|
+ @Operation(summary = "发送营养分析消息(基于菌群报告)")
|
|
|
|
|
+ @PostMapping("/nutrition/send")
|
|
|
|
|
+ public Result<Map<String, Object>> sendNutritionMessage(
|
|
|
|
|
+ @RequestAttribute("userId") Long userId,
|
|
|
|
|
+ @RequestBody Map<String, String> params) {
|
|
|
|
|
+ String query = params.get("query");
|
|
|
|
|
+ String conversationId = params.get("conversationId");
|
|
|
|
|
+ String reportIdStr = params.get("reportId");
|
|
|
|
|
+
|
|
|
|
|
+ if (query == null || query.trim().isEmpty()) {
|
|
|
|
|
+ return Result.error("消息不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (reportIdStr == null || reportIdStr.trim().isEmpty()) {
|
|
|
|
|
+ return Result.error("需要reportId进行营养分析");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Long reportId = Long.valueOf(reportIdStr);
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 菌群分析
|
|
|
|
|
+ HealthAnalysisResult analysis = healthAnalysisService.analyze(reportId);
|
|
|
|
|
+ if (analysis == null) {
|
|
|
|
|
+ return Result.error("报告不存在或分析失败: reportId=" + reportId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 构建 Dify inputs(菌群分析 + 家庭上下文)
|
|
|
|
|
+ Map<String, Object> inputs = familyContextService.buildContext(userId);
|
|
|
|
|
+ inputs.put("health_analysis", analysis);
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 调用精准营养助手
|
|
|
|
|
+ Map<String, Object> difyResp = aiService.sendNutritionMessage(
|
|
|
|
|
+ query, String.valueOf(userId), conversationId, inputs);
|
|
|
|
|
+
|
|
|
|
|
+ String answer = (String) difyResp.getOrDefault("answer", "");
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 解析 [RECOMMEND] 标记
|
|
|
|
|
+ List<RecommendationResult> recommendations = null;
|
|
|
|
|
+ String cleanAnswer = answer;
|
|
|
|
|
+ Matcher matcher = RECOMMEND_PATTERN.matcher(answer);
|
|
|
|
|
+ if (matcher.find()) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ String json = matcher.group(1);
|
|
|
|
|
+ // 简单 JSON 解析(不用 Jackson 避免异常链)
|
|
|
|
|
+ Map<String, Object> rec = parseSimpleJson(json);
|
|
|
|
|
+ if (rec != null) {
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
|
+ List<String> tags = (List<String>) rec.get("tags");
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
|
+ List<String> types = (List<String>) rec.get("types");
|
|
|
|
|
+ Object limitObj = rec.get("limit");
|
|
|
|
|
+ int limit = limitObj instanceof Number ? ((Number) limitObj).intValue() : 3;
|
|
|
|
|
+
|
|
|
|
|
+ if (tags != null && !tags.isEmpty()) {
|
|
|
|
|
+ RecommendationQuery rq = new RecommendationQuery();
|
|
|
|
|
+ rq.setNutritionTags(tags);
|
|
|
|
|
+ rq.setTypes(types != null && !types.isEmpty() ? types : null);
|
|
|
|
|
+ rq.setLimit(limit);
|
|
|
|
|
+ recommendations = recommendationService.search(rq);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("解析 [RECOMMEND] 标记失败, 忽略: {}", e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ // 从回答中移除标记
|
|
|
|
|
+ cleanAnswer = matcher.replaceAll("").trim();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 组装结果
|
|
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
|
|
+ result.put("answer", cleanAnswer);
|
|
|
|
|
+ result.put("conversationId", difyResp.getOrDefault("conversationId", ""));
|
|
|
|
|
+ result.put("recommendations", recommendations != null ? recommendations : Collections.emptyList());
|
|
|
|
|
+ return Result.success(result);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 极简 JSON 解析(仅用于 [RECOMMEND] 标记中的简单对象)
|
|
|
|
|
+ * 避免引入 Jackson 依赖或抛异常
|
|
|
|
|
+ */
|
|
|
|
|
+ private Map<String, Object> parseSimpleJson(String json) {
|
|
|
|
|
+ json = json.trim();
|
|
|
|
|
+ if (!json.startsWith("{") || !json.endsWith("}")) return null;
|
|
|
|
|
+ json = json.substring(1, json.length() - 1).trim();
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> map = new LinkedHashMap<>();
|
|
|
|
|
+ // 按逗号分割顶层键值对(忽略引号内的逗号)
|
|
|
|
|
+ List<String> pairs = splitOutsideQuotes(json);
|
|
|
|
|
+ for (String pair : pairs) {
|
|
|
|
|
+ int colonIdx = pair.indexOf(':');
|
|
|
|
|
+ if (colonIdx < 0) continue;
|
|
|
|
|
+ String key = pair.substring(0, colonIdx).trim();
|
|
|
|
|
+ String value = pair.substring(colonIdx + 1).trim();
|
|
|
|
|
+ key = stripQuotes(key);
|
|
|
|
|
+ if (key.isEmpty()) continue;
|
|
|
|
|
+
|
|
|
|
|
+ if (value.startsWith("[") && value.endsWith("]")) {
|
|
|
|
|
+ // 解析数组
|
|
|
|
|
+ String inner = value.substring(1, value.length() - 1).trim();
|
|
|
|
|
+ List<String> items = new ArrayList<>();
|
|
|
|
|
+ for (String item : splitOutsideQuotes(inner)) {
|
|
|
|
|
+ items.add(stripQuotes(item.trim()));
|
|
|
|
|
+ }
|
|
|
|
|
+ map.put(key, items);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ map.put(key, stripQuotes(value));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return map;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<String> splitOutsideQuotes(String s) {
|
|
|
|
|
+ List<String> parts = new ArrayList<>();
|
|
|
|
|
+ int depth = 0;
|
|
|
|
|
+ boolean inStr = false;
|
|
|
|
|
+ int start = 0;
|
|
|
|
|
+ for (int i = 0; i < s.length(); i++) {
|
|
|
|
|
+ char c = s.charAt(i);
|
|
|
|
|
+ if (c == '"' && (i == 0 || s.charAt(i - 1) != '\\')) {
|
|
|
|
|
+ inStr = !inStr;
|
|
|
|
|
+ } else if (!inStr) {
|
|
|
|
|
+ if (c == '{' || c == '[') depth++;
|
|
|
|
|
+ else if (c == '}' || c == ']') depth--;
|
|
|
|
|
+ else if (c == ',' && depth == 0) {
|
|
|
|
|
+ parts.add(s.substring(start, i));
|
|
|
|
|
+ start = i + 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (start < s.length()) {
|
|
|
|
|
+ parts.add(s.substring(start));
|
|
|
|
|
+ }
|
|
|
|
|
+ return parts;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String stripQuotes(String s) {
|
|
|
|
|
+ if (s == null) return "";
|
|
|
|
|
+ s = s.trim();
|
|
|
|
|
+ if (s.startsWith("\"") && s.endsWith("\"") && s.length() >= 2) {
|
|
|
|
|
+ return s.substring(1, s.length() - 1);
|
|
|
|
|
+ }
|
|
|
|
|
+ return s;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|