feat(报告):AI 聚合残片生成报告草稿
- AiServiceClient:调用 lpt-ai 独立服务,未配置/失败时返回 empty
- GET /study-sessions/{sessionNum}/report-draft:AI 聚合(含预期对比),降级为残片拼接
- 草稿仅作为编辑起点,最终报告仍由用户确认后保存
- application.yml 增加 lpt.ai-service.url 配置
This commit is contained in:
@@ -99,4 +99,12 @@ public class StudySessionController {
|
|||||||
return CommonResult.success(allFragments);
|
return CommonResult.success(allFragments);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成学习报告草稿:AI 聚合残片(不可用时降级为拼接),返回给前端作为编辑起点
|
||||||
|
*/
|
||||||
|
@GetMapping("/{sessionNum}/report-draft")
|
||||||
|
public CommonResult<String> getReportDraft(@PathVariable("sessionNum") String sessionNum) throws ErrorParameterException {
|
||||||
|
return CommonResult.success("请求成功", studySessionsServiceImpl.generateReportDraft(sessionNum));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package com.guo.learningprogresstracker.service.impl;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* lpt-ai 独立 AI 服务客户端。
|
||||||
|
* <p>通过 {@code lpt.ai-service.url} 配置服务地址;未配置或调用失败时返回 empty,由调用方降级。</p>
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "lpt.ai-service")
|
||||||
|
@Setter
|
||||||
|
public class AiServiceClient {
|
||||||
|
|
||||||
|
/** lpt-ai 服务地址,如 http://localhost:5199 */
|
||||||
|
private String url;
|
||||||
|
/** 超时秒数 */
|
||||||
|
private int timeoutSeconds = 90;
|
||||||
|
|
||||||
|
private HttpClient httpClient;
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
void init() {
|
||||||
|
httpClient = HttpClient.newBuilder()
|
||||||
|
.connectTimeout(Duration.ofSeconds(10))
|
||||||
|
.build();
|
||||||
|
if (isConfigured()) {
|
||||||
|
log.info("AiServiceClient 已配置: {}", url);
|
||||||
|
} else {
|
||||||
|
log.info("AiServiceClient 未配置,AI 聚合功能不可用(将降级为拼接)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isConfigured() {
|
||||||
|
return url != null && !url.isBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用 lpt-ai 将残片聚合为学习报告草稿。
|
||||||
|
*
|
||||||
|
* @return 聚合后的报告文本;服务未配置或调用失败时返回 empty
|
||||||
|
*/
|
||||||
|
public Optional<String> aggregateReport(String taskName, List<String> fragments, String expectation) {
|
||||||
|
if (!isConfigured() || fragments == null || fragments.isEmpty()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
List<Map<String, String>> fragmentBodies = fragments.stream()
|
||||||
|
.map(content -> Map.of("content", content))
|
||||||
|
.toList();
|
||||||
|
Map<String, Object> body = expectation == null || expectation.isBlank()
|
||||||
|
? Map.of("taskName", taskName, "fragments", fragmentBodies)
|
||||||
|
: Map.of("taskName", taskName, "fragments", fragmentBodies, "expectation", expectation);
|
||||||
|
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(url + "/ai/aggregate-report"))
|
||||||
|
.timeout(Duration.ofSeconds(timeoutSeconds))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (response.statusCode() != 200) {
|
||||||
|
log.warn("lpt-ai 聚合报告失败: status={}, body={}", response.statusCode(),
|
||||||
|
response.body() != null ? response.body().substring(0, Math.min(200, response.body().length())) : "");
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
JsonNode json = objectMapper.readTree(response.body());
|
||||||
|
String report = json.path("report").asText(null);
|
||||||
|
return Optional.ofNullable(report).filter(s -> !s.isBlank());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("lpt-ai 调用异常,将降级: {}", e.getMessage());
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -16,6 +16,7 @@ import com.guo.learningprogresstracker.mapStruct.StudySessionConvert;
|
|||||||
import com.guo.learningprogresstracker.mapper.StudyReportFragmentsMapper;
|
import com.guo.learningprogresstracker.mapper.StudyReportFragmentsMapper;
|
||||||
import com.guo.learningprogresstracker.mapper.StudyReportsMapper;
|
import com.guo.learningprogresstracker.mapper.StudyReportsMapper;
|
||||||
import com.guo.learningprogresstracker.mapper.StudySessionsMapper;
|
import com.guo.learningprogresstracker.mapper.StudySessionsMapper;
|
||||||
|
import com.guo.learningprogresstracker.service.StudyExpectationsService;
|
||||||
import com.guo.learningprogresstracker.service.StudySessionsService;
|
import com.guo.learningprogresstracker.service.StudySessionsService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -41,6 +42,8 @@ public class StudySessionsServiceImpl extends ServiceImpl<StudySessionsMapper, S
|
|||||||
private final StudyReportFragmentsServiceImpl studyReportFragmentsServiceImpl;
|
private final StudyReportFragmentsServiceImpl studyReportFragmentsServiceImpl;
|
||||||
private final StudyReportFragmentsMapper studyReportFragmentsMapper;
|
private final StudyReportFragmentsMapper studyReportFragmentsMapper;
|
||||||
private final StudyReportsMapper studyReportsMapper;
|
private final StudyReportsMapper studyReportsMapper;
|
||||||
|
private final AiServiceClient aiServiceClient;
|
||||||
|
private final StudyExpectationsService studyExpectationsService;
|
||||||
|
|
||||||
public StudySessionResponse startOrContinueStudySession(String taskNum) throws NotFindEntitiesException, ServiceException {
|
public StudySessionResponse startOrContinueStudySession(String taskNum) throws NotFindEntitiesException, ServiceException {
|
||||||
TaskEntity taskEntity = tasksServiceImpl.getOneOpt(
|
TaskEntity taskEntity = tasksServiceImpl.getOneOpt(
|
||||||
@@ -158,4 +161,30 @@ public class StudySessionsServiceImpl extends ServiceImpl<StudySessionsMapper, S
|
|||||||
return StudySessionConvert.MAPPER.toStudySessionResponse(session);
|
return StudySessionConvert.MAPPER.toStudySessionResponse(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成学习报告草稿:优先调用 lpt-ai 聚合残片,服务不可用时降级为按序拼接。
|
||||||
|
* 草稿仅作为编辑起点返回,不落库——最终报告由用户确认后经 endedStudySession 保存。
|
||||||
|
*/
|
||||||
|
public String generateReportDraft(String sessionNum) throws ErrorParameterException {
|
||||||
|
StudySessionsEntity session = this.getOneOpt(Wrappers.lambdaQuery(StudySessionsEntity.class)
|
||||||
|
.eq(StudySessionsEntity::getSessionNum, sessionNum))
|
||||||
|
.orElseThrow(() -> new ErrorParameterException("会话[" + sessionNum + "]不存在"));
|
||||||
|
|
||||||
|
ArrayList<String> fragments = getAllFragments(sessionNum);
|
||||||
|
if (fragments.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
String taskName = tasksServiceImpl.getOneOpt(
|
||||||
|
Wrappers.lambdaQuery(TaskEntity.class).eq(TaskEntity::getTaskNum, session.getTaskNum()))
|
||||||
|
.map(TaskEntity::getTaskName)
|
||||||
|
.orElse("学习任务");
|
||||||
|
String expectation = Optional.ofNullable(studyExpectationsService.getBySessionNum(sessionNum))
|
||||||
|
.map(e -> e.getDescription())
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
return aiServiceClient.aggregateReport(taskName, fragments, expectation)
|
||||||
|
.orElseGet(() -> String.join("\n", fragments));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,4 +58,8 @@ lpt:
|
|||||||
endpoint:
|
endpoint:
|
||||||
api-key:
|
api-key:
|
||||||
model: claude-fable-5
|
model: claude-fable-5
|
||||||
timeout-seconds: 60
|
timeout-seconds: 60
|
||||||
|
# lpt-ai 独立服务地址(可选,未配置时报告草稿降级为残片拼接)
|
||||||
|
ai-service:
|
||||||
|
url:
|
||||||
|
timeout-seconds: 90
|
||||||
Reference in New Issue
Block a user