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);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成学习报告草稿: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.StudyReportsMapper;
|
||||
import com.guo.learningprogresstracker.mapper.StudySessionsMapper;
|
||||
import com.guo.learningprogresstracker.service.StudyExpectationsService;
|
||||
import com.guo.learningprogresstracker.service.StudySessionsService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -41,6 +42,8 @@ public class StudySessionsServiceImpl extends ServiceImpl<StudySessionsMapper, S
|
||||
private final StudyReportFragmentsServiceImpl studyReportFragmentsServiceImpl;
|
||||
private final StudyReportFragmentsMapper studyReportFragmentsMapper;
|
||||
private final StudyReportsMapper studyReportsMapper;
|
||||
private final AiServiceClient aiServiceClient;
|
||||
private final StudyExpectationsService studyExpectationsService;
|
||||
|
||||
public StudySessionResponse startOrContinueStudySession(String taskNum) throws NotFindEntitiesException, ServiceException {
|
||||
TaskEntity taskEntity = tasksServiceImpl.getOneOpt(
|
||||
@@ -158,4 +161,30 @@ public class StudySessionsServiceImpl extends ServiceImpl<StudySessionsMapper, S
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -59,3 +59,7 @@ lpt:
|
||||
api-key:
|
||||
model: claude-fable-5
|
||||
timeout-seconds: 60
|
||||
# lpt-ai 独立服务地址(可选,未配置时报告草稿降级为残片拼接)
|
||||
ai-service:
|
||||
url:
|
||||
timeout-seconds: 90
|
||||
Reference in New Issue
Block a user