feat(feedback): 补提交 feedback 相关源码(漏提交的新建文件)
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
package com.superbiz.agent.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
public class AsyncConfig {
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.superbiz.agent.controller;
|
||||
|
||||
import com.superbiz.agent.dto.FeedbackRequest;
|
||||
import com.superbiz.agent.dto.FeedbackResponse;
|
||||
import com.superbiz.agent.service.FeedbackService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class FeedbackController {
|
||||
|
||||
@Autowired
|
||||
private FeedbackService feedbackService;
|
||||
|
||||
@PostMapping("/feedback")
|
||||
public ResponseEntity<FeedbackResponse> submitFeedback(@RequestBody FeedbackRequest request) {
|
||||
FeedbackResponse response = feedbackService.submitFeedback(request.getSessionId(), request.getFeedback());
|
||||
if (!response.isSuccess()) {
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.superbiz.agent.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class FeedbackRequest {
|
||||
private String sessionId;
|
||||
private String feedback;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.superbiz.agent.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public class FeedbackResponse {
|
||||
private boolean success;
|
||||
private String message;
|
||||
private String caseId;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.superbiz.agent.service;
|
||||
|
||||
import com.superbiz.agent.domain.entity.CaseLibrary;
|
||||
import com.superbiz.agent.domain.entity.DiagnosisSession;
|
||||
import com.superbiz.agent.domain.enums.FaultCategory;
|
||||
import com.superbiz.agent.domain.enums.SourceType;
|
||||
import com.superbiz.agent.repository.CaseLibraryRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class CaseLibraryService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CaseLibraryService.class);
|
||||
|
||||
@Autowired
|
||||
private CaseLibraryRepository caseLibraryRepository;
|
||||
|
||||
public CaseLibrary createFromSession(DiagnosisSession session) {
|
||||
return caseLibraryRepository.findByDiagnosisId(session.getSessionId())
|
||||
.orElseGet(() -> {
|
||||
String content = session.getAnswer();
|
||||
if (content == null || content.isBlank()) {
|
||||
content = session.getQuery() + "\n(自动提取失败,请人工补充)";
|
||||
}
|
||||
|
||||
String title = session.getQuery();
|
||||
if (title.length() > 100) {
|
||||
title = title.substring(0, 100);
|
||||
}
|
||||
|
||||
CaseLibrary caseLibrary = CaseLibrary.builder()
|
||||
.caseId(UUID.randomUUID().toString())
|
||||
.diagnosisId(session.getSessionId())
|
||||
.sourceType(SourceType.AUTO)
|
||||
.faultCategory(FaultCategory.GENERAL)
|
||||
.title(title)
|
||||
.rootCause(content)
|
||||
.solution(content)
|
||||
.createdBy("system")
|
||||
.referenceCount(0)
|
||||
.build();
|
||||
|
||||
CaseLibrary saved = caseLibraryRepository.save(caseLibrary);
|
||||
logger.info("案例已沉淀: caseId={}, sessionId={}", saved.getCaseId(), session.getSessionId());
|
||||
return saved;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.superbiz.agent.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.superbiz.agent.domain.entity.DiagnosisSession;
|
||||
import com.superbiz.agent.domain.entity.ToolInvocation;
|
||||
import com.superbiz.agent.repository.DiagnosisSessionRepository;
|
||||
import com.superbiz.agent.repository.ToolInvocationRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 证据评分服务
|
||||
*
|
||||
* 当前实现:基于 tool_invocation 事实的规则引擎,输出 evidence_score(0-100)。
|
||||
* 扩展预留:LLM 观点辅助评估(evaluateWithLlm),未来可叠加到 factors 中作为独立维度。
|
||||
*/
|
||||
@Service
|
||||
public class EvaluationService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(EvaluationService.class);
|
||||
|
||||
@Autowired
|
||||
private DiagnosisSessionRepository diagnosisSessionRepository;
|
||||
|
||||
@Autowired
|
||||
private ToolInvocationRepository toolInvocationRepository;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Async
|
||||
public void evaluate(String sessionId, String answer) {
|
||||
diagnosisSessionRepository.findBySessionId(sessionId).ifPresent(session -> {
|
||||
try {
|
||||
List<ToolInvocation> toolInvocations = toolInvocationRepository.findBySessionId(sessionId);
|
||||
String selfEvaluation = evaluateWithRules(session, toolInvocations);
|
||||
session.setSelfEvaluation(selfEvaluation);
|
||||
diagnosisSessionRepository.save(session);
|
||||
logger.info("证据评分已写入: sessionId={}, result={}", sessionId, selfEvaluation);
|
||||
} catch (Exception e) {
|
||||
logger.error("评分失败: sessionId={}", sessionId, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 规则引擎(事实层)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private String evaluateWithRules(DiagnosisSession session, List<ToolInvocation> invocations) {
|
||||
List<Map<String, Object>> factors = new ArrayList<>();
|
||||
|
||||
if ("FAILED".equals(session.getStatus())) {
|
||||
factors.add(factor("execution_failed", -100, "执行失败"));
|
||||
return buildResult(0, factors);
|
||||
}
|
||||
|
||||
int total = invocations.size();
|
||||
long successCount = invocations.stream().filter(t -> Boolean.TRUE.equals(t.getSuccess())).count();
|
||||
boolean hasRetrieval = invocations.stream().anyMatch(t -> t.getRetrievalLayer() != null);
|
||||
boolean hasL0Hit = invocations.stream()
|
||||
.anyMatch(t -> t.getL0MatchCount() != null && t.getL0MatchCount() > 0);
|
||||
boolean hasL1Hit = invocations.stream()
|
||||
.anyMatch(t -> t.getL1MatchCount() != null && t.getL1MatchCount() > 0);
|
||||
|
||||
if (total == 0) {
|
||||
factors.add(factor("no_tool_call", 0, "无工具调用,无法评估证据充分度"));
|
||||
return buildResult(0, factors);
|
||||
}
|
||||
|
||||
int score = 0;
|
||||
|
||||
// 工具成功调用
|
||||
if (successCount > 0) {
|
||||
int delta = 30;
|
||||
factors.add(factor("has_successful_tool_call", delta, "有成功的工具调用(" + successCount + "次)"));
|
||||
score += delta;
|
||||
}
|
||||
|
||||
// 检索命中(L0 精确匹配,证据最强)
|
||||
if (hasL0Hit) {
|
||||
int delta = 35;
|
||||
factors.add(factor("l0_exact_match", delta, "L0 精确匹配命中"));
|
||||
score += delta;
|
||||
}
|
||||
// 检索命中(L1 语义匹配)
|
||||
else if (hasL1Hit) {
|
||||
int delta = 20;
|
||||
factors.add(factor("l1_semantic_match", delta, "L1 语义匹配命中"));
|
||||
score += delta;
|
||||
}
|
||||
// 有检索但无命中
|
||||
else if (hasRetrieval) {
|
||||
int delta = -10;
|
||||
factors.add(factor("retrieval_no_hit", delta, "检索工具调用但无匹配结果"));
|
||||
score += delta;
|
||||
}
|
||||
|
||||
// 全部工具调用失败
|
||||
if (successCount == 0) {
|
||||
int delta = -20;
|
||||
factors.add(factor("all_tool_calls_failed", delta, "所有工具调用均失败"));
|
||||
score += delta;
|
||||
}
|
||||
|
||||
score = Math.max(0, Math.min(100, score));
|
||||
return buildResult(score, factors);
|
||||
}
|
||||
|
||||
private Map<String, Object> factor(String name, int delta, String description) {
|
||||
return Map.of("name", name, "delta", delta, "description", description);
|
||||
}
|
||||
|
||||
private String buildResult(int score, List<Map<String, Object>> factors) {
|
||||
try {
|
||||
Map<String, Object> result = Map.of(
|
||||
"evidence_score", score,
|
||||
"source", "rule",
|
||||
"factors", factors
|
||||
// llm_opinion: null ← 预留字段,LLM 观点叠加时在此处扩展
|
||||
);
|
||||
return objectMapper.writeValueAsString(result);
|
||||
} catch (Exception e) {
|
||||
logger.error("序列化评分结果失败", e);
|
||||
return "{\"evidence_score\":0,\"source\":\"rule\",\"factors\":[]}";
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 预留:LLM 观点辅助(Phase 2)
|
||||
// 实现时在此处添加 evaluateWithLlm(session, answer) 方法,
|
||||
// 返回结构化观点(如 has_root_cause、has_solution 等),
|
||||
// 作为独立 factors 叠加到 buildResult 中,不改变现有规则逻辑。
|
||||
// -------------------------------------------------------------------------
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.superbiz.agent.service;
|
||||
|
||||
import com.superbiz.agent.domain.entity.DiagnosisSession;
|
||||
import com.superbiz.agent.dto.FeedbackResponse;
|
||||
import com.superbiz.agent.repository.DiagnosisSessionRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class FeedbackService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(FeedbackService.class);
|
||||
|
||||
private static final String FEEDBACK_USEFUL = "useful";
|
||||
private static final String FEEDBACK_NOT_USEFUL = "not_useful";
|
||||
|
||||
@Autowired
|
||||
private DiagnosisSessionRepository diagnosisSessionRepository;
|
||||
|
||||
@Autowired
|
||||
private CaseLibraryService caseLibraryService;
|
||||
|
||||
public FeedbackResponse submitFeedback(String sessionId, String feedback) {
|
||||
if (sessionId == null || sessionId.isBlank()) {
|
||||
return FeedbackResponse.builder().success(false).message("sessionId 不能为空").build();
|
||||
}
|
||||
if (!FEEDBACK_USEFUL.equals(feedback) && !FEEDBACK_NOT_USEFUL.equals(feedback)) {
|
||||
return FeedbackResponse.builder().success(false)
|
||||
.message("feedback 只能是 useful 或 not_useful").build();
|
||||
}
|
||||
|
||||
DiagnosisSession session = diagnosisSessionRepository.findBySessionId(sessionId)
|
||||
.orElse(null);
|
||||
if (session == null) {
|
||||
return FeedbackResponse.builder().success(false).message("会话不存在").build();
|
||||
}
|
||||
|
||||
session.setFeedback(feedback);
|
||||
|
||||
String caseId = null;
|
||||
if (FEEDBACK_USEFUL.equals(feedback)) {
|
||||
var caseLibrary = caseLibraryService.createFromSession(session);
|
||||
caseId = caseLibrary.getCaseId();
|
||||
}
|
||||
|
||||
diagnosisSessionRepository.save(session);
|
||||
logger.info("反馈已记录: sessionId={}, feedback={}, caseId={}", sessionId, feedback, caseId);
|
||||
|
||||
return FeedbackResponse.builder()
|
||||
.success(true)
|
||||
.message("反馈已记录")
|
||||
.caseId(caseId)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE diagnosis_session ADD COLUMN answer LONGTEXT COMMENT 'Agent 返回给用户的完整答案';
|
||||
Reference in New Issue
Block a user