Переглянути джерело

feat(indicator): AiGateway.classifyIndicator + LangGraph /api/v1/indicator/classify

iwt 3 днів тому
батько
коміт
dedefede5e

+ 31 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java

@@ -415,6 +415,37 @@ public class AiGateway {
         }
     }
 
+    /**
+     * 指标归类兜底:将未命中的原始指标名交给 LangGraph 归类为已有/新定义。
+     * @return 建议 JSON 字符串(含 code/name)或 null(关闭/熔断/失败)
+     */
+    public String classifyIndicator(String sourceType, String rawName, java.util.List<String> candidates) {
+        if (!enabled || isCircuitOpen()) {
+            return null;
+        }
+        try {
+            ObjectNode body = objectMapper.createObjectNode();
+            body.put("source_type", sourceType);
+            body.put("raw_name", rawName);
+            if (candidates != null) {
+                com.fasterxml.jackson.databind.node.ArrayNode arr = body.putArray("candidates");
+                candidates.forEach(arr::add);
+            }
+            HttpEntity<String> entity = new HttpEntity<>(body.toString(), createJsonHeaders());
+            String url = baseUrl + "/api/v1/indicator/classify";
+            ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
+            if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
+                consecutiveFailures.set(0);
+                return response.getBody();
+            }
+            return null;
+        } catch (Exception e) {
+            log.warn("AiGateway classifyIndicator 调用失败: {}", e.getMessage());
+            recordFailure();
+            return null;
+        }
+    }
+
     /**
      * 识别图片中的食材
      */

+ 64 - 0
cfc-langgraph/app/api/indicator_classify.py

@@ -0,0 +1,64 @@
+from fastapi import APIRouter
+from pydantic import BaseModel
+from typing import Optional, List
+from app.llm.client import get_llm
+from langchain_core.messages import SystemMessage, HumanMessage
+import logging
+import json
+
+logger = logging.getLogger(__name__)
+router = APIRouter(prefix="/api/v1", tags=["indicator_classify"])
+_llm = None
+
+
+def get_llm_instance():
+    global _llm
+    if _llm is None:
+        _llm = get_llm()
+    return _llm
+
+
+class ClassifyRequest(BaseModel):
+    source_type: str
+    raw_name: str
+    candidates: Optional[List[str]] = None
+
+
+class ClassifyResponse(BaseModel):
+    code: int = 200
+    message: str = "ok"
+    data: dict = {}
+
+
+SYSTEM_PROMPT = (
+    "你是健康报告指标归一化归类助手。给定一个报告中的原始指标/菌属/食材名称,"
+    "把它归类为统一指标字典中的一个标准定义。只输出 JSON:"
+    '{"code": "indicator.<类别>.<标准名>" | "bacteria.<标准名>" | "food.<标准名>", '
+    '"name": "标准显示名", "category": "分类"}。'
+    "若候选列表非空,优先从候选中选择语义最接近的标准 code。"
+)
+
+
+@router.post("/indicator/classify", response_model=ClassifyResponse)
+async def classify_indicator(req: ClassifyRequest):
+    logger.info("indicator_classify: source=%s raw=%s", req.source_type, req.raw_name)
+    llm = get_llm_instance()
+    prompt = f"来源类型: {req.source_type}\n原始名称: {req.raw_name}\n"
+    if req.candidates:
+        prompt += "候选标准定义: " + ", ".join(req.candidates) + "\n"
+    try:
+        messages = [
+            SystemMessage(content=SYSTEM_PROMPT),
+            HumanMessage(content=prompt),
+        ]
+        response = await llm.ainvoke(messages)
+        text = response.content.strip()
+        if "```json" in text:
+            text = text.split("```json")[1].split("```")[0].strip()
+        elif "```" in text:
+            text = text.split("```")[1].strip()
+        data = json.loads(text)
+        return ClassifyResponse(data=data)
+    except Exception as e:
+        logger.error("指标归类失败: %s", e, exc_info=True)
+        return ClassifyResponse(code=500, message=f"归类失败: {str(e)}", data={})

+ 2 - 1
cfc-langgraph/app/main.py

@@ -3,7 +3,7 @@ import time
 import asyncio
 import logging
 from fastapi import FastAPI, Request
-from app.api import health, recommend, chat, analyze, tongue, adapter, report_parse, meal, logs, audio, innate_portrait, self_check, knowledge_base, portrait, article_quiz
+from app.api import health, recommend, chat, analyze, tongue, adapter, report_parse, meal, logs, audio, innate_portrait, self_check, knowledge_base, portrait, article_quiz, indicator_classify
 from app import monitoring
 from src.app import router as questionnaire_router
 from app.middleware import RequestLogMiddleware
@@ -33,6 +33,7 @@ app.include_router(self_check.router)
 app.include_router(knowledge_base.router)
 app.include_router(portrait.router)
 app.include_router(article_quiz.router)
+app.include_router(indicator_classify.router)
 
 
 @app.on_event("startup")